159 lines
4.6 KiB
PHP
159 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Enums\NavGradientDirection;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
class Theme extends Model
|
|
{
|
|
protected $table = 'site_themes';
|
|
|
|
protected $guarded = ['id'];
|
|
|
|
protected $casts = [
|
|
'nav_gradient_direction' => NavGradientDirection::class,
|
|
];
|
|
|
|
/** Stock values — must match the migration defaults and the @theme tokens in app.css. */
|
|
public const DEFAULTS = [
|
|
'primary_color' => '#2563eb',
|
|
'secondary_color' => '#3b82f6',
|
|
'tertiary_color' => '#1d4ed8',
|
|
'nav_gradient_from' => '#ffffff',
|
|
'nav_gradient_mid' => null,
|
|
'nav_gradient_to' => null,
|
|
'nav_gradient_direction' => 'none',
|
|
];
|
|
|
|
protected static function booted(): void
|
|
{
|
|
static::saved(fn () => Cache::forget('site-theme'));
|
|
static::deleted(fn () => Cache::forget('site-theme'));
|
|
}
|
|
|
|
/** The single site-wide theme row, cached until the next save. */
|
|
public static function current(): self
|
|
{
|
|
// refresh() hydrates the DB column defaults into the fresh model.
|
|
return Cache::rememberForever(
|
|
'site-theme',
|
|
fn () => static::query()->first() ?? static::query()->create([])->refresh()
|
|
);
|
|
}
|
|
|
|
/** True when the theme matches the stock look exactly (no override needed). */
|
|
public function isDefault(): bool
|
|
{
|
|
if ($this->logo_path !== null) {
|
|
return false;
|
|
}
|
|
|
|
foreach (self::DEFAULTS as $key => $default) {
|
|
$value = $this->{$key};
|
|
if ($value instanceof \BackedEnum) {
|
|
$value = $value->value;
|
|
}
|
|
|
|
if ($value === null || $default === null) {
|
|
if ($value !== $default) {
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (strtolower((string) $value) !== strtolower((string) $default)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function primaryHover(): string
|
|
{
|
|
return self::darken($this->primary_color, 0.15);
|
|
}
|
|
|
|
/** CSS background value for the top bar: solid color or gradient. */
|
|
public function navBackgroundCss(): string
|
|
{
|
|
$stops = $this->activeGradientStops();
|
|
|
|
if ($this->nav_gradient_direction === NavGradientDirection::None || count($stops) < 2) {
|
|
return $this->nav_gradient_from;
|
|
}
|
|
|
|
$list = implode(', ', $stops);
|
|
|
|
return $this->nav_gradient_direction === NavGradientDirection::Radial
|
|
? "radial-gradient(ellipse at top, {$list})"
|
|
: "linear-gradient({$this->nav_gradient_direction->cssKeyword()}, {$list})";
|
|
}
|
|
|
|
/** Whether the nav background is dark enough to need white text (WCAG contrast). */
|
|
public function navIsDark(): bool
|
|
{
|
|
$stops = $this->nav_gradient_direction === NavGradientDirection::None
|
|
? [$this->nav_gradient_from]
|
|
: $this->activeGradientStops();
|
|
|
|
$l = array_sum(array_map(self::luminance(...), $stops)) / count($stops);
|
|
|
|
// White text wins when its contrast beats gray-900's (luminance ~0.0106).
|
|
return (1.05 / ($l + 0.05)) >= (($l + 0.05) / 0.0606);
|
|
}
|
|
|
|
public function navLinkColor(): string
|
|
{
|
|
return $this->navIsDark() ? '#ffffff' : $this->primary_color;
|
|
}
|
|
|
|
public function navTextColor(): string
|
|
{
|
|
return $this->navIsDark() ? '#ffffff' : '#111827';
|
|
}
|
|
|
|
public function navMutedColor(): string
|
|
{
|
|
return $this->navIsDark() ? 'rgba(255, 255, 255, 0.75)' : '#6b7280';
|
|
}
|
|
|
|
/** @return list<string> */
|
|
private function activeGradientStops(): array
|
|
{
|
|
return array_values(array_filter([
|
|
$this->nav_gradient_from,
|
|
$this->nav_gradient_mid,
|
|
$this->nav_gradient_to,
|
|
]));
|
|
}
|
|
|
|
/** WCAG relative luminance of a #rrggbb color. */
|
|
private static function luminance(string $hex): float
|
|
{
|
|
[$r, $g, $b] = sscanf($hex, '#%02x%02x%02x');
|
|
|
|
$lin = function (int $c): float {
|
|
$c = $c / 255;
|
|
|
|
return $c <= 0.03928 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4;
|
|
};
|
|
|
|
return 0.2126 * $lin($r) + 0.7152 * $lin($g) + 0.0722 * $lin($b);
|
|
}
|
|
|
|
private static function darken(string $hex, float $amount): string
|
|
{
|
|
[$r, $g, $b] = sscanf($hex, '#%02x%02x%02x');
|
|
|
|
return sprintf(
|
|
'#%02x%02x%02x',
|
|
(int) round($r * (1 - $amount)),
|
|
(int) round($g * (1 - $amount)),
|
|
(int) round($b * (1 - $amount))
|
|
);
|
|
}
|
|
}
|