feat(themes): add light, light-slate, and light-rose themes
Some checks failed
CI / Backend (push) Successful in 30s
Release / Test backend (push) Failing after 11s
Release / Docker / backend (push) Has been skipped
Release / Docker / runner (push) Has been skipped
CI / UI (push) Successful in 35s
Release / Docker / caddy (push) Successful in 58s
Release / Check ui (push) Successful in 1m7s
CI / Backend (pull_request) Successful in 31s
CI / UI (pull_request) Successful in 1m1s
Release / Docker / ui (push) Failing after 2m43s
Release / Gitea Release (push) Has been skipped

Three light variants mirroring the existing dark set. All use CSS custom
properties so no component changes are needed. Theme dots in the header
and mobile drawer show a separator between dark/light groups; light-theme
swatches get a subtle ring so they're visible on light backgrounds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Admin
2026-03-30 19:13:25 +05:00
parent 90dbecfa17
commit e8d7108753
9 changed files with 88 additions and 16 deletions

View File

@@ -160,6 +160,9 @@
"profile_theme_amber": "Amber", "profile_theme_amber": "Amber",
"profile_theme_slate": "Slate", "profile_theme_slate": "Slate",
"profile_theme_rose": "Rose", "profile_theme_rose": "Rose",
"profile_theme_light": "Light",
"profile_theme_light_slate": "Light Blue",
"profile_theme_light_rose": "Light Rose",
"profile_reading_heading": "Reading settings", "profile_reading_heading": "Reading settings",
"profile_voice_label": "Default voice", "profile_voice_label": "Default voice",
"profile_speed_label": "Playback speed", "profile_speed_label": "Playback speed",

View File

@@ -160,6 +160,9 @@
"profile_theme_amber": "Ambre", "profile_theme_amber": "Ambre",
"profile_theme_slate": "Ardoise", "profile_theme_slate": "Ardoise",
"profile_theme_rose": "Rose", "profile_theme_rose": "Rose",
"profile_theme_light": "Light",
"profile_theme_light_slate": "Light Blue",
"profile_theme_light_rose": "Light Rose",
"profile_reading_heading": "Paramètres de lecture", "profile_reading_heading": "Paramètres de lecture",
"profile_voice_label": "Voix par défaut", "profile_voice_label": "Voix par défaut",
"profile_speed_label": "Vitesse de lecture", "profile_speed_label": "Vitesse de lecture",

View File

@@ -160,6 +160,9 @@
"profile_theme_amber": "Amber", "profile_theme_amber": "Amber",
"profile_theme_slate": "Abu-abu", "profile_theme_slate": "Abu-abu",
"profile_theme_rose": "Mawar", "profile_theme_rose": "Mawar",
"profile_theme_light": "Light",
"profile_theme_light_slate": "Light Blue",
"profile_theme_light_rose": "Light Rose",
"profile_reading_heading": "Pengaturan membaca", "profile_reading_heading": "Pengaturan membaca",
"profile_voice_label": "Suara default", "profile_voice_label": "Suara default",
"profile_speed_label": "Kecepatan pemutaran", "profile_speed_label": "Kecepatan pemutaran",

View File

@@ -160,6 +160,9 @@
"profile_theme_amber": "Âmbar", "profile_theme_amber": "Âmbar",
"profile_theme_slate": "Ardósia", "profile_theme_slate": "Ardósia",
"profile_theme_rose": "Rosa", "profile_theme_rose": "Rosa",
"profile_theme_light": "Light",
"profile_theme_light_slate": "Light Blue",
"profile_theme_light_rose": "Light Rose",
"profile_reading_heading": "Configurações de leitura", "profile_reading_heading": "Configurações de leitura",
"profile_voice_label": "Voz padrão", "profile_voice_label": "Voz padrão",
"profile_speed_label": "Velocidade de reprodução", "profile_speed_label": "Velocidade de reprodução",

View File

@@ -160,6 +160,9 @@
"profile_theme_amber": "Янтарь", "profile_theme_amber": "Янтарь",
"profile_theme_slate": "Сланец", "profile_theme_slate": "Сланец",
"profile_theme_rose": "Роза", "profile_theme_rose": "Роза",
"profile_theme_light": "Light",
"profile_theme_light_slate": "Light Blue",
"profile_theme_light_rose": "Light Rose",
"profile_reading_heading": "Настройки чтения", "profile_reading_heading": "Настройки чтения",
"profile_voice_label": "Голос по умолчанию", "profile_voice_label": "Голос по умолчанию",
"profile_speed_label": "Скорость воспроизведения", "profile_speed_label": "Скорость воспроизведения",

View File

@@ -55,6 +55,48 @@
--color-success: #4ade80; /* green-400 */ --color-success: #4ade80; /* green-400 */
} }
/* ── Light amber theme ────────────────────────────────────────────────── */
[data-theme="light"] {
--color-brand: #d97706; /* amber-600 */
--color-brand-dim: #b45309; /* amber-700 */
--color-surface: #ffffff;
--color-surface-2: #f4f4f5; /* zinc-100 */
--color-surface-3: #e4e4e7; /* zinc-200 */
--color-muted: #71717a; /* zinc-500 */
--color-text: #18181b; /* zinc-900 */
--color-border: #d4d4d8; /* zinc-300 */
--color-danger: #dc2626; /* red-600 */
--color-success: #16a34a; /* green-600 */
}
/* ── Light slate theme ────────────────────────────────────────────────── */
[data-theme="light-slate"] {
--color-brand: #4f46e5; /* indigo-600 */
--color-brand-dim: #4338ca; /* indigo-700 */
--color-surface: #f8fafc; /* slate-50 */
--color-surface-2: #f1f5f9; /* slate-100 */
--color-surface-3: #e2e8f0; /* slate-200 */
--color-muted: #64748b; /* slate-500 */
--color-text: #0f172a; /* slate-900 */
--color-border: #cbd5e1; /* slate-300 */
--color-danger: #dc2626; /* red-600 */
--color-success: #16a34a; /* green-600 */
}
/* ── Light rose theme ─────────────────────────────────────────────────── */
[data-theme="light-rose"] {
--color-brand: #e11d48; /* rose-600 */
--color-brand-dim: #be123c; /* rose-700 */
--color-surface: #fff1f2; /* rose-50 */
--color-surface-2: #ffe4e6; /* rose-100 */
--color-surface-3: #fecdd3; /* rose-200 */
--color-muted: #9f1239; /* rose-800 at 60% */
--color-text: #0f0a0b; /* near black */
--color-border: #fda4af; /* rose-300 */
--color-danger: #dc2626; /* red-600 */
--color-success: #16a34a; /* green-600 */
}
html { html {
background-color: var(--color-surface); background-color: var(--color-surface);
color: var(--color-text); color: var(--color-text);

View File

@@ -22,9 +22,12 @@
let langMenuOpen = $state(false); let langMenuOpen = $state(false);
const THEMES = [ const THEMES = [
{ id: 'amber', color: '#f59e0b' }, { id: 'amber', color: '#f59e0b' },
{ id: 'slate', color: '#818cf8' }, { id: 'slate', color: '#818cf8' },
{ id: 'rose', color: '#fb7185' }, { id: 'rose', color: '#fb7185' },
{ id: 'light', color: '#d97706', light: true },
{ id: 'light-slate', color: '#4f46e5', light: true },
{ id: 'light-rose', color: '#e11d48', light: true },
]; ];
// Chapter list drawer state for the mini-player // Chapter list drawer state for the mini-player
@@ -316,12 +319,15 @@
<div class="ml-auto flex items-center gap-2"> <div class="ml-auto flex items-center gap-2">
<!-- Theme dots (desktop) --> <!-- Theme dots (desktop) -->
<div class="hidden sm:flex items-center gap-1 mr-1"> <div class="hidden sm:flex items-center gap-1 mr-1">
{#each THEMES as t} {#each THEMES as t, i}
{#if i === 3}
<span class="w-px h-3 bg-(--color-border) mx-0.5"></span>
{/if}
<button <button
type="button" type="button"
onclick={() => { currentTheme = t.id; }} onclick={() => { currentTheme = t.id; }}
title={t.id} title={t.id}
class="w-3.5 h-3.5 rounded-full border-2 transition-all {currentTheme === t.id ? 'border-(--color-text) scale-110' : 'border-transparent opacity-50 hover:opacity-100'}" class="w-3.5 h-3.5 rounded-full border-2 transition-all {currentTheme === t.id ? 'border-(--color-text) scale-110' : t.light ? 'border-(--color-border) opacity-70 hover:opacity-100' : 'border-transparent opacity-50 hover:opacity-100'}"
style="background: {t.color};" style="background: {t.color};"
></button> ></button>
{/each} {/each}
@@ -498,13 +504,16 @@
<div class="my-1 border-t border-(--color-border)/60"></div> <div class="my-1 border-t border-(--color-border)/60"></div>
<div class="px-3 py-2.5 flex items-center justify-between"> <div class="px-3 py-2.5 flex items-center justify-between">
<span class="text-xs text-(--color-muted) uppercase tracking-widest">{m.profile_theme_label()}</span> <span class="text-xs text-(--color-muted) uppercase tracking-widest">{m.profile_theme_label()}</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-1.5">
{#each THEMES as t} {#each THEMES as t, i}
{#if i === 3}
<span class="w-px h-4 bg-(--color-border) mx-0.5"></span>
{/if}
<button <button
type="button" type="button"
onclick={() => { currentTheme = t.id; }} onclick={() => { currentTheme = t.id; }}
title={t.id} title={t.id}
class="w-5 h-5 rounded-full border-2 transition-all {currentTheme === t.id ? 'border-(--color-text) scale-110' : 'border-transparent opacity-50 hover:opacity-100'}" class="w-5 h-5 rounded-full border-2 transition-all {currentTheme === t.id ? 'border-(--color-text) scale-110' : t.light ? 'border-(--color-border) opacity-70 hover:opacity-100' : 'border-transparent opacity-50 hover:opacity-100'}"
style="background: {t.color};" style="background: {t.color};"
></button> ></button>
{/each} {/each}

View File

@@ -44,7 +44,7 @@ export const PUT: RequestHandler = async ({ request, locals }) => {
} }
// theme is optional — if provided it must be a known value // theme is optional — if provided it must be a known value
const validThemes = ['amber', 'slate', 'rose']; const validThemes = ['amber', 'slate', 'rose', 'light', 'light-slate', 'light-rose'];
if (body.theme !== undefined && !validThemes.includes(body.theme)) { if (body.theme !== undefined && !validThemes.includes(body.theme)) {
error(400, `Invalid theme — must be one of: ${validThemes.join(', ')}`); error(400, `Invalid theme — must be one of: ${validThemes.join(', ')}`);
} }

View File

@@ -83,10 +83,13 @@
let selectedFontFamily = $state(untrack(() => data.settings?.fontFamily ?? settingsCtx?.fontFamily ?? 'system')); let selectedFontFamily = $state(untrack(() => data.settings?.fontFamily ?? settingsCtx?.fontFamily ?? 'system'));
let selectedFontSize = $state(untrack(() => data.settings?.fontSize ?? settingsCtx?.fontSize ?? 1.0)); let selectedFontSize = $state(untrack(() => data.settings?.fontSize ?? settingsCtx?.fontSize ?? 1.0));
const THEMES: { id: string; label: () => string; swatch: string }[] = [ const THEMES: { id: string; label: () => string; swatch: string; light?: boolean }[] = [
{ id: 'amber', label: () => m.profile_theme_amber(), swatch: '#f59e0b' }, { id: 'amber', label: () => m.profile_theme_amber(), swatch: '#f59e0b' },
{ id: 'slate', label: () => m.profile_theme_slate(), swatch: '#818cf8' }, { id: 'slate', label: () => m.profile_theme_slate(), swatch: '#818cf8' },
{ id: 'rose', label: () => m.profile_theme_rose(), swatch: '#fb7185' }, { id: 'rose', label: () => m.profile_theme_rose(), swatch: '#fb7185' },
{ id: 'light', label: () => m.profile_theme_light(), swatch: '#d97706', light: true },
{ id: 'light-slate', label: () => m.profile_theme_light_slate(), swatch: '#4f46e5', light: true },
{ id: 'light-rose', label: () => m.profile_theme_light_rose(), swatch: '#e11d48', light: true },
]; ];
const FONTS = [ const FONTS = [
@@ -332,8 +335,11 @@
<!-- Theme --> <!-- Theme -->
<div class="px-6 py-5 space-y-3"> <div class="px-6 py-5 space-y-3">
<p class="text-sm font-medium text-(--color-text)">{m.profile_theme_label()}</p> <p class="text-sm font-medium text-(--color-text)">{m.profile_theme_label()}</p>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap items-center">
{#each THEMES as t} {#each THEMES as t, i}
{#if i === 3}
<span class="w-px h-6 bg-(--color-border) mx-1 self-center"></span>
{/if}
<button <button
type="button" type="button"
onclick={() => (selectedTheme = t.id)} onclick={() => (selectedTheme = t.id)}
@@ -343,7 +349,7 @@
: 'border-(--color-border) bg-(--color-surface-3) text-(--color-muted) hover:border-(--color-brand)/50 hover:text-(--color-text)'}" : 'border-(--color-border) bg-(--color-surface-3) text-(--color-muted) hover:border-(--color-brand)/50 hover:text-(--color-text)'}"
aria-pressed={selectedTheme === t.id} aria-pressed={selectedTheme === t.id}
> >
<span class="w-3 h-3 rounded-full shrink-0" style="background: {t.swatch};"></span> <span class="w-3 h-3 rounded-full shrink-0 {t.light ? 'ring-1 ring-(--color-border)' : ''}" style="background: {t.swatch};"></span>
{t.label()} {t.label()}
</button> </button>
{/each} {/each}