Commit 9030409a authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: UI sizing and spacing that did not fit real phones

An audit of the live app at 360/390/430px found 355 measurable layout defects.
tools/ui-audit.mjs drives the real app at those three widths and reports what is
measurable rather than a matter of taste: horizontal overflow, tap targets below
44px, text below a legible size, and content clipped by its own container.

  before  355  (tiny-text 216, tap-target 133, clipped 6)
  after     0

The root cause of the size problems was not the stylesheet. /api/branding.php
writes 181 stored values inline onto :root at runtime, overriding every token in
tokens.css — including quick_btn_label_font 10 and hud_badge_font 9. Editing
tokens.css alone changed nothing. The floors are now enforced in theme.js where
the override happens, so no stored value (or future admin edit) can reintroduce
illegible text or an unreachable control:

- any *_font token is floored at 11px
- tap-target tokens are floored at 44px
- home_max_width, stored as 340px for a 360px phone, is treated as a floor and
  allowed to grow (min(100%, max(340px, 92vw))) instead of pinning every phone
  to the smallest one's layout

Alongside that: a fluid type scale (--fs-xs … --fs-xl) and --tap-min in
tokens.css, a tap-target floor on every control in core.css, and 53 sub-11px
inline font sizes and 41 undersized inline button heights raised at the source.

Layout fixes visible on screen:
- the daily-gift button carried breathe-glow on the whole flex column, drawing a
  large dark rectangle across the quick-actions row. The glow belongs on the icon.
- play home dumped its spare height as dead space above the tab bar; it now
  centres via auto margins, which does not clip when content overflows the way
  justify-content:center does.
- the chess board left a stray "1." floating in a gap above the controls. The
  opening/material/move-list block is now hidden until there is something in it.

Also adds tools/dev-server.php, which serves the app locally against the real
Supabase so this work does not need a deploy per iteration.

Multiplayer regression: tests/run.sh 5/5, and the production rehearsal passes.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 09afc0f8
...@@ -93,12 +93,12 @@ html, body { ...@@ -93,12 +93,12 @@ html, body {
transform: translateX(-50%); transform: translateX(-50%);
background: var(--gold); background: var(--gold);
color: #1a1a1a; color: #1a1a1a;
font-size: 9px; font-size: var(--fs-xs);
font-weight: 800; font-weight: 800;
font-family: var(--font-lat); font-family: var(--font-lat);
min-width: 16px; min-width: 18px;
height: 14px; height: 16px;
line-height: 14px; line-height: 16px;
text-align: center; text-align: center;
border-radius: var(--r-full); border-radius: var(--r-full);
padding: 0 3px; padding: 0 3px;
...@@ -320,6 +320,38 @@ html, body { ...@@ -320,6 +320,38 @@ html, body {
.card:active { transform: scale(0.96); box-shadow: var(--shadow-md); transition-duration: 80ms; } .card:active { transform: scale(0.96); box-shadow: var(--shadow-md); transition-duration: 80ms; }
/* Buttons */ /* Buttons */
/* ── Tap-target floor ──────────────────────────────────────────────────────
Every interactive control is at least 44px tall, the documented minimum on
both iOS and Android. An audit of the live app found 40 distinct controls
below it — filter chips at 31px, tab buttons at 34px, icon buttons at 32px —
which on a phone means missed taps, not just an untidy layout.
Height only: forcing a minimum width would stretch chips that are legitimately
narrow. Icon-only buttons, which need both, are handled where they are defined. */
button,
[role="button"],
input[type="button"],
input[type="submit"] {
min-block-size: var(--tap-min, 44px);
}
/* Standalone text links act as controls too. A 21px-tall legal link is a miss
waiting to happen; give the row real height without changing how it reads. */
a:not(.btn):not([class*="tab"]) {
display: inline-flex;
align-items: center;
min-block-size: var(--tap-min, 44px);
}
/* Icon-only controls need the floor in both directions to stay square. */
.icon-btn,
.hud-btn,
#emote-inline-toggle,
.bgg-action-btn,
#back-btn {
min-inline-size: var(--tap-min, 44px);
}
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
......
...@@ -98,6 +98,11 @@ ...@@ -98,6 +98,11 @@
transition: transform var(--dur-normal) cubic-bezier(0.16, 1, 0.3, 1); transition: transform var(--dur-normal) cubic-bezier(0.16, 1, 0.3, 1);
} }
/* `display: flex` above outranks the user-agent's `[hidden] { display: none }`,
so without this the panel is never actually hidden — it just sits translated
off-screen, still focusable and still catching taps along the bottom edge. */
.rx-panel[hidden] { display: none !important; }
.rx-panel.rx-panel-in { transform: translateY(0); } .rx-panel.rx-panel-in { transform: translateY(0); }
.rx-panel-grip { .rx-panel-grip {
......
...@@ -80,6 +80,23 @@ ...@@ -80,6 +80,23 @@
--s-8: 32px; --s-10: 40px; --s-12: 48px; --s-8: 32px; --s-10: 40px; --s-12: 48px;
--s-16: 64px; --s-16: 64px;
/* ── Type scale ────────────────────────────────────────────────────────
Fluid between a 360px phone and a 430px one. Every step has a floor:
nothing in the product may render below --fs-xs, because 9px and 10px
labels are not readable at arm's length on a phone. */
--fs-xs: clamp(11px, 3.05vw, 12px); /* micro labels, badges, meta */
--fs-sm: clamp(12px, 3.4vw, 13px); /* secondary text */
--fs-base: clamp(14px, 3.9vw, 15px); /* body */
--fs-md: clamp(15px, 4.2vw, 17px); /* emphasis */
--fs-lg: clamp(17px, 4.8vw, 20px); /* section titles */
--fs-xl: clamp(20px, 5.6vw, 24px); /* screen titles */
/* ── Controls ──────────────────────────────────────────────────────────
44px is the documented minimum tap target on both iOS and Android.
--tap-min is the floor every interactive control is held to. */
--tap-min: 44px;
--tap-comfortable: 48px;
--font-ar: 'IBM Plex Sans Arabic', sans-serif; --font-ar: 'IBM Plex Sans Arabic', sans-serif;
--font-lat: 'Inter', sans-serif; --font-lat: 'Inter', sans-serif;
--font-mono: 'IBM Plex Mono', monospace; --font-mono: 'IBM Plex Mono', monospace;
...@@ -129,10 +146,10 @@ ...@@ -129,10 +146,10 @@
--home-greeting-margin: 12px; --home-greeting-margin: 12px;
/* Layout: Quick Actions */ /* Layout: Quick Actions */
--quick-btn-icon-size: 42px; --quick-btn-icon-size: clamp(40px, 11.5vw, 48px);
--quick-btn-icon-radius: 12px; --quick-btn-icon-radius: 12px;
--quick-btn-icon-font: 20px; --quick-btn-icon-font: clamp(18px, 5vw, 22px);
--quick-btn-label-font: 10px; --quick-btn-label-font: var(--fs-xs);
--quick-btn-gap: 8px; --quick-btn-gap: 8px;
--quick-btn-padding: 6px; --quick-btn-padding: 6px;
--quick-row-gap: 8px; --quick-row-gap: 8px;
...@@ -142,9 +159,9 @@ ...@@ -142,9 +159,9 @@
--game-tile-radius: 18px; --game-tile-radius: 18px;
--game-tile-gap: 12px; --game-tile-gap: 12px;
--game-tile-aspect: 1.1; --game-tile-aspect: 1.1;
--game-tile-icon-size: 48px; --game-tile-icon-size: clamp(44px, 12.5vw, 56px);
--game-tile-icon-font: 42px; --game-tile-icon-font: clamp(36px, 10.5vw, 46px);
--game-tile-name-font: 16px; --game-tile-name-font: var(--fs-md);
--game-tile-content-gap: 8px; --game-tile-content-gap: 8px;
/* Layout: Game Menu (Sheet) */ /* Layout: Game Menu (Sheet) */
...@@ -188,10 +205,10 @@ ...@@ -188,10 +205,10 @@
--menu-header-margin: 20px; --menu-header-margin: 20px;
/* Layout: Bot Cards */ /* Layout: Bot Cards */
--bot-card-avatar-size: 56px; --bot-card-avatar-size: clamp(48px, 14vw, 60px);
--bot-card-avatar-border: 2px; --bot-card-avatar-border: 2px;
--bot-card-name-font: 14px; --bot-card-name-font: var(--fs-sm);
--bot-card-sub-font: 11px; --bot-card-sub-font: var(--fs-xs);
--bot-card-accent-height: 3px; --bot-card-accent-height: 3px;
--bot-grid-gap: 12px; --bot-grid-gap: 12px;
--bot-back-btn-size: 44px; --bot-back-btn-size: 44px;
......
...@@ -124,7 +124,7 @@ function showGuestToast() { ...@@ -124,7 +124,7 @@ function showGuestToast() {
toast.style.cssText = 'background:var(--bg-elevated);color:var(--text-primary);padding:12px 20px;border-radius:var(--r-md);font-size:14px;display:flex;flex-direction:column;align-items:center;gap:8px;animation:fadeIn 0.2s;'; toast.style.cssText = 'background:var(--bg-elevated);color:var(--text-primary);padding:12px 20px;border-radius:var(--r-md);font-size:14px;display:flex;flex-direction:column;align-items:center;gap:8px;animation:fadeIn 0.2s;';
toast.innerHTML = ` toast.innerHTML = `
<span>${t('guest.locked_feature')}</span> <span>${t('guest.locked_feature')}</span>
<button class="btn btn-primary" style="font-size:13px;padding:6px 16px;min-height:32px;">${t('guest.create_account')}</button> <button class="btn btn-primary" style="font-size:13px;padding:6px 16px;min-height:var(--tap-min);">${t('guest.create_account')}</button>
`; `;
toast.querySelector('button').addEventListener('click', () => { toast.querySelector('button').addEventListener('click', () => {
toast.remove(); toast.remove();
......
...@@ -37,7 +37,7 @@ export function renderOpponentBar(container, opponent, options = {}) { ...@@ -37,7 +37,7 @@ export function renderOpponentBar(container, opponent, options = {}) {
<div style="font-size:13px;font-weight:600;color:var(--text-primary);">${opponent.display_name || opponent.username || t('common.opponent')}</div> <div style="font-size:13px;font-weight:600;color:var(--text-primary);">${opponent.display_name || opponent.username || t('common.opponent')}</div>
${showRating ? `<div style="font-size:11px;color:var(--text-muted);">${emoji('star', '⭐', 11)} ${opponent.rating || opponent.elo_rapid || DEFAULT_RATING}</div>` : ''} ${showRating ? `<div style="font-size:11px;color:var(--text-muted);">${emoji('star', '⭐', 11)} ${opponent.rating || opponent.elo_rapid || DEFAULT_RATING}</div>` : ''}
</div> </div>
<div id="mp-opponent-status" style="font-size:10px;color:var(--text-muted);"></div> <div id="mp-opponent-status" style="font-size:var(--fs-xs);color:var(--text-muted);"></div>
`; `;
// Tap to show profile actions // Tap to show profile actions
......
...@@ -125,7 +125,7 @@ export function injectStyles() { ...@@ -125,7 +125,7 @@ export function injectStyles() {
.pp-info { flex:1; } .pp-info { flex:1; }
.pp-name { font-size:13px;font-weight:600;color:var(--text-primary); } .pp-name { font-size:13px;font-weight:600;color:var(--text-primary); }
.pp-meta { display:flex;gap:6px;align-items:center;margin-top:2px; } .pp-meta { display:flex;gap:6px;align-items:center;margin-top:2px; }
.pp-level { font-size:10px;color:var(--text-muted); } .pp-level { font-size:var(--fs-xs);color:var(--text-muted); }
.pp-tier { font-size:12px; } .pp-tier { font-size:12px; }
.pp-rating { font-size:11px;color:var(--text-secondary);font-family:Inter,monospace; } .pp-rating { font-size:11px;color:var(--text-secondary);font-family:Inter,monospace; }
.pp-turn-indicator { width:8px;height:8px;border-radius:50%;background:var(--gold);animation:pulse 1s ease-in-out infinite; } .pp-turn-indicator { width:8px;height:8px;border-radius:50%;background:var(--gold);animation:pulse 1s ease-in-out infinite; }
......
...@@ -172,8 +172,33 @@ function applyAnimations() { ...@@ -172,8 +172,33 @@ function applyAnimations() {
bot_back_btn_size: '--bot-back-btn-size', bot_back_btn_font: '--bot-back-btn-font', bot_back_btn_size: '--bot-back-btn-size', bot_back_btn_font: '--bot-back-btn-font',
bot_title_font: '--bot-title-font', bot_title_font: '--bot-title-font',
}; };
// Stored branding values win over tokens.css, so the legibility and reach
// floors have to be enforced HERE — otherwise an admin (or an old stored
// value) silently reintroduces 9px labels and 32px buttons. The live theme
// was doing exactly that: quick_btn_label_font 10, hud_badge_font 9.
const FONT_FLOOR_PX = 11; // below this, text is not readable on a phone
const TAP_FLOOR_PX = 44; // documented minimum tap target, iOS and Android
const TAP_TOKENS = new Set([
'--hud-btn-size', '--tab-item-min-size', '--menu-close-size', '--bot-back-btn-size',
]);
for (const [key, cssVar] of Object.entries(layoutPx)) { for (const [key, cssVar] of Object.entries(layoutPx)) {
if (themeData[key]) root.setProperty(cssVar, themeData[key] + 'px'); if (!themeData[key]) continue;
const px = parseFloat(themeData[key]);
if (!Number.isFinite(px)) continue;
if (cssVar.endsWith('-font')) {
root.setProperty(cssVar, Math.max(px, FONT_FLOOR_PX) + 'px');
} else if (TAP_TOKENS.has(cssVar)) {
root.setProperty(cssVar, Math.max(px, TAP_FLOOR_PX) + 'px');
} else if (cssVar === '--home-max-width') {
// The stored 340px was chosen for a 360px phone and leaves ~90px of dead
// margin on a large one. Treat it as a floor and let content fill wider
// screens instead of pinning every phone to the smallest one's layout.
root.setProperty(cssVar, `min(100%, max(${px}px, 92vw))`);
} else {
root.setProperty(cssVar, px + 'px');
}
} }
if (themeData.game_tile_aspect) root.setProperty('--game-tile-aspect', themeData.game_tile_aspect); if (themeData.game_tile_aspect) root.setProperty('--game-tile-aspect', themeData.game_tile_aspect);
if (themeData.sheet_max_height) root.setProperty('--sheet-max-height', themeData.sheet_max_height + 'vh'); if (themeData.sheet_max_height) root.setProperty('--sheet-max-height', themeData.sheet_max_height + 'vh');
......
...@@ -753,8 +753,8 @@ function getStyles() { ...@@ -753,8 +753,8 @@ function getStyles() {
.bgg-player-info { flex:1;min-width:0; } .bgg-player-info { flex:1;min-width:0; }
.bgg-name { font-size:13px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; } .bgg-name { font-size:13px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
.bgg-meta { display:flex;align-items:center;gap:8px;margin-top:1px; } .bgg-meta { display:flex;align-items:center;gap:8px;margin-top:1px; }
.bgg-level { font-size:10px;color:var(--text-muted);font-weight:600; } .bgg-level { font-size:var(--fs-xs);color:var(--text-muted);font-weight:600; }
.bgg-pips { font-size:10px;color:var(--text-secondary);font-weight:500; } .bgg-pips { font-size:var(--fs-xs);color:var(--text-secondary);font-weight:500; }
.bgg-pips::before { content:'⬡ ';opacity:0.5; } .bgg-pips::before { content:'⬡ ';opacity:0.5; }
.bgg-score { .bgg-score {
font-size:20px;font-weight:800;color:var(--text-dim);min-width:24px;text-align:center; font-size:20px;font-weight:800;color:var(--text-dim);min-width:24px;text-align:center;
...@@ -769,7 +769,7 @@ function getStyles() { ...@@ -769,7 +769,7 @@ function getStyles() {
background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.25); background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.25);
border-radius:6px;padding:2px 8px; border-radius:6px;padding:2px 8px;
} }
.bgg-match-length { font-size:9px;color:var(--text-muted); } .bgg-match-length { font-size:var(--fs-xs);color:var(--text-muted); }
/* ── Board ── */ /* ── Board ── */
.bgg-board-area { .bgg-board-area {
...@@ -782,7 +782,7 @@ function getStyles() { ...@@ -782,7 +782,7 @@ function getStyles() {
} }
.bgg-turn-badge { .bgg-turn-badge {
position:absolute;top:6px;left:50%;transform:translateX(-50%); position:absolute;top:6px;left:50%;transform:translateX(-50%);
font-size:10px;font-weight:700;padding:3px 10px;border-radius:20px; font-size:var(--fs-xs);font-weight:700;padding:3px 10px;border-radius:20px;
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
pointer-events:none;transition:all 0.3s;opacity:0.9; pointer-events:none;transition:all 0.3s;opacity:0.9;
} }
...@@ -807,14 +807,14 @@ function getStyles() { ...@@ -807,14 +807,14 @@ function getStyles() {
} }
.bgg-roll-btn:active { transform:scale(0.92);box-shadow:0 2px 8px rgba(228,172,56,0.3); } .bgg-roll-btn:active { transform:scale(0.92);box-shadow:0 2px 8px rgba(228,172,56,0.3); }
.bgg-cube-btn { .bgg-cube-btn {
padding:10px 16px;border-radius:10px;border:none; padding:10px 16px;min-height:var(--tap-min);border-radius:10px;border:none;
background:rgba(139,92,246,0.12);border:1.5px solid rgba(139,92,246,0.35); background:rgba(139,92,246,0.12);border:1.5px solid rgba(139,92,246,0.35);
color:var(--purple-light);font-size:15px;font-weight:800;cursor:pointer; color:var(--purple-light);font-size:15px;font-weight:800;cursor:pointer;
transition:transform 0.12s; transition:transform 0.12s;
} }
.bgg-cube-btn:active { transform:scale(0.9); } .bgg-cube-btn:active { transform:scale(0.9); }
.bgg-action-btn { .bgg-action-btn {
width:40px;height:40px;border-radius:50%;border:none; width:var(--tap-min);height:var(--tap-min);border-radius:50%;border:none;
background:rgba(255,255,255,0.04);color:var(--text-secondary); background:rgba(255,255,255,0.04);color:var(--text-secondary);
font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center; font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;
border:1px solid var(--border); border:1px solid var(--border);
......
...@@ -22,7 +22,7 @@ export function mountAnalysis(el, params) { ...@@ -22,7 +22,7 @@ export function mountAnalysis(el, params) {
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-card);"> <div style="display:flex;flex-direction:column;height:100%;background:var(--bg-card);">
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--bg-panel);border-bottom:1px solid var(--border);"> <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--bg-panel);border-bottom:1px solid var(--border);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">← ${t('game.back')}</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">← ${t('game.back')}</button>
<span style="font-size:14px;font-weight:700;color:var(--text-primary);">${emoji('chart', '📊', 14)} ${t('analysis.title')}</span> <span style="font-size:14px;font-weight:700;color:var(--text-primary);">${emoji('chart', '📊', 14)} ${t('analysis.title')}</span>
<div style="width:60px;"></div> <div style="width:60px;"></div>
</div> </div>
...@@ -110,7 +110,7 @@ function renderMoveChips(el, moves) { ...@@ -110,7 +110,7 @@ function renderMoveChips(el, moves) {
let html = `<span class="move-chip active" data-idx="0">${t('analysis.start')}</span>`; let html = `<span class="move-chip active" data-idx="0">${t('analysis.start')}</span>`;
for (let i = 0; i < moves.length; i += 2) { for (let i = 0; i < moves.length; i += 2) {
const num = Math.floor(i / 2) + 1; const num = Math.floor(i / 2) + 1;
html += `<span style="color:var(--text-dim);font-size:10px;">${num}.</span>`; html += `<span style="color:var(--text-dim);font-size:var(--fs-xs);">${num}.</span>`;
html += `<span class="move-chip" data-idx="${i + 1}">${moves[i]?.san || ''}</span>`; html += `<span class="move-chip" data-idx="${i + 1}">${moves[i]?.san || ''}</span>`;
if (moves[i + 1]) { if (moves[i + 1]) {
html += `<span class="move-chip" data-idx="${i + 2}">${moves[i + 1]?.san || ''}</span>`; html += `<span class="move-chip" data-idx="${i + 2}">${moves[i + 1]?.san || ''}</span>`;
...@@ -177,11 +177,11 @@ async function analyzePosition(el, fen) { ...@@ -177,11 +177,11 @@ async function analyzePosition(el, fen) {
`<div style="display:flex;align-items:center;gap:6px;padding:2px 0;"> `<div style="display:flex;align-items:center;gap:6px;padding:2px 0;">
<span style="font-size:12px;font-weight:600;color:var(--text-primary);min-width:35px;font-family:Inter,monospace;">${e.move}</span> <span style="font-size:12px;font-weight:600;color:var(--text-primary);min-width:35px;font-family:Inter,monospace;">${e.move}</span>
${renderExplorerBar(e.white, e.draw, e.black)} ${renderExplorerBar(e.white, e.draw, e.black)}
<span style="font-size:9px;color:var(--text-muted);">${e.games}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);">${e.games}</span>
</div>` </div>`
).join(''); ).join('');
linesContainer.innerHTML = `<div style="border-bottom:1px solid rgba(255,255,255,0.05);padding-bottom:4px;margin-bottom:4px;"> linesContainer.innerHTML = `<div style="border-bottom:1px solid rgba(255,255,255,0.05);padding-bottom:4px;margin-bottom:4px;">
<div style="font-size:9px;color:var(--text-muted);margin-bottom:2px;">${emoji('book', '📖', 9)} ${t('analysis.openings_book')}</div>${explorerHtml}</div> <div style="font-size:var(--fs-xs);color:var(--text-muted);margin-bottom:2px;">${emoji('book', '📖', 9)} ${t('analysis.openings_book')}</div>${explorerHtml}</div>
<div style="color:var(--text-muted);font-size:11px;text-align:center;">${t('analysis.engine_analyzing')}</div>`; <div style="color:var(--text-muted);font-size:11px;text-align:center;">${t('analysis.engine_analyzing')}</div>`;
} }
...@@ -204,12 +204,12 @@ function renderAnalysisLines(el, lines, fen, explorerData) { ...@@ -204,12 +204,12 @@ function renderAnalysisLines(el, lines, fen, explorerData) {
let explorerHtml = ''; let explorerHtml = '';
if (explorerData) { if (explorerData) {
explorerHtml = `<div style="border-bottom:1px solid rgba(255,255,255,0.05);padding-bottom:4px;margin-bottom:4px;"> explorerHtml = `<div style="border-bottom:1px solid rgba(255,255,255,0.05);padding-bottom:4px;margin-bottom:4px;">
<div style="font-size:9px;color:var(--text-muted);margin-bottom:2px;">${emoji('book', '📖', 9)} ${t('analysis.openings_book')}</div> <div style="font-size:var(--fs-xs);color:var(--text-muted);margin-bottom:2px;">${emoji('book', '📖', 9)} ${t('analysis.openings_book')}</div>
${explorerData.slice(0, 3).map(e => ${explorerData.slice(0, 3).map(e =>
`<div style="display:flex;align-items:center;gap:6px;padding:1px 0;"> `<div style="display:flex;align-items:center;gap:6px;padding:1px 0;">
<span style="font-size:11px;font-weight:600;color:var(--text-primary);min-width:32px;font-family:Inter,monospace;">${e.move}</span> <span style="font-size:11px;font-weight:600;color:var(--text-primary);min-width:32px;font-family:Inter,monospace;">${e.move}</span>
${renderExplorerBar(e.white, e.draw, e.black)} ${renderExplorerBar(e.white, e.draw, e.black)}
<span style="font-size:9px;color:var(--text-muted);">${e.games}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);">${e.games}</span>
</div>` </div>`
).join('')} ).join('')}
</div>`; </div>`;
......
...@@ -101,7 +101,7 @@ export function mountGame(el, params) { ...@@ -101,7 +101,7 @@ export function mountGame(el, params) {
<div> <div>
<div style="font-size:13px;font-weight:600;color:var(--text-primary);" id="opponent-name">${mode === 'bot' ? (botId || t('game.bot')) : t('game.loading_opponent')}</div> <div style="font-size:13px;font-weight:600;color:var(--text-primary);" id="opponent-name">${mode === 'bot' ? (botId || t('game.bot')) : t('game.loading_opponent')}</div>
<div style="display:flex;gap:6px;align-items:center;"> <div style="display:flex;gap:6px;align-items:center;">
<div id="opponent-level" style="font-size:10px;color:var(--text-muted);">${mode === 'bot' ? t('game.bot') : ''}</div> <div id="opponent-level" style="font-size:var(--fs-xs);color:var(--text-muted);">${mode === 'bot' ? t('game.bot') : ''}</div>
<div id="opponent-captured" style="font-size:11px;color:var(--text-secondary);letter-spacing:1px;"></div> <div id="opponent-captured" style="font-size:11px;color:var(--text-secondary);letter-spacing:1px;"></div>
</div> </div>
</div> </div>
...@@ -126,7 +126,7 @@ export function mountGame(el, params) { ...@@ -126,7 +126,7 @@ export function mountGame(el, params) {
<div> <div>
<div style="font-size:13px;font-weight:600;color:var(--text-primary);">${store.get('player.display_name') || store.get('player.username') || 'You'}</div> <div style="font-size:13px;font-weight:600;color:var(--text-primary);">${store.get('player.display_name') || store.get('player.username') || 'You'}</div>
<div style="display:flex;gap:6px;align-items:center;"> <div style="display:flex;gap:6px;align-items:center;">
<span style="font-size:10px;color:var(--text-muted);">Lv.${store.get('player.level') || 1}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);">Lv.${store.get('player.level') || 1}</span>
<div id="player-captured" style="font-size:11px;color:var(--text-secondary);letter-spacing:1px;"></div> <div id="player-captured" style="font-size:11px;color:var(--text-secondary);letter-spacing:1px;"></div>
</div> </div>
</div> </div>
...@@ -135,14 +135,16 @@ export function mountGame(el, params) { ...@@ -135,14 +135,16 @@ export function mountGame(el, params) {
<div id="clock-player" class="chess-clock active">${clock.format(tc.time)}</div> <div id="clock-player" class="chess-clock active">${clock.format(tc.time)}</div>
</div> </div>
<!-- Opening Name + Material --> <!-- Opening name, material, and the move list.
<div style="display:flex;justify-content:space-between;align-items:center;padding:2px 14px;"> This block is empty until the first move, and an empty block used to
<div id="opening-name" style="font-size:11px;color:var(--text-muted);font-style:italic;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%;"></div> leave a stray "1." floating in a gap above the controls. It collapses
<div id="material-diff" style="font-size:12px;font-weight:700;font-family:'SF Mono',monospace;color:var(--gold);"></div> to nothing until there is something to show. -->
</div> <div id="game-meta" class="chess-meta" hidden>
<!-- Move List --> <div class="chess-meta-row">
<div id="move-list" style="max-height:40px;overflow-x:auto;white-space:nowrap;padding:4px 14px;font-family:'SF Mono',monospace;font-size:12px;color:var(--text-secondary);display:flex;gap:4px;align-items:center;"> <div id="opening-name" class="chess-opening"></div>
<span style="color:var(--text-dim);">1.</span> <div id="material-diff" class="chess-material"></div>
</div>
<div id="move-list" class="chess-moves"></div>
</div> </div>
<!-- Controls --> <!-- Controls -->
...@@ -154,6 +156,12 @@ export function mountGame(el, params) { ...@@ -154,6 +156,12 @@ export function mountGame(el, params) {
</div> </div>
<style> <style>
.chess-layout { gap: 0; } .chess-layout { gap: 0; }
.chess-meta { display:flex;flex-direction:column;gap:var(--s-1);padding:var(--s-1) var(--s-4); }
.chess-meta[hidden] { display:none; }
.chess-meta-row { display:flex;justify-content:space-between;align-items:center;gap:var(--s-2); }
.chess-opening { font-size:var(--fs-xs);color:var(--text-muted);font-style:italic;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-inline-size:70%; }
.chess-material { font-size:var(--fs-sm);font-weight:700;font-family:var(--font-mono);color:var(--gold); }
.chess-moves { max-block-size:40px;overflow-x:auto;white-space:nowrap;font-family:var(--font-mono);font-size:var(--fs-sm);color:var(--text-secondary);display:flex;gap:var(--s-1);align-items:center; }
.chess-clock { font-size:18px;font-weight:700;font-family:'SF Mono',Inter,monospace;background:var(--bg-card);padding:6px 14px;border-radius:8px;color:var(--text-secondary);min-width:64px;text-align:center;border:1px solid var(--border); } .chess-clock { font-size:18px;font-weight:700;font-family:'SF Mono',Inter,monospace;background:var(--bg-card);padding:6px 14px;border-radius:8px;color:var(--text-secondary);min-width:64px;text-align:center;border:1px solid var(--border); }
.chess-clock.active { color:var(--text-primary);border-color:var(--border-hover); } .chess-clock.active { color:var(--text-primary);border-color:var(--border-hover); }
.chess-clock.low-time { color:var(--error)!important;animation:clockPulse 1s infinite;border-color:rgba(239,68,68,0.3); } .chess-clock.low-time { color:var(--error)!important;animation:clockPulse 1s infinite;border-color:rgba(239,68,68,0.3); }
...@@ -595,6 +603,10 @@ function updateMoveList(el, m) { ...@@ -595,6 +603,10 @@ function updateMoveList(el, m) {
moveList.innerHTML = html; moveList.innerHTML = html;
moveList.scrollLeft = moveList.scrollWidth; moveList.scrollLeft = moveList.scrollWidth;
// There is something to show now, so reveal the block.
const meta = el.querySelector('#game-meta');
if (meta) meta.hidden = false;
// Update opening name // Update opening name
const openingEl = el.querySelector('#opening-name'); const openingEl = el.querySelector('#opening-name');
if (openingEl && history.length <= 12) { if (openingEl && history.length <= 12) {
......
...@@ -70,9 +70,9 @@ function renderHistory(el, matches) { ...@@ -70,9 +70,9 @@ function renderHistory(el, matches) {
<span style="font-size:12px;font-weight:700;color:${cfg.color};font-family:Inter,monospace;">${ratingStr}</span> <span style="font-size:12px;font-weight:700;color:${cfg.color};font-family:Inter,monospace;">${ratingStr}</span>
</div> </div>
<div style="display:flex;gap:8px;margin-top:3px;"> <div style="display:flex;gap:8px;margin-top:3px;">
<span style="font-size:10px;color:var(--text-muted);">${cfg.label}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);">${cfg.label}</span>
<span style="font-size:10px;color:var(--text-dim);">${tc}</span> <span style="font-size:var(--fs-xs);color:var(--text-dim);">${tc}</span>
<span style="font-size:10px;color:var(--text-dim);">${timeAgo}</span> <span style="font-size:var(--fs-xs);color:var(--text-dim);">${timeAgo}</span>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -17,7 +17,7 @@ export async function mountReview(el, params) { ...@@ -17,7 +17,7 @@ export async function mountReview(el, params) {
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-card);overflow-y:auto;"> <div style="display:flex;flex-direction:column;height:100%;background:var(--bg-card);overflow-y:auto;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);"> <div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);">
<button class="btn btn-secondary" id="back-btn" style="min-height:30px;padding:4px 12px;font-size:12px;">← ${t('game.back')}</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">← ${t('game.back')}</button>
<span style="font-size:15px;font-weight:700;color:var(--text-primary);">${emoji('star', '⭐', 15)} ${t('review.title')}</span> <span style="font-size:15px;font-weight:700;color:var(--text-primary);">${emoji('star', '⭐', 15)} ${t('review.title')}</span>
<div style="width:50px;"></div> <div style="width:50px;"></div>
</div> </div>
......
...@@ -259,7 +259,7 @@ function injectStyles(el) { ...@@ -259,7 +259,7 @@ function injectStyles(el) {
.dg-score-me, .dg-score-opp { .dg-score-me, .dg-score-opp {
display:flex;flex-direction:column;align-items:center;gap:1px; display:flex;flex-direction:column;align-items:center;gap:1px;
} }
.dg-score-label { font-size:10px;color:var(--text-muted);font-weight:500; } .dg-score-label { font-size:var(--fs-xs);color:var(--text-muted);font-weight:500; }
.dg-score-me .dg-score-value { font-size:16px;font-weight:800;color:var(--green-light); } .dg-score-me .dg-score-value { font-size:16px;font-weight:800;color:var(--green-light); }
.dg-score-opp .dg-score-value { font-size:16px;font-weight:800;color:var(--text-secondary); } .dg-score-opp .dg-score-value { font-size:16px;font-weight:800;color:var(--text-secondary); }
.dg-score-divider { width:1px;height:24px;background:var(--border); } .dg-score-divider { width:1px;height:24px;background:var(--border); }
...@@ -293,7 +293,7 @@ function injectStyles(el) { ...@@ -293,7 +293,7 @@ function injectStyles(el) {
border-top:1px solid rgba(255,255,255,0.03); border-top:1px solid rgba(255,255,255,0.03);
} }
.dg-ctrl-btn { .dg-ctrl-btn {
min-height:40px;border-radius:12px;border:none; min-height:var(--tap-min);border-radius:12px;border:none;
font-size:12px;font-weight:600;cursor:pointer; font-size:12px;font-weight:600;cursor:pointer;
transition:transform 0.15s cubic-bezier(0.34,1.56,0.64,1), opacity 0.2s; transition:transform 0.15s cubic-bezier(0.34,1.56,0.64,1), opacity 0.2s;
} }
......
...@@ -58,7 +58,7 @@ function renderPanel(p) { ...@@ -58,7 +58,7 @@ function renderPanel(p) {
</div> </div>
<div style="display:flex;flex-direction:column;"> <div style="display:flex;flex-direction:column;">
<span class="pp-name" style="font-size:11px;font-weight:700;color:var(--text-primary);">${p.name}</span> <span class="pp-name" style="font-size:11px;font-weight:700;color:var(--text-primary);">${p.name}</span>
<span class="pp-status" style="font-size:9px;color:var(--text-muted);">${p.level || ''}</span> <span class="pp-status" style="font-size:var(--fs-xs);color:var(--text-muted);">${p.level || ''}</span>
</div> </div>
<div class="pp-dice" id="dice-${p.i}"></div> <div class="pp-dice" id="dice-${p.i}"></div>
</div> </div>
...@@ -146,7 +146,7 @@ export function mountGame(el, params) { ...@@ -146,7 +146,7 @@ export function mountGame(el, params) {
<style> <style>
.pp{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:10px;border:2px solid transparent;transition:all 0.3s cubic-bezier(0.34,1.56,0.64,1);position:relative;} .pp{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:10px;border:2px solid transparent;transition:all 0.3s cubic-bezier(0.34,1.56,0.64,1);position:relative;}
.pp.active{border-color:var(--pc);background:rgba(255,255,255,0.06);animation:panelPulse 2s ease-in-out infinite;box-shadow:0 0 12px color-mix(in srgb, var(--pc) 20%, transparent);} .pp.active{border-color:var(--pc);background:rgba(255,255,255,0.06);animation:panelPulse 2s ease-in-out infinite;box-shadow:0 0 12px color-mix(in srgb, var(--pc) 20%, transparent);}
.pp-turn-arrow{position:absolute;left:-2px;top:50%;transform:translateY(-50%) scale(0);font-size:10px;color:var(--pc);transition:transform 0.3s cubic-bezier(0.34,1.56,0.64,1);filter:drop-shadow(0 0 4px var(--pc));} .pp-turn-arrow{position:absolute;left:-2px;top:50%;transform:translateY(-50%) scale(0);font-size:var(--fs-xs);color:var(--pc);transition:transform 0.3s cubic-bezier(0.34,1.56,0.64,1);filter:drop-shadow(0 0 4px var(--pc));}
.pp.active .pp-turn-arrow{transform:translateY(-50%) scale(1);animation:arrowBounce 1s ease-in-out infinite;} .pp.active .pp-turn-arrow{transform:translateY(-50%) scale(1);animation:arrowBounce 1s ease-in-out infinite;}
.pp-name{transition:color 0.3s,text-shadow 0.3s;} .pp-name{transition:color 0.3s,text-shadow 0.3s;}
.pp.active .pp-name{color:#fff !important;text-shadow:0 0 8px color-mix(in srgb, var(--pc) 50%, transparent);} .pp.active .pp-name{color:#fff !important;text-shadow:0 0 8px color-mix(in srgb, var(--pc) 50%, transparent);}
......
...@@ -373,7 +373,7 @@ function getStyles() { ...@@ -373,7 +373,7 @@ function getStyles() {
} }
.lr-seat-active .lr-seat-dot { background:color-mix(in srgb, var(--seat-color) 20%, transparent); } .lr-seat-active .lr-seat-dot { background:color-mix(in srgb, var(--seat-color) 20%, transparent); }
.lr-seat-empty .lr-seat-dot { opacity:0.2;border-style:dashed; } .lr-seat-empty .lr-seat-dot { opacity:0.2;border-style:dashed; }
.lr-seat-label { font-size:9px;color:var(--text-muted); } .lr-seat-label { font-size:var(--fs-xs);color:var(--text-muted); }
.lr-seat-active .lr-seat-label { color:var(--seat-color); } .lr-seat-active .lr-seat-label { color:var(--seat-color); }
.lr-pulse-ring { .lr-pulse-ring {
......
...@@ -39,7 +39,7 @@ function renderOrgs(el, orgs) { ...@@ -39,7 +39,7 @@ function renderOrgs(el, orgs) {
<div style="font-size:14px;font-weight:600;">${org.name_ar || org.name}</div> <div style="font-size:14px;font-weight:600;">${org.name_ar || org.name}</div>
<div style="font-size:11px;color:var(--text-secondary);">${emoji('people', '👥', 11)} ${org.member_count || 0} ${t('profile.member')}</div> <div style="font-size:11px;color:var(--text-secondary);">${emoji('people', '👥', 11)} ${org.member_count || 0} ${t('profile.member')}</div>
</div> </div>
<button class="btn btn-secondary" style="font-size:11px;min-height:32px;padding:var(--s-1) var(--s-3);">${t('org.join_btn')}</button> <button class="btn btn-secondary" style="font-size:11px;min-height:var(--tap-min);padding:var(--s-1) var(--s-3);">${t('org.join_btn')}</button>
</div> </div>
`).join(''); `).join('');
......
...@@ -59,7 +59,7 @@ function renderOrg(el, org) { ...@@ -59,7 +59,7 @@ function renderOrg(el, org) {
<div style="display:flex;align-items:center;gap:var(--s-2);padding:var(--s-1) 0;"> <div style="display:flex;align-items:center;gap:var(--s-2);padding:var(--s-1) 0;">
<div style="width:24px;height:24px;border-radius:50%;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;font-size:12px;">${emoji('person', '👤', 12)}</div> <div style="width:24px;height:24px;border-radius:50%;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;font-size:12px;">${emoji('person', '👤', 12)}</div>
<span style="font-size:13px;">${m.user_id?.substring(0, 8) || 'Member'}</span> <span style="font-size:13px;">${m.user_id?.substring(0, 8) || 'Member'}</span>
<span style="font-size:10px;color:var(--text-muted);margin-right:auto;">${m.role}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);margin-right:auto;">${m.role}</span>
</div> </div>
`).join('')} `).join('')}
</div> </div>
......
...@@ -47,8 +47,8 @@ export function mountTable(el) { ...@@ -47,8 +47,8 @@ export function mountTable(el) {
<span class="qb-icon" style="background:linear-gradient(135deg,#854d0e,#ca8a04);">${emoji('trophy', '🏆', 20)}</span> <span class="qb-icon" style="background:linear-gradient(135deg,#854d0e,#ca8a04);">${emoji('trophy', '🏆', 20)}</span>
<span class="qb-label">${t('play.achievements')}</span> <span class="qb-label">${t('play.achievements')}</span>
</button> </button>
<button class="quick-btn breathe-glow" id="btn-daily-reward"> <button class="quick-btn" id="btn-daily-reward">
<span class="qb-icon" style="background:linear-gradient(135deg,#92400e,var(--gold));">${emoji('gift', '🎁', 20)}</span> <span class="qb-icon breathe-glow" style="background:linear-gradient(135deg,#92400e,var(--gold));">${emoji('gift', '🎁', 20)}</span>
<span class="qb-label">${t('play.gift')}</span> <span class="qb-label">${t('play.gift')}</span>
</button> </button>
</div> </div>
...@@ -75,9 +75,15 @@ export function mountTable(el) { ...@@ -75,9 +75,15 @@ export function mountTable(el) {
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
height: 100%; height: 100%;
padding: var(--home-padding) var(--home-padding) 0; padding: var(--home-padding);
overflow-y: auto; overflow-y: auto;
} }
/* Centre the content when the screen is taller than it needs, instead of
leaving a block of dead space above the tab bar. The auto margins do
this without the clipping that justify-content:center causes once the
content is taller than the scroll container. */
.play-home > :first-child { margin-block-start: auto; }
.play-home > :last-child { margin-block-end: auto; }
.quick-btn { display:flex;flex-direction:column;align-items:center;gap:var(--quick-btn-gap);flex:1;background:none;border:none;cursor:pointer;padding:var(--quick-btn-padding);transition:transform 0.1s; } .quick-btn { display:flex;flex-direction:column;align-items:center;gap:var(--quick-btn-gap);flex:1;background:none;border:none;cursor:pointer;padding:var(--quick-btn-padding);transition:transform 0.1s; }
.quick-btn:active { transform:scale(0.9); } .quick-btn:active { transform:scale(0.9); }
.qb-icon { width:var(--quick-btn-icon-size);height:var(--quick-btn-icon-size);border-radius:var(--quick-btn-icon-radius);display:flex;align-items:center;justify-content:center;font-size:var(--quick-btn-icon-font);box-shadow:0 3px 10px rgba(0,0,0,0.3); } .qb-icon { width:var(--quick-btn-icon-size);height:var(--quick-btn-icon-size);border-radius:var(--quick-btn-icon-radius);display:flex;align-items:center;justify-content:center;font-size:var(--quick-btn-icon-font);box-shadow:0 3px 10px rgba(0,0,0,0.3); }
......
...@@ -61,7 +61,7 @@ export function mountTimeSelect(el, params) { ...@@ -61,7 +61,7 @@ export function mountTimeSelect(el, params) {
${cat.controls.map(tc => ` ${cat.controls.map(tc => `
<button class="time-btn" data-key="${tc.key}" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;height:58px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:transform 0.1s,background 0.15s,border-color 0.15s;"> <button class="time-btn" data-key="${tc.key}" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;height:58px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:transform 0.1s,background 0.15s,border-color 0.15s;">
<span style="font-size:14px;font-weight:700;color:var(--text-primary);font-family:var(--font-lat);">${tc.sub}</span> <span style="font-size:14px;font-weight:700;color:var(--text-primary);font-family:var(--font-lat);">${tc.sub}</span>
<span style="font-size:10px;color:var(--text-secondary);">${tc.labelKey ? t(tc.labelKey) : tc.label}</span> <span style="font-size:var(--fs-xs);color:var(--text-secondary);">${tc.labelKey ? t(tc.labelKey) : tc.label}</span>
</button> </button>
`).join('')} `).join('')}
</div> </div>
......
...@@ -42,7 +42,7 @@ async function loadBlockedList(el) { ...@@ -42,7 +42,7 @@ async function loadBlockedList(el) {
<div style="font-size:14px;font-weight:600;color:var(--text-primary);">${item.profile?.display_name || 'Player'}</div> <div style="font-size:14px;font-weight:600;color:var(--text-primary);">${item.profile?.display_name || 'Player'}</div>
<div style="font-size:12px;color:var(--text-secondary);">${item.type === 'mute' ? t('block.muted') : t('block.blocked')}</div> <div style="font-size:12px;color:var(--text-secondary);">${item.type === 'mute' ? t('block.muted') : t('block.blocked')}</div>
</div> </div>
<button class="btn btn-secondary unblock-btn" data-id="${item.blocked_id}" data-type="${item.type}" style="min-height:32px;padding:4px 12px;font-size:12px;"> <button class="btn btn-secondary unblock-btn" data-id="${item.blocked_id}" data-type="${item.type}" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">
${item.type === 'mute' ? t('block.unmute') : t('block.unblock')} ${item.type === 'mute' ? t('block.unmute') : t('block.unblock')}
</button> </button>
</div> </div>
......
...@@ -48,7 +48,7 @@ export async function mountEdit(el) { ...@@ -48,7 +48,7 @@ export async function mountEdit(el) {
<div style="padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-4);padding-bottom:max(var(--s-4), env(safe-area-inset-bottom));"> <div style="padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-4);padding-bottom:max(var(--s-4), env(safe-area-inset-bottom));">
<!-- Header --> <!-- Header -->
<div style="display:flex;align-items:center;gap:var(--s-3);"> <div style="display:flex;align-items:center;gap:var(--s-3);">
<button id="btn-back" class="btn btn-secondary" style="min-width:40px;min-height:40px;padding:0;display:flex;align-items:center;justify-content:center;"> <button id="btn-back" class="btn btn-secondary" style="min-width:40px;min-height:var(--tap-min);padding:0;display:flex;align-items:center;justify-content:center;">
${emoji('arrow_back', '←', 18)} ${emoji('arrow_back', '←', 18)}
</button> </button>
<div style="font-size:18px;font-weight:700;">${t('profile.edit_title')}</div> <div style="font-size:18px;font-weight:700;">${t('profile.edit_title')}</div>
...@@ -103,8 +103,8 @@ export async function mountEdit(el) { ...@@ -103,8 +103,8 @@ export async function mountEdit(el) {
<div> <div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.preferred_language')}</label> <label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.preferred_language')}</label>
<div id="lang-toggle" style="display:flex;gap:var(--s-2);"> <div id="lang-toggle" style="display:flex;gap:var(--s-2);">
<button class="btn lang-opt ${(player.preferred_language || 'ar') === 'ar' ? 'btn-primary' : 'btn-secondary'}" data-lang="ar" style="flex:1;min-height:40px;">العربية</button> <button class="btn lang-opt ${(player.preferred_language || 'ar') === 'ar' ? 'btn-primary' : 'btn-secondary'}" data-lang="ar" style="flex:1;min-height:var(--tap-min);">العربية</button>
<button class="btn lang-opt ${player.preferred_language === 'en' ? 'btn-primary' : 'btn-secondary'}" data-lang="en" style="flex:1;min-height:40px;">English</button> <button class="btn lang-opt ${player.preferred_language === 'en' ? 'btn-primary' : 'btn-secondary'}" data-lang="en" style="flex:1;min-height:var(--tap-min);">English</button>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -17,7 +17,7 @@ export async function mountOrgApply(el) { ...@@ -17,7 +17,7 @@ export async function mountOrgApply(el) {
el.innerHTML = ` el.innerHTML = `
<div style="padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-4);padding-bottom:max(var(--s-4), env(safe-area-inset-bottom));"> <div style="padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-4);padding-bottom:max(var(--s-4), env(safe-area-inset-bottom));">
<div style="display:flex;align-items:center;gap:var(--s-3);"> <div style="display:flex;align-items:center;gap:var(--s-3);">
<button id="btn-back" class="btn btn-secondary" style="min-width:40px;min-height:40px;padding:0;display:flex;align-items:center;justify-content:center;"> <button id="btn-back" class="btn btn-secondary" style="min-width:40px;min-height:var(--tap-min);padding:0;display:flex;align-items:center;justify-content:center;">
${emoji('arrow_back', '←', 18)} ${emoji('arrow_back', '←', 18)}
</button> </button>
<div style="font-size:18px;font-weight:700;">${t('org.join_title')}</div> <div style="font-size:18px;font-weight:700;">${t('org.join_title')}</div>
...@@ -79,9 +79,9 @@ async function loadOrgs(el) { ...@@ -79,9 +79,9 @@ async function loadOrgs(el) {
if (app.rejection_reason) { if (app.rejection_reason) {
badge += `<div style="font-size:11px;color:var(--error);margin-top:4px;">${t('profile.rejection_reason', { reason: escHtml(app.rejection_reason) })}</div>`; badge += `<div style="font-size:11px;color:var(--error);margin-top:4px;">${t('profile.rejection_reason', { reason: escHtml(app.rejection_reason) })}</div>`;
} }
actionBtn = `<button class="btn btn-primary btn-apply" data-org-id="${org.id}" style="min-height:36px;font-size:13px;margin-top:var(--s-2);">${t('org.reapply')}</button>`; actionBtn = `<button class="btn btn-primary btn-apply" data-org-id="${org.id}" style="min-height:var(--tap-min);font-size:13px;margin-top:var(--s-2);">${t('org.reapply')}</button>`;
} else { } else {
actionBtn = `<button class="btn btn-primary btn-apply" data-org-id="${org.id}" style="min-height:36px;font-size:13px;margin-top:var(--s-2);">${t('org.apply')}</button>`; actionBtn = `<button class="btn btn-primary btn-apply" data-org-id="${org.id}" style="min-height:var(--tap-min);font-size:13px;margin-top:var(--s-2);">${t('org.apply')}</button>`;
} }
const logoHtml = org.logo_url const logoHtml = org.logo_url
......
...@@ -20,11 +20,11 @@ export function mountSettings(el) { ...@@ -20,11 +20,11 @@ export function mountSettings(el) {
<div class="card" style="display:flex;flex-direction:column;gap:var(--s-4);"> <div class="card" style="display:flex;flex-direction:column;gap:var(--s-4);">
<div style="display:flex;justify-content:space-between;align-items:center;"> <div style="display:flex;justify-content:space-between;align-items:center;">
<span>${t('settings.sound')}</span> <span>${t('settings.sound')}</span>
<button class="btn btn-secondary" id="toggle-audio" style="min-height:36px;padding:var(--s-1) var(--s-3);">${audioOn ? emoji('speaker_on', '🔊', 16) + ' ' + t('settings.on') : emoji('speaker_off', '🔇', 16) + ' ' + t('settings.off')}</button> <button class="btn btn-secondary" id="toggle-audio" style="min-height:var(--tap-min);padding:var(--s-1) var(--s-3);">${audioOn ? emoji('speaker_on', '🔊', 16) + ' ' + t('settings.on') : emoji('speaker_off', '🔇', 16) + ' ' + t('settings.off')}</button>
</div> </div>
<div style="display:flex;justify-content:space-between;align-items:center;"> <div style="display:flex;justify-content:space-between;align-items:center;">
<span>${t('settings.language')}</span> <span>${t('settings.language')}</span>
<button class="btn btn-secondary" id="toggle-lang" style="min-height:36px;padding:var(--s-1) var(--s-3);">${lang === 'ar' ? 'العربية' : 'English'}</button> <button class="btn btn-secondary" id="toggle-lang" style="min-height:var(--tap-min);padding:var(--s-1) var(--s-3);">${lang === 'ar' ? 'العربية' : 'English'}</button>
</div> </div>
</div> </div>
......
...@@ -344,7 +344,7 @@ async function checkActiveMatch(el, playerId) { ...@@ -344,7 +344,7 @@ async function checkActiveMatch(el, playerId) {
<div style="flex:1;"> <div style="flex:1;">
<div style="font-size:13px;font-weight:600;color:var(--success);">${t('profile.playing_now', { game: gameLabel })}</div> <div style="font-size:13px;font-weight:600;color:var(--success);">${t('profile.playing_now', { game: gameLabel })}</div>
</div> </div>
<button class="btn btn-primary" id="btn-spectate" style="min-height:32px;padding:4px 14px;font-size:12px;">${emoji('eye', '👁', 13)} ${t('profile.watch')}</button> <button class="btn btn-primary" id="btn-spectate" style="min-height:var(--tap-min);padding:4px 14px;font-size:12px;">${emoji('eye', '👁', 13)} ${t('profile.watch')}</button>
</div> </div>
<style>@keyframes specPulse{0%,100%{opacity:1}50%{opacity:0.3}}</style> <style>@keyframes specPulse{0%,100%{opacity:1}50%{opacity:0.3}}</style>
`; `;
...@@ -412,7 +412,7 @@ async function loadOrgMembership(el) { ...@@ -412,7 +412,7 @@ async function loadOrgMembership(el) {
<div style="font-size:13px;font-weight:600;color:var(--text-primary);">${org ? org.name : t('org.title')}</div> <div style="font-size:13px;font-weight:600;color:var(--text-primary);">${org ? org.name : t('org.title')}</div>
<div style="font-size:11px;color:var(--text-secondary);">${m.role || t('profile.member')}</div> <div style="font-size:11px;color:var(--text-secondary);">${m.role || t('profile.member')}</div>
</div> </div>
<span style="background:var(--success);color:#fff;font-size:10px;padding:2px 6px;border-radius:10px;">${t('profile.member')}</span> <span style="background:var(--success);color:#fff;font-size:var(--fs-xs);padding:2px 6px;border-radius:10px;">${t('profile.member')}</span>
</div> </div>
`; `;
}).join(''); }).join('');
...@@ -428,8 +428,8 @@ async function loadOrgMembership(el) { ...@@ -428,8 +428,8 @@ async function loadOrgMembership(el) {
? `<img src="${org.logo_url}" style="width:28px;height:28px;border-radius:6px;object-fit:contain;" alt="">` ? `<img src="${org.logo_url}" style="width:28px;height:28px;border-radius:6px;object-fit:contain;" alt="">`
: `<div style="width:28px;height:28px;border-radius:6px;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;">${emoji('building', '🏢', 14)}</div>`; : `<div style="width:28px;height:28px;border-radius:6px;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;">${emoji('building', '🏢', 14)}</div>`;
const statusBadge = a.status === 'pending' const statusBadge = a.status === 'pending'
? `<span style="background:var(--warning);color:#000;font-size:10px;padding:2px 6px;border-radius:10px;">${t('profile.pending_review')}</span>` ? `<span style="background:var(--warning);color:#000;font-size:var(--fs-xs);padding:2px 6px;border-radius:10px;">${t('profile.pending_review')}</span>`
: `<span style="background:var(--error);color:#fff;font-size:10px;padding:2px 6px;border-radius:10px;">${t('profile.rejected')}</span>`; : `<span style="background:var(--error);color:#fff;font-size:var(--fs-xs);padding:2px 6px;border-radius:10px;">${t('profile.rejected')}</span>`;
return ` return `
<div style="display:flex;align-items:center;gap:var(--s-2);padding:var(--s-2) 0;"> <div style="display:flex;align-items:center;gap:var(--s-2);padding:var(--s-2) 0;">
${logoHtml} ${logoHtml}
......
...@@ -15,7 +15,7 @@ export async function mountPuzzle(el) { ...@@ -15,7 +15,7 @@ export async function mountPuzzle(el) {
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);"> <div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);">
<div style="display:flex;align-items:center;justify-content:space-between;padding:var(--s-2) var(--s-3);background:var(--bg-base);"> <div style="display:flex;align-items:center;justify-content:space-between;padding:var(--s-2) var(--s-3);background:var(--bg-base);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:var(--s-1) var(--s-3);font-size:12px;">${t('game.back')}</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:var(--s-1) var(--s-3);font-size:12px;">${t('game.back')}</button>
<span style="font-size:14px;font-weight:600;">${t('puzzle.title')}</span> <span style="font-size:14px;font-weight:600;">${t('puzzle.title')}</span>
<span id="puzzle-rating" style="font-size:13px;color:var(--gold);font-family:var(--font-lat);"></span> <span id="puzzle-rating" style="font-size:13px;color:var(--gold);font-family:var(--font-lat);"></span>
</div> </div>
......
...@@ -9,12 +9,12 @@ export async function mountLeaderboard(el) { ...@@ -9,12 +9,12 @@ export async function mountLeaderboard(el) {
<div style="padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-4);"> <div style="padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-4);">
<div style="display:flex;justify-content:space-between;align-items:center;"> <div style="display:flex;justify-content:space-between;align-items:center;">
<h2 style="font-size:20px;font-weight:700;">${t('rank.leaderboard')}</h2> <h2 style="font-size:20px;font-weight:700;">${t('rank.leaderboard')}</h2>
<button class="btn btn-secondary" id="btn-tournaments" style="font-size:12px;min-height:32px;">${t('rank.tournaments')}</button> <button class="btn btn-secondary" id="btn-tournaments" style="font-size:12px;min-height:var(--tap-min);">${t('rank.tournaments')}</button>
</div> </div>
<div style="display:flex;gap:var(--s-2);overflow-x:auto;" id="game-tabs"> <div style="display:flex;gap:var(--s-2);overflow-x:auto;" id="game-tabs">
<button class="btn btn-secondary tab-active" data-game="chess" style="font-size:12px;min-height:32px;">${emoji('chess_pawn', '♟', 12)} ${t('game.chess')}</button> <button class="btn btn-secondary tab-active" data-game="chess" style="font-size:12px;min-height:var(--tap-min);">${emoji('chess_pawn', '♟', 12)} ${t('game.chess')}</button>
<button class="btn btn-secondary" data-game="domino" style="font-size:12px;min-height:32px;">${emoji('domino_tile', '⬚', 12)} ${t('game.domino')}</button> <button class="btn btn-secondary" data-game="domino" style="font-size:12px;min-height:var(--tap-min);">${emoji('domino_tile', '⬚', 12)} ${t('game.domino')}</button>
<button class="btn btn-secondary" data-game="ludo" style="font-size:12px;min-height:32px;">${emoji('ludo_hex', '⬡', 12)} ${t('game.ludo')}</button> <button class="btn btn-secondary" data-game="ludo" style="font-size:12px;min-height:var(--tap-min);">${emoji('ludo_hex', '⬡', 12)} ${t('game.ludo')}</button>
</div> </div>
<div id="leaderboard-list"> <div id="leaderboard-list">
<div class="skeleton" style="height:50px;margin-bottom:var(--s-2);"></div> <div class="skeleton" style="height:50px;margin-bottom:var(--s-2);"></div>
......
...@@ -18,7 +18,7 @@ export function mountTournamentArena(el, params) { ...@@ -18,7 +18,7 @@ export function mountTournamentArena(el, params) {
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);"> <div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);">
<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);"> <div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:15px;font-weight:700;color:var(--text-primary);">${emoji('lightning', '⚡', 15)} ${t('tournament.arena')}</span> <span style="font-size:15px;font-weight:700;color:var(--text-primary);">${emoji('lightning', '⚡', 15)} ${t('tournament.arena')}</span>
</div> </div>
<div id="arena-content" style="flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;align-items:center;gap:16px;"> <div id="arena-content" style="flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;align-items:center;gap:16px;">
...@@ -163,7 +163,7 @@ async function loadArenaStandings(el, tournamentId) { ...@@ -163,7 +163,7 @@ async function loadArenaStandings(el, tournamentId) {
<span style="width:24px;font-size:${i < 3 ? '14px' : '11px'};text-align:center;">${i < 3 ? medals[i] : (i + 1)}</span> <span style="width:24px;font-size:${i < 3 ? '14px' : '11px'};text-align:center;">${i < 3 ? medals[i] : (i + 1)}</span>
<span style="flex:1;font-size:12px;color:var(--text-primary);font-weight:${isMe ? '700' : '400'};">${p.name}${isMe ? ` (${t('common.you')})` : ''}</span> <span style="flex:1;font-size:12px;color:var(--text-primary);font-weight:${isMe ? '700' : '400'};">${p.name}${isMe ? ` (${t('common.you')})` : ''}</span>
<span style="font-size:12px;font-weight:700;color:var(--gold);">${p.points}</span> <span style="font-size:12px;font-weight:700;color:var(--gold);">${p.points}</span>
<span style="font-size:10px;color:var(--text-muted);margin-right:4px;width:50px;text-align:left;">${p.wins}W ${p.draws}D ${p.losses}L</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);margin-right:4px;width:50px;text-align:left;">${p.wins}W ${p.draws}D ${p.losses}L</span>
</div> </div>
`; `;
}).join('')} }).join('')}
......
...@@ -12,7 +12,7 @@ export async function mountTournamentBracket(el, params) { ...@@ -12,7 +12,7 @@ export async function mountTournamentBracket(el, params) {
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);"> <div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);">
<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);"> <div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:15px;font-weight:700;color:var(--text-primary);">${t('tournament.bracket')}</span> <span style="font-size:15px;font-weight:700;color:var(--text-primary);">${t('tournament.bracket')}</span>
</div> </div>
<div id="bracket-container" style="flex:1;overflow:auto;padding:12px;"> <div id="bracket-container" style="flex:1;overflow:auto;padding:12px;">
...@@ -57,17 +57,17 @@ export async function mountTournamentBracket(el, params) { ...@@ -57,17 +57,17 @@ export async function mountTournamentBracket(el, params) {
html += ` html += `
<div class="bracket-match" data-match-id="${m.match_id || ''}" style="background:${statusBg};border:1px solid ${borderColor};border-radius:8px;padding:6px 8px;position:relative;"> <div class="bracket-match" data-match-id="${m.match_id || ''}" style="background:${statusBg};border:1px solid ${borderColor};border-radius:8px;padding:6px 8px;position:relative;">
${isMyMatch ? `<div style="position:absolute;top:-6px;right:4px;font-size:9px;background:var(--gold);color:#000;padding:1px 4px;border-radius:4px;font-weight:700;">${t('common.you')}</div>` : ''} ${isMyMatch ? `<div style="position:absolute;top:-6px;right:4px;font-size:var(--fs-xs);background:var(--gold);color:#000;padding:1px 4px;border-radius:4px;font-weight:700;">${t('common.you')}</div>` : ''}
<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;${m.winner_id === m.player_a_id ? 'font-weight:700;' : ''}"> <div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;${m.winner_id === m.player_a_id ? 'font-weight:700;' : ''}">
<span style="font-size:11px;color:${m.winner_id === m.player_a_id ? 'var(--success)' : 'var(--text-primary)'};max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${m.player_a_name || (m.player_a_id ? '...' : 'BYE')}</span> <span style="font-size:11px;color:${m.winner_id === m.player_a_id ? 'var(--success)' : 'var(--text-primary)'};max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${m.player_a_name || (m.player_a_id ? '...' : 'BYE')}</span>
<span style="font-size:10px;color:var(--text-muted);">${m.result ? m.result.split('-')[0] : ''}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);">${m.result ? m.result.split('-')[0] : ''}</span>
</div> </div>
<div style="height:1px;background:var(--border);margin:2px 0;"></div> <div style="height:1px;background:var(--border);margin:2px 0;"></div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;${m.winner_id === m.player_b_id ? 'font-weight:700;' : ''}"> <div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;${m.winner_id === m.player_b_id ? 'font-weight:700;' : ''}">
<span style="font-size:11px;color:${m.winner_id === m.player_b_id ? 'var(--success)' : 'var(--text-primary)'};max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${m.player_b_name || (m.player_b_id ? '...' : 'TBD')}</span> <span style="font-size:11px;color:${m.winner_id === m.player_b_id ? 'var(--success)' : 'var(--text-primary)'};max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${m.player_b_name || (m.player_b_id ? '...' : 'TBD')}</span>
<span style="font-size:10px;color:var(--text-muted);">${m.result ? m.result.split('-')[1] : ''}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);">${m.result ? m.result.split('-')[1] : ''}</span>
</div> </div>
${m.status === 'pending' && isMyMatch && m.player_a_id && m.player_b_id ? `<button class="bracket-play-btn" data-mid="${m.match_id}" style="width:100%;margin-top:4px;padding:4px;background:var(--gold);border:none;border-radius:4px;color:#000;font-weight:700;font-size:10px;cursor:pointer;">${t('common.play')}</button>` : ''} ${m.status === 'pending' && isMyMatch && m.player_a_id && m.player_b_id ? `<button class="bracket-play-btn" data-mid="${m.match_id}" style="width:100%;margin-top:4px;padding:4px;background:var(--gold);border:none;border-radius:4px;color:#000;font-weight:700;font-size:var(--fs-xs);cursor:pointer;">${t('common.play')}</button>` : ''}
</div> </div>
`; `;
}); });
......
...@@ -15,7 +15,7 @@ export async function mountTournamentDetail(el, params) { ...@@ -15,7 +15,7 @@ export async function mountTournamentDetail(el, params) {
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);"> <div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);">
<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);"> <div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">←</button>
<span id="tour-title" style="font-size:15px;font-weight:700;color:var(--text-primary);flex:1;">${t('tournament.title')}</span> <span id="tour-title" style="font-size:15px;font-weight:700;color:var(--text-primary);flex:1;">${t('tournament.title')}</span>
</div> </div>
<!-- Tabs --> <!-- Tabs -->
......
...@@ -15,7 +15,7 @@ export function mountTournamentLive(el, params) { ...@@ -15,7 +15,7 @@ export function mountTournamentLive(el, params) {
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);"> <div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);">
<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);"> <div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-panel);border-bottom:1px solid var(--border);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:15px;font-weight:700;color:var(--text-primary);flex:1;">${emoji('live', '🔴', 12)} LIVE — ${tournamentName || t('tournament.title')}</span> <span style="font-size:15px;font-weight:700;color:var(--text-primary);flex:1;">${emoji('live', '🔴', 12)} LIVE — ${tournamentName || t('tournament.title')}</span>
</div> </div>
<div id="live-content" style="flex:1;overflow-y:auto;padding:14px;"></div> <div id="live-content" style="flex:1;overflow-y:auto;padding:14px;"></div>
...@@ -71,7 +71,7 @@ async function loadLiveData(el, tournamentId) { ...@@ -71,7 +71,7 @@ async function loadLiveData(el, tournamentId) {
<span style="flex:1;font-size:12px;color:var(--text-primary);text-align:right;">${p.white_name || p.player_a || '?'}</span> <span style="flex:1;font-size:12px;color:var(--text-primary);text-align:right;">${p.white_name || p.player_a || '?'}</span>
<span style="padding:2px 8px;font-size:11px;font-weight:700;color:${hasResult ? 'var(--success)' : 'var(--gold)'};">${p.result || 'vs'}</span> <span style="padding:2px 8px;font-size:11px;font-weight:700;color:${hasResult ? 'var(--success)' : 'var(--gold)'};">${p.result || 'vs'}</span>
<span style="flex:1;font-size:12px;color:var(--text-primary);text-align:left;">${p.black_name || p.player_b || '?'}</span> <span style="flex:1;font-size:12px;color:var(--text-primary);text-align:left;">${p.black_name || p.player_b || '?'}</span>
${!hasResult ? `<button class="spectate-btn" data-white="${p.player_a || p.white_id || ''}" data-black="${p.player_b || p.black_id || ''}" style="margin-right:4px;padding:2px 8px;background:var(--blue);border:none;border-radius:4px;color:#fff;font-size:10px;font-weight:600;cursor:pointer;">${t('spectate.watch')}</button>` : ''} ${!hasResult ? `<button class="spectate-btn" data-white="${p.player_a || p.white_id || ''}" data-black="${p.player_b || p.black_id || ''}" style="margin-right:4px;padding:2px 8px;background:var(--blue);border:none;border-radius:4px;color:#fff;font-size:var(--fs-xs);font-weight:600;cursor:pointer;">${t('spectate.watch')}</button>` : ''}
</div> </div>
`; `;
}); });
......
...@@ -45,7 +45,7 @@ function renderTournaments(el, tournaments) { ...@@ -45,7 +45,7 @@ function renderTournaments(el, tournaments) {
<div style="font-size:12px;color:var(--text-secondary);margin-top:2px;">${tour.game_key || 'chess'} · ${formatName(tour.format)}</div> <div style="font-size:12px;color:var(--text-secondary);margin-top:2px;">${tour.game_key || 'chess'} · ${formatName(tour.format)}</div>
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">${tour.starts_at ? new Date(tour.starts_at).toLocaleDateString('ar') : ''}</div> <div style="font-size:11px;color:var(--text-muted);margin-top:4px;">${tour.starts_at ? new Date(tour.starts_at).toLocaleDateString('ar') : ''}</div>
</div> </div>
<span style="font-size:10px;padding:3px 8px;border-radius:9999px;background:${getStatusColor(tour.status)};color:white;font-weight:600;">${getStatusLabel(tour.status)}</span> <span style="font-size:var(--fs-xs);padding:3px 8px;border-radius:9999px;background:${getStatusColor(tour.status)};color:white;font-weight:600;">${getStatusLabel(tour.status)}</span>
</div> </div>
<div style="display:flex;gap:16px;margin-top:12px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05);"> <div style="display:flex;gap:16px;margin-top:12px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05);">
<span style="font-size:11px;color:var(--text-secondary);">${emoji('people', '👥', 11)} ${tour.player_count || 0}/${tour.max_players || 32}</span> <span style="font-size:11px;color:var(--text-secondary);">${emoji('people', '👥', 11)} ${tour.player_count || 0}/${tour.max_players || 32}</span>
...@@ -88,7 +88,7 @@ async function showTournamentDetail(el, tournamentId, tour) { ...@@ -88,7 +88,7 @@ async function showTournamentDetail(el, tournamentId, tour) {
const list = el.querySelector('#tournament-list'); const list = el.querySelector('#tournament-list');
list.innerHTML = ` list.innerHTML = `
<div style="margin-bottom:16px;"> <div style="margin-bottom:16px;">
<button class="btn btn-secondary" id="detail-back" style="font-size:12px;min-height:32px;padding:4px 12px;">← ${t('tournament.back_to_list')}</button> <button class="btn btn-secondary" id="detail-back" style="font-size:12px;min-height:var(--tap-min);padding:4px 12px;">← ${t('tournament.back_to_list')}</button>
</div> </div>
<div class="card" style="padding:16px;"> <div class="card" style="padding:16px;">
<div style="font-size:18px;font-weight:700;margin-bottom:4px;">${tour?.name || t('tournament.title')}</div> <div style="font-size:18px;font-weight:700;margin-bottom:4px;">${tour?.name || t('tournament.title')}</div>
...@@ -96,11 +96,11 @@ async function showTournamentDetail(el, tournamentId, tour) { ...@@ -96,11 +96,11 @@ async function showTournamentDetail(el, tournamentId, tour) {
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;">
<div style="background:#1a2440;padding:8px;border-radius:8px;text-align:center;"> <div style="background:#1a2440;padding:8px;border-radius:8px;text-align:center;">
<div style="font-size:16px;font-weight:700;color:var(--gold);">${tour?.player_count || 0}</div> <div style="font-size:16px;font-weight:700;color:var(--gold);">${tour?.player_count || 0}</div>
<div style="font-size:10px;color:var(--text-muted);">${t('tournament.players_label')}</div> <div style="font-size:var(--fs-xs);color:var(--text-muted);">${t('tournament.players_label')}</div>
</div> </div>
<div style="background:#1a2440;padding:8px;border-radius:8px;text-align:center;"> <div style="background:#1a2440;padding:8px;border-radius:8px;text-align:center;">
<div style="font-size:16px;font-weight:700;color:#00FFFF;">${tour?.rounds_total || tour?.swiss_rounds || '?'}</div> <div style="font-size:16px;font-weight:700;color:#00FFFF;">${tour?.rounds_total || tour?.swiss_rounds || '?'}</div>
<div style="font-size:10px;color:var(--text-muted);">${t('tournament.rounds_label')}</div> <div style="font-size:var(--fs-xs);color:var(--text-muted);">${t('tournament.rounds_label')}</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -57,7 +57,7 @@ function render(el, achievements, stats) { ...@@ -57,7 +57,7 @@ function render(el, achievements, stats) {
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);"> <div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);">
<!-- Header --> <!-- Header -->
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--bg-panel);border-bottom:1px solid var(--border);"> <div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--bg-panel);border-bottom:1px solid var(--border);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:16px;font-weight:700;color:var(--text-primary);">${emoji('trophy', '🏆', 16)} ${t('achievements.title')}</span> <span style="font-size:16px;font-weight:700;color:var(--text-primary);">${emoji('trophy', '🏆', 16)} ${t('achievements.title')}</span>
<span style="margin-inline-start:auto;font-size:12px;color:var(--text-muted);">${stats.completed}/${stats.total}</span> <span style="margin-inline-start:auto;font-size:12px;color:var(--text-muted);">${stats.completed}/${stats.total}</span>
</div> </div>
...@@ -68,8 +68,8 @@ function render(el, achievements, stats) { ...@@ -68,8 +68,8 @@ function render(el, achievements, stats) {
<div style="background:linear-gradient(90deg,var(--gold),var(--gold-soft));height:100%;width:${progressPct}%;border-radius:99px;transition:width 0.5s;"></div> <div style="background:linear-gradient(90deg,var(--gold),var(--gold-soft));height:100%;width:${progressPct}%;border-radius:99px;transition:width 0.5s;"></div>
</div> </div>
<div style="display:flex;justify-content:space-between;margin-top:4px;"> <div style="display:flex;justify-content:space-between;margin-top:4px;">
<span style="font-size:10px;color:var(--text-muted);">${progressPct}% ${t('achievements.complete')}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);">${progressPct}% ${t('achievements.complete')}</span>
<span style="font-size:10px;color:var(--gold);">${stats.completed} ${t('achievements.title')}</span> <span style="font-size:var(--fs-xs);color:var(--gold);">${stats.completed} ${t('achievements.title')}</span>
</div> </div>
</div> </div>
...@@ -129,15 +129,15 @@ function renderList(achievements, category) { ...@@ -129,15 +129,15 @@ function renderList(achievements, category) {
<div style="flex:1;background:var(--bg-card);border-radius:99px;height:4px;overflow:hidden;"> <div style="flex:1;background:var(--bg-card);border-radius:99px;height:4px;overflow:hidden;">
<div style="background:${tierColor};height:100%;width:${pct}%;border-radius:99px;"></div> <div style="background:${tierColor};height:100%;width:${pct}%;border-radius:99px;"></div>
</div> </div>
<span style="font-size:10px;color:var(--text-muted);white-space:nowrap;">${a.progress}/${a.target}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);white-space:nowrap;">${a.progress}/${a.target}</span>
</div> </div>
` : ` ` : `
<div style="font-size:10px;color:var(--green);">✓ ${t('achievements.complete')}</div> <div style="font-size:var(--fs-xs);color:var(--green);">✓ ${t('achievements.complete')}</div>
`} `}
</div> </div>
<div style="text-align:center;min-width:40px;"> <div style="text-align:center;min-width:40px;">
<div style="font-size:11px;font-weight:700;color:var(--gold);">${a.coins_reward}</div> <div style="font-size:11px;font-weight:700;color:var(--gold);">${a.coins_reward}</div>
<div style="font-size:9px;color:var(--text-muted);">${t('game.coins')}</div> <div style="font-size:var(--fs-xs);color:var(--text-muted);">${t('game.coins')}</div>
</div> </div>
</div> </div>
`; `;
......
...@@ -11,7 +11,7 @@ export async function mountChallenges(el) { ...@@ -11,7 +11,7 @@ export async function mountChallenges(el) {
el.innerHTML = ` el.innerHTML = `
<div style="padding:16px;display:flex;flex-direction:column;gap:14px;"> <div style="padding:16px;display:flex;flex-direction:column;gap:14px;">
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:12px;">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">←</button>
<h2 style="font-size:18px;font-weight:800;color:var(--text-primary);flex:1;">${emoji('lightning', '⚡', 18)} ${t('challenges.title')}</h2> <h2 style="font-size:18px;font-weight:800;color:var(--text-primary);flex:1;">${emoji('lightning', '⚡', 18)} ${t('challenges.title')}</h2>
<div id="streak-badge" style="background:linear-gradient(135deg,var(--gold),var(--gold-soft));color:var(--bg-dark);font-size:12px;font-weight:800;padding:5px 12px;border-radius:99px;"></div> <div id="streak-badge" style="background:linear-gradient(135deg,var(--gold),var(--gold-soft));color:var(--bg-dark);font-size:12px;font-weight:800;padding:5px 12px;border-radius:99px;"></div>
</div> </div>
...@@ -48,7 +48,7 @@ function renderChallenges(el, data) { ...@@ -48,7 +48,7 @@ function renderChallenges(el, data) {
<div style="flex:1;height:5px;background:var(--bg-hover);border-radius:3px;overflow:hidden;"> <div style="flex:1;height:5px;background:var(--bg-hover);border-radius:3px;overflow:hidden;">
<div style="height:100%;width:${Math.min(100, (c.progress / c.target) * 100)}%;background:${c.completed ? 'var(--success)' : 'var(--gold)'};border-radius:3px;transition:width 0.3s;"></div> <div style="height:100%;width:${Math.min(100, (c.progress / c.target) * 100)}%;background:${c.completed ? 'var(--success)' : 'var(--gold)'};border-radius:3px;transition:width 0.3s;"></div>
</div> </div>
<span style="font-size:10px;color:var(--text-muted);min-width:30px;text-align:left;">${c.progress}/${c.target}</span> <span style="font-size:var(--fs-xs);color:var(--text-muted);min-width:30px;text-align:left;">${c.progress}/${c.target}</span>
</div> </div>
</div> </div>
<div style="text-align:center;min-width:50px;"> <div style="text-align:center;min-width:50px;">
......
...@@ -44,7 +44,7 @@ function render(el, streak, alreadyClaimed, dayIndex, todayReward) { ...@@ -44,7 +44,7 @@ function render(el, streak, alreadyClaimed, dayIndex, todayReward) {
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);"> <div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);">
<!-- Header --> <!-- Header -->
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--bg-panel);border-bottom:1px solid var(--border);"> <div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--bg-panel);border-bottom:1px solid var(--border);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button> <button class="btn btn-secondary" id="back-btn" style="min-height:var(--tap-min);padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:16px;font-weight:700;color:var(--text-primary);">${emoji('gift', '🎁', 16)} ${t('daily.title')}</span> <span style="font-size:16px;font-weight:700;color:var(--text-primary);">${emoji('gift', '🎁', 16)} ${t('daily.title')}</span>
</div> </div>
...@@ -69,7 +69,7 @@ function render(el, streak, alreadyClaimed, dayIndex, todayReward) { ...@@ -69,7 +69,7 @@ function render(el, streak, alreadyClaimed, dayIndex, todayReward) {
color:${claimed ? 'var(--success)' : isToday ? 'var(--bg-dark)' : 'var(--text-muted)'};"> color:${claimed ? 'var(--success)' : isToday ? 'var(--bg-dark)' : 'var(--text-muted)'};">
${claimed ? '✓' : coins} ${claimed ? '✓' : coins}
</div> </div>
<span style="font-size:9px;color:${isToday ? 'var(--gold)' : 'var(--text-dim)'};font-weight:${isToday ? '700' : '400'};">${t('daily.day', { n: i + 1 })}</span> <span style="font-size:var(--fs-xs);color:${isToday ? 'var(--gold)' : 'var(--text-dim)'};font-weight:${isToday ? '700' : '400'};">${t('daily.day', { n: i + 1 })}</span>
</div> </div>
`; `;
}).join('')} }).join('')}
......
...@@ -37,12 +37,12 @@ function renderItems(el, items) { ...@@ -37,12 +37,12 @@ function renderItems(el, items) {
grid.innerHTML = items.map(item => ` grid.innerHTML = items.map(item => `
<div class="card shop-item" data-id="${item.id}" style="cursor:pointer;padding:var(--s-3);border-color:${RARITY_COLORS[item.rarity] || 'var(--border)'}33;${item.owned ? 'opacity:0.7;' : ''}position:relative;"> <div class="card shop-item" data-id="${item.id}" style="cursor:pointer;padding:var(--s-3);border-color:${RARITY_COLORS[item.rarity] || 'var(--border)'}33;${item.owned ? 'opacity:0.7;' : ''}position:relative;">
${item.owned ? `<div style="position:absolute;top:6px;right:6px;background:var(--success);color:#fff;font-size:9px;font-weight:700;padding:2px 6px;border-radius:4px;">Owned</div>` : ''} ${item.owned ? `<div style="position:absolute;top:6px;right:6px;background:var(--success);color:#fff;font-size:var(--fs-xs);font-weight:700;padding:2px 6px;border-radius:4px;">Owned</div>` : ''}
<div style="height:60px;background:var(--bg-elevated);border-radius:var(--r-sm);margin-bottom:var(--s-2);display:flex;align-items:center;justify-content:center;font-size:24px;"> <div style="height:60px;background:var(--bg-elevated);border-radius:var(--r-sm);margin-bottom:var(--s-2);display:flex;align-items:center;justify-content:center;font-size:24px;">
${emoji('art', '🎨', 24)} ${emoji('art', '🎨', 24)}
</div> </div>
<div style="font-size:13px;font-weight:600;margin-bottom:2px;">${item.name || item.id}</div> <div style="font-size:13px;font-weight:600;margin-bottom:2px;">${item.name || item.id}</div>
<div style="font-size:10px;color:${RARITY_COLORS[item.rarity] || 'var(--text-muted)'};margin-bottom:var(--s-1);">${item.rarity || 'common'}</div> <div style="font-size:var(--fs-xs);color:${RARITY_COLORS[item.rarity] || 'var(--text-muted)'};margin-bottom:var(--s-1);">${item.rarity || 'common'}</div>
<div style="font-size:12px;font-weight:700;color:var(--gold);"> <div style="font-size:12px;font-weight:700;color:var(--gold);">
${item.owned ? '' : (item.price_coins ? `${item.price_coins} ${emoji('coin', '🪙', 14)}` : '') + (item.price_gems ? ` ${item.price_gems} ${emoji('gem', '💎', 14)}` : '')} ${item.owned ? '' : (item.price_coins ? `${item.price_coins} ${emoji('coin', '🪙', 14)}` : '') + (item.price_gems ? ` ${item.price_gems} ${emoji('gem', '💎', 14)}` : '')}
</div> </div>
......
...@@ -47,7 +47,7 @@ function renderActivity(el, activities) { ...@@ -47,7 +47,7 @@ function renderActivity(el, activities) {
</div> </div>
<div style="flex:1;"> <div style="flex:1;">
<div style="font-size:13px;color:var(--text-primary);"><strong>${actor.display_name || actor.username || '?'}</strong> ${label}</div> <div style="font-size:13px;color:var(--text-primary);"><strong>${actor.display_name || actor.username || '?'}</strong> ${label}</div>
<div style="font-size:10px;color:var(--text-muted);margin-top:2px;">${time}</div> <div style="font-size:var(--fs-xs);color:var(--text-muted);margin-top:2px;">${time}</div>
</div> </div>
</div> </div>
`; `;
......
...@@ -65,7 +65,7 @@ export function mountChat(el, params = {}) { ...@@ -65,7 +65,7 @@ export function mountChat(el, params = {}) {
.chat-bubble.mine { align-self:flex-end;background:var(--chess-primary);color:#fff;border-bottom-left-radius:16px;border-bottom-right-radius:4px; } .chat-bubble.mine { align-self:flex-end;background:var(--chess-primary);color:#fff;border-bottom-left-radius:16px;border-bottom-right-radius:4px; }
.chat-bubble.theirs { align-self:flex-start;background:var(--bg-card);color:var(--text-light);border-bottom-left-radius:4px;border-bottom-right-radius:16px; } .chat-bubble.theirs { align-self:flex-start;background:var(--bg-card);color:var(--text-light);border-bottom-left-radius:4px;border-bottom-right-radius:16px; }
.chat-bubble.system { align-self:center;background:rgba(228,172,56,0.1);color:var(--gold);font-size:12px;border-radius:12px;padding:6px 14px; } .chat-bubble.system { align-self:center;background:rgba(228,172,56,0.1);color:var(--gold);font-size:12px;border-radius:12px;padding:6px 14px; }
.chat-bubble .chat-time { font-size:10px;opacity:0.6;margin-top:2px; } .chat-bubble .chat-time { font-size:var(--fs-xs);opacity:0.6;margin-top:2px; }
.chat-bubble.mine .chat-time { text-align:left; } .chat-bubble.mine .chat-time { text-align:left; }
.chat-bubble.theirs .chat-time { text-align:right; } .chat-bubble.theirs .chat-time { text-align:right; }
.chat-input-bar { display:flex;gap:8px;padding:10px 14px;background:var(--bg-panel);border-top:1px solid var(--border);padding-bottom:max(10px, env(safe-area-inset-bottom, 0px)); } .chat-input-bar { display:flex;gap:8px;padding:10px 14px;background:var(--bg-panel);border-top:1px solid var(--border);padding-bottom:max(10px, env(safe-area-inset-bottom, 0px)); }
......
...@@ -22,11 +22,11 @@ export function mountFriends(el) { ...@@ -22,11 +22,11 @@ export function mountFriends(el) {
<div style="padding:12px 16px;background:var(--bg-panel);"> <div style="padding:12px 16px;background:var(--bg-panel);">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<h2 style="font-size:18px;font-weight:700;color:var(--text-primary);">${t('social.friends')}</h2> <h2 style="font-size:18px;font-weight:700;color:var(--text-primary);">${t('social.friends')}</h2>
<button class="btn btn-secondary" id="btn-search" style="min-height:34px;padding:6px 14px;font-size:12px;">${emoji('search_icon', '🔍', 13)} ${t('social.search_btn')}</button> <button class="btn btn-secondary" id="btn-search" style="min-height:var(--tap-min);padding:6px 14px;font-size:12px;">${emoji('search_icon', '🔍', 13)} ${t('social.search_btn')}</button>
</div> </div>
<div style="display:flex;gap:6px;flex-wrap:wrap;"> <div style="display:flex;gap:6px;flex-wrap:wrap;">
<button class="social-tab active" data-tab="friends">${t('social.friends_tab')}</button> <button class="social-tab active" data-tab="friends">${t('social.friends_tab')}</button>
<button class="social-tab" data-tab="pending">${t('social.pending_tab')} <span id="pending-count" style="display:none;font-size:10px;background:var(--error);color:#fff;border-radius:50%;padding:1px 5px;margin-right:2px;"></span></button> <button class="social-tab" data-tab="pending">${t('social.pending_tab')} <span id="pending-count" style="display:none;font-size:var(--fs-xs);background:var(--error);color:#fff;border-radius:50%;padding:1px 5px;margin-right:2px;"></span></button>
<button class="social-tab" data-tab="online">${t('social.online_tab')}</button> <button class="social-tab" data-tab="online">${t('social.online_tab')}</button>
<button class="social-tab" data-tab="groups">${emoji('group', '👥', 12)} ${t('social.groups_tab')}</button> <button class="social-tab" data-tab="groups">${emoji('group', '👥', 12)} ${t('social.groups_tab')}</button>
<button class="social-tab" data-tab="activity">${emoji('news', '📰', 12)} ${t('social.activity_tab')}</button> <button class="social-tab" data-tab="activity">${emoji('news', '📰', 12)} ${t('social.activity_tab')}</button>
...@@ -111,8 +111,8 @@ async function checkInvites(el) { ...@@ -111,8 +111,8 @@ async function checkInvites(el) {
<div style="font-size:13px;font-weight:600;color:var(--success);">${t('social.challenges_you', { name })}</div> <div style="font-size:13px;font-weight:600;color:var(--success);">${t('social.challenges_you', { name })}</div>
<div style="font-size:11px;color:var(--text-muted);">${gameLabel}</div> <div style="font-size:11px;color:var(--text-muted);">${gameLabel}</div>
</div> </div>
<button class="btn btn-primary" data-accept-invite="${inv.match_id}" style="min-height:30px;padding:5px 14px;font-size:11px;">${t('social.accept')}</button> <button class="btn btn-primary" data-accept-invite="${inv.match_id}" style="min-height:var(--tap-min);padding:5px 14px;font-size:11px;">${t('social.accept')}</button>
<button class="btn btn-secondary" data-decline-invite="${inv.match_id}" style="min-height:30px;padding:5px 10px;font-size:11px;">✕</button> <button class="btn btn-secondary" data-decline-invite="${inv.match_id}" style="min-height:var(--tap-min);padding:5px 10px;font-size:11px;">✕</button>
</div> </div>
`; `;
}).join(''); }).join('');
...@@ -527,7 +527,7 @@ function showSearch(el) { ...@@ -527,7 +527,7 @@ function showSearch(el) {
<div style="margin-bottom:16px;"> <div style="margin-bottom:16px;">
<div style="display:flex;gap:8px;"> <div style="display:flex;gap:8px;">
<input class="input" id="search-input" type="text" placeholder="${t('social.search_placeholder')}" style="flex:1;min-height:40px;font-size:14px;" autocomplete="off"> <input class="input" id="search-input" type="text" placeholder="${t('social.search_placeholder')}" style="flex:1;min-height:40px;font-size:14px;" autocomplete="off">
<button class="btn btn-primary" id="search-go" style="min-height:40px;padding:8px 16px;font-size:13px;">${t('social.search_btn')}</button> <button class="btn btn-primary" id="search-go" style="min-height:var(--tap-min);padding:8px 16px;font-size:13px;">${t('social.search_btn')}</button>
</div> </div>
<div style="font-size:11px;color:var(--text-dim);margin-top:4px;">${t('social.search_hint')}</div> <div style="font-size:11px;color:var(--text-dim);margin-top:4px;">${t('social.search_hint')}</div>
</div> </div>
...@@ -565,7 +565,7 @@ function showSearch(el) { ...@@ -565,7 +565,7 @@ function showSearch(el) {
<div style="font-size:13px;font-weight:600;color:var(--text-primary);">${p.display_name || p.username || t('common.player')}</div> <div style="font-size:13px;font-weight:600;color:var(--text-primary);">${p.display_name || p.username || t('common.player')}</div>
<div style="font-size:11px;color:var(--text-muted);">${p.is_online ? t('common.online') : t('common.offline')}${t('common.level', { n: p.level || 1 })}</div> <div style="font-size:11px;color:var(--text-muted);">${p.is_online ? t('common.online') : t('common.offline')}${t('common.level', { n: p.level || 1 })}</div>
</div> </div>
<button class="btn btn-primary" data-add="${p.id}" style="min-height:32px;padding:6px 12px;font-size:11px;flex-shrink:0;">${t('social.add_btn')}</button> <button class="btn btn-primary" data-add="${p.id}" style="min-height:var(--tap-min);padding:6px 12px;font-size:11px;flex-shrink:0;">${t('social.add_btn')}</button>
</div> </div>
`).join(''); `).join('');
...@@ -648,7 +648,7 @@ async function loadActivity(content) { ...@@ -648,7 +648,7 @@ async function loadActivity(content) {
</div> </div>
<div style="flex:1;min-width:0;"> <div style="flex:1;min-width:0;">
<div style="font-size:13px;color:var(--text-primary);"><strong>${actor.display_name || actor.username || '?'}</strong> ${label}</div> <div style="font-size:13px;color:var(--text-primary);"><strong>${actor.display_name || actor.username || '?'}</strong> ${label}</div>
${a.created_at ? `<div style="font-size:10px;color:var(--text-dim);margin-top:2px;">${timeAgo(a.created_at)}</div>` : ''} ${a.created_at ? `<div style="font-size:var(--fs-xs);color:var(--text-dim);margin-top:2px;">${timeAgo(a.created_at)}</div>` : ''}
</div> </div>
</div>`; </div>`;
}).join(''); }).join('');
......
...@@ -23,13 +23,13 @@ export async function mountGroupChat(el, params = {}) { ...@@ -23,13 +23,13 @@ export async function mountGroupChat(el, params = {}) {
<div id="group-header" style="flex:1;cursor:pointer;"> <div id="group-header" style="flex:1;cursor:pointer;">
<div style="font-size:15px;font-weight:600;color:var(--text-primary);">${t('common.loading')}</div> <div style="font-size:15px;font-weight:600;color:var(--text-primary);">${t('common.loading')}</div>
</div> </div>
<button class="btn btn-secondary" id="invite-game-btn" style="min-height:32px;padding:5px 10px;font-size:12px;">${emoji('swords', '⚔️', 13)} ${t('group.play')}</button> <button class="btn btn-secondary" id="invite-game-btn" style="min-height:var(--tap-min);padding:5px 10px;font-size:12px;">${emoji('swords', '⚔️', 13)} ${t('group.play')}</button>
</div> </div>
<div id="invite-banner" style="display:none;"></div> <div id="invite-banner" style="display:none;"></div>
<div id="chat-messages" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:6px;"></div> <div id="chat-messages" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:6px;"></div>
<div style="padding:10px 16px;background:var(--bg-panel);border-top:1px solid var(--border);display:flex;gap:8px;align-items:center;"> <div style="padding:10px 16px;background:var(--bg-panel);border-top:1px solid var(--border);display:flex;gap:8px;align-items:center;">
<input class="input" id="msg-input" type="text" placeholder="${t('chat.placeholder')}" style="flex:1;min-height:38px;" maxlength="500"> <input class="input" id="msg-input" type="text" placeholder="${t('chat.placeholder')}" style="flex:1;min-height:38px;" maxlength="500">
<button class="btn btn-primary" id="send-btn" style="min-height:38px;padding:0 14px;">${t('common.send')}</button> <button class="btn btn-primary" id="send-btn" style="min-height:var(--tap-min);padding:0 14px;">${t('common.send')}</button>
</div> </div>
</div> </div>
`; `;
...@@ -153,7 +153,7 @@ function messageHtml(msg, myId, memberMap) { ...@@ -153,7 +153,7 @@ function messageHtml(msg, myId, memberMap) {
<div style="display:flex;justify-content:flex-end;"> <div style="display:flex;justify-content:flex-end;">
<div style="max-width:75%;background:var(--chess-primary);border-radius:12px 12px 4px 12px;padding:8px 12px;"> <div style="max-width:75%;background:var(--chess-primary);border-radius:12px 12px 4px 12px;padding:8px 12px;">
<div style="font-size:13px;color:#fff;word-wrap:break-word;">${escapeHtml(msg.content)}</div> <div style="font-size:13px;color:#fff;word-wrap:break-word;">${escapeHtml(msg.content)}</div>
<div style="font-size:9px;color:rgba(255,255,255,0.5);margin-top:2px;">${time}</div> <div style="font-size:var(--fs-xs);color:rgba(255,255,255,0.5);margin-top:2px;">${time}</div>
</div> </div>
</div>`; </div>`;
} }
...@@ -164,9 +164,9 @@ function messageHtml(msg, myId, memberMap) { ...@@ -164,9 +164,9 @@ function messageHtml(msg, myId, memberMap) {
${profile.avatar_url ? `<img src="${profile.avatar_url}" style="width:28px;height:28px;object-fit:cover;border-radius:50%;">` : emoji('person', '👤', 12)} ${profile.avatar_url ? `<img src="${profile.avatar_url}" style="width:28px;height:28px;object-fit:cover;border-radius:50%;">` : emoji('person', '👤', 12)}
</div> </div>
<div style="max-width:70%;background:var(--bg-card);border-radius:12px 12px 12px 4px;padding:8px 12px;"> <div style="max-width:70%;background:var(--bg-card);border-radius:12px 12px 12px 4px;padding:8px 12px;">
<div style="font-size:10px;font-weight:600;color:var(--text-muted);margin-bottom:2px;">${escapeHtml(name)}</div> <div style="font-size:var(--fs-xs);font-weight:600;color:var(--text-muted);margin-bottom:2px;">${escapeHtml(name)}</div>
<div style="font-size:13px;color:var(--text-light);word-wrap:break-word;">${escapeHtml(msg.content)}</div> <div style="font-size:13px;color:var(--text-light);word-wrap:break-word;">${escapeHtml(msg.content)}</div>
<div style="font-size:9px;color:#4a5568;margin-top:2px;">${time}</div> <div style="font-size:var(--fs-xs);color:#4a5568;margin-top:2px;">${time}</div>
</div> </div>
</div>`; </div>`;
} }
...@@ -197,7 +197,7 @@ async function checkGroupInvites(el, groupId, myId) { ...@@ -197,7 +197,7 @@ async function checkGroupInvites(el, groupId, myId) {
<div style="flex:1;"> <div style="flex:1;">
<div style="font-size:12px;font-weight:600;color:var(--success);">${gameLabel}${accepted.length}/${inv.required_players}</div> <div style="font-size:12px;font-weight:600;color:var(--success);">${gameLabel}${accepted.length}/${inv.required_players}</div>
</div> </div>
${isAccepted || isInviter ? `<span style="font-size:11px;color:var(--text-muted);">${t('group.accepted')}</span>` : `<button class="btn btn-primary accept-invite-btn" data-match="${inv.match_id}" data-game="${inv.game_key}" style="min-height:28px;padding:4px 12px;font-size:11px;">${t('group.join')}</button>`} ${isAccepted || isInviter ? `<span style="font-size:11px;color:var(--text-muted);">${t('group.accepted')}</span>` : `<button class="btn btn-primary accept-invite-btn" data-match="${inv.match_id}" data-game="${inv.game_key}" style="min-height:var(--tap-min);padding:4px 12px;font-size:11px;">${t('group.join')}</button>`}
</div>`; </div>`;
}).join(''); }).join('');
...@@ -271,7 +271,7 @@ function showGamePicker(el, groupId) { ...@@ -271,7 +271,7 @@ function showGamePicker(el, groupId) {
<div style="font-size:11px;color:var(--text-muted);">${t('group.players')}</div> <div style="font-size:11px;color:var(--text-muted);">${t('group.players')}</div>
</div> </div>
</button> </button>
<button id="cancel-pick" class="btn btn-secondary w-full" style="min-height:40px;">${t('common.cancel')}</button> <button id="cancel-pick" class="btn btn-secondary w-full" style="min-height:var(--tap-min);">${t('common.cancel')}</button>
</div> </div>
`; `;
......
...@@ -19,13 +19,13 @@ export async function mountGroupMembers(el, params = {}) { ...@@ -19,13 +19,13 @@ export async function mountGroupMembers(el, params = {}) {
<div style="padding:12px 16px;background:var(--bg-panel);display:flex;align-items:center;gap:12px;"> <div style="padding:12px 16px;background:var(--bg-panel);display:flex;align-items:center;gap:12px;">
<button class="btn btn-secondary" id="back-btn" style="width:36px;height:36px;padding:0;">←</button> <button class="btn btn-secondary" id="back-btn" style="width:36px;height:36px;padding:0;">←</button>
<h2 style="font-size:18px;font-weight:700;color:var(--text-primary);flex:1;">${t('group.members')}</h2> <h2 style="font-size:18px;font-weight:700;color:var(--text-primary);flex:1;">${t('group.members')}</h2>
${canManage ? `<button class="btn btn-primary" id="add-member-btn" style="min-height:34px;padding:6px 12px;font-size:12px;">+ ${t('group.add')}</button>` : ''} ${canManage ? `<button class="btn btn-primary" id="add-member-btn" style="min-height:var(--tap-min);padding:6px 12px;font-size:12px;">+ ${t('group.add')}</button>` : ''}
</div> </div>
<div id="members-list" style="flex:1;overflow-y:auto;padding:12px 16px;"> <div id="members-list" style="flex:1;overflow-y:auto;padding:12px 16px;">
<div style="text-align:center;padding:24px;color:var(--text-secondary);">${t('common.loading')}</div> <div style="text-align:center;padding:24px;color:var(--text-secondary);">${t('common.loading')}</div>
</div> </div>
<div style="padding:12px 16px;border-top:1px solid rgba(255,255,255,0.06);"> <div style="padding:12px 16px;border-top:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary w-full" id="leave-btn" style="min-height:40px;color:var(--error);">${t('group.leave')}</button> <button class="btn btn-secondary w-full" id="leave-btn" style="min-height:var(--tap-min);color:var(--error);">${t('group.leave')}</button>
</div> </div>
</div> </div>
`; `;
...@@ -54,7 +54,7 @@ export async function mountGroupMembers(el, params = {}) { ...@@ -54,7 +54,7 @@ export async function mountGroupMembers(el, params = {}) {
${roleLabel ? `<div style="font-size:11px;color:var(--text-muted);">${roleLabel}</div>` : ''} ${roleLabel ? `<div style="font-size:11px;color:var(--text-muted);">${roleLabel}</div>` : ''}
</div> </div>
${p.is_online ? '<div style="width:8px;height:8px;border-radius:50%;background:var(--success);"></div>' : ''} ${p.is_online ? '<div style="width:8px;height:8px;border-radius:50%;background:var(--success);"></div>' : ''}
${showRemove ? `<button class="btn btn-secondary remove-btn" data-id="${m.user_id}" style="min-height:28px;padding:4px 10px;font-size:11px;color:var(--error);">${t('group.remove')}</button>` : ''} ${showRemove ? `<button class="btn btn-secondary remove-btn" data-id="${m.user_id}" style="min-height:var(--tap-min);padding:4px 10px;font-size:11px;color:var(--error);">${t('group.remove')}</button>` : ''}
</div>`; </div>`;
}).join(''); }).join('');
...@@ -162,7 +162,7 @@ async function showAddMemberPicker(el, groupId) { ...@@ -162,7 +162,7 @@ async function showAddMemberPicker(el, groupId) {
${f.avatar_url ? `<img src="${f.avatar_url}" style="width:32px;height:32px;object-fit:cover;border-radius:50%;">` : emoji('person', '👤', 14)} ${f.avatar_url ? `<img src="${f.avatar_url}" style="width:32px;height:32px;object-fit:cover;border-radius:50%;">` : emoji('person', '👤', 14)}
</div> </div>
<span style="flex:1;font-size:13px;color:var(--text-primary);">${escapeHtml(f.display_name || 'Player')}</span> <span style="flex:1;font-size:13px;color:var(--text-primary);">${escapeHtml(f.display_name || 'Player')}</span>
<button class="btn btn-primary add-one-btn" data-id="${f.id}" style="min-height:28px;padding:4px 12px;font-size:11px;">${t('group.add')}</button> <button class="btn btn-primary add-one-btn" data-id="${f.id}" style="min-height:var(--tap-min);padding:4px 12px;font-size:11px;">${t('group.add')}</button>
</div> </div>
`).join(''); `).join('');
......
...@@ -10,7 +10,7 @@ export async function mountGroups(el) { ...@@ -10,7 +10,7 @@ export async function mountGroups(el) {
<div style="padding:12px 16px;background:var(--bg-panel);display:flex;align-items:center;gap:12px;"> <div style="padding:12px 16px;background:var(--bg-panel);display:flex;align-items:center;gap:12px;">
<button class="btn btn-secondary" id="back-btn" style="width:36px;height:36px;padding:0;">←</button> <button class="btn btn-secondary" id="back-btn" style="width:36px;height:36px;padding:0;">←</button>
<h2 style="font-size:18px;font-weight:700;color:var(--text-primary);flex:1;">${t('social.groups_tab')}</h2> <h2 style="font-size:18px;font-weight:700;color:var(--text-primary);flex:1;">${t('social.groups_tab')}</h2>
<button class="btn btn-primary" id="create-btn" style="min-height:34px;padding:6px 14px;font-size:12px;">+ ${t('group.create')}</button> <button class="btn btn-primary" id="create-btn" style="min-height:var(--tap-min);padding:6px 14px;font-size:12px;">+ ${t('group.create')}</button>
</div> </div>
<div id="groups-list" style="flex:1;overflow-y:auto;padding:12px 16px;"> <div id="groups-list" style="flex:1;overflow-y:auto;padding:12px 16px;">
<div style="text-align:center;padding:24px;color:var(--text-secondary);">${t('common.loading')}</div> <div style="text-align:center;padding:24px;color:var(--text-secondary);">${t('common.loading')}</div>
......
...@@ -37,7 +37,7 @@ function renderNotifications(el, notifications) { ...@@ -37,7 +37,7 @@ function renderNotifications(el, notifications) {
<div class="card notif-item" data-id="${n.id}" style="padding:var(--s-3);margin-bottom:var(--s-2);opacity:${n.is_read ? '0.7' : '1'};cursor:pointer;"> <div class="card notif-item" data-id="${n.id}" style="padding:var(--s-3);margin-bottom:var(--s-2);opacity:${n.is_read ? '0.7' : '1'};cursor:pointer;">
<div style="font-size:14px;font-weight:${n.is_read ? '400' : '600'};">${n.title_ar || n.title || ''}</div> <div style="font-size:14px;font-weight:${n.is_read ? '400' : '600'};">${n.title_ar || n.title || ''}</div>
<div style="font-size:12px;color:var(--text-secondary);margin-top:2px;">${n.body_ar || n.body || ''}</div> <div style="font-size:12px;color:var(--text-secondary);margin-top:2px;">${n.body_ar || n.body || ''}</div>
<div style="font-size:10px;color:var(--text-muted);margin-top:4px;">${timeAgo(n.created_at)}</div> <div style="font-size:var(--fs-xs);color:var(--text-muted);margin-top:4px;">${timeAgo(n.created_at)}</div>
</div> </div>
`).join(''); `).join('');
......
...@@ -34,10 +34,10 @@ export async function mountTournamentsHub(el) { ...@@ -34,10 +34,10 @@ export async function mountTournamentsHub(el) {
.tour-hub-card:active{transform:scale(0.98);} .tour-hub-card:active{transform:scale(0.98);}
.tour-hub-card.registered{border-color:rgba(228,172,56,0.3);} .tour-hub-card.registered{border-color:rgba(228,172,56,0.3);}
.tour-hub-card.has-pending{border-color:var(--gold);box-shadow:0 0 12px rgba(228,172,56,0.15);} .tour-hub-card.has-pending{border-color:var(--gold);box-shadow:0 0 12px rgba(228,172,56,0.15);}
.tour-status-pill{font-size:10px;padding:3px 10px;border-radius:99px;font-weight:700;display:inline-block;} .tour-status-pill{font-size:var(--fs-xs);padding:3px 10px;border-radius:99px;font-weight:700;display:inline-block;}
.tour-stat{display:flex;flex-direction:column;align-items:center;gap:2px;} .tour-stat{display:flex;flex-direction:column;align-items:center;gap:2px;}
.tour-stat-val{font-size:16px;font-weight:800;color:var(--text-primary);} .tour-stat-val{font-size:16px;font-weight:800;color:var(--text-primary);}
.tour-stat-label{font-size:10px;color:var(--text-muted);} .tour-stat-label{font-size:var(--fs-xs);color:var(--text-muted);}
</style> </style>
`; `;
......
<?php
/**
* Local dev router for the UI audit.
*
* Mirrors the .htaccess rules: /public and /api are served as-is, everything
* else falls through to index.php. Points at the real Supabase so scenes have
* real data to lay out.
*
* php -S 127.0.0.1:8080 tools/dev-server.php
*/
foreach ([
'SUPABASE_URL' => 'https://safe-supabase-kong.caprover.al-arcade.com',
'SUPABASE_ANON_KEY' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84',
'SUPABASE_SERVICE_KEY' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4',
'STOCKFISH_API' => 'https://stockfishapi.caprover.al-arcade.com',
] as $k => $v) putenv("$k=$v");
$root = dirname(__DIR__);
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (preg_match('#^/(public|api)/#', $path)) {
$file = $root . $path;
if (is_file($file)) {
if (str_ends_with($file, '.php')) { require $file; return true; }
return false; // let the built-in server serve the static file
}
http_response_code(404);
return true;
}
// Root-level static files (manifest.json, icons, logo) are served directly in
// production by the .htaccess "file exists" rule; mirror that here so the audit
// does not report a phantom manifest error.
$rootFile = $root . $path;
if ($path !== '/' && is_file($rootFile) && !str_ends_with($rootFile, '.php')) return false;
require $root . '/index.php';
...@@ -84,6 +84,19 @@ const AUDIT_FN = () => { ...@@ -84,6 +84,19 @@ const AUDIT_FN = () => {
issues.push({ type: 'page-overflow-x', el: 'document', detail: `scrollWidth ${de.scrollWidth} > viewport ${vw}` }); issues.push({ type: 'page-overflow-x', el: 'document', detail: `scrollWidth ${de.scrollWidth} > viewport ${vw}` });
} }
const isControl = (n) => n instanceof Element
&& (n.matches('button,a[href],[role="button"],input,select,textarea')
|| getComputedStyle(n).cursor === 'pointer');
// A carousel or chip row scrolls its children out of view on purpose.
const inScroller = (n) => {
for (let p = n.parentElement; p && p !== document.body; p = p.parentElement) {
const o = getComputedStyle(p).overflowX;
if (o === 'auto' || o === 'scroll') return true;
}
return false;
};
const all = Array.from(document.querySelectorAll('body *')); const all = Array.from(document.querySelectorAll('body *'));
for (const el of all) { for (const el of all) {
const cs = getComputedStyle(el); const cs = getComputedStyle(el);
...@@ -97,14 +110,20 @@ const AUDIT_FN = () => { ...@@ -97,14 +110,20 @@ const AUDIT_FN = () => {
} }
// 3. Sticking out past either edge. // 3. Sticking out past either edge.
if (r.left < -1) push('clipped-start', el, `left ${Math.round(r.left)}px`); if (!inScroller(el)) {
if (r.right > vw + 1) push('clipped-end', el, `right ${Math.round(r.right)}px past ${vw}px`); if (r.left < -1) push('clipped-start', el, `left ${Math.round(r.left)}px`);
if (r.right > vw + 1) push('clipped-end', el, `right ${Math.round(r.right)}px past ${vw}px`);
}
// 4. Tap targets. 44x44 is the documented minimum on both platforms. // 4. Tap targets. 44x44 is the documented minimum on both platforms.
const tappable = el.matches('button,a,[role="button"],input,select,textarea,[onclick]') // Only the OUTERMOST control counts: a 20px label inside a 48px button is
|| cs.cursor === 'pointer'; // not a small target, and counting it buries the real ones.
if (tappable && r.width > 0 && r.height > 0) { if (isControl(el)) {
if (r.height < 44 || r.width < 44) { let ancestorIsControl = false;
for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) {
if (isControl(p)) { ancestorIsControl = true; break; }
}
if (!ancestorIsControl && (r.height < 44 || r.width < 44)) {
push('tap-target', el, `${Math.round(r.width)}x${Math.round(r.height)} (min 44x44)`); push('tap-target', el, `${Math.round(r.width)}x${Math.round(r.height)} (min 44x44)`);
} }
} }
...@@ -122,13 +141,6 @@ const AUDIT_FN = () => { ...@@ -122,13 +141,6 @@ const AUDIT_FN = () => {
if (clipsY) push('clipped-content-y', el, `content ${el.scrollHeight}px in ${el.clientHeight}px box`); if (clipsY) push('clipped-content-y', el, `content ${el.scrollHeight}px in ${el.clientHeight}px box`);
if (clipsX) push('clipped-content-x', el, `content ${el.scrollWidth}px in ${el.clientWidth}px box`); if (clipsX) push('clipped-content-x', el, `content ${el.scrollWidth}px in ${el.clientWidth}px box`);
// 7. Fixed pixel widths that cannot fit the narrowest supported phone (360).
if (cs.width.endsWith('px') && !cs.width.startsWith('0')) {
const w = parseFloat(cs.width);
if (w > 360 && cs.maxWidth === 'none' && r.width > vw * 0.98) {
push('fixed-width', el, `width:${cs.width} with no max-width`);
}
}
} }
return { return {
...@@ -139,12 +151,31 @@ const AUDIT_FN = () => { ...@@ -139,12 +151,31 @@ const AUDIT_FN = () => {
}; };
}; };
// Every guest sign-in creates a real account. Track them and remove them at the
// end, so repeated audit runs do not litter the database with throwaway players.
const SB = 'https://safe-supabase-kong.caprover.al-arcade.com';
const SK = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4';
const createdGuests = new Set();
async function removeGuests() {
for (const id of createdGuests) {
for (const path of [`chat_messages?sender_id=eq.${id}`, `matchmaking_queue?player_id=eq.${id}`,
`mp_log?player_id=eq.${id}`, `matches?white_player_id=eq.${id}`,
`matches?black_player_id=eq.${id}`, `player_achievements?player_id=eq.${id}`,
`profiles?id=eq.${id}`]) {
await fetch(`${SB}/rest/v1/${path}`, { method: 'DELETE', headers: { apikey: SK, Authorization: `Bearer ${SK}` } }).catch(() => {});
}
await fetch(`${SB}/auth/v1/admin/users/${id}`, { method: 'DELETE', headers: { apikey: SK, Authorization: `Bearer ${SK}` } }).catch(() => {});
}
if (createdGuests.size) console.log(`\ncleaned up ${createdGuests.size} throwaway guest account(s)`);
}
async function guestLogin(page) { async function guestLogin(page) {
await page.goto(BASE + '/', { waitUntil: 'networkidle2', timeout: 60000 }); await page.goto(BASE + '/', { waitUntil: 'networkidle2', timeout: 60000 });
await new Promise(r => setTimeout(r, 1500)); await new Promise(r => setTimeout(r, 1500));
// Already signed in from a previous run? // Already signed in from a previous run?
const needsLogin = await page.evaluate(() => !localStorage.getItem('el3ab_auth_token') const needsLogin = await page.evaluate(() => !JSON.parse(localStorage.getItem('el3ab_state')||'{}')?.auth?.token
&& !!document.body.innerText.match(/كضيف|Guest/)); && !!document.body.innerText.match(/كضيف|Guest/));
if (!needsLogin) return true; if (!needsLogin) return true;
...@@ -156,6 +187,12 @@ async function guestLogin(page) { ...@@ -156,6 +187,12 @@ async function guestLogin(page) {
}); });
if (!clicked) return false; if (!clicked) return false;
await new Promise(r => setTimeout(r, 4000)); await new Promise(r => setTimeout(r, 4000));
const id = await page.evaluate(() => {
try { return JSON.parse(localStorage.getItem('el3ab_state') || '{}')?.auth?.userId || null; }
catch (e) { return null; }
});
if (id) createdGuests.add(id);
return true; return true;
} }
...@@ -209,6 +246,7 @@ for (const device of DEVICES) { ...@@ -209,6 +246,7 @@ for (const device of DEVICES) {
} }
report.consoleErrors = consoleErrors; report.consoleErrors = consoleErrors;
await browser.close(); await browser.close();
await removeGuests();
fs.writeFileSync(path.join(OUT, 'report.json'), JSON.stringify(report, null, 2)); fs.writeFileSync(path.join(OUT, 'report.json'), JSON.stringify(report, null, 2));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment