Commit 9b743e4f authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(branding): edit the colours the game reads; say which images it uses

Both apps keep their theme in platform_theme, but the game only applies the
keys in its theme.js COLOR_KEYS (back_*, card, ink, gold, board_*, ludo_*,
bg_* ...). The palette this page edited (color_primary, surface_* ...) is
the manager's own theme, so no change here ever reached a player. A new
"app colours" tab edits exactly the game's keys, in groups with Arabic
names, showing each one's default read from the game's tokens.css; reset
removes the override so the app falls back to its design. The manager
palette stays, labelled as the manager's.

Every asset slot is marked as read by the game, read only for uploads made
after 2026-10-19 (logo, tab icons), or ignored (the old emoji slots and
the game-icon slots the app now draws itself), and new uploads may create
any slot the game reads (icon_*, chess_piece_*, chess_bot_*,
emoji_dice_1..6). Uploads go through SupabaseStorage: the hand-written
request lacked the apikey header and Kong refused every upload.

Flash messages are JSON-encoded into the toast script; a multi-line
message (such as a storage error) used to break the script.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent c0f05e75
...@@ -195,6 +195,8 @@ return [ ...@@ -195,6 +195,8 @@ return [
'branding/update-color' => ['module' => 'branding', 'action' => 'updateColor'], 'branding/update-color' => ['module' => 'branding', 'action' => 'updateColor'],
'branding/reset-colors' => ['module' => 'branding', 'action' => 'resetColors'], 'branding/reset-colors' => ['module' => 'branding', 'action' => 'resetColors'],
'branding/upload-asset' => ['module' => 'branding', 'action' => 'uploadAsset'], 'branding/upload-asset' => ['module' => 'branding', 'action' => 'uploadAsset'],
'branding/game-color' => ['module' => 'branding', 'action' => 'updateGameColor'],
'branding/game-color/reset' => ['module' => 'branding', 'action' => 'resetGameColor'],
// Analytics // Analytics
'analytics' => ['module' => 'analytics', 'action' => 'index'], 'analytics' => ['module' => 'analytics', 'action' => 'index'],
......
...@@ -25,6 +25,12 @@ class ThemeService ...@@ -25,6 +25,12 @@ class ThemeService
'text_tertiary' => '--text-muted', 'text_tertiary' => '--text-muted',
]; ];
/** Is this platform_theme key part of the manager's own palette? */
public static function isManagerKey(string $id): bool
{
return isset(self::$idToCssVar[$id]);
}
public static function getColors(): array public static function getColors(): array
{ {
if (self::$colors !== null) { if (self::$colors !== null) {
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
<?php if ($flash): ?> <?php if ($flash): ?>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
showToast('<?= View::e($flash['message']) ?>', '<?= $flash['type'] ?>'); showToast(<?= json_encode((string)$flash['message'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>, <?= json_encode((string)$flash['type']) ?>);
}); });
</script> </script>
<?php endif; ?> <?php endif; ?>
......
This diff is collapsed.
...@@ -3,14 +3,61 @@ ...@@ -3,14 +3,61 @@
</div> </div>
<div class="tabs mb-5"> <div class="tabs mb-5">
<button class="tab active" onclick="switchBrandTab(this, 'customizeTab')">تخصيص الواجهة</button> <button class="tab active" onclick="switchBrandTab(this, 'gameColorsTab')">ألوان التطبيق</button>
<button class="tab" onclick="switchBrandTab(this, 'colorsTab')">ألوان المنصة</button> <button class="tab" onclick="switchBrandTab(this, 'assetsTab')">صور وأيقونات التطبيق</button>
<button class="tab" onclick="switchBrandTab(this, 'assetsTab')">الأصول المرئية</button> <button class="tab" onclick="switchBrandTab(this, 'colorsTab')">ألوان لوحة الإدارة</button>
<button class="tab" onclick="switchBrandTab(this, 'previewTab')">معاينة حية</button> <button class="tab" onclick="switchBrandTab(this, 'customizeTab')">تخصيص لوحة الإدارة</button>
<button class="tab" onclick="switchBrandTab(this, 'previewTab')">معاينة</button>
</div>
<!-- Game Colors Tab -->
<div id="gameColorsTab" class="tab-content">
<div class="card mb-4 p-4">
<p class="text-sm">هذه هي الألوان التي يسمح التطبيق بتغييرها (تصميم Night Arcade). اللون غير المحدد يأخذ قيمته من تصميم التطبيق. يرى اللاعبون التغيير عند فتح التطبيق.</p>
<p class="text-sm text-muted mt-2">يمكن أيضًا تعديلها من لوحة الهوية في التطبيق نفسه: <a href="<?= View::e($gameAdminUrl) ?>" target="_blank" rel="noopener" class="text-link" dir="ltr"><?= View::e($gameAdminUrl) ?></a> — الطرفان يكتبان القيم نفسها.</p>
</div>
<?php foreach ($gameColors as $group => $items): ?>
<div class="card mb-4">
<div class="card-header"><h3 class="card-title"><?= View::e($group) ?></h3></div>
<div class="grid grid-3 gap-3 p-4">
<?php foreach ($items as $c): ?>
<?php $shown = $c['value'] ?: ($c['default'] ?: '#000000'); ?>
<div class="card card-inner p-3">
<div class="flex items-center gap-3">
<div style="width: 40px; height: 40px; border-radius: var(--radius-md); background: <?= View::e($shown) ?>; border: 1px solid var(--border); flex-shrink: 0;"></div>
<div class="flex-1">
<div class="font-medium text-sm"><?= View::e($c['label']) ?></div>
<div class="text-xs text-muted" dir="ltr"><?= View::e($c['id']) ?></div>
<div class="text-xs <?= $c['value'] ? 'text-warning' : 'text-muted' ?>">
<?= $c['value'] ? 'مخصص' : 'الافتراضي' ?><?= $c['default'] ? ' — الافتراضي ' . View::e($c['default']) : '' ?>
</div>
</div>
</div>
<div class="flex items-center gap-2 mt-3">
<form method="POST" action="/branding/game-color" class="flex items-center gap-2">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<input type="hidden" name="id" value="<?= View::e($c['id']) ?>">
<input type="color" name="value" value="<?= View::e($shown) ?>" aria-label="<?= View::e($c['label']) ?>" style="width: 40px; height: 32px; border: none; cursor: pointer;">
<button type="submit" class="btn btn-primary btn-sm">حفظ</button>
</form>
<?php if ($c['value']): ?>
<form method="POST" action="/branding/game-color/reset">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<input type="hidden" name="id" value="<?= View::e($c['id']) ?>">
<button type="submit" class="btn btn-ghost btn-sm">الافتراضي</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div> </div>
<!-- Customization Tab --> <!-- Customization Tab -->
<div id="customizeTab" class="tab-content"> <div id="customizeTab" class="tab-content hidden">
<p class="text-sm text-muted mb-4">تفضيلات شكل لوحة الإدارة على هذا المتصفح فقط.</p>
<div class="customize-grid"> <div class="customize-grid">
<!-- 1. Sidebar Width --> <!-- 1. Sidebar Width -->
<div class="customize-card"> <div class="customize-card">
...@@ -276,7 +323,7 @@ ...@@ -276,7 +323,7 @@
<div class="card"><div class="empty-state"><h3 class="empty-state-title">لا توجد ألوان محفوظة</h3><p class="empty-state-text">أضف ألوان العلامة التجارية من جدول platform_theme</p></div></div> <div class="card"><div class="empty-state"><h3 class="empty-state-title">لا توجد ألوان محفوظة</h3><p class="empty-state-text">أضف ألوان العلامة التجارية من جدول platform_theme</p></div></div>
<?php else: ?> <?php else: ?>
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<p class="text-sm text-secondary">ألوان هوية El3ab — عدّل أي لون وسيُطبّق مباشرة على الواجهة</p> <p class="text-sm text-secondary">ألوان لوحة الإدارة هذه فقط. التطبيق لا يقرأ هذه المفاتيح؛ ألوانه في تبويب "ألوان التطبيق".</p>
<form method="POST" action="/branding/reset-colors" style="margin:0;"> <form method="POST" action="/branding/reset-colors" style="margin:0;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>"> <input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="btn btn-ghost btn-sm" onclick="return confirm('هل أنت متأكد؟ سيتم إعادة جميع الألوان إلى الافتراضي.')"> <button type="submit" class="btn btn-ghost btn-sm" onclick="return confirm('هل أنت متأكد؟ سيتم إعادة جميع الألوان إلى الافتراضي.')">
...@@ -310,12 +357,27 @@ ...@@ -310,12 +357,27 @@
<!-- Assets Tab --> <!-- Assets Tab -->
<div id="assetsTab" class="tab-content hidden"> <div id="assetsTab" class="tab-content hidden">
<?php $stateLabels = ['live' => ['يقرؤه التطبيق', 'badge-success'], 'conditional' => ['بشرط', 'badge-warning'], 'ignored' => ['لا يقرؤه التطبيق', 'badge-default']]; ?>
<div class="card mb-4 p-4">
<p class="text-sm">الخانات التي يقرؤها التطبيق: قطع الشطرنج (chess_piece_wK ...)، صور البوتات (chess_bot_المعرف)، أي أيقونة (icon_الاسم، مثل icon_coin و icon_crown)، صور التفاعلات (icon_emote_...)، وجوه نرد اللودو (emoji_dice_1 إلى 6)، والأصوات (sfx_...). الشعار وأيقونات الشريط السفلي (tab_...) تُقرأ فقط إن رُفعت بعد 19 أكتوبر 2026. باقي الخانات القديمة، ومنها خانات الرموز التعبيرية، لا يقرؤها التطبيق.</p>
<form method="POST" action="/branding/upload-asset" enctype="multipart/form-data" class="flex gap-2 items-end flex-wrap mt-3">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="form-group" style="margin-bottom:0;">
<label class="form-label">رفع لخانة جديدة</label>
<input type="text" name="id" class="form-input" required dir="ltr" placeholder="icon_coin" pattern="[a-zA-Z0-9_-]+">
</div>
<input type="file" name="asset" accept="image/png,image/jpeg,image/svg+xml,image/webp" class="form-input" style="font-size: 12px; max-width: 260px;" required>
<button type="submit" class="btn btn-primary btn-sm">رفع</button>
</form>
<p class="text-xs text-muted mt-2">الأصوات تُرفع من لوحة الهوية في التطبيق لأنها ليست صورًا.</p>
</div>
<?php if (empty($assets)): ?> <?php if (empty($assets)): ?>
<div class="card"><div class="empty-state"><h3 class="empty-state-title">لا توجد أصول مرئية</h3></div></div> <div class="card"><div class="empty-state"><h3 class="empty-state-title">لا توجد أصول مرئية</h3></div></div>
<?php else: ?> <?php else: ?>
<div class="grid grid-3"> <div class="grid grid-3">
<?php foreach ($assets as $asset): ?> <?php foreach ($assets as $asset): ?>
<div class="card"> <?php $st = $stateLabels[$asset['game_state']] ?? $stateLabels['ignored']; ?>
<div class="card" style="<?= $asset['game_state'] === 'ignored' ? 'opacity: 0.7;' : '' ?>">
<div class="mb-3" style="height: 100px; border-radius: var(--radius-md); background: var(--bg-primary); display: flex; align-items: center; justify-content: center; overflow: hidden;"> <div class="mb-3" style="height: 100px; border-radius: var(--radius-md); background: var(--bg-primary); display: flex; align-items: center; justify-content: center; overflow: hidden;">
<?php if ($asset['asset_url']): ?> <?php if ($asset['asset_url']): ?>
<img src="<?= View::e($asset['asset_url']) ?>" style="max-height: 100%; max-width: 100%; object-fit: contain;" alt=""> <img src="<?= View::e($asset['asset_url']) ?>" style="max-height: 100%; max-width: 100%; object-fit: contain;" alt="">
...@@ -323,8 +385,12 @@ ...@@ -323,8 +385,12 @@
<span class="text-muted text-xs">لا يوجد ملف</span> <span class="text-muted text-xs">لا يوجد ملف</span>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="flex items-center justify-between gap-2">
<h4 class="font-medium text-sm"><?= View::e($asset['label_ar'] ?? $asset['label']) ?></h4> <h4 class="font-medium text-sm"><?= View::e($asset['label_ar'] ?? $asset['label']) ?></h4>
<p class="text-xs text-muted mb-3"><?= View::e($asset['id']) ?></p> <span class="badge <?= $st[1] ?> badge-sm"><?= $st[0] ?></span>
</div>
<p class="text-xs text-muted" dir="ltr"><?= View::e($asset['id']) ?></p>
<p class="text-xs text-secondary mb-3"><?= View::e($asset['game_note']) ?><?php if (!empty($asset['updated_at'])): ?> — آخر رفع <?= Time::show($asset['updated_at'], 'Y/m/d') ?><?php endif; ?></p>
<form method="POST" action="/branding/upload-asset" enctype="multipart/form-data"> <form method="POST" action="/branding/upload-asset" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>"> <input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<input type="hidden" name="id" value="<?= View::e($asset['id']) ?>"> <input type="hidden" name="id" value="<?= View::e($asset['id']) ?>">
......
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