Commit 79079d7a authored by Mahmoud Aglan's avatar Mahmoud Aglan

css update

parent a8bfc7e5
...@@ -14,7 +14,8 @@ MenuRegistry::register('branches_settings', [ ...@@ -14,7 +14,8 @@ MenuRegistry::register('branches_settings', [
'children' => [ 'children' => [
['label_ar' => 'الفروع', 'label_en' => 'Branches', 'route' => '/branches', 'permission' => 'settings.view', 'order' => 1], ['label_ar' => 'الفروع', 'label_en' => 'Branches', 'route' => '/branches', 'permission' => 'settings.view', 'order' => 1],
['label_ar' => 'إعدادات النظام', 'label_en' => 'Settings', 'route' => '/settings', 'permission' => 'settings.view', 'order' => 2], ['label_ar' => 'إعدادات النظام', 'label_en' => 'Settings', 'route' => '/settings', 'permission' => 'settings.view', 'order' => 2],
['label_ar' => 'العلامة التجارية', 'label_en' => 'Branding', 'route' => '/settings/branding', 'permission' => 'settings.edit', 'order' => 3], ['label_ar' => 'المظهر والتخصيص', 'label_en' => 'Appearance', 'route' => '/settings/appearance', 'permission' => 'settings.edit', 'order' => 3],
['label_ar' => 'العلامة التجارية', 'label_en' => 'Branding', 'route' => '/settings/branding', 'permission' => 'settings.edit', 'order' => 4],
['label_ar' => 'سجل المراجعة', 'label_en' => 'Audit Log', 'route' => '/audit', 'permission' => 'report.view_audit', 'order' => 4], ['label_ar' => 'سجل المراجعة', 'label_en' => 'Audit Log', 'route' => '/audit', 'permission' => 'report.view_audit', 'order' => 4],
['label_ar' => 'التنبيهات', 'label_en' => 'Alerts', 'route' => '/alerts', 'permission' => 'alert.view', 'order' => 5], ['label_ar' => 'التنبيهات', 'label_en' => 'Alerts', 'route' => '/alerts', 'permission' => 'alert.view', 'order' => 5],
], ],
......
<?php
declare(strict_types=1);
namespace App\Modules\Settings\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\Settings\Models\SystemConfig;
class AppearanceController extends Controller
{
private const DEFAULTS = [
'appearance.brand_color' => '#0D7377',
'appearance.accent_color' => '#6366f1',
'appearance.sidebar_style' => 'dark',
'appearance.sidebar_width' => 'normal',
'appearance.card_radius' => 'rounded',
'appearance.font_size' => 'normal',
'appearance.animation' => 'full',
'appearance.header_style' => 'clean',
'appearance.table_style' => 'striped',
'appearance.density' => 'comfortable',
];
private const COLOR_PRESETS = [
['label' => 'تيل كلاسيك', 'value' => '#0D7377'],
['label' => 'أزرق ملكي', 'value' => '#2563EB'],
['label' => 'بنفسجي أنيق', 'value' => '#7C3AED'],
['label' => 'أخضر زمردي', 'value' => '#059669'],
['label' => 'وردي عصري', 'value' => '#DB2777'],
['label' => 'برتقالي دافئ', 'value' => '#EA580C'],
['label' => 'رمادي حديث', 'value' => '#475569'],
['label' => 'ذهبي فاخر', 'value' => '#B45309'],
];
public function index(Request $request): Response
{
$this->authorize('settings.edit');
$settings = [];
foreach (self::DEFAULTS as $key => $default) {
$row = SystemConfig::get($key);
$settings[str_replace('appearance.', '', $key)] = $row ? $row['config_value'] : $default;
}
return $this->view('Settings.Views.appearance', [
'settings' => $settings,
'colorPresets' => self::COLOR_PRESETS,
]);
}
public function update(Request $request): Response
{
$this->authorize('settings.edit');
$fields = [
'brand_color' => $_POST['brand_color'] ?? '',
'accent_color' => $_POST['accent_color'] ?? '',
'sidebar_style' => $_POST['sidebar_style'] ?? 'dark',
'sidebar_width' => $_POST['sidebar_width'] ?? 'normal',
'card_radius' => $_POST['card_radius'] ?? 'rounded',
'font_size' => $_POST['font_size'] ?? 'normal',
'animation' => $_POST['animation'] ?? 'full',
'header_style' => $_POST['header_style'] ?? 'clean',
'table_style' => $_POST['table_style'] ?? 'striped',
'density' => $_POST['density'] ?? 'comfortable',
];
foreach ($fields as $name => $value) {
$key = 'appearance.' . $name;
if ($name === 'brand_color' || $name === 'accent_color') {
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $value)) {
$value = self::DEFAULTS[$key];
}
}
SystemConfig::set($key, $value);
}
return $this->redirect('/settings/appearance')->withSuccess('تم حفظ إعدادات المظهر بنجاح');
}
public static function getThemeVars(): array
{
$vars = [];
foreach (self::DEFAULTS as $key => $default) {
$row = SystemConfig::get($key);
$name = str_replace('appearance.', '', $key);
$vars[$name] = $row ? $row['config_value'] : $default;
}
return $vars;
}
public static function getCssOverrides(): string
{
$v = self::getThemeVars();
$css = ':root{';
if ($v['brand_color'] !== '#0D7377') {
$hex = $v['brand_color'];
$r = hexdec(substr($hex, 1, 2));
$g = hexdec(substr($hex, 3, 2));
$b = hexdec(substr($hex, 5, 2));
$css .= "--brand-primary:{$hex};";
$css .= "--brand-primary-rgb:{$r},{$g},{$b};";
$css .= "--brand-primary-light:{$hex}cc;";
$css .= "--brand-primary-dark:{$hex};";
$css .= "--border-focus:{$hex};";
}
if ($v['accent_color'] !== '#6366f1') {
$hex = $v['accent_color'];
$r = hexdec(substr($hex, 1, 2));
$g = hexdec(substr($hex, 3, 2));
$b = hexdec(substr($hex, 5, 2));
$css .= "--brand-accent:{$hex};";
$css .= "--brand-accent-rgb:{$r},{$g},{$b};";
}
$radiusMap = ['sharp' => '4px', 'rounded' => '10px', 'pill' => '18px'];
if (isset($radiusMap[$v['card_radius']])) {
$r = $radiusMap[$v['card_radius']];
$css .= "--radius-md:{$r};";
}
$fontMap = ['compact' => '13px', 'normal' => '14px', 'large' => '15px'];
if (isset($fontMap[$v['font_size']])) {
$css .= "--font-size-base:{$fontMap[$v['font_size']]};";
}
$widthMap = ['narrow' => '240px', 'normal' => '270px', 'wide' => '300px'];
if (isset($widthMap[$v['sidebar_width']])) {
$css .= "--sidebar-width:{$widthMap[$v['sidebar_width']]};";
}
$css .= '}';
if ($v['animation'] === 'none') {
$css .= '*,*::before,*::after{animation-duration:0s!important;transition-duration:0s!important;}';
} elseif ($v['animation'] === 'subtle') {
$css .= ':root{--duration-fast:100ms;--duration-normal:150ms;--duration-slow:200ms;}';
}
if ($v['sidebar_style'] === 'light') {
$css .= ':root{--sidebar-bg:#ffffff;--sidebar-item-hover:rgba(0,0,0,0.04);--sidebar-item-active:rgba(13,115,119,0.08);--sidebar-border:rgba(0,0,0,0.06);}';
$css .= '.sidebar{border-left:1px solid var(--border-light);}.sidebar .sidebar-brand-name,.sidebar .sidebar-item-label,.sidebar .sidebar-section-title{color:#1A1A2E!important;}.sidebar .sidebar-item i{color:#64748B!important;}.sidebar .sidebar-item.active i{color:var(--brand-primary)!important;}';
} elseif ($v['sidebar_style'] === 'gradient') {
$css .= ':root{--sidebar-bg:linear-gradient(180deg,#0f0f1a 0%,#1a1a3e 100%);}';
$css .= '.sidebar{background:var(--sidebar-bg);}';
}
if ($v['density'] === 'compact') {
$css .= '.page-content{padding:16px;}.card{padding:14px;}.data-table td,.data-table th{padding:8px 12px;}.form-input,.form-select,.form-textarea{padding:7px 12px;}';
} elseif ($v['density'] === 'spacious') {
$css .= '.page-content{padding:32px;}.card{padding:24px;}.data-table td,.data-table th{padding:14px 18px;}';
}
if ($v['table_style'] === 'bordered') {
$css .= '.data-table td,.data-table th{border:1px solid var(--border-light);}';
} elseif ($v['table_style'] === 'minimal') {
$css .= '.data-table thead{background:transparent;}.data-table th{border-bottom:2px solid var(--border-light);background:transparent;}.data-table td{border:none;}';
}
return $css;
}
}
...@@ -6,6 +6,10 @@ return [ ...@@ -6,6 +6,10 @@ return [
['GET', '/settings/group/{group}', 'Settings\Controllers\SettingsController@editGroup', ['auth'], 'settings.edit'], ['GET', '/settings/group/{group}', 'Settings\Controllers\SettingsController@editGroup', ['auth'], 'settings.edit'],
['POST', '/settings/group/{group}', 'Settings\Controllers\SettingsController@updateGroup', ['auth', 'csrf'], 'settings.edit'], ['POST', '/settings/group/{group}', 'Settings\Controllers\SettingsController@updateGroup', ['auth', 'csrf'], 'settings.edit'],
// Appearance
['GET', '/settings/appearance', 'Settings\Controllers\AppearanceController@index', ['auth'], 'settings.edit'],
['POST', '/settings/appearance', 'Settings\Controllers\AppearanceController@update', ['auth', 'csrf'], 'settings.edit'],
// Branding // Branding
['GET', '/settings/branding', 'Settings\Controllers\BrandingController@index', ['auth'], 'settings.edit'], ['GET', '/settings/branding', 'Settings\Controllers\BrandingController@index', ['auth'], 'settings.edit'],
['POST', '/settings/branding/logo', 'Settings\Controllers\BrandingController@updateLogo', ['auth', 'csrf'], 'settings.edit'], ['POST', '/settings/branding/logo', 'Settings\Controllers\BrandingController@updateLogo', ['auth', 'csrf'], 'settings.edit'],
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>المظهر والتخصيص<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.appearance-page{max-width:1100px;margin:0 auto}
.appearance-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px}
.appearance-header h1{font-size:22px;font-weight:800;color:#1A1A2E;display:flex;align-items:center;gap:10px}
.appearance-header h1 i{color:var(--brand-primary)}
.appearance-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}
@media(max-width:900px){.appearance-grid{grid-template-columns:1fr}}
.appear-card{background:#fff;border:1px solid #E5E7EB;border-radius:14px;padding:22px;transition:border-color .2s,box-shadow .2s}
.appear-card:hover{border-color:var(--brand-primary);box-shadow:0 4px 20px rgba(13,115,119,0.06)}
.appear-card-title{font-size:14px;font-weight:700;color:#1A1A2E;margin:0 0 16px;display:flex;align-items:center;gap:8px}
.appear-card-title i{width:18px;height:18px;color:var(--brand-primary)}
/* Color Picker */
.color-row{display:flex;align-items:center;gap:12px;margin-bottom:14px}
.color-row label{font-size:13px;font-weight:600;color:#374151;min-width:100px}
.color-input-wrap{display:flex;align-items:center;gap:8px;flex:1}
.color-swatch{width:36px;height:36px;border-radius:10px;border:2px solid #E5E7EB;cursor:pointer;transition:transform .15s,box-shadow .15s;overflow:hidden;position:relative}
.color-swatch:hover{transform:scale(1.1);box-shadow:0 2px 8px rgba(0,0,0,0.15)}
.color-swatch input[type="color"]{position:absolute;inset:-4px;width:calc(100% + 8px);height:calc(100% + 8px);border:none;cursor:pointer;opacity:0}
.color-hex{width:90px;font-size:12px;font-family:monospace;padding:6px 10px;border:1px solid #E5E7EB;border-radius:8px;text-align:center;direction:ltr}
.color-presets{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.color-preset{width:28px;height:28px;border-radius:8px;border:2px solid transparent;cursor:pointer;transition:transform .15s,border-color .15s}
.color-preset:hover{transform:scale(1.15)}
.color-preset.active{border-color:#1A1A2E;box-shadow:0 0 0 2px rgba(26,26,46,0.15)}
/* Option Groups */
.opt-group{margin-bottom:14px}
.opt-group-label{font-size:12px;font-weight:600;color:#6B7280;margin-bottom:8px}
.opt-pills{display:flex;gap:6px;flex-wrap:wrap}
.opt-pill{padding:7px 14px;border:1px solid #E5E7EB;border-radius:9px;font-size:12.5px;font-weight:600;color:#374151;cursor:pointer;transition:all .15s;background:#FAFAFA}
.opt-pill:hover{border-color:var(--brand-primary);color:var(--brand-primary);background:rgba(13,115,119,0.04)}
.opt-pill.active{background:var(--brand-primary);color:#fff;border-color:var(--brand-primary)}
.opt-pill input{display:none}
/* Preview Panel */
.preview-panel{grid-column:1/-1;background:#F8FAFC;border:1px solid #E2E8F0;border-radius:14px;padding:20px;margin-top:4px}
.preview-title{font-size:13px;font-weight:700;color:#6B7280;margin:0 0 14px;display:flex;align-items:center;gap:6px}
.preview-mock{display:flex;gap:16px;min-height:180px;border-radius:10px;overflow:hidden;border:1px solid #E5E7EB}
.preview-sidebar{width:60px;padding:12px 8px;display:flex;flex-direction:column;align-items:center;gap:10px;transition:background .3s}
.preview-sidebar .dot{width:28px;height:28px;border-radius:8px;opacity:0.3;transition:all .3s}
.preview-sidebar .dot.active{opacity:1}
.preview-main{flex:1;padding:16px;background:#fff;display:flex;flex-direction:column;gap:10px}
.preview-card{border-radius:10px;padding:12px 16px;border:1px solid #E5E7EB;transition:all .3s}
.preview-card .bar{height:8px;border-radius:4px;margin-bottom:6px;transition:all .3s}
.preview-card .bar-short{width:40%}
.preview-card .bar-long{width:75%}
.preview-btn{display:inline-block;padding:6px 16px;border-radius:8px;font-size:11px;font-weight:700;color:#fff;transition:all .3s}
/* Save bar */
.save-bar{position:sticky;bottom:0;background:#fff;border-top:1px solid #E5E7EB;padding:14px 0;margin-top:24px;display:flex;justify-content:flex-end;gap:10px;z-index:10;border-radius:0 0 14px 14px}
.save-bar .btn{padding:10px 28px;border-radius:10px;font-size:14px;font-weight:700;border:none;cursor:pointer;transition:all .15s}
.save-bar .btn-primary{background:var(--brand-primary);color:#fff}
.save-bar .btn-primary:hover{opacity:0.9;transform:translateY(-1px)}
.save-bar .btn-ghost{background:#F3F4F6;color:#374151}
.save-bar .btn-ghost:hover{background:#E5E7EB}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php use App\Core\CSRF; ?>
<div class="appearance-page">
<div class="appearance-header">
<h1><i data-lucide="palette"></i> المظهر والتخصيص</h1>
</div>
<form method="POST" action="/settings/appearance" id="appearance-form">
<input type="hidden" name="_csrf_token" value="<?= e(CSRF::token()) ?>">
<div class="appearance-grid">
<!-- Colors -->
<div class="appear-card">
<h3 class="appear-card-title"><i data-lucide="droplet"></i> الألوان</h3>
<div class="color-row">
<label>اللون الأساسي</label>
<div class="color-input-wrap">
<div class="color-swatch" style="background:<?= e($settings['brand_color']) ?>">
<input type="color" name="brand_color" value="<?= e($settings['brand_color']) ?>" onchange="updateColor(this,'brand')">
</div>
<input type="text" class="color-hex" id="brand-hex" value="<?= e($settings['brand_color']) ?>" onchange="syncHex(this,'brand_color')">
</div>
</div>
<div class="color-presets" id="brand-presets">
<?php foreach ($colorPresets as $p): ?>
<div class="color-preset <?= $p['value'] === $settings['brand_color'] ? 'active' : '' ?>"
style="background:<?= e($p['value']) ?>"
title="<?= e($p['label']) ?>"
onclick="selectPreset(this,'<?= e($p['value']) ?>','brand')"></div>
<?php endforeach; ?>
</div>
<div class="color-row" style="margin-top:18px">
<label>لون التمييز</label>
<div class="color-input-wrap">
<div class="color-swatch" style="background:<?= e($settings['accent_color']) ?>">
<input type="color" name="accent_color" value="<?= e($settings['accent_color']) ?>" onchange="updateColor(this,'accent')">
</div>
<input type="text" class="color-hex" id="accent-hex" value="<?= e($settings['accent_color']) ?>" onchange="syncHex(this,'accent_color')">
</div>
</div>
</div>
<!-- Sidebar -->
<div class="appear-card">
<h3 class="appear-card-title"><i data-lucide="panel-left"></i> القائمة الجانبية</h3>
<div class="opt-group">
<div class="opt-group-label">نمط القائمة</div>
<div class="opt-pills">
<?php foreach (['dark' => 'داكن', 'light' => 'فاتح', 'gradient' => 'متدرج'] as $v => $l): ?>
<label class="opt-pill <?= $settings['sidebar_style'] === $v ? 'active' : '' ?>">
<input type="radio" name="sidebar_style" value="<?= $v ?>" <?= $settings['sidebar_style'] === $v ? 'checked' : '' ?> onchange="activatePill(this)"> <?= $l ?>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="opt-group">
<div class="opt-group-label">عرض القائمة</div>
<div class="opt-pills">
<?php foreach (['narrow' => 'ضيق', 'normal' => 'عادي', 'wide' => 'عريض'] as $v => $l): ?>
<label class="opt-pill <?= $settings['sidebar_width'] === $v ? 'active' : '' ?>">
<input type="radio" name="sidebar_width" value="<?= $v ?>" <?= $settings['sidebar_width'] === $v ? 'checked' : '' ?> onchange="activatePill(this)"> <?= $l ?>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Typography & Radius -->
<div class="appear-card">
<h3 class="appear-card-title"><i data-lucide="type"></i> الخطوط والحواف</h3>
<div class="opt-group">
<div class="opt-group-label">حجم الخط</div>
<div class="opt-pills">
<?php foreach (['compact' => 'مضغوط', 'normal' => 'عادي', 'large' => 'كبير'] as $v => $l): ?>
<label class="opt-pill <?= $settings['font_size'] === $v ? 'active' : '' ?>">
<input type="radio" name="font_size" value="<?= $v ?>" <?= $settings['font_size'] === $v ? 'checked' : '' ?> onchange="activatePill(this)"> <?= $l ?>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="opt-group">
<div class="opt-group-label">استدارة البطاقات</div>
<div class="opt-pills">
<?php foreach (['sharp' => 'حاد', 'rounded' => 'مستدير', 'pill' => 'كبسولة'] as $v => $l): ?>
<label class="opt-pill <?= $settings['card_radius'] === $v ? 'active' : '' ?>">
<input type="radio" name="card_radius" value="<?= $v ?>" <?= $settings['card_radius'] === $v ? 'checked' : '' ?> onchange="activatePill(this)"> <?= $l ?>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Animation & Density -->
<div class="appear-card">
<h3 class="appear-card-title"><i data-lucide="zap"></i> الحركة والكثافة</h3>
<div class="opt-group">
<div class="opt-group-label">الحركات والانتقالات</div>
<div class="opt-pills">
<?php foreach (['full' => 'كاملة', 'subtle' => 'خفيفة', 'none' => 'بدون'] as $v => $l): ?>
<label class="opt-pill <?= $settings['animation'] === $v ? 'active' : '' ?>">
<input type="radio" name="animation" value="<?= $v ?>" <?= $settings['animation'] === $v ? 'checked' : '' ?> onchange="activatePill(this)"> <?= $l ?>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="opt-group">
<div class="opt-group-label">كثافة العرض</div>
<div class="opt-pills">
<?php foreach (['compact' => 'مضغوط', 'comfortable' => 'مريح', 'spacious' => 'واسع'] as $v => $l): ?>
<label class="opt-pill <?= $settings['density'] === $v ? 'active' : '' ?>">
<input type="radio" name="density" value="<?= $v ?>" <?= $settings['density'] === $v ? 'checked' : '' ?> onchange="activatePill(this)"> <?= $l ?>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Tables & Header -->
<div class="appear-card">
<h3 class="appear-card-title"><i data-lucide="table"></i> الجداول والرأس</h3>
<div class="opt-group">
<div class="opt-group-label">نمط الجداول</div>
<div class="opt-pills">
<?php foreach (['striped' => 'مخطط', 'bordered' => 'بإطار', 'minimal' => 'بسيط'] as $v => $l): ?>
<label class="opt-pill <?= $settings['table_style'] === $v ? 'active' : '' ?>">
<input type="radio" name="table_style" value="<?= $v ?>" <?= $settings['table_style'] === $v ? 'checked' : '' ?> onchange="activatePill(this)"> <?= $l ?>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="opt-group">
<div class="opt-group-label">نمط الرأس</div>
<div class="opt-pills">
<?php foreach (['clean' => 'نظيف', 'bordered' => 'بخط سفلي'] as $v => $l): ?>
<label class="opt-pill <?= $settings['header_style'] === $v ? 'active' : '' ?>">
<input type="radio" name="header_style" value="<?= $v ?>" <?= $settings['header_style'] === $v ? 'checked' : '' ?> onchange="activatePill(this)"> <?= $l ?>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Live Preview -->
<div class="preview-panel">
<div class="preview-title"><i data-lucide="eye" style="width:14px;height:14px"></i> معاينة حية</div>
<div class="preview-mock" id="preview-mock">
<div class="preview-sidebar" id="preview-sidebar">
<div class="dot active" id="preview-dot-1"></div>
<div class="dot" id="preview-dot-2"></div>
<div class="dot" id="preview-dot-3"></div>
</div>
<div class="preview-main">
<div class="preview-card" id="preview-card">
<div class="bar bar-short" id="preview-bar-1"></div>
<div class="bar bar-long" id="preview-bar-2"></div>
</div>
<div style="display:flex;gap:8px">
<div class="preview-btn" id="preview-btn">زر تجريبي</div>
</div>
</div>
</div>
</div>
</div>
<div class="save-bar">
<button type="button" class="btn btn-ghost" onclick="resetDefaults()">استعادة الافتراضي</button>
<button type="submit" class="btn btn-primary">حفظ التغييرات</button>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
updatePreview();
});
function updateColor(input, type) {
var swatch = input.parentElement;
swatch.style.background = input.value;
document.getElementById(type + '-hex').value = input.value;
if (type === 'brand') {
document.querySelectorAll('#brand-presets .color-preset').forEach(function(p) {
p.classList.toggle('active', p.style.backgroundColor === input.value);
});
}
updatePreview();
}
function syncHex(input, name) {
var val = input.value.trim();
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
var colorInput = document.querySelector('input[name="' + name + '"]');
colorInput.value = val;
colorInput.parentElement.style.background = val;
updatePreview();
}
}
function selectPreset(el, value, type) {
document.querySelector('input[name="' + type + '_color"]').value = value;
el.closest('.color-presets').querySelectorAll('.color-preset').forEach(function(p) { p.classList.remove('active'); });
el.classList.add('active');
var swatch = document.querySelector('input[name="' + type + '_color"]').parentElement;
swatch.style.background = value;
document.getElementById(type + '-hex').value = value;
updatePreview();
}
function activatePill(input) {
var pills = input.closest('.opt-pills').querySelectorAll('.opt-pill');
pills.forEach(function(p) { p.classList.remove('active'); });
input.closest('.opt-pill').classList.add('active');
updatePreview();
}
function updatePreview() {
var brand = document.querySelector('input[name="brand_color"]').value;
var sidebar = document.querySelector('input[name="sidebar_style"]:checked').value;
var radius = document.querySelector('input[name="card_radius"]:checked').value;
var sidebarEl = document.getElementById('preview-sidebar');
var cardEl = document.getElementById('preview-card');
var btnEl = document.getElementById('preview-btn');
var dot1 = document.getElementById('preview-dot-1');
// Sidebar style
if (sidebar === 'dark') sidebarEl.style.background = '#1A1A2E';
else if (sidebar === 'light') sidebarEl.style.background = '#ffffff';
else sidebarEl.style.background = 'linear-gradient(180deg,#0f0f1a,#1a1a3e)';
// Dots
dot1.style.background = brand;
document.getElementById('preview-dot-2').style.background = sidebar === 'light' ? '#E5E7EB' : 'rgba(255,255,255,0.2)';
document.getElementById('preview-dot-3').style.background = sidebar === 'light' ? '#E5E7EB' : 'rgba(255,255,255,0.2)';
// Card radius
var rMap = {sharp:'4px', rounded:'10px', pill:'18px'};
cardEl.style.borderRadius = rMap[radius] || '10px';
// Button
btnEl.style.background = brand;
btnEl.style.borderRadius = rMap[radius] || '10px';
// Bars
document.getElementById('preview-bar-1').style.background = brand + '40';
document.getElementById('preview-bar-2').style.background = '#E5E7EB';
}
function resetDefaults() {
if (!confirm('استعادة جميع الإعدادات للقيم الافتراضية؟')) return;
document.querySelector('input[name="brand_color"]').value = '#0D7377';
document.querySelector('input[name="accent_color"]').value = '#6366f1';
document.querySelectorAll('input[value="dark"][name="sidebar_style"]')[0].checked = true;
document.querySelectorAll('input[value="normal"][name="sidebar_width"]')[0].checked = true;
document.querySelectorAll('input[value="rounded"][name="card_radius"]')[0].checked = true;
document.querySelectorAll('input[value="normal"][name="font_size"]')[0].checked = true;
document.querySelectorAll('input[value="full"][name="animation"]')[0].checked = true;
document.querySelectorAll('input[value="clean"][name="header_style"]')[0].checked = true;
document.querySelectorAll('input[value="striped"][name="table_style"]')[0].checked = true;
document.querySelectorAll('input[value="comfortable"][name="density"]')[0].checked = true;
document.querySelectorAll('.opt-pill').forEach(function(p) {
var input = p.querySelector('input');
p.classList.toggle('active', input && input.checked);
});
document.querySelector('input[name="brand_color"]').parentElement.style.background = '#0D7377';
document.querySelector('input[name="accent_color"]').parentElement.style.background = '#6366f1';
document.getElementById('brand-hex').value = '#0D7377';
document.getElementById('accent-hex').value = '#6366f1';
updatePreview();
}
</script>
<?php $__template->endSection(); ?>
...@@ -634,6 +634,13 @@ final class TutorialRegistry ...@@ -634,6 +634,13 @@ final class TutorialRegistry
['title' => 'تخصيص المحتوى', 'body' => 'حدد العناصر الظاهرة: الشعار، اسم النادي، بيانات العضو، تفاصيل الدفعة، كود QR.'], ['title' => 'تخصيص المحتوى', 'body' => 'حدد العناصر الظاهرة: الشعار، اسم النادي، بيانات العضو، تفاصيل الدفعة، كود QR.'],
['title' => 'معاينة وحفظ', 'body' => 'اضغط <span class="field">معاينة</span> لرؤية النتيجة. ثم <span class="field">حفظ</span>.<span class="success">التصميم يُطبق على جميع الإيصالات الجديدة فوراً.</span>'], ['title' => 'معاينة وحفظ', 'body' => 'اضغط <span class="field">معاينة</span> لرؤية النتيجة. ثم <span class="field">حفظ</span>.<span class="success">التصميم يُطبق على جميع الإيصالات الجديدة فوراً.</span>'],
], ],
'settings.appearance' => [
['title' => 'فتح إعدادات المظهر', 'body' => 'من القائمة الجانبية: <span class="field">الإعدادات</span> > <span class="field">المظهر والتخصيص</span>. تفتح صفحة تحتوي على جميع خيارات تخصيص شكل النظام.'],
['title' => 'تغيير الألوان', 'body' => 'اختر <span class="field">اللون الأساسي</span> من القائمة الجاهزة (8 ألوان) أو أدخل لون مخصص بصيغة HEX. يمكنك أيضاً تغيير <span class="field">لون التمييز</span> المستخدم في الأزرار الثانوية.<span class="info">اللون الأساسي يُطبق على القائمة الجانبية والأزرار والعناصر التفاعلية.</span>'],
['title' => 'نمط القائمة الجانبية', 'body' => 'اختر أحد الأنماط:<ul><li><strong>داكن</strong> — الافتراضي، خلفية سوداء أنيقة</li><li><strong>فاتح</strong> — خلفية بيضاء بحدود خفيفة</li><li><strong>متدرج</strong> — تدرج لوني احترافي</li></ul>يمكنك أيضاً تغيير عرض القائمة (ضيق، عادي، عريض).'],
['title' => 'الخطوط والحواف والكثافة', 'body' => 'تحكم في:<ul><li><span class="field">حجم الخط</span>: مضغوط (13px)، عادي (14px)، كبير (15px)</li><li><span class="field">استدارة البطاقات</span>: حاد، مستدير، كبسولة</li><li><span class="field">الحركات</span>: كاملة، خفيفة، بدون</li><li><span class="field">كثافة العرض</span>: مضغوط، مريح، واسع</li></ul>'],
['title' => 'المعاينة والحفظ', 'body' => 'لوحة المعاينة الحية أسفل الصفحة تعرض تأثير التغييرات فوراً. بعد الرضا عن الشكل اضغط <span class="field">حفظ التغييرات</span>.<span class="success">التغييرات تُطبق فوراً على جميع المستخدمين. يمكنك العودة للإعدادات الافتراضية في أي وقت بزر «استعادة الافتراضي».</span>'],
],
// ── FINES ── // ── FINES ──
'fines.impose-fine' => [ 'fines.impose-fine' => [
['title' => 'فرض غرامة', 'body' => 'من ملف العضو > <span class="field">مخالفات</span> > <span class="field">غرامة جديدة</span>.'], ['title' => 'فرض غرامة', 'body' => 'من ملف العضو > <span class="field">مخالفات</span> > <span class="field">غرامة جديدة</span>.'],
...@@ -1770,6 +1777,14 @@ final class TutorialRegistry ...@@ -1770,6 +1777,14 @@ final class TutorialRegistry
'category' => 'customization', 'category' => 'customization',
'order' => 3, 'order' => 3,
], ],
'appearance' => [
'title' => 'المظهر والتخصيص',
'subtitle' => 'تغيير ألوان النظام ونمط القائمة والخطوط والحركات',
'icon' => 'paintbrush',
'color' => '#8B5CF6',
'category' => 'customization',
'order' => 4,
],
]; ];
} }
......
...@@ -32,6 +32,7 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); ...@@ -32,6 +32,7 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<!-- Main Stylesheet --> <!-- Main Stylesheet -->
<link rel="stylesheet" href="<?= url('assets/css/main.css') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/css/main.css') ?: time() ?>"> <link rel="stylesheet" href="<?= url('assets/css/main.css') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/css/main.css') ?: time() ?>">
<style><?= \App\Modules\Settings\Controllers\AppearanceController::getCssOverrides() ?></style>
<?= $__template->yield('styles', '') ?> <?= $__template->yield('styles', '') ?>
</head> </head>
<body> <body>
......
...@@ -2137,3 +2137,188 @@ code { ...@@ -2137,3 +2137,188 @@ code {
.p-lg { padding: 20px; } .p-lg { padding: 20px; }
.w-full { width: 100%; } .w-full { width: 100%; }
.hidden { display: none; } .hidden { display: none; }
/* ══════════════════════════════════════════════════
PREMIUM ANIMATIONS & MICRO-INTERACTIONS
══════════════════════════════════════════════════ */
/* Page content entrance */
.page-content {
animation: pageEnter var(--duration-slow) var(--ease-out) both;
}
@keyframes pageEnter {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
/* Card hover lift — only for interactive/grid cards */
.stats-card,
.card-interactive,
.appear-card {
transition: transform var(--duration-normal) var(--ease-out),
box-shadow var(--duration-normal) var(--ease-out),
border-color var(--duration-fast) ease;
}
/* Staggered card entrance */
.card:nth-child(1) { animation-delay: 0ms; }
.card:nth-child(2) { animation-delay: 50ms; }
.card:nth-child(3) { animation-delay: 100ms; }
.card:nth-child(4) { animation-delay: 150ms; }
.card:nth-child(5) { animation-delay: 200ms; }
.card:nth-child(6) { animation-delay: 250ms; }
/* Table row entrance */
.data-table tbody tr {
animation: rowSlideIn var(--duration-normal) var(--ease-out) both;
}
.data-table tbody tr:nth-child(1) { animation-delay: 0ms; }
.data-table tbody tr:nth-child(2) { animation-delay: 30ms; }
.data-table tbody tr:nth-child(3) { animation-delay: 60ms; }
.data-table tbody tr:nth-child(4) { animation-delay: 90ms; }
.data-table tbody tr:nth-child(5) { animation-delay: 120ms; }
.data-table tbody tr:nth-child(6) { animation-delay: 150ms; }
.data-table tbody tr:nth-child(7) { animation-delay: 180ms; }
.data-table tbody tr:nth-child(8) { animation-delay: 210ms; }
.data-table tbody tr:nth-child(9) { animation-delay: 240ms; }
.data-table tbody tr:nth-child(10) { animation-delay: 270ms; }
@keyframes rowSlideIn {
from { opacity: 0; transform: translateX(8px); }
to { opacity: 1; transform: none; }
}
/* Table row hover enhancement */
.data-table tbody tr {
transition: background var(--duration-fast) ease;
}
/* Button ghost/secondary hover lift */
.btn-secondary:hover,
.btn-ghost:hover {
transform: translateY(-1px);
}
/* Form group label transition on focus */
.form-group:focus-within .form-label {
transform: translateX(-2px);
}
/* Badge pulse for warning status indicators */
.badge-warning {
animation: badgePulse 2s ease-in-out infinite;
}
/* Modal backdrop enhancement */
.modal-overlay {
animation: overlayFadeIn var(--duration-normal) ease both;
}
/* Sidebar item hover slide (RTL: shift content slightly left) */
.sidebar-item {
transition: background var(--duration-fast) ease,
padding-left var(--duration-fast) var(--ease-out);
}
.sidebar-item:hover {
padding-left: 4px;
}
/* Stat card counter animation */
.stat-value {
transition: color var(--duration-normal) ease;
}
/* Smooth scrollbar */
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25);
}
/* Focus ring for accessibility */
:focus-visible {
outline: 2px solid var(--brand-primary);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* Enhanced card header with subtle gradient */
.card-header {
position: relative;
}
.card-header::after {
content: '';
position: absolute;
bottom: 0;
right: 0;
left: 0;
height: 1px;
background: linear-gradient(90deg, var(--border-light), rgba(var(--brand-primary-rgb), 0.2), var(--border-light));
}
/* Badge border enhancement */
.badge-primary { border: 1px solid rgba(var(--brand-primary-rgb), 0.15); }
.badge-success { border: 1px solid var(--success-border); }
.badge-danger { border: 1px solid var(--danger-border); }
.badge-warning { border: 1px solid var(--warning-border); }
.badge-info { border: 1px solid var(--info-border); }
/* Form section title accent */
.form-section-title::before {
content: '';
width: 4px;
height: 18px;
background: var(--brand-primary);
border-radius: 2px;
flex-shrink: 0;
}
/* Pagination active glow */
.page-link.active {
box-shadow: 0 2px 8px rgba(var(--brand-primary-rgb), 0.35);
}
/* Stats card number font feature */
.stats-card-value {
font-feature-settings: 'tnum';
}
/* Tabs active indicator glow */
.tab-link.active {
text-shadow: 0 0 8px rgba(var(--brand-primary-rgb), 0.15);
}
/* Tooltip hover */
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 6px);
right: 50%;
transform: translateX(50%) translateY(4px);
background: #1A1A2E;
color: #fff;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity var(--duration-fast) ease, transform var(--duration-fast) ease;
z-index: var(--z-toast);
}
[data-tooltip]:hover::after {
opacity: 1;
transform: translateX(50%) translateY(0);
}
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