Update the theme

This commit is contained in:
DariusIII
2026-08-28 21:01:47 +02:00
parent 103406de59
commit 14498890ad
13 changed files with 528 additions and 74 deletions
+1 -1
View File
@@ -176,7 +176,7 @@ class ProfileController extends BasePageController
// Update color scheme
if ($request->has('color_scheme')) {
$schemeValue = $request->input('color_scheme');
if (in_array($schemeValue, ['blue', 'emerald', 'violet'], true)) {
if (in_array($schemeValue, ['blue', 'indigo', 'cyan', 'teal', 'emerald', 'violet', 'pink', 'rose', 'red', 'orange', 'amber'], true)) {
User::where('id', $userid)->update(['color_scheme' => $schemeValue]);
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ class UpdateProfileRequest extends FormRequest
'email' => ['nullable', 'string', 'email', 'max:255', 'unique:users,email,'.$userId, new ValidEmailDomain],
'password' => ['nullable', 'string', 'min:8', 'confirmed', 'regex:/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/'],
'theme_preference' => ['sometimes', 'in:light,dark,system'],
'color_scheme' => ['sometimes', 'in:blue,emerald,violet'],
'color_scheme' => ['sometimes', 'in:blue,indigo,cyan,teal,emerald,violet,pink,rose,red,orange,amber'],
];
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ class UpdateThemeRequest extends FormRequest
{
return [
'theme_preference' => ['sometimes', 'in:light,dark,system'],
'color_scheme' => ['sometimes', 'in:blue,emerald,violet'],
'color_scheme' => ['sometimes', 'in:blue,indigo,cyan,teal,emerald,violet,pink,rose,red,orange,amber'],
];
}
}
+288
View File
@@ -121,6 +121,294 @@
--color-primary-950: #2e1065;
}
[data-color-scheme="rose"] {
--surface-body: #fff1f2;
--surface-body-dark: #1f0a12;
--surface-sidebar: #3b1825;
--surface-sidebar-dark: #13050a;
--surface-header: #3b1825;
--surface-header-dark: #13050a;
--surface-card: #ffffff;
--surface-card-dark: #32131e;
--surface-dropdown: #2b1019;
--surface-dropdown-dark: #13050a;
--surface-footer: #3b1825;
--surface-footer-dark: #13050a;
--surface-hover: #fff1f2;
--surface-hover-dark: #4c0519;
--border-default: #fecdd3;
--border-default-dark: #9f1239;
--text-muted: #be123c;
--text-muted-dark: #fda4af;
--surface-panel-alt: #fff1f2;
--surface-panel-alt-dark: #3b1825;
--surface-chrome-border: rgba(255,255,255,0.08);
--surface-chrome-border-dark: rgba(255,255,255,0.05);
--color-primary-50: #fff1f2;
--color-primary-100: #ffe4e6;
--color-primary-200: #fecdd3;
--color-primary-300: #fda4af;
--color-primary-400: #fb7185;
--color-primary-500: #f43f5e;
--color-primary-600: #e11d48;
--color-primary-700: #be123c;
--color-primary-800: #9f1239;
--color-primary-900: #881337;
--color-primary-950: #4c0519;
}
[data-color-scheme="amber"] {
--surface-body: #fffbeb;
--surface-body-dark: #1c1304;
--surface-sidebar: #3b2a0e;
--surface-sidebar-dark: #120b02;
--surface-header: #3b2a0e;
--surface-header-dark: #120b02;
--surface-card: #ffffff;
--surface-card-dark: #33240d;
--surface-dropdown: #2c1d08;
--surface-dropdown-dark: #120b02;
--surface-footer: #3b2a0e;
--surface-footer-dark: #120b02;
--surface-hover: #fffbeb;
--surface-hover-dark: #78350f;
--border-default: #fde68a;
--border-default-dark: #92400e;
--text-muted: #b45309;
--text-muted-dark: #fcd34d;
--surface-panel-alt: #fffbeb;
--surface-panel-alt-dark: #3b2a0e;
--surface-chrome-border: rgba(255,255,255,0.08);
--surface-chrome-border-dark: rgba(255,255,255,0.05);
--color-primary-50: #fffbeb;
--color-primary-100: #fef3c7;
--color-primary-200: #fde68a;
--color-primary-300: #fcd34d;
--color-primary-400: #fbbf24;
--color-primary-500: #f59e0b;
--color-primary-600: #d97706;
--color-primary-700: #b45309;
--color-primary-800: #92400e;
--color-primary-900: #78350f;
--color-primary-950: #451a03;
}
[data-color-scheme="cyan"] {
--surface-body: #ecfeff;
--surface-body-dark: #06191d;
--surface-sidebar: #12343c;
--surface-sidebar-dark: #031014;
--surface-header: #12343c;
--surface-header-dark: #031014;
--surface-card: #ffffff;
--surface-card-dark: #10313a;
--surface-dropdown: #0b2a32;
--surface-dropdown-dark: #031014;
--surface-footer: #12343c;
--surface-footer-dark: #031014;
--surface-hover: #ecfeff;
--surface-hover-dark: #164e63;
--border-default: #a5f3fc;
--border-default-dark: #155e75;
--text-muted: #0e7490;
--text-muted-dark: #67e8f9;
--surface-panel-alt: #ecfeff;
--surface-panel-alt-dark: #12343c;
--surface-chrome-border: rgba(255,255,255,0.08);
--surface-chrome-border-dark: rgba(255,255,255,0.05);
--color-primary-50: #ecfeff;
--color-primary-100: #cffafe;
--color-primary-200: #a5f3fc;
--color-primary-300: #67e8f9;
--color-primary-400: #22d3ee;
--color-primary-500: #06b6d4;
--color-primary-600: #0891b2;
--color-primary-700: #0e7490;
--color-primary-800: #155e75;
--color-primary-900: #164e63;
--color-primary-950: #083344;
}
[data-color-scheme="indigo"] {
--surface-body: #eef2ff;
--surface-body-dark: #0b1024;
--surface-sidebar: #1e2850;
--surface-sidebar-dark: #070b1b;
--surface-header: #1e2850;
--surface-header-dark: #070b1b;
--surface-card: #ffffff;
--surface-card-dark: #182143;
--surface-dropdown: #141c39;
--surface-dropdown-dark: #070b1b;
--surface-footer: #1e2850;
--surface-footer-dark: #070b1b;
--surface-hover: #eef2ff;
--surface-hover-dark: #312e81;
--border-default: #c7d2fe;
--border-default-dark: #3730a3;
--text-muted: #4338ca;
--text-muted-dark: #a5b4fc;
--surface-panel-alt: #eef2ff;
--surface-panel-alt-dark: #1e2850;
--surface-chrome-border: rgba(255,255,255,0.08);
--surface-chrome-border-dark: rgba(255,255,255,0.05);
--color-primary-50: #eef2ff;
--color-primary-100: #e0e7ff;
--color-primary-200: #c7d2fe;
--color-primary-300: #a5b4fc;
--color-primary-400: #818cf8;
--color-primary-500: #6366f1;
--color-primary-600: #4f46e5;
--color-primary-700: #4338ca;
--color-primary-800: #3730a3;
--color-primary-900: #312e81;
--color-primary-950: #1e1b4b;
}
[data-color-scheme="teal"] {
--surface-body: #f0fdfa;
--surface-body-dark: #061a18;
--surface-sidebar: #123b36;
--surface-sidebar-dark: #021412;
--surface-header: #123b36;
--surface-header-dark: #021412;
--surface-card: #ffffff;
--surface-card-dark: #10332f;
--surface-dropdown: #0b2b27;
--surface-dropdown-dark: #021412;
--surface-footer: #123b36;
--surface-footer-dark: #021412;
--surface-hover: #f0fdfa;
--surface-hover-dark: #134e4a;
--border-default: #99f6e4;
--border-default-dark: #115e59;
--text-muted: #0f766e;
--text-muted-dark: #5eead4;
--surface-panel-alt: #f0fdfa;
--surface-panel-alt-dark: #123b36;
--surface-chrome-border: rgba(255,255,255,0.08);
--surface-chrome-border-dark: rgba(255,255,255,0.05);
--color-primary-50: #f0fdfa;
--color-primary-100: #ccfbf1;
--color-primary-200: #99f6e4;
--color-primary-300: #5eead4;
--color-primary-400: #2dd4bf;
--color-primary-500: #14b8a6;
--color-primary-600: #0d9488;
--color-primary-700: #0f766e;
--color-primary-800: #115e59;
--color-primary-900: #134e4a;
--color-primary-950: #042f2e;
}
[data-color-scheme="orange"] {
--surface-body: #fff7ed;
--surface-body-dark: #1f1007;
--surface-sidebar: #452515;
--surface-sidebar-dark: #160a03;
--surface-header: #452515;
--surface-header-dark: #160a03;
--surface-card: #ffffff;
--surface-card-dark: #3a2012;
--surface-dropdown: #32190b;
--surface-dropdown-dark: #160a03;
--surface-footer: #452515;
--surface-footer-dark: #160a03;
--surface-hover: #fff7ed;
--surface-hover-dark: #7c2d12;
--border-default: #fed7aa;
--border-default-dark: #9a3412;
--text-muted: #c2410c;
--text-muted-dark: #fdba74;
--surface-panel-alt: #fff7ed;
--surface-panel-alt-dark: #452515;
--surface-chrome-border: rgba(255,255,255,0.08);
--surface-chrome-border-dark: rgba(255,255,255,0.05);
--color-primary-50: #fff7ed;
--color-primary-100: #ffedd5;
--color-primary-200: #fed7aa;
--color-primary-300: #fdba74;
--color-primary-400: #fb923c;
--color-primary-500: #f97316;
--color-primary-600: #ea580c;
--color-primary-700: #c2410c;
--color-primary-800: #9a3412;
--color-primary-900: #7c2d12;
--color-primary-950: #431407;
}
[data-color-scheme="red"] {
--surface-body: #fef2f2;
--surface-body-dark: #200a0a;
--surface-sidebar: #461818;
--surface-sidebar-dark: #170505;
--surface-header: #461818;
--surface-header-dark: #170505;
--surface-card: #ffffff;
--surface-card-dark: #3a1414;
--surface-dropdown: #310f0f;
--surface-dropdown-dark: #170505;
--surface-footer: #461818;
--surface-footer-dark: #170505;
--surface-hover: #fef2f2;
--surface-hover-dark: #7f1d1d;
--border-default: #fecaca;
--border-default-dark: #991b1b;
--text-muted: #b91c1c;
--text-muted-dark: #fca5a5;
--surface-panel-alt: #fef2f2;
--surface-panel-alt-dark: #461818;
--surface-chrome-border: rgba(255,255,255,0.08);
--surface-chrome-border-dark: rgba(255,255,255,0.05);
--color-primary-50: #fef2f2;
--color-primary-100: #fee2e2;
--color-primary-200: #fecaca;
--color-primary-300: #fca5a5;
--color-primary-400: #f87171;
--color-primary-500: #ef4444;
--color-primary-600: #dc2626;
--color-primary-700: #b91c1c;
--color-primary-800: #991b1b;
--color-primary-900: #7f1d1d;
--color-primary-950: #450a0a;
}
[data-color-scheme="pink"] {
--surface-body: #fdf2f8;
--surface-body-dark: #200a17;
--surface-sidebar: #45172f;
--surface-sidebar-dark: #17040f;
--surface-header: #45172f;
--surface-header-dark: #17040f;
--surface-card: #ffffff;
--surface-card-dark: #391326;
--surface-dropdown: #310f20;
--surface-dropdown-dark: #17040f;
--surface-footer: #45172f;
--surface-footer-dark: #17040f;
--surface-hover: #fdf2f8;
--surface-hover-dark: #831843;
--border-default: #fbcfe8;
--border-default-dark: #9d174d;
--text-muted: #be185d;
--text-muted-dark: #f9a8d4;
--surface-panel-alt: #fdf2f8;
--surface-panel-alt-dark: #45172f;
--surface-chrome-border: rgba(255,255,255,0.08);
--surface-chrome-border-dark: rgba(255,255,255,0.05);
--color-primary-50: #fdf2f8;
--color-primary-100: #fce7f3;
--color-primary-200: #fbcfe8;
--color-primary-300: #f9a8d4;
--color-primary-400: #f472b6;
--color-primary-500: #ec4899;
--color-primary-600: #db2777;
--color-primary-700: #be185d;
--color-primary-800: #9d174d;
--color-primary-900: #831843;
--color-primary-950: #500724;
}
/* Theme customization (replaces theme.extend in tailwind.config.js) */
@theme {
--font-sans: "Figtree", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+6 -3
View File
@@ -5,6 +5,7 @@
import Alpine from '@alpinejs/csp';
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const colorSchemes = ['blue', 'indigo', 'cyan', 'teal', 'emerald', 'violet', 'pink', 'rose', 'red', 'orange', 'amber'];
Alpine.store('theme', {
current: 'light',
@@ -19,18 +20,19 @@ Alpine.store('theme', {
const schemeMeta = document.querySelector('meta[name="color-scheme-preference"]');
const schemeData = document.getElementById('current-theme-data');
this.colorScheme = (isAuth && isAuth.content === 'true')
const preferredScheme = (isAuth && isAuth.content === 'true')
? (schemeMeta ? schemeMeta.content : (schemeData?.dataset?.colorScheme || 'blue'))
: (localStorage.getItem('color_scheme') || 'blue');
this.colorScheme = colorSchemes.includes(preferredScheme) ? preferredScheme : 'blue';
this.apply();
this.applyScheme();
this._listenOS();
},
/** Set and persist color scheme (blue, emerald, violet) */
/** Set and persist a supported color scheme. */
setScheme(scheme) {
if (!['blue', 'emerald', 'violet'].includes(scheme)) return;
if (!colorSchemes.includes(scheme)) return;
this.colorScheme = scheme;
this.applyScheme();
this._save();
@@ -114,6 +116,7 @@ Alpine.store('theme', {
// Update color scheme swatch buttons
document.querySelectorAll('.dropdown-scheme-btn, .mobile-scheme-btn').forEach(function(btn) {
var isActive = btn.dataset.scheme === self.colorScheme;
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
btn.classList.remove('ring-2', 'ring-offset-2', 'ring-offset-gray-900', 'ring-primary-500', 'ring-white');
if (isActive) {
btn.classList.add('ring-2', 'ring-offset-2', 'ring-offset-gray-900', 'ring-primary-500', 'dark:ring-offset-gray-950');
@@ -6,13 +6,13 @@
'current' => null,
])
<header {{ $attributes->merge(['class' => 'app-page-header workspace-hero relative mb-6 overflow-hidden rounded-2xl px-5 py-6 text-white shadow-lg sm:px-7 sm:py-8']) }}>
<header {{ $attributes->merge(['class' => 'app-page-header workspace-hero relative mb-6 overflow-hidden rounded-2xl px-5 py-4 text-white shadow-lg sm:px-7 sm:py-5']) }}>
<div class="workspace-hero__glow workspace-hero__glow--one" aria-hidden="true"></div>
<div class="workspace-hero__glow workspace-hero__glow--two" aria-hidden="true"></div>
<div class="relative z-10 flex flex-col gap-6 xl:flex-row xl:items-end xl:justify-between">
<div class="min-w-0 max-w-3xl">
<nav aria-label="Breadcrumb" class="mb-4 text-sm text-white/70">
<div class="relative z-10 flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
<div class="min-w-0 max-w-3xl xl:flex-1">
<nav aria-label="Breadcrumb" class="mb-2 text-sm text-white/70">
<ol class="flex flex-wrap items-center gap-2">
<li><a href="{{ url('/') }}" class="rounded hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70">Home</a></li>
<li aria-hidden="true"><i class="fas fa-chevron-right text-[0.65rem]"></i></li>
@@ -33,20 +33,25 @@
</div>
@if($description)
<p class="mt-4 max-w-2xl text-sm leading-6 text-white/75 sm:text-base">{{ $description }}</p>
<p class="mt-2 max-w-2xl text-sm leading-5 text-white/75 sm:text-base">{{ $description }}</p>
@endif
</div>
@if(isset($stats) || isset($actions))
<div class="app-page-header__meta flex min-w-0 flex-wrap items-center gap-3 xl:max-w-[48%] xl:justify-end">
@isset($stats)
<div class="mt-5 flex flex-wrap gap-2 text-sm">
<div class="flex flex-wrap gap-2 text-sm xl:justify-end">
{{ $stats }}
</div>
@endisset
</div>
@isset($actions)
<div class="app-page-header__actions flex shrink-0 flex-wrap items-center gap-3">
<div class="app-page-header__actions flex flex-wrap items-center gap-3">
{{ $actions }}
</div>
@endisset
</div>
@endif
</div>
</header>
+10 -7
View File
@@ -16,13 +16,13 @@
@endphp
<div class="space-y-5" x-data="moviesPage" data-movie-layout="{{ $movie_layout ?? 2 }}">
<section class="movies-hero relative overflow-hidden rounded-2xl px-5 py-6 text-white shadow-lg sm:px-7 sm:py-8" aria-labelledby="movies-heading">
<section class="movies-hero relative overflow-hidden rounded-2xl px-5 py-4 text-white shadow-lg sm:px-7 sm:py-5" aria-labelledby="movies-heading">
<div class="movies-hero__glow movies-hero__glow--one" aria-hidden="true"></div>
<div class="movies-hero__glow movies-hero__glow--two" aria-hidden="true"></div>
<div class="relative z-10 flex flex-col gap-6 xl:flex-row xl:items-end xl:justify-between">
<div class="max-w-3xl">
<nav aria-label="Breadcrumb" class="mb-4 text-sm text-white/70">
<div class="relative z-10 flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
<div class="min-w-0 max-w-3xl xl:flex-1">
<nav aria-label="Breadcrumb" class="mb-2 text-sm text-white/70">
<ol class="flex flex-wrap items-center gap-2">
<li><a href="{{ url($site['home_link'] ?? '/') }}" class="rounded hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70">Home</a></li>
<li aria-hidden="true"><i class="fas fa-chevron-right text-[0.65rem]"></i></li>
@@ -38,11 +38,14 @@
</div>
</div>
<p class="mt-4 max-w-2xl text-sm leading-6 text-white/75 sm:text-base">
<p class="mt-2 max-w-2xl text-sm leading-5 text-white/75 sm:text-base">
Browse available movies, compare recent releases, and narrow the catalog by title, genre, year, or rating.
</p>
<div class="mt-5 flex flex-wrap gap-2 text-sm">
</div>
<div class="movies-hero__meta flex min-w-0 flex-wrap items-center gap-3 xl:max-w-[48%] xl:justify-end">
<div class="flex flex-wrap gap-2 text-sm xl:justify-end">
<span class="movies-hero__stat">
<i class="fas fa-film" aria-hidden="true"></i>
{{ isset($results) ? number_format($results->total()) : 0 }} movies
@@ -58,7 +61,6 @@
</span>
@endif
</div>
</div>
<div class="flex flex-wrap items-center gap-3">
<button type="button"
@@ -76,6 +78,7 @@
</a>
</div>
</div>
</div>
</section>
<section class="surface-panel overflow-hidden rounded-2xl border shadow-sm" aria-labelledby="movie-filters-heading">
@@ -1,16 +1,28 @@
{{-- Shared color scheme switcher. JS hooks by button class (see alpine/stores/theme.js). Expects: $switcherId, $btnClass, $mobile (bool) --}}
@php $currentScheme = $userColorScheme ?? 'blue'; @endphp
<div class="flex items-center justify-center {{ $mobile ? 'gap-3' : 'gap-2' }}" id="{{ $switcherId }}">
<button type="button" data-scheme="blue" title="Blue"
class="{{ $btnClass }} {{ $mobile ? 'w-9 h-9' : 'w-8 h-8' }} rounded-full bg-blue-600 transition {{ $mobile ? 'touch-target ' : '' }}ring-offset-2 ring-offset-gray-900 {{ $currentScheme === 'blue' ? 'ring-2 ring-primary-500' : '' }}">
<span class="sr-only">Blue</span>
</button>
<button type="button" data-scheme="emerald" title="Emerald"
class="{{ $btnClass }} {{ $mobile ? 'w-9 h-9' : 'w-8 h-8' }} rounded-full bg-emerald-600 transition {{ $mobile ? 'touch-target ' : '' }}ring-offset-2 ring-offset-gray-900 {{ $currentScheme === 'emerald' ? 'ring-2 ring-primary-500' : '' }}">
<span class="sr-only">Emerald</span>
</button>
<button type="button" data-scheme="violet" title="Violet"
class="{{ $btnClass }} {{ $mobile ? 'w-9 h-9' : 'w-8 h-8' }} rounded-full bg-violet-600 transition {{ $mobile ? 'touch-target ' : '' }}ring-offset-2 ring-offset-gray-900 {{ $currentScheme === 'violet' ? 'ring-2 ring-primary-500' : '' }}">
<span class="sr-only">Violet</span>
@php
$currentScheme = $userColorScheme ?? 'blue';
$colorSchemes = [
['value' => 'blue', 'label' => 'Blue', 'swatch' => 'bg-blue-600'],
['value' => 'indigo', 'label' => 'Indigo', 'swatch' => 'bg-indigo-600'],
['value' => 'cyan', 'label' => 'Cyan', 'swatch' => 'bg-cyan-600'],
['value' => 'teal', 'label' => 'Teal', 'swatch' => 'bg-teal-600'],
['value' => 'emerald', 'label' => 'Emerald', 'swatch' => 'bg-emerald-600'],
['value' => 'violet', 'label' => 'Violet', 'swatch' => 'bg-violet-600'],
['value' => 'pink', 'label' => 'Pink', 'swatch' => 'bg-pink-600'],
['value' => 'rose', 'label' => 'Rose', 'swatch' => 'bg-rose-600'],
['value' => 'red', 'label' => 'Red', 'swatch' => 'bg-red-600'],
['value' => 'orange', 'label' => 'Orange', 'swatch' => 'bg-orange-600'],
['value' => 'amber', 'label' => 'Amber', 'swatch' => 'bg-amber-600'],
];
@endphp
<div class="grid grid-cols-4 place-items-center {{ $mobile ? 'gap-3' : 'gap-2' }}" id="{{ $switcherId }}">
@foreach($colorSchemes as $scheme)
<button type="button"
data-scheme="{{ $scheme['value'] }}"
title="{{ $scheme['label'] }}"
aria-pressed="{{ $currentScheme === $scheme['value'] ? 'true' : 'false' }}"
class="{{ $btnClass }} {{ $mobile ? 'w-9 h-9' : 'w-8 h-8' }} rounded-full {{ $scheme['swatch'] }} transition {{ $mobile ? 'touch-target ' : '' }}ring-offset-2 ring-offset-gray-900 {{ $currentScheme === $scheme['value'] ? 'ring-2 ring-primary-500' : '' }}">
<span class="sr-only">{{ $scheme['label'] }}</span>
</button>
@endforeach
</div>
@@ -16,6 +16,10 @@
var t = localStorage.getItem('theme') || 'light';
var scheme = localStorage.getItem('color_scheme') || 'blue';
@endauth
var allowedSchemes = ['blue', 'indigo', 'cyan', 'teal', 'emerald', 'violet', 'pink', 'rose', 'red', 'orange', 'amber'];
if (allowedSchemes.indexOf(scheme) === -1) {
scheme = 'blue';
}
var isDark = t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
d.classList.add('dark');
@@ -34,4 +38,20 @@ html[data-color-scheme="emerald"], html[data-color-scheme="emerald"] body { back
html.dark[data-color-scheme="emerald"], html.dark[data-color-scheme="emerald"] body { background-color: #071a12; color: #d1fae5; }
html[data-color-scheme="violet"], html[data-color-scheme="violet"] body { background-color: #faf5ff; color: #1e293b; }
html.dark[data-color-scheme="violet"], html.dark[data-color-scheme="violet"] body { background-color: #120b20; color: #e9d5ff; }
html[data-color-scheme="rose"], html[data-color-scheme="rose"] body { background-color: #fff1f2; color: #1e293b; }
html.dark[data-color-scheme="rose"], html.dark[data-color-scheme="rose"] body { background-color: #1f0a12; color: #fecdd3; }
html[data-color-scheme="amber"], html[data-color-scheme="amber"] body { background-color: #fffbeb; color: #1e293b; }
html.dark[data-color-scheme="amber"], html.dark[data-color-scheme="amber"] body { background-color: #1c1304; color: #fde68a; }
html[data-color-scheme="cyan"], html[data-color-scheme="cyan"] body { background-color: #ecfeff; color: #1e293b; }
html.dark[data-color-scheme="cyan"], html.dark[data-color-scheme="cyan"] body { background-color: #06191d; color: #cffafe; }
html[data-color-scheme="indigo"], html[data-color-scheme="indigo"] body { background-color: #eef2ff; color: #1e293b; }
html.dark[data-color-scheme="indigo"], html.dark[data-color-scheme="indigo"] body { background-color: #0b1024; color: #c7d2fe; }
html[data-color-scheme="teal"], html[data-color-scheme="teal"] body { background-color: #f0fdfa; color: #1e293b; }
html.dark[data-color-scheme="teal"], html.dark[data-color-scheme="teal"] body { background-color: #061a18; color: #ccfbf1; }
html[data-color-scheme="orange"], html[data-color-scheme="orange"] body { background-color: #fff7ed; color: #1e293b; }
html.dark[data-color-scheme="orange"], html.dark[data-color-scheme="orange"] body { background-color: #1f1007; color: #ffedd5; }
html[data-color-scheme="red"], html[data-color-scheme="red"] body { background-color: #fef2f2; color: #1e293b; }
html.dark[data-color-scheme="red"], html.dark[data-color-scheme="red"] body { background-color: #200a0a; color: #fee2e2; }
html[data-color-scheme="pink"], html[data-color-scheme="pink"] body { background-color: #fdf2f8; color: #1e293b; }
html.dark[data-color-scheme="pink"], html.dark[data-color-scheme="pink"] body { background-color: #200a17; color: #fce7f3; }
</style>
+28 -19
View File
@@ -128,26 +128,35 @@
<i class="fas fa-swatchbook mr-2 text-primary-600"></i>Color Scheme
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">Choose the main color scheme for the site (sidebar, header, buttons, and accents).</p>
<div class="flex flex-wrap items-center gap-4">
@php $currentScheme = $user->color_scheme ?? 'blue'; @endphp
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="color_scheme" value="blue" {{ $currentScheme === 'blue' ? 'checked' : '' }}
class="sr-only profile-scheme-radio" data-scheme="blue">
<span class="w-10 h-10 rounded-full bg-blue-600 ring-2 ring-offset-2 transition {{ $currentScheme === 'blue' ? 'ring-primary-500 ring-offset-gray-100 dark:ring-offset-gray-800' : 'ring-transparent ring-offset-gray-100 dark:ring-offset-gray-800 hover:ring-gray-300' }}" aria-hidden="true"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Blue</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="color_scheme" value="emerald" {{ $currentScheme === 'emerald' ? 'checked' : '' }}
class="sr-only profile-scheme-radio" data-scheme="emerald">
<span class="w-10 h-10 rounded-full bg-emerald-600 ring-2 ring-offset-2 transition {{ $currentScheme === 'emerald' ? 'ring-primary-500 ring-offset-gray-100 dark:ring-offset-gray-800' : 'ring-transparent ring-offset-gray-100 dark:ring-offset-gray-800 hover:ring-gray-300' }}" aria-hidden="true"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Emerald</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="color_scheme" value="violet" {{ $currentScheme === 'violet' ? 'checked' : '' }}
class="sr-only profile-scheme-radio" data-scheme="violet">
<span class="w-10 h-10 rounded-full bg-violet-600 ring-2 ring-offset-2 transition {{ $currentScheme === 'violet' ? 'ring-primary-500 ring-offset-gray-100 dark:ring-offset-gray-800' : 'ring-transparent ring-offset-gray-100 dark:ring-offset-gray-800 hover:ring-gray-300' }}" aria-hidden="true"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Violet</span>
@php
$currentScheme = $user->color_scheme ?? 'blue';
$colorSchemes = [
['value' => 'blue', 'label' => 'Blue', 'swatch' => 'bg-blue-600'],
['value' => 'indigo', 'label' => 'Indigo', 'swatch' => 'bg-indigo-600'],
['value' => 'cyan', 'label' => 'Cyan', 'swatch' => 'bg-cyan-600'],
['value' => 'teal', 'label' => 'Teal', 'swatch' => 'bg-teal-600'],
['value' => 'emerald', 'label' => 'Emerald', 'swatch' => 'bg-emerald-600'],
['value' => 'violet', 'label' => 'Violet', 'swatch' => 'bg-violet-600'],
['value' => 'pink', 'label' => 'Pink', 'swatch' => 'bg-pink-600'],
['value' => 'rose', 'label' => 'Rose', 'swatch' => 'bg-rose-600'],
['value' => 'red', 'label' => 'Red', 'swatch' => 'bg-red-600'],
['value' => 'orange', 'label' => 'Orange', 'swatch' => 'bg-orange-600'],
['value' => 'amber', 'label' => 'Amber', 'swatch' => 'bg-amber-600'],
];
@endphp
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-4">
@foreach($colorSchemes as $scheme)
<label class="flex cursor-pointer items-center gap-3 rounded-xl border border-gray-200 p-3 transition hover:border-primary-300 hover:bg-primary-50 dark:border-gray-700 dark:hover:border-primary-700 dark:hover:bg-primary-950/30">
<input type="radio"
name="color_scheme"
value="{{ $scheme['value'] }}"
{{ $currentScheme === $scheme['value'] ? 'checked' : '' }}
class="sr-only profile-scheme-radio"
data-scheme="{{ $scheme['value'] }}">
<span class="h-10 w-10 shrink-0 rounded-full {{ $scheme['swatch'] }} ring-2 ring-offset-2 transition {{ $currentScheme === $scheme['value'] ? 'ring-primary-500 ring-offset-gray-100 dark:ring-offset-gray-800' : 'ring-transparent ring-offset-gray-100 hover:ring-gray-300 dark:ring-offset-gray-800' }}" aria-hidden="true"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ $scheme['label'] }}</span>
</label>
@endforeach
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
<i class="fas fa-info-circle mr-1"></i>Save your profile to apply the color scheme. You can also change it from the user menu in the header.
+15
View File
@@ -96,6 +96,21 @@ class LayoutShellMarkupTest extends TestCase
$this->assertStringContainsString('aria-label="Pagination"', $this->view('components/admin/pagination.blade.php'));
}
public function test_shared_user_hero_uses_compact_vertical_spacing(): void
{
$hero = $this->view('components/page-header.blade.php');
$this->assertStringContainsString('px-5 py-4', $hero);
$this->assertStringContainsString('sm:px-7 sm:py-5', $hero);
$this->assertStringContainsString('flex flex-col gap-4', $hero);
$this->assertStringContainsString('aria-label="Breadcrumb" class="mb-2', $hero);
$this->assertStringContainsString('class="mt-2 max-w-2xl text-sm leading-5', $hero);
$this->assertStringContainsString('app-page-header__meta flex min-w-0 flex-wrap', $hero);
$this->assertStringContainsString('@if(isset($stats) || isset($actions))', $hero);
$this->assertStringNotContainsString('mt-3 flex flex-wrap gap-2 text-sm', $hero);
$this->assertStringNotContainsString('sm:py-8', $hero);
}
public function test_shared_view_styles_cover_application_admin_and_guest_surfaces(): void
{
$stylesheet = $this->resource('css/app.css');
+14
View File
@@ -23,6 +23,20 @@ class MoviesPageMarkupTest extends TestCase
$this->assertStringContainsString(':aria-label="layout === 1', $markup);
}
public function test_movie_hero_matches_the_compact_shared_hero_spacing(): void
{
$markup = $this->resource('views/movies/index.blade.php');
$this->assertStringContainsString('movies-hero relative overflow-hidden rounded-2xl px-5 py-4', $markup);
$this->assertStringContainsString('sm:px-7 sm:py-5', $markup);
$this->assertStringContainsString('relative z-10 flex flex-col gap-4', $markup);
$this->assertStringContainsString('aria-label="Breadcrumb" class="mb-2', $markup);
$this->assertStringContainsString('class="mt-2 max-w-2xl text-sm leading-5', $markup);
$this->assertStringContainsString('movies-hero__meta flex min-w-0 flex-wrap', $markup);
$this->assertStringNotContainsString('mt-3 flex flex-wrap gap-2 text-sm', $markup);
$this->assertStringNotContainsString('sm:py-8', $markup);
}
public function test_movie_cards_reserve_poster_space_and_reuse_release_rows(): void
{
$card = $this->resource('views/movies/partials/movie-card.blade.php');
+85
View File
@@ -2,10 +2,19 @@
namespace Tests\Unit;
use App\Http\Requests\UpdateThemeRequest;
use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator;
use Illuminate\Validation\Factory;
use PHPUnit\Framework\TestCase;
class ThemeTableStylesTest extends TestCase
{
/**
* @var list<string>
*/
private const COLOR_SCHEMES = ['blue', 'indigo', 'cyan', 'teal', 'emerald', 'violet', 'pink', 'rose', 'red', 'orange', 'amber'];
public function test_light_color_schemes_use_pale_table_hover_surfaces(): void
{
$stylesheet = (string) file_get_contents(__DIR__.'/../../resources/css/app.css');
@@ -13,6 +22,14 @@ class ThemeTableStylesTest extends TestCase
$this->assertStringContainsString('--surface-hover: #f1f5f9;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #ecfdf5;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #f5f3ff;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #fff1f2;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #fffbeb;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #ecfeff;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #eef2ff;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #f0fdfa;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #fff7ed;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #fef2f2;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #fdf2f8;', $stylesheet);
$this->assertMatchesRegularExpression(
'/main tbody tr:hover\s*\{\s*background-color: var\(--surface-hover\) !important;\s*\}/',
$stylesheet,
@@ -26,9 +43,77 @@ class ThemeTableStylesTest extends TestCase
$this->assertStringContainsString('--surface-hover-dark: #1e293b;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #14532d;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #3b0764;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #4c0519;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #78350f;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #164e63;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #312e81;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #134e4a;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #7c2d12;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #7f1d1d;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #831843;', $stylesheet);
$this->assertMatchesRegularExpression(
'/\.dark main tbody tr:hover\s*\{\s*background-color: var\(--surface-hover-dark\) !important;\s*\}/',
$stylesheet,
);
}
public function test_every_color_scheme_defines_complete_surface_and_accent_tokens(): void
{
$stylesheet = (string) file_get_contents(__DIR__.'/../../resources/css/app.css');
foreach (self::COLOR_SCHEMES as $scheme) {
$matched = preg_match(
'/\[data-color-scheme="'.preg_quote($scheme, '/').'"\]\s*\{(?<tokens>.*?)\}/s',
$stylesheet,
$matches,
);
$this->assertSame(1, $matched, "{$scheme} must define a color-scheme token block.");
foreach (['--surface-body:', '--surface-body-dark:', '--surface-hover:', '--surface-hover-dark:', '--color-primary-50:', '--color-primary-500:', '--color-primary-950:'] as $token) {
$this->assertStringContainsString($token, $matches['tokens'], "{$scheme} must define {$token}");
}
}
}
public function test_color_scheme_options_stay_in_sync_across_the_ui_and_persistence_layers(): void
{
$switcher = (string) file_get_contents(__DIR__.'/../../resources/views/partials/scheme-switcher.blade.php');
$profile = (string) file_get_contents(__DIR__.'/../../resources/views/profile/edit.blade.php');
$themeInit = (string) file_get_contents(__DIR__.'/../../resources/views/partials/theme-init.blade.php');
$themeStore = (string) file_get_contents(__DIR__.'/../../resources/js/alpine/stores/theme.js');
$profileRequest = (string) file_get_contents(__DIR__.'/../../app/Http/Requests/UpdateProfileRequest.php');
$profileController = (string) file_get_contents(__DIR__.'/../../app/Http/Controllers/ProfileController.php');
$schemeList = implode(',', self::COLOR_SCHEMES);
$javascriptSchemeList = "'".implode("', '", self::COLOR_SCHEMES)."'";
$this->assertStringContainsString("const colorSchemes = [{$javascriptSchemeList}]", $themeStore);
$this->assertStringContainsString("in:{$schemeList}", $profileRequest);
$this->assertStringContainsString($javascriptSchemeList, $themeInit);
$this->assertStringContainsString($javascriptSchemeList, $profileController);
foreach (self::COLOR_SCHEMES as $scheme) {
$this->assertStringContainsString("'value' => '{$scheme}'", $switcher);
$this->assertStringContainsString("'value' => '{$scheme}'", $profile);
if ($scheme !== 'blue') {
$this->assertStringContainsString("data-color-scheme=\"{$scheme}\"", $themeInit);
}
}
}
public function test_theme_update_validation_accepts_each_supported_scheme_and_rejects_unknown_values(): void
{
$validator = new Factory(new Translator(new ArrayLoader, 'en'));
$rules = (new UpdateThemeRequest)->rules();
foreach (self::COLOR_SCHEMES as $scheme) {
$this->assertFalse(
$validator->make(['color_scheme' => $scheme], $rules)->fails(),
"{$scheme} should be accepted as a color scheme.",
);
}
$this->assertTrue($validator->make(['color_scheme' => 'unknown'], $rules)->fails());
}
}