Commit 6ec2d6af authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: 40 UI/UX improvements — spacing, sounds, feedback, visual polish

Spacing & Layout Fixes:
- Fixed bottom nav overlapping content (added 24px extra padding)
- Fixed profile stat grid overflow (4-column grid, 2-col on small mobile)
- Fixed ludo mobile panel cramped (more padding + margin-bottom)
- Fixed ludo board wrapper spacing on mobile

Sounds (Ludo):
- Dice roll sound + haptic vibration on every roll
- Move sound on piece placement
- Capture sound + stronger vibration on kills
- Six rolled special sound
- Home reached celebration sound
- Win/lose game end sounds
- Celebration overlay on human win

Sounds (Chess):
- Added celebration overlay on chess win

Haptic Feedback:
- Button ripple tap vibration (5ms)
- Dice roll vibration (15ms)
- Capture vibration (30ms)
- App.vibrate() helper

Visual Improvements:
- Better avatar placeholder (gradient background)
- Better empty state (flex column, centered, larger padding)
- Better input focus glow (3px cyan ring)
- Better button active states (darker variants)
- Animated gradient text utility (.text-gradient)
- Progress bar component
- Notification dot component
- Divider with text component
- Pill selection group component
- Section divider utility
- Game result animation (scale-in)
- Action pulse animation utility
- Focus-visible outlines for a11y
- Better list item tap feedback
- Better card-body padding on mobile (14px)
- Games Hub subtitle text
- Proper game-specific icons (domino, backgammon, cards)
- Stat grid 4-column variant with mobile responsive fallback
- Gold button shimmer animation
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent f04a3f66
...@@ -3,7 +3,10 @@ ...@@ -3,7 +3,10 @@
<div class="space-y-6 bg-animated" id="games-content"> <div class="space-y-6 bg-animated" id="games-content">
<h2 class="page-title">العاب</h2> <div class="text-center">
<h2 class="page-title" style="margin-bottom:8px;">العاب</h2>
<p class="text-muted text-sm">اختر لعبتك المفضلة وابدأ</p>
</div>
<div class="games-grid"> <div class="games-grid">
...@@ -38,7 +41,7 @@ ...@@ -38,7 +41,7 @@
<!-- Coming Soon: Dominoes --> <!-- Coming Soon: Dominoes -->
<div class="game-card game-card--soon"> <div class="game-card game-card--soon">
<div class="game-card-cover game-card-cover--domino"> <div class="game-card-cover game-card-cover--domino">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-puzzle"></use></svg> <svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-domino"></use></svg>
<span class="game-card-badge">قريبا</span> <span class="game-card-badge">قريبا</span>
</div> </div>
<div class="game-card-info"> <div class="game-card-info">
...@@ -51,7 +54,7 @@ ...@@ -51,7 +54,7 @@
<!-- Coming Soon: Backgammon --> <!-- Coming Soon: Backgammon -->
<div class="game-card game-card--soon"> <div class="game-card game-card--soon">
<div class="game-card-cover game-card-cover--backgammon"> <div class="game-card-cover game-card-cover--backgammon">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-puzzle"></use></svg> <svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-backgammon"></use></svg>
<span class="game-card-badge">قريبا</span> <span class="game-card-badge">قريبا</span>
</div> </div>
<div class="game-card-info"> <div class="game-card-info">
...@@ -64,7 +67,7 @@ ...@@ -64,7 +67,7 @@
<!-- Coming Soon: Trix --> <!-- Coming Soon: Trix -->
<div class="game-card game-card--soon"> <div class="game-card game-card--soon">
<div class="game-card-cover game-card-cover--trix"> <div class="game-card-cover game-card-cover--trix">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-puzzle"></use></svg> <svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-cards"></use></svg>
<span class="game-card-badge">قريبا</span> <span class="game-card-badge">قريبا</span>
</div> </div>
<div class="game-card-info"> <div class="game-card-info">
...@@ -77,7 +80,7 @@ ...@@ -77,7 +80,7 @@
<!-- Coming Soon: Baloot --> <!-- Coming Soon: Baloot -->
<div class="game-card game-card--soon"> <div class="game-card game-card--soon">
<div class="game-card-cover game-card-cover--baloot"> <div class="game-card-cover game-card-cover--baloot">
<svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-puzzle"></use></svg> <svg class="game-card-icon"><use href="/public/icons/sprite.svg#icon-cards"></use></svg>
<span class="game-card-badge">قريبا</span> <span class="game-card-badge">قريبا</span>
</div> </div>
<div class="game-card-info"> <div class="game-card-info">
......
...@@ -19,21 +19,21 @@ ...@@ -19,21 +19,21 @@
</div> </div>
<!-- Stats Grid --> <!-- Stats Grid -->
<div class="stat-grid"> <div class="stat-grid stat-grid-4">
<div class="stat-item"> <div class="stat-card">
<div class="stat-value" id="stat-games">0</div> <div class="stat-value" id="stat-games">0</div>
<div class="stat-label">مباريات</div> <div class="stat-label">مباريات</div>
</div> </div>
<div class="stat-item"> <div class="stat-card">
<div class="stat-value" id="stat-wins">0</div> <div class="stat-value text-success" id="stat-wins">0</div>
<div class="stat-label">فوز</div> <div class="stat-label">فوز</div>
</div> </div>
<div class="stat-item"> <div class="stat-card">
<div class="stat-value" id="stat-draws">0</div> <div class="stat-value text-muted" id="stat-draws">0</div>
<div class="stat-label">تعادل</div> <div class="stat-label">تعادل</div>
</div> </div>
<div class="stat-item"> <div class="stat-card">
<div class="stat-value" id="stat-losses">0</div> <div class="stat-value text-error" id="stat-losses">0</div>
<div class="stat-label">خسارة</div> <div class="stat-label">خسارة</div>
</div> </div>
</div> </div>
......
...@@ -302,7 +302,7 @@ img { ...@@ -302,7 +302,7 @@ img {
/* Main content */ /* Main content */
.main { .main {
flex: 1; flex: 1;
padding-bottom: calc(var(--nav-bottom-h) + env(safe-area-inset-bottom)); padding-bottom: calc(var(--nav-bottom-h) + env(safe-area-inset-bottom) + 24px);
} }
.main-inner { .main-inner {
...@@ -652,10 +652,26 @@ img { ...@@ -652,10 +652,26 @@ img {
/* Empty state */ /* Empty state */
.empty-state { .empty-state {
padding: 48px 24px; padding: 56px 24px;
text-align: center; text-align: center;
color: var(--text-3); color: var(--text-3);
font-size: 14px; font-size: 14px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.empty-state .icon,
.empty-state .icon-lg {
color: var(--text-3);
opacity: 0.5;
margin-bottom: 4px;
}
.empty-state p {
max-width: 240px;
line-height: 1.5;
} }
/* Skeleton loader */ /* Skeleton loader */
...@@ -677,6 +693,16 @@ img { ...@@ -677,6 +693,16 @@ img {
gap: 8px; gap: 8px;
} }
.stat-grid-4 {
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 400px) {
.stat-grid-4 {
grid-template-columns: repeat(2, 1fr);
}
}
.stat-card { .stat-card {
background: var(--bg-2); background: var(--bg-2);
border: 1px solid var(--border); border: 1px solid var(--border);
...@@ -1065,6 +1091,176 @@ img { ...@@ -1065,6 +1091,176 @@ img {
} }
} }
/* ===== Visual Improvements ===== */
/* Better avatar placeholder */
.avatar {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--bg-3), var(--bg-2));
}
/* Profile header gradient */
.profile-card-header {
background: linear-gradient(135deg, rgba(21, 215, 255, 0.08), rgba(231, 168, 50, 0.05));
}
/* Better list items */
.list-item {
transition: background 0.15s var(--ease);
}
.list-item:active {
background: rgba(255, 255, 255, 0.03);
}
/* Better tabs */
.tab {
min-width: 64px;
text-align: center;
}
/* Section divider */
.section-divider {
height: 1px;
background: var(--border);
margin: 24px 0;
}
/* Better input focus glow */
.input:focus {
box-shadow: 0 0 0 3px rgba(21, 215, 255, 0.1);
}
/* Game mode card improvements */
.card-body {
transition: background 0.15s var(--ease);
}
/* Better button hover states */
.btn-gold:active { background: var(--gold-dark); }
.btn-cyan:active { background: var(--cyan-dark); }
/* Selection pills (time control, bot count, etc) */
.pill-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.pill {
min-width: 44px;
height: 36px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
background: var(--bg-3);
border: 1px solid var(--border);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s var(--ease);
}
.pill.active,
.pill:active {
background: var(--cyan);
border-color: var(--cyan);
color: var(--text-inverse);
}
/* Progress bar component */
.progress-bar {
width: 100%;
height: 6px;
background: var(--bg-3);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: var(--radius-full);
background: linear-gradient(90deg, var(--cyan), var(--gold));
transition: width 0.5s var(--ease);
}
/* Notification dot */
.notif-dot {
position: absolute;
top: 4px;
right: 4px;
width: 8px;
height: 8px;
background: var(--error);
border-radius: 50%;
border: 2px solid var(--bg-1);
}
/* Better card-body padding on mobile */
@media (max-width: 767px) {
.card-body { padding: 14px; }
.card-body-lg { padding: 18px; }
}
/* Divider with text */
.divider-text {
display: flex;
align-items: center;
gap: 12px;
color: var(--text-3);
font-size: 12px;
margin: 16px 0;
}
.divider-text::before,
.divider-text::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
/* Animated gradient text */
.text-gradient {
background: linear-gradient(135deg, var(--gold), var(--cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Focus visible for a11y */
.btn:focus-visible,
.tab:focus-visible,
.input:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 2px;
}
/* Game result overlay improvements */
.game-result {
animation: result-in 0.4s var(--ease);
}
@keyframes result-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
/* Haptic feedback visual pulse on game actions */
.pulse-feedback {
animation: action-pulse 0.3s var(--ease);
}
@keyframes action-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
/* ===== Win Streak Fire Badge ===== */ /* ===== Win Streak Fire Badge ===== */
.streak-fire::before { .streak-fire::before {
content: '🔥'; content: '🔥';
......
...@@ -587,21 +587,24 @@ ...@@ -587,21 +587,24 @@
.ludo-layout { .ludo-layout {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 0; padding: 0 4px;
padding-bottom: 24px;
} }
.ludo-board-column { width: 100%; } .ludo-board-column { width: 100%; }
.ludo-board-wrapper { max-width: 100%; } .ludo-board-wrapper { max-width: 100%; margin: 0 auto; }
.ludo-side-panel { display: none; } .ludo-side-panel { display: none; }
.ludo-mobile-panel { .ludo-mobile-panel {
display: flex; display: flex;
margin-top: 8px; margin-top: 12px;
margin-bottom: 24px;
padding: 16px;
} }
.ludo-chat-toggle { display: flex; } .ludo-chat-toggle { display: flex; }
.ludo-players-row { max-width: 100%; } .ludo-players-row { max-width: 100%; margin-bottom: 6px; }
} }
/* Piece land bounce */ /* Piece land bounce */
......
...@@ -199,4 +199,23 @@ ...@@ -199,4 +199,23 @@
<path d="M2 10a2 2 0 012-2h3.5a2 2 0 011.4.6L10 9.6a2 2 0 001.4.6h1.2a2 2 0 001.4-.6l1.1-1a2 2 0 011.4-.6H20a2 2 0 012 2v4a4 4 0 01-4 4H6a4 4 0 01-4-4v-4z"/> <path d="M2 10a2 2 0 012-2h3.5a2 2 0 011.4.6L10 9.6a2 2 0 001.4.6h1.2a2 2 0 001.4-.6l1.1-1a2 2 0 011.4-.6H20a2 2 0 012 2v4a4 4 0 01-4 4H6a4 4 0 01-4-4v-4z"/>
</symbol> </symbol>
<symbol id="icon-domino" viewBox="0 0 24 24">
<rect x="3" y="5" width="18" height="14" rx="2"/>
<line x1="12" y1="5" x2="12" y2="19"/>
<circle cx="7" cy="9" r="1" fill="currentColor" stroke="none"/>
<circle cx="7" cy="15" r="1" fill="currentColor" stroke="none"/>
<circle cx="17" cy="12" r="1" fill="currentColor" stroke="none"/>
</symbol>
<symbol id="icon-backgammon" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M7 3v7l2-3.5L7 3zM11 3v7l2-3.5L11 3zM17 21v-7l-2 3.5L17 21zM13 21v-7l-2 3.5L13 21z"/>
</symbol>
<symbol id="icon-cards" viewBox="0 0 24 24">
<rect x="4" y="4" width="12" height="16" rx="2"/>
<rect x="8" y="2" width="12" height="16" rx="2"/>
<path d="M14 8l-2 4 2 4"/>
</symbol>
</svg> </svg>
...@@ -194,6 +194,10 @@ const App = { ...@@ -194,6 +194,10 @@ const App = {
click: [800, 0.05, 'sine'], click: [800, 0.05, 'sine'],
dice: [350, 0.1, 'triangle'], dice: [350, 0.1, 'triangle'],
notify: [600, 0.15, 'sine'], notify: [600, 0.15, 'sine'],
home: [659, 0.2, 'sine'],
six: [700, 0.08, 'triangle'],
turn: [500, 0.06, 'sine'],
error: [200, 0.15, 'square'],
}; };
const [freq, dur, wave] = sounds[type] || sounds.click; const [freq, dur, wave] = sounds[type] || sounds.click;
osc.type = wave; osc.type = wave;
...@@ -230,6 +234,11 @@ const App = { ...@@ -230,6 +234,11 @@ const App = {
if (!main) { window.location.href = url; return; } if (!main) { window.location.href = url; return; }
main.classList.add('page-exit'); main.classList.add('page-exit');
setTimeout(() => { window.location.href = url; }, 200); setTimeout(() => { window.location.href = url; }, 200);
},
// ===== Haptic Feedback =====
vibrate(ms) {
if (navigator.vibrate) navigator.vibrate(ms || 10);
} }
}; };
...@@ -238,10 +247,11 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -238,10 +247,11 @@ document.addEventListener('DOMContentLoaded', () => {
App.loadProfile(); App.loadProfile();
} }
// Button ripple effect // Button ripple effect + haptic
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const btn = e.target.closest('.btn'); const btn = e.target.closest('.btn');
if (!btn) return; if (!btn) return;
App.vibrate(5);
const rect = btn.getBoundingClientRect(); const rect = btn.getBoundingClientRect();
const size = Math.max(rect.width, rect.height); const size = Math.max(rect.width, rect.height);
const ripple = document.createElement('span'); const ripple = document.createElement('span');
......
...@@ -577,6 +577,9 @@ const Game = { ...@@ -577,6 +577,9 @@ const Game = {
} }
this.playGameEndSound(winner === this.playerColor); this.playGameEndSound(winner === this.playerColor);
if (winner === this.playerColor && typeof App !== 'undefined' && App.celebrate) {
App.celebrate(title);
}
this.showResult(title, subtitle); this.showResult(title, subtitle);
const result = winner === this.playerColor ? 'win' : (winner ? 'loss' : 'draw'); const result = winner === this.playerColor ? 'win' : (winner ? 'loss' : 'draw');
......
...@@ -83,6 +83,10 @@ var LudoGame = (function() { ...@@ -83,6 +83,10 @@ var LudoGame = (function() {
var value = 1 + Math.floor(Math.random() * 6); var value = 1 + Math.floor(Math.random() * 6);
state.diceValue = value; state.diceValue = value;
if (typeof App !== 'undefined') {
App.playSound('dice');
App.vibrate(15);
}
UI.animateDiceRoll(value, function() { UI.animateDiceRoll(value, function() {
afterRoll(); afterRoll();
...@@ -93,6 +97,7 @@ var LudoGame = (function() { ...@@ -93,6 +97,7 @@ var LudoGame = (function() {
var player = currentPlayer(); var player = currentPlayer();
var eligible = Bot.getEligiblePieces(player, state.diceValue, state.positions, state.activePlayers); var eligible = Bot.getEligiblePieces(player, state.diceValue, state.positions, state.activePlayers);
if (state.diceValue === 6 && typeof App !== 'undefined' && App.playSound) App.playSound('six');
UI.addLogEntry(getPlayerLabel(player) + ' رمى ' + state.diceValue); UI.addLogEntry(getPlayerLabel(player) + ' رمى ' + state.diceValue);
if (eligible.length === 0) { if (eligible.length === 0) {
...@@ -157,6 +162,7 @@ var LudoGame = (function() { ...@@ -157,6 +162,7 @@ var LudoGame = (function() {
state.positions[player][pieceIdx] = newPos; state.positions[player][pieceIdx] = newPos;
UI.setPiecePosition(player, pieceIdx, newPos); UI.setPiecePosition(player, pieceIdx, newPos);
UI.updateStacking(state.positions, state.activePlayers); UI.updateStacking(state.positions, state.activePlayers);
if (typeof App !== 'undefined' && App.playSound) App.playSound('move');
var killed = checkKill(player, pieceIdx, newPos); var killed = checkKill(player, pieceIdx, newPos);
var reachedHome = newPos === C.HOME_POSITIONS[player]; var reachedHome = newPos === C.HOME_POSITIONS[player];
...@@ -165,11 +171,13 @@ var LudoGame = (function() { ...@@ -165,11 +171,13 @@ var LudoGame = (function() {
if (killed) { if (killed) {
UI.captureExplosion(killed.player, newPos); UI.captureExplosion(killed.player, newPos);
if (typeof App !== 'undefined') App.vibrate(30);
UI.addLogEntry(getPlayerLabel(player) + ' اكل قطعة ' + getPlayerLabel(killed.player)); UI.addLogEntry(getPlayerLabel(player) + ' اكل قطعة ' + getPlayerLabel(killed.player));
extraTurn = true; extraTurn = true;
} }
if (reachedHome) { if (reachedHome) {
if (typeof App !== 'undefined' && App.playSound) App.playSound('win');
UI.addLogEntry(getPlayerLabel(player) + ' وصّل قطعة للبيت!'); UI.addLogEntry(getPlayerLabel(player) + ' وصّل قطعة للبيت!');
extraTurn = true; extraTurn = true;
...@@ -256,11 +264,20 @@ var LudoGame = (function() { ...@@ -256,11 +264,20 @@ var LudoGame = (function() {
remaining.forEach(function(p) { state.winners.push(p); }); remaining.forEach(function(p) { state.winners.push(p); });
var winner = state.winners[0]; var winner = state.winners[0];
var isHumanWinner = !isCurrentPlayerBot() || winner === state.activePlayers[0];
var title = getPlayerLabel(winner) + ' فاز!'; var title = getPlayerLabel(winner) + ' فاز!';
var subtitle = 'ترتيب: ' + state.winners.map(function(p, i) { var subtitle = 'ترتيب: ' + state.winners.map(function(p, i) {
return (i + 1) + '. ' + getPlayerLabel(p); return (i + 1) + '. ' + getPlayerLabel(p);
}).join(' | '); }).join(' | ');
if (typeof App !== 'undefined') {
if (isHumanWinner) {
App.playSound('win');
App.celebrate(title);
} else {
App.playSound('lose');
}
}
UI.showResult(title, subtitle); UI.showResult(title, subtitle);
UI.addLogEntry('انتهت اللعبة! الفائز: ' + getPlayerLabel(winner)); UI.addLogEntry('انتهت اللعبة! الفائز: ' + getPlayerLabel(winner));
......
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