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 // Update color scheme
if ($request->has('color_scheme')) { if ($request->has('color_scheme')) {
$schemeValue = $request->input('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]); 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], '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,}$/'], 'password' => ['nullable', 'string', 'min:8', 'confirmed', 'regex:/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/'],
'theme_preference' => ['sometimes', 'in:light,dark,system'], '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 [ return [
'theme_preference' => ['sometimes', 'in:light,dark,system'], '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; --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 customization (replaces theme.extend in tailwind.config.js) */
@theme { @theme {
--font-sans: "Figtree", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --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'; import Alpine from '@alpinejs/csp';
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const colorSchemes = ['blue', 'indigo', 'cyan', 'teal', 'emerald', 'violet', 'pink', 'rose', 'red', 'orange', 'amber'];
Alpine.store('theme', { Alpine.store('theme', {
current: 'light', current: 'light',
@@ -19,18 +20,19 @@ Alpine.store('theme', {
const schemeMeta = document.querySelector('meta[name="color-scheme-preference"]'); const schemeMeta = document.querySelector('meta[name="color-scheme-preference"]');
const schemeData = document.getElementById('current-theme-data'); 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')) ? (schemeMeta ? schemeMeta.content : (schemeData?.dataset?.colorScheme || 'blue'))
: (localStorage.getItem('color_scheme') || 'blue'); : (localStorage.getItem('color_scheme') || 'blue');
this.colorScheme = colorSchemes.includes(preferredScheme) ? preferredScheme : 'blue';
this.apply(); this.apply();
this.applyScheme(); this.applyScheme();
this._listenOS(); this._listenOS();
}, },
/** Set and persist color scheme (blue, emerald, violet) */ /** Set and persist a supported color scheme. */
setScheme(scheme) { setScheme(scheme) {
if (!['blue', 'emerald', 'violet'].includes(scheme)) return; if (!colorSchemes.includes(scheme)) return;
this.colorScheme = scheme; this.colorScheme = scheme;
this.applyScheme(); this.applyScheme();
this._save(); this._save();
@@ -114,6 +116,7 @@ Alpine.store('theme', {
// Update color scheme swatch buttons // Update color scheme swatch buttons
document.querySelectorAll('.dropdown-scheme-btn, .mobile-scheme-btn').forEach(function(btn) { document.querySelectorAll('.dropdown-scheme-btn, .mobile-scheme-btn').forEach(function(btn) {
var isActive = btn.dataset.scheme === self.colorScheme; 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'); btn.classList.remove('ring-2', 'ring-offset-2', 'ring-offset-gray-900', 'ring-primary-500', 'ring-white');
if (isActive) { if (isActive) {
btn.classList.add('ring-2', 'ring-offset-2', 'ring-offset-gray-900', 'ring-primary-500', 'dark:ring-offset-gray-950'); 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, '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--one" aria-hidden="true"></div>
<div class="workspace-hero__glow workspace-hero__glow--two" 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="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"> <div class="min-w-0 max-w-3xl xl:flex-1">
<nav aria-label="Breadcrumb" class="mb-4 text-sm text-white/70"> <nav aria-label="Breadcrumb" class="mb-2 text-sm text-white/70">
<ol class="flex flex-wrap items-center gap-2"> <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><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> <li aria-hidden="true"><i class="fas fa-chevron-right text-[0.65rem]"></i></li>
@@ -33,20 +33,25 @@
</div> </div>
@if($description) @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 @endif
@isset($stats)
<div class="mt-5 flex flex-wrap gap-2 text-sm">
{{ $stats }}
</div>
@endisset
</div> </div>
@isset($actions) @if(isset($stats) || isset($actions))
<div class="app-page-header__actions flex shrink-0 flex-wrap items-center gap-3"> <div class="app-page-header__meta flex min-w-0 flex-wrap items-center gap-3 xl:max-w-[48%] xl:justify-end">
{{ $actions }} @isset($stats)
<div class="flex flex-wrap gap-2 text-sm xl:justify-end">
{{ $stats }}
</div>
@endisset
@isset($actions)
<div class="app-page-header__actions flex flex-wrap items-center gap-3">
{{ $actions }}
</div>
@endisset
</div> </div>
@endisset @endif
</div> </div>
</header> </header>
+23 -20
View File
@@ -16,13 +16,13 @@
@endphp @endphp
<div class="space-y-5" x-data="moviesPage" data-movie-layout="{{ $movie_layout ?? 2 }}"> <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--one" aria-hidden="true"></div>
<div class="movies-hero__glow movies-hero__glow--two" 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="relative z-10 flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
<div class="max-w-3xl"> <div class="min-w-0 max-w-3xl xl:flex-1">
<nav aria-label="Breadcrumb" class="mb-4 text-sm text-white/70"> <nav aria-label="Breadcrumb" class="mb-2 text-sm text-white/70">
<ol class="flex flex-wrap items-center gap-2"> <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><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> <li aria-hidden="true"><i class="fas fa-chevron-right text-[0.65rem]"></i></li>
@@ -38,11 +38,14 @@
</div> </div>
</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. Browse available movies, compare recent releases, and narrow the catalog by title, genre, year, or rating.
</p> </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"> <span class="movies-hero__stat">
<i class="fas fa-film" aria-hidden="true"></i> <i class="fas fa-film" aria-hidden="true"></i>
{{ isset($results) ? number_format($results->total()) : 0 }} movies {{ isset($results) ? number_format($results->total()) : 0 }} movies
@@ -58,22 +61,22 @@
</span> </span>
@endif @endif
</div> </div>
</div>
<div class="flex flex-wrap items-center gap-3"> <div class="flex flex-wrap items-center gap-3">
<button type="button" <button type="button"
@click="toggleLayout()" @click="toggleLayout()"
class="movies-hero__action" class="movies-hero__action"
:aria-label="layout === 1 ? 'Switch to two-column movie grid' : 'Switch to one-column movie list'" :aria-label="layout === 1 ? 'Switch to two-column movie grid' : 'Switch to one-column movie list'"
title="Change movie layout"> title="Change movie layout">
<i :class="layoutIcon" aria-hidden="true"></i> <i :class="layoutIcon" aria-hidden="true"></i>
<span x-text="layoutLabel"></span> <span x-text="layoutLabel"></span>
</button> </button>
<a href="{{ route('trending-movies') }}" class="movies-hero__action movies-hero__action--accent"> <a href="{{ route('trending-movies') }}" class="movies-hero__action movies-hero__action--accent">
<i class="fas fa-fire" aria-hidden="true"></i> <i class="fas fa-fire" aria-hidden="true"></i>
Trending now Trending now
</a> </a>
</div>
</div> </div>
</div> </div>
</section> </section>
@@ -1,16 +1,28 @@
{{-- Shared color scheme switcher. JS hooks by button class (see alpine/stores/theme.js). Expects: $switcherId, $btnClass, $mobile (bool) --}} {{-- Shared color scheme switcher. JS hooks by button class (see alpine/stores/theme.js). Expects: $switcherId, $btnClass, $mobile (bool) --}}
@php $currentScheme = $userColorScheme ?? 'blue'; @endphp @php
<div class="flex items-center justify-center {{ $mobile ? 'gap-3' : 'gap-2' }}" id="{{ $switcherId }}"> $currentScheme = $userColorScheme ?? 'blue';
<button type="button" data-scheme="blue" title="Blue" $colorSchemes = [
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' : '' }}"> ['value' => 'blue', 'label' => 'Blue', 'swatch' => 'bg-blue-600'],
<span class="sr-only">Blue</span> ['value' => 'indigo', 'label' => 'Indigo', 'swatch' => 'bg-indigo-600'],
</button> ['value' => 'cyan', 'label' => 'Cyan', 'swatch' => 'bg-cyan-600'],
<button type="button" data-scheme="emerald" title="Emerald" ['value' => 'teal', 'label' => 'Teal', 'swatch' => 'bg-teal-600'],
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' : '' }}"> ['value' => 'emerald', 'label' => 'Emerald', 'swatch' => 'bg-emerald-600'],
<span class="sr-only">Emerald</span> ['value' => 'violet', 'label' => 'Violet', 'swatch' => 'bg-violet-600'],
</button> ['value' => 'pink', 'label' => 'Pink', 'swatch' => 'bg-pink-600'],
<button type="button" data-scheme="violet" title="Violet" ['value' => 'rose', 'label' => 'Rose', 'swatch' => 'bg-rose-600'],
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' : '' }}"> ['value' => 'red', 'label' => 'Red', 'swatch' => 'bg-red-600'],
<span class="sr-only">Violet</span> ['value' => 'orange', 'label' => 'Orange', 'swatch' => 'bg-orange-600'],
</button> ['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> </div>
@@ -16,6 +16,10 @@
var t = localStorage.getItem('theme') || 'light'; var t = localStorage.getItem('theme') || 'light';
var scheme = localStorage.getItem('color_scheme') || 'blue'; var scheme = localStorage.getItem('color_scheme') || 'blue';
@endauth @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); var isDark = t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) { if (isDark) {
d.classList.add('dark'); 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.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[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.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> </style>
+29 -20
View File
@@ -128,26 +128,35 @@
<i class="fas fa-swatchbook mr-2 text-primary-600"></i>Color Scheme <i class="fas fa-swatchbook mr-2 text-primary-600"></i>Color Scheme
</h3> </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> <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
@php $currentScheme = $user->color_scheme ?? 'blue'; @endphp $currentScheme = $user->color_scheme ?? 'blue';
<label class="flex items-center gap-2 cursor-pointer"> $colorSchemes = [
<input type="radio" name="color_scheme" value="blue" {{ $currentScheme === 'blue' ? 'checked' : '' }} ['value' => 'blue', 'label' => 'Blue', 'swatch' => 'bg-blue-600'],
class="sr-only profile-scheme-radio" data-scheme="blue"> ['value' => 'indigo', 'label' => 'Indigo', 'swatch' => 'bg-indigo-600'],
<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> ['value' => 'cyan', 'label' => 'Cyan', 'swatch' => 'bg-cyan-600'],
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Blue</span> ['value' => 'teal', 'label' => 'Teal', 'swatch' => 'bg-teal-600'],
</label> ['value' => 'emerald', 'label' => 'Emerald', 'swatch' => 'bg-emerald-600'],
<label class="flex items-center gap-2 cursor-pointer"> ['value' => 'violet', 'label' => 'Violet', 'swatch' => 'bg-violet-600'],
<input type="radio" name="color_scheme" value="emerald" {{ $currentScheme === 'emerald' ? 'checked' : '' }} ['value' => 'pink', 'label' => 'Pink', 'swatch' => 'bg-pink-600'],
class="sr-only profile-scheme-radio" data-scheme="emerald"> ['value' => 'rose', 'label' => 'Rose', 'swatch' => 'bg-rose-600'],
<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> ['value' => 'red', 'label' => 'Red', 'swatch' => 'bg-red-600'],
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Emerald</span> ['value' => 'orange', 'label' => 'Orange', 'swatch' => 'bg-orange-600'],
</label> ['value' => 'amber', 'label' => 'Amber', 'swatch' => 'bg-amber-600'],
<label class="flex items-center gap-2 cursor-pointer"> ];
<input type="radio" name="color_scheme" value="violet" {{ $currentScheme === 'violet' ? 'checked' : '' }} @endphp
class="sr-only profile-scheme-radio" data-scheme="violet"> <div class="grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-4">
<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> @foreach($colorSchemes as $scheme)
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Violet</span> <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">
</label> <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> </div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400"> <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. <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')); $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 public function test_shared_view_styles_cover_application_admin_and_guest_surfaces(): void
{ {
$stylesheet = $this->resource('css/app.css'); $stylesheet = $this->resource('css/app.css');
+14
View File
@@ -23,6 +23,20 @@ class MoviesPageMarkupTest extends TestCase
$this->assertStringContainsString(':aria-label="layout === 1', $markup); $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 public function test_movie_cards_reserve_poster_space_and_reuse_release_rows(): void
{ {
$card = $this->resource('views/movies/partials/movie-card.blade.php'); $card = $this->resource('views/movies/partials/movie-card.blade.php');
+85
View File
@@ -2,10 +2,19 @@
namespace Tests\Unit; namespace Tests\Unit;
use App\Http\Requests\UpdateThemeRequest;
use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator;
use Illuminate\Validation\Factory;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class ThemeTableStylesTest extends 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 public function test_light_color_schemes_use_pale_table_hover_surfaces(): void
{ {
$stylesheet = (string) file_get_contents(__DIR__.'/../../resources/css/app.css'); $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: #f1f5f9;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #ecfdf5;', $stylesheet); $this->assertStringContainsString('--surface-hover: #ecfdf5;', $stylesheet);
$this->assertStringContainsString('--surface-hover: #f5f3ff;', $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( $this->assertMatchesRegularExpression(
'/main tbody tr:hover\s*\{\s*background-color: var\(--surface-hover\) !important;\s*\}/', '/main tbody tr:hover\s*\{\s*background-color: var\(--surface-hover\) !important;\s*\}/',
$stylesheet, $stylesheet,
@@ -26,9 +43,77 @@ class ThemeTableStylesTest extends TestCase
$this->assertStringContainsString('--surface-hover-dark: #1e293b;', $stylesheet); $this->assertStringContainsString('--surface-hover-dark: #1e293b;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #14532d;', $stylesheet); $this->assertStringContainsString('--surface-hover-dark: #14532d;', $stylesheet);
$this->assertStringContainsString('--surface-hover-dark: #3b0764;', $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( $this->assertMatchesRegularExpression(
'/\.dark main tbody tr:hover\s*\{\s*background-color: var\(--surface-hover-dark\) !important;\s*\}/', '/\.dark main tbody tr:hover\s*\{\s*background-color: var\(--surface-hover-dark\) !important;\s*\}/',
$stylesheet, $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());
}
} }