Files
RSS-Feed/src/View.php
T
Harwinder Sekhon 20f4fd7095 Redesign UI color system: neutral ink chrome, sources own the chroma
The light lavender theme carried its color in decoration -- four colored
background blobs, a conic-gradient brand mark, gradient-clipped wordmark,
a four-color animated button, a spinning conic empty mark, and a five-color
spectrum rail. All of that competed with the only color on screen that
carries meaning: which source a story came from.

- One indigo-slate ink ramp (bg / surface / line / ink) carries every neutral
  surface; the only saturated hues are the three sources, used strictly as
  signals -- filter pill, card spine, source chip, timestamp, feed health.
- Rule for every filled state: bright fill, deep ink type. Source tokens kept
  in sync between :root and View::sourceColor().
- Signature "signal bars": a CSS brand mark of three ascending bars in the
  source hues, and the header rail re-cut into three wire segments that double
  as the palette legend. The empty state reuses the mark, unlit and static.
- Bootstrap switched to data-bs-theme="dark" with matching form controls,
  alerts, and focus rings; --ink-faint recalibrated so small text clears
  WCAG 4.5:1 on every surface. Reduced-motion behavior unchanged.
- README documents the two rules next to View::sourceColor().
2026-10-08 11:59:01 -04:00

104 lines
2.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App;
final class View
{
public static function e(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
public static function sourceLabel(string $source): string
{
return match ($source) {
'youtube' => 'YouTube',
'reddit' => 'Reddit',
'rt' => 'RT',
default => ucfirst($source),
};
}
public static function sourceBadge(string $source): string
{
return match ($source) {
'youtube' => 'text-bg-danger',
'reddit' => 'text-bg-warning',
'rt' => 'text-bg-dark',
default => 'text-bg-secondary',
};
}
public static function sourceColor(string $source): string
{
return match ($source) {
'youtube' => '#ff3b5c',
'reddit' => '#ff7a2f',
'rt' => '#1fcb93',
default => '#f1f3f7',
};
}
public static function rangeLabel(string $range): string
{
return match ($range) {
'1h' => '1 hour',
'2h' => '2 hours',
'6h' => '6 hours',
'24h' => '24 hours',
'7d' => '7 days',
default => 'All time',
};
}
public static function render(string $template, array $data = []): string
{
$path = dirname(__DIR__) . '/templates/' . $template . '.php';
if (!is_file($path)) {
throw new \RuntimeException(sprintf('Template "%s" not found.', $template));
}
extract($data, EXTR_SKIP);
ob_start();
include $path;
return (string) ob_get_clean();
}
public static function timeAgo(?string $iso): string
{
if ($iso === null || $iso === '') {
return '';
}
$timestamp = strtotime($iso);
if ($timestamp === false) {
return '';
}
$diff = time() - $timestamp;
if ($diff < 60) {
return 'just now';
}
$units = [
31536000 => 'y',
2592000 => 'mo',
604800 => 'w',
86400 => 'd',
3600 => 'h',
60 => 'm',
];
foreach ($units as $seconds => $label) {
if ($diff >= $seconds) {
return floor($diff / $seconds) . $label . ' ago';
}
}
return 'just now';
}
}