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 @@
<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">
......@@ -38,7 +41,7 @@
<!-- Coming Soon: Dominoes -->
<div class="game-card game-card--soon">
<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>
</div>
<div class="game-card-info">
......@@ -51,7 +54,7 @@
<!-- Coming Soon: Backgammon -->
<div class="game-card game-card--soon">
<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>
</div>
<div class="game-card-info">
......@@ -64,7 +67,7 @@
<!-- Coming Soon: Trix -->
<div class="game-card game-card--soon">
<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>
</div>
<div class="game-card-info">
......@@ -77,7 +80,7 @@
<!-- Coming Soon: Baloot -->
<div class="game-card game-card--soon">
<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>
</div>
<div class="game-card-info">
......
......@@ -19,21 +19,21 @@
</div>
<!-- Stats Grid -->
<div class="stat-grid">
<div class="stat-item">
<div class="stat-grid stat-grid-4">
<div class="stat-card">
<div class="stat-value" id="stat-games">0</div>
<div class="stat-label">مباريات</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-wins">0</div>
<div class="stat-card">
<div class="stat-value text-success" id="stat-wins">0</div>
<div class="stat-label">فوز</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-draws">0</div>
<div class="stat-card">
<div class="stat-value text-muted" id="stat-draws">0</div>
<div class="stat-label">تعادل</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-losses">0</div>
<div class="stat-card">
<div class="stat-value text-error" id="stat-losses">0</div>
<div class="stat-label">خسارة</div>
</div>
</div>
......
......@@ -302,7 +302,7 @@ img {
/* Main content */
.main {
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 {
......@@ -652,10 +652,26 @@ img {
/* Empty state */
.empty-state {
padding: 48px 24px;
padding: 56px 24px;
text-align: center;
color: var(--text-3);
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 */
......@@ -677,6 +693,16 @@ img {
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 {
background: var(--bg-2);
border: 1px solid var(--border);
......@@ -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 ===== */
.streak-fire::before {
content: '🔥';
......
......@@ -587,21 +587,24 @@
.ludo-layout {
flex-direction: column;
align-items: center;
padding: 0;
padding: 0 4px;
padding-bottom: 24px;
}
.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-mobile-panel {
display: flex;
margin-top: 8px;
margin-top: 12px;
margin-bottom: 24px;
padding: 16px;
}
.ludo-chat-toggle { display: flex; }
.ludo-players-row { max-width: 100%; }
.ludo-players-row { max-width: 100%; margin-bottom: 6px; }
}
/* Piece land bounce */
......
......@@ -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"/>
</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>
......@@ -194,6 +194,10 @@ const App = {
click: [800, 0.05, 'sine'],
dice: [350, 0.1, 'triangle'],
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;
osc.type = wave;
......@@ -230,6 +234,11 @@ const App = {
if (!main) { window.location.href = url; return; }
main.classList.add('page-exit');
setTimeout(() => { window.location.href = url; }, 200);
},
// ===== Haptic Feedback =====
vibrate(ms) {
if (navigator.vibrate) navigator.vibrate(ms || 10);
}
};
......@@ -238,10 +247,11 @@ document.addEventListener('DOMContentLoaded', () => {
App.loadProfile();
}
// Button ripple effect
// Button ripple effect + haptic
document.addEventListener('click', (e) => {
const btn = e.target.closest('.btn');
if (!btn) return;
App.vibrate(5);
const rect = btn.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const ripple = document.createElement('span');
......
......@@ -577,6 +577,9 @@ const Game = {
}
this.playGameEndSound(winner === this.playerColor);
if (winner === this.playerColor && typeof App !== 'undefined' && App.celebrate) {
App.celebrate(title);
}
this.showResult(title, subtitle);
const result = winner === this.playerColor ? 'win' : (winner ? 'loss' : 'draw');
......
......@@ -83,6 +83,10 @@ var LudoGame = (function() {
var value = 1 + Math.floor(Math.random() * 6);
state.diceValue = value;
if (typeof App !== 'undefined') {
App.playSound('dice');
App.vibrate(15);
}
UI.animateDiceRoll(value, function() {
afterRoll();
......@@ -93,6 +97,7 @@ var LudoGame = (function() {
var player = currentPlayer();
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);
if (eligible.length === 0) {
......@@ -157,6 +162,7 @@ var LudoGame = (function() {
state.positions[player][pieceIdx] = newPos;
UI.setPiecePosition(player, pieceIdx, newPos);
UI.updateStacking(state.positions, state.activePlayers);
if (typeof App !== 'undefined' && App.playSound) App.playSound('move');
var killed = checkKill(player, pieceIdx, newPos);
var reachedHome = newPos === C.HOME_POSITIONS[player];
......@@ -165,11 +171,13 @@ var LudoGame = (function() {
if (killed) {
UI.captureExplosion(killed.player, newPos);
if (typeof App !== 'undefined') App.vibrate(30);
UI.addLogEntry(getPlayerLabel(player) + ' اكل قطعة ' + getPlayerLabel(killed.player));
extraTurn = true;
}
if (reachedHome) {
if (typeof App !== 'undefined' && App.playSound) App.playSound('win');
UI.addLogEntry(getPlayerLabel(player) + ' وصّل قطعة للبيت!');
extraTurn = true;
......@@ -256,11 +264,20 @@ var LudoGame = (function() {
remaining.forEach(function(p) { state.winners.push(p); });
var winner = state.winners[0];
var isHumanWinner = !isCurrentPlayerBot() || winner === state.activePlayers[0];
var title = getPlayerLabel(winner) + ' فاز!';
var subtitle = 'ترتيب: ' + state.winners.map(function(p, i) {
return (i + 1) + '. ' + getPlayerLabel(p);
}).join(' | ');
if (typeof App !== 'undefined') {
if (isHumanWinner) {
App.playSound('win');
App.celebrate(title);
} else {
App.playSound('lose');
}
}
UI.showResult(title, subtitle);
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