Commit 00ea47bc authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: branding admin fully connected to live system

Theme loader (theme.js):
- Reads theme.json on boot, applies CSS custom properties
- All colors, animations, button shapes, and radii are live-editable
- assetImg() helper renders uploaded assets at exact expected size
  with object-fit:contain (prevents pixelation/oversizing)

Admin panel updates:
- Button shapes section: radius, height, font weight, shadow, borders
- 25 asset upload slots (up from 19) including tab bar icons
- Each slot shows expected dimensions + usage hint in Arabic
- Upload accepts SVG, PNG, JPG, WebP, GIF
- Preview renders at exact expected size (not pixelated, not huge)
- Old files for same slot are cleaned up on re-upload

CSS connected to theme variables:
- --btn-press-scale, --btn-min-height, --btn-weight, --btn-shadow
- --r-full (button radius), --r-btn-secondary, --r-input
- --card-border-width
- All existing color variables mapped from theme.json
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c64ac838
This diff is collapsed.
...@@ -153,7 +153,7 @@ html, body { ...@@ -153,7 +153,7 @@ html, body {
/* Cards */ /* Cards */
.card { .card {
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border); border: var(--card-border-width, 1px) solid var(--border);
border-radius: var(--r-xl); border-radius: var(--r-xl);
padding: var(--s-4); padding: var(--s-4);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
...@@ -170,27 +170,28 @@ html, body { ...@@ -170,27 +170,28 @@ html, body {
gap: var(--s-2); gap: var(--s-2);
padding: var(--s-3) var(--s-5); padding: var(--s-3) var(--s-5);
border: none; border: none;
border-radius: var(--r-full); border-radius: var(--r-full, 9999px);
font-family: var(--font-ar); font-family: var(--font-ar);
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: var(--btn-weight, 700);
cursor: pointer; cursor: pointer;
transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast); transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast);
min-height: 44px; min-height: var(--btn-min-height, 44px);
} }
.btn:active { transform: scale(0.92); transition-duration: 50ms; } .btn:active { transform: scale(var(--btn-press-scale, 0.92)); transition-duration: 50ms; }
.btn-primary { .btn-primary {
background: linear-gradient(135deg, var(--gold), var(--gold-soft)); background: linear-gradient(135deg, var(--gold), var(--gold-soft));
color: #1a1a1a; color: #1a1a1a;
box-shadow: 0 4px 16px rgba(228,172,56,0.3); box-shadow: var(--btn-shadow, 0 4px 16px rgba(228,172,56,0.3));
} }
.btn-secondary { .btn-secondary {
background: var(--bg-elevated); background: var(--bg-elevated);
color: var(--text-primary); color: var(--text-primary);
border: 1px solid var(--border-hover); border: var(--card-border-width, 1px) solid var(--border-hover);
border-radius: var(--r-btn-secondary, var(--r-full, 9999px));
} }
.btn-game { .btn-game {
...@@ -206,14 +207,14 @@ html, body { ...@@ -206,14 +207,14 @@ html, body {
width: 100%; width: 100%;
padding: var(--s-3) var(--s-4); padding: var(--s-3) var(--s-4);
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--border); border: var(--card-border-width, 1px) solid var(--border);
border-radius: var(--r-md); border-radius: var(--r-input, var(--r-md));
color: var(--text-primary); color: var(--text-primary);
font-family: var(--font-ar); font-family: var(--font-ar);
font-size: 15px; font-size: 15px;
outline: none; outline: none;
transition: border-color var(--dur-fast); transition: border-color var(--dur-fast);
min-height: 44px; min-height: var(--btn-min-height, 44px);
} }
.input:focus { border-color: var(--gold); } .input:focus { border-color: var(--gold); }
......
const THEME_URL = '/public/assets/brand/theme.json';
let themeData = null;
export async function load() {
try {
const res = await fetch(THEME_URL + '?t=' + Date.now());
if (!res.ok) return;
themeData = await res.json();
applyColors();
applyAnimations();
} catch (e) {}
}
export function getAsset(slot, fallback) {
if (themeData?.assets?.[slot]) return themeData.assets[slot];
return fallback || null;
}
export function getColor(key, fallback) {
return themeData?.[key] || fallback;
}
function applyColors() {
if (!themeData) return;
const root = document.documentElement.style;
const map = {
bg_deep: '--bg-deep',
bg_base: '--bg-base',
bg_card: '--bg-card',
bg_elevated: '--bg-elevated',
gold: '--gold',
gold_soft: '--gold-soft',
blue: '--blue',
cyan: '--cyan',
purple: '--purple',
success: '--success',
error: '--error',
text_primary: '--text-primary',
text_secondary: '--text-secondary',
chess_primary: '--chess-primary',
chess_secondary: '--chess-secondary',
domino_primary: '--domino-primary',
domino_secondary: '--domino-secondary',
ludo_primary: '--ludo-primary',
ludo_secondary: '--ludo-secondary',
backgammon_primary: '--backgammon-primary',
backgammon_secondary: '--backgammon-secondary'
};
for (const [key, cssVar] of Object.entries(map)) {
if (themeData[key]) {
root.setProperty(cssVar, themeData[key]);
}
}
}
function applyAnimations() {
if (!themeData) return;
const root = document.documentElement.style;
if (themeData.anim_speed) root.setProperty('--dur-normal', themeData.anim_speed + 'ms');
if (themeData.anim_fast) root.setProperty('--dur-fast', themeData.anim_fast + 'ms');
if (themeData.anim_slow) root.setProperty('--dur-slow', themeData.anim_slow + 'ms');
if (themeData.border_radius) root.setProperty('--r-md', themeData.border_radius + 'px');
if (themeData.card_radius) root.setProperty('--r-xl', themeData.card_radius + 'px');
if (themeData.btn_scale) root.setProperty('--btn-press-scale', themeData.btn_scale);
if (themeData.btn_radius) root.setProperty('--r-full', themeData.btn_radius + 'px');
if (themeData.btn_secondary_radius) root.setProperty('--r-btn-secondary', themeData.btn_secondary_radius + 'px');
if (themeData.btn_height) root.setProperty('--btn-min-height', themeData.btn_height + 'px');
if (themeData.btn_font_weight) root.setProperty('--btn-weight', themeData.btn_font_weight);
if (themeData.btn_shadow) root.setProperty('--btn-shadow', themeData.btn_shadow);
if (themeData.card_border_width) root.setProperty('--card-border-width', themeData.card_border_width + 'px');
if (themeData.input_radius) root.setProperty('--r-input', themeData.input_radius + 'px');
}
export function assetImg(slot, fallbackEmoji, width, height) {
const url = getAsset(slot);
if (url) {
return `<img src="${url}" alt="" style="width:${width}px;height:${height}px;object-fit:contain;image-rendering:auto;" draggable="false">`;
}
return `<span style="font-size:${Math.min(width, height) * 0.7}px;line-height:1;">${fallbackEmoji}</span>`;
}
...@@ -4,10 +4,12 @@ import * as scene from './core/scene.js'; ...@@ -4,10 +4,12 @@ import * as scene from './core/scene.js';
import * as audio from './core/audio.js'; import * as audio from './core/audio.js';
import * as input from './core/input.js'; import * as input from './core/input.js';
import * as hud from './core/hud.js'; import * as hud from './core/hud.js';
import * as theme from './core/theme.js';
import { setLang } from './core/i18n.js'; import { setLang } from './core/i18n.js';
async function boot() { async function boot() {
setLang(store.get('language') || 'ar'); setLang(store.get('language') || 'ar');
await theme.load();
audio.init(); audio.init();
input.init(); input.init();
scene.init(); scene.init();
......
...@@ -3,6 +3,7 @@ import * as scene from '../../../core/scene.js'; ...@@ -3,6 +3,7 @@ import * as scene from '../../../core/scene.js';
import * as store from '../../../core/store.js'; import * as store from '../../../core/store.js';
import * as audio from '../../../core/audio.js'; import * as audio from '../../../core/audio.js';
import { t } from '../../../core/i18n.js'; import { t } from '../../../core/i18n.js';
import { assetImg } from '../../../core/theme.js';
const games = [ const games = [
{ key: 'chess', name: 'شطرنج', nameEn: 'Chess', color: '#2563EB', secondary: '#F5B731', icon: '♟', gradient: 'linear-gradient(135deg, #1e40af, #3b82f6)' }, { key: 'chess', name: 'شطرنج', nameEn: 'Chess', color: '#2563EB', secondary: '#F5B731', icon: '♟', gradient: 'linear-gradient(135deg, #1e40af, #3b82f6)' },
...@@ -19,7 +20,7 @@ export function mountTable(el) { ...@@ -19,7 +20,7 @@ export function mountTable(el) {
<div class="game-tile" data-game="${g.key}" style="--game-color:${g.color};--game-gradient:${g.gradient};"> <div class="game-tile" data-game="${g.key}" style="--game-color:${g.color};--game-gradient:${g.gradient};">
<div class="game-tile-bg" style="background:${g.gradient};"></div> <div class="game-tile-bg" style="background:${g.gradient};"></div>
<div class="game-tile-content"> <div class="game-tile-content">
<div class="game-tile-icon">${g.icon}</div> <div class="game-tile-icon">${assetImg(g.key + '_icon', g.icon, 48, 48)}</div>
<div class="game-tile-name">${g.name}</div> <div class="game-tile-name">${g.name}</div>
</div> </div>
</div> </div>
......
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