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
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:
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "Скорость воспроизведения",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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(', ')}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user