feat: color themes, dark mode, HC/sepia themes, moon tooltip, docs update

This commit is contained in:
catarrh committed 2026-08-02 23:45:56 +03:00
1 parent 857adce5da
commit f45791579b
16 files changed
+389 -240

No files matched your search

+20
View File
@@ -472,6 +472,11 @@ export interface Translations {
fontPreviewNoteInactive: string;
fontSize: string;
systemFont: string;
theme: string;
themeDefault: string;
themeDark: string;
themeSepia: string;
themeHC: string;
bibleSettings: string;
defaultTranslation: string;
showVerseNumbers: string;
@@ -1022,6 +1027,11 @@ const translations: Record<LanguageCode, Translations> = {
fontPreviewNoteInactive: 'System font active — rank icons will use Ponomar font regardless',
fontSize: 'Liturgical Text Size: ',
systemFont: 'System Default',
theme: 'Color Scheme',
themeDefault: 'Default',
themeDark: 'Dark',
themeSepia: 'Sepia',
themeHC: 'High Contrast',
bibleSettings: 'Bible',
defaultTranslation: 'Default Translation',
showVerseNumbers: 'Show verse numbers',
@@ -1570,6 +1580,11 @@ const translations: Record<LanguageCode, Translations> = {
fontPreviewNoteInactive: 'Системный шрифт активен — иконы чинов будут использовать Понома́рь в любом случае',
fontSize: 'Размер литургического текста: ',
systemFont: 'Системный шрифт',
theme: 'Цветовая схема',
themeDefault: 'Стандартная',
themeDark: 'Тёмная',
themeSepia: 'Сепия',
themeHC: 'Высокая контрастность',
bibleSettings: 'Библия',
defaultTranslation: 'Перевод по умолчанию',
showVerseNumbers: 'Показывать номера стихов',
@@ -2118,6 +2133,11 @@ const translations: Record<LanguageCode, Translations> = {
fontPreviewNoteInactive: 'Системный шрифтъ активенъ — иконы чинѣвъ будутъ использовать Понома́рь въ любомъ случаи',
fontSize: 'Размеръ литургическаго текста: ',
systemFont: 'Системный шрифтъ',
theme: 'Цвѣтова́ѧ схе́ма',
themeDefault: 'Станда́ртнаѧ',
themeDark: 'Тёмнаѧ',
themeSepia: 'Се́пїѧ',
themeHC: 'Высо́каѧ контра́стность',
bibleSettings: 'Библия',
defaultTranslation: 'Переводъ по умолчанiю',
showVerseNumbers: 'Показывать номера стиховъ',
+92 -12
View File
@@ -1,17 +1,97 @@
@import "tailwindcss";
@theme {
--color-parchment: #f5f0e8;
--color-parchment-dark: #e8dfd0;
--color-navy: #1a1a2e;
--color-navy-light: #2a2a4e;
--color-gold: #c5a55a;
--color-gold-light: #d4b96e;
--color-red: #8b0000;
--color-red-light: #a52a2a;
--color-purple: #4a0e4e;
--color-green: #2d5a27;
--color-blue: #1a3a6e;
--color-parchment: var(--clr-parchment);
--color-parchment-dark: var(--clr-parchment-dark);
--color-navy: var(--clr-navy);
--color-navy-light: var(--clr-navy-light);
--color-gold: var(--clr-gold);
--color-gold-light: var(--clr-gold-light);
--color-red: var(--clr-red);
--color-red-light: var(--clr-red-light);
--color-purple: var(--clr-purple);
--color-green: var(--clr-green);
--color-blue: var(--clr-blue);
--color-surface: var(--clr-surface);
--color-header-bg: var(--clr-header-bg);
--color-header-text: var(--clr-header-text);
--color-dropdown-bg: var(--clr-dropdown-bg);
}
:root, .theme-default {
--clr-parchment: #f5f0e8;
--clr-parchment-dark: #e8dfd0;
--clr-navy: #1a1a2e;
--clr-navy-light: #2a2a4e;
--clr-gold: #c5a55a;
--clr-gold-light: #d4b96e;
--clr-red: #8b0000;
--clr-red-light: #a52a2a;
--clr-purple: #4a0e4e;
--clr-green: #2d5a27;
--clr-blue: #1a3a6e;
--clr-surface: #ffffff;
--clr-header-bg: #1a1a2e;
--clr-header-text: #f5f0e8;
--clr-dropdown-bg: #2a2a4e;
--clr-browser-chrome: #1a1a2e;
}
.theme-dark {
--clr-parchment: #121212;
--clr-parchment-dark: #0a0a1a;
--clr-navy: #e0e0e0;
--clr-navy-light: #a0a0b0;
--clr-gold: #c5a55a;
--clr-gold-light: #d4b96e;
--clr-red: #ff6b6b;
--clr-red-light: #ff4444;
--clr-purple: #b07cc6;
--clr-green: #6bcf7f;
--clr-blue: #6ba3d6;
--clr-surface: #2a2a3e;
--clr-header-bg: #1a1a2e;
--clr-header-text: #f5f0e8;
--clr-dropdown-bg: #0e0e1a;
--clr-browser-chrome: #1a1a2e;
}
.theme-sepia {
--clr-parchment: #f4ecd8;
--clr-parchment-dark: #e8dcc8;
--clr-navy: #3b2f2f;
--clr-navy-light: #5c4a4a;
--clr-gold: #8b7355;
--clr-gold-light: #a08868;
--clr-red: #8b3a3a;
--clr-red-light: #a54a4a;
--clr-purple: #4a0e4e;
--clr-green: #2d5a27;
--clr-blue: #1a3a6e;
--clr-surface: #faf3e6;
--clr-header-bg: #3b2f2f;
--clr-header-text: #f4ecd8;
--clr-dropdown-bg: #5c4a4a;
--clr-browser-chrome: #3b2f2f;
}
.theme-hc {
--clr-parchment: #ffffff;
--clr-parchment-dark: #e6e6e6;
--clr-navy: #000000;
--clr-navy-light: #222222;
--clr-gold: #666666;
--clr-gold-light: #888888;
--clr-red: #000000;
--clr-red-light: #333333;
--clr-purple: #555555;
--clr-green: #555555;
--clr-blue: #555555;
--clr-surface: #ffffff;
--clr-header-bg: #000000;
--clr-header-text: #ffffff;
--clr-dropdown-bg: #222222;
--clr-browser-chrome: #000000;
}
@font-face {
@@ -166,7 +246,7 @@ nav details.nav-group > div {
z-index: 50;
min-width: 14rem;
border-radius: 0.5rem;
background-color: var(--color-navy-light);
background-color: var(--color-dropdown-bg);
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
padding: 0.5rem;
display: flex;
+12 -2
View File
@@ -25,6 +25,15 @@ export class App {
this.currentDate = JDate.today();
this.settings = loadSettings();
document.documentElement.style.setProperty('--liturgical-font-size', this.settings.fontSize + 'px');
this.applyTheme();
}
private applyTheme() {
document.documentElement.className = 'theme-' + (this.settings.theme || 'default');
const chromeColor = getComputedStyle(document.documentElement).getPropertyValue('--clr-browser-chrome').trim();
if (chromeColor) {
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', chromeColor);
}
}
init() {
@@ -115,10 +124,11 @@ export class App {
private render() {
this.settings = loadSettings();
this.applyTheme();
const t = getTranslations(this.settings.language as LanguageCode);
this.container.innerHTML = `
<div class="app-container min-h-screen flex flex-col">
<header class="bg-navy text-parchment p-3 flex items-center shadow-md relative">
<header class="bg-header-bg text-header-text p-3 flex items-center shadow-md relative">
<span id="app-title" class="hidden md:inline-flex items-center gap-2 text-lg font-bold whitespace-nowrap absolute left-3 top-1/2 -translate-y-1/2">
<img src="/icons/icon-192.png" class="w-6 h-6" alt="">
${t.nav.appTitle}
@@ -127,7 +137,7 @@ export class App {
<a href="#calendar" class="nav-link hover:text-gold transition-colors" data-view="calendar">${t.nav.calendar}</a>
<details class="nav-group relative" data-nav="library">
<summary class="cursor-pointer list-none hover:text-gold transition-colors flex items-center gap-1">${t.nav.library} <span class="text-xs">▾</span></summary>
<div class="absolute left-0 top-full mt-1 z-50 min-w-56 rounded-lg bg-navy-light shadow-xl p-2 flex flex-col gap-1">
<div class="absolute left-0 top-full mt-1 z-50 min-w-56 rounded-lg bg-dropdown-bg shadow-xl p-2 flex flex-col gap-1">
<a href="#bible" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="bible"><span>📖</span> <span class="font-bold">${t.nav.bible}</span></a>
<a href="#prayer" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="prayer">${t.nav.prayer}</a>
<a href="#akathists" class="nav-link hover:text-gold transition-colors px-2 py-1" data-view="akathists">${t.nav.akathists}</a>
+9 -9
View File
@@ -532,9 +532,9 @@ export class CalendarView {
let classes = 'day-cell p-1 text-center text-sm cursor-pointer rounded transition-colors ';
if (isSelected) classes += 'bg-navy text-parchment font-bold ';
else if (isToday) classes += 'bg-gold/20 font-bold ';
else if (dayRank >= 6) classes += 'bg-red-100 text-red-800 font-bold ';
else if (fast === 1) classes += 'bg-purple-50 ';
else if (fast === 2) classes += 'bg-blue-50 ';
else if (dayRank >= 6) classes += 'bg-red/20 text-red font-bold ';
else if (fast === 1) classes += 'bg-purple/20 ';
else if (fast === 2) classes += 'bg-blue/20 ';
gridHtml += `<div class="${classes}" data-day="${d}">${d}</div>`;
@@ -583,21 +583,21 @@ export class CalendarView {
<div id="day-title" class="mb-2"></div>
<div id="astro-panel" class="mb-2 p-2 bg-white/50 border border-gold/20 rounded-lg text-sm text-navy">
<div id="astro-panel" class="mb-2 p-2 bg-surface/50 border border-gold/20 rounded-lg text-sm text-navy">
<span class="text-navy-light italic">${this.t.loading}</span>
</div>
<div id="commemorations-panel" class="mb-2 p-2 bg-white/50 border border-gold/20 rounded-lg ${fnClass}">
<div id="commemorations-panel" class="mb-2 p-2 bg-surface/50 border border-gold/20 rounded-lg ${fnClass}">
<h3 class="font-bold text-red mb-2">${this.t.calendar.commemorations}</h3>
<p class="text-sm text-navy-light italic">${this.t.loading}</p>
</div>
<div id="fasting-panel" class="mb-2 p-2 bg-white/50 border border-gold/20 rounded-lg ${fnClass}">
<div id="fasting-panel" class="mb-2 p-2 bg-surface/50 border border-gold/20 rounded-lg ${fnClass}">
<h3 class="font-bold text-red mb-2">${this.t.calendar.fastingRule}</h3>
<p class="text-sm italic text-navy-light">${this.t.loading}</p>
</div>
<div id="readings-panel" class="mb-2 p-2 bg-white/50 border border-gold/20 rounded-lg ${fnClass}">
<div id="readings-panel" class="mb-2 p-2 bg-surface/50 border border-gold/20 rounded-lg ${fnClass}">
<h3 class="font-bold text-red mb-2">${this.t.calendar.readings}</h3>
<p class="text-sm text-navy-light italic">${this.t.loading}</p>
</div>
@@ -795,7 +795,7 @@ export class CalendarView {
const icon = this.t.calendar.rankIcons[rank] || '';
const iconHtml = icon ? `<span class="font-slavonic text-lg mr-1">${icon}</span>` : '';
const lifeHtml = lifeText
? `<div class="life-text hidden mt-2 p-3 bg-white border border-gold/20 rounded text-sm leading-relaxed max-h-60 overflow-y-auto">${lifeText}</div>`
? `<div class="life-text hidden mt-2 p-3 bg-surface border border-gold/20 rounded text-sm leading-relaxed max-h-60 overflow-y-auto">${lifeText}</div>`
: '';
commHtmlParts.push(
`<div class="comm-item">
@@ -957,7 +957,7 @@ commPanel.addEventListener('click', (e) => {
<span class="text-navy">${solar?.sunset ?? '—'}</span>
<span class="mx-2 text-gold/40">|</span>
<span class="font-bold text-navy">${this.t.calendar.moonPhase}:</span>
<span class="text-navy">${moon.emoji} ${moonName} ${moon.trend === 'waxing' ? '↗' : '↘'} (${this.t.calendar.moonDay} ${moon.moonDay}, ${moon.illumination}%)</span>
<span class="text-navy"><span title="${moonName}">${moon.emoji}</span> ${moon.trend === 'waxing' ? '↗' : '↘'} ${this.t.calendar.moonDay} ${moon.moonDay}, ${moon.illumination}%</span>
`;
} else {
astroPanel.innerHTML = `<span class="text-navy-light italic">${this.t.calendar.noLocation}</span>`;
+2 -2
View File
@@ -72,13 +72,13 @@ export class MenaionView {
class="sub-nav-chip text-sm transition-colors rounded-lg px-3 py-1
${item.id === 'menaion'
? 'border border-gold bg-gold/10 text-navy font-bold'
: 'border border-gold/20 bg-white/50 text-navy hover:border-gold/50'}"
: 'border border-gold/20 bg-surface/50 text-navy hover:border-gold/50'}"
data-view="${item.view}"
>${item.label}</button>
`).join('')}
</div>
<div id="menaion-calendar"></div>
<div id="menaion-content" class="bg-white/50 border border-gold/20 rounded-lg p-4 min-h-[300px]">
<div id="menaion-content" class="bg-surface/50 border border-gold/20 rounded-lg p-4 min-h-[300px]">
<p class="text-navy-light italic">${this.t.prayer.selectSection}</p>
</div>
</div>
+3 -3
View File
@@ -220,18 +220,18 @@ export class PrayerView {
class="sub-nav-chip text-sm transition-colors rounded-lg px-3 py-1
${this.collection === item.id
? 'border border-gold bg-gold/10 text-navy font-bold'
: 'border border-gold/20 bg-white/50 text-navy hover:border-gold/50'}"
: 'border border-gold/20 bg-surface/50 text-navy hover:border-gold/50'}"
data-view="${item.view}"
>${item.label}</button>
`).join('')}
</div>
<p class="text-navy-light text-sm mb-2">${subtitle}</p>
<select id="prayer-select" class="w-full border border-gold/20 bg-white/50 text-navy rounded-lg px-3 py-2 text-sm mb-3">
<select id="prayer-select" class="w-full border border-gold/20 bg-surface/50 text-navy rounded-lg px-3 py-2 text-sm mb-3">
${sections.map(s => `
<option value="${s.id}" ${this.activeSection === s.id ? 'selected' : ''}>${s.name}</option>
`).join('')}
</select>
<div id="prayer-content" class="bg-white/50 border border-gold/20 rounded-lg p-4 min-h-[300px]">
<div id="prayer-content" class="bg-surface/50 border border-gold/20 rounded-lg p-4 min-h-[300px]">
<p class="text-navy-light italic">${this.t.prayer.selectSection}</p>
</div>
</div>
+3 -3
View File
@@ -86,7 +86,7 @@ export class ServiceView {
const availableServices = this.getAvailableServices(dayInfo);
const fontClass = this.getFontClass();
const contentClass = `bg-white/50 border border-gold/20 rounded-lg p-4 ${this.activeService ? 'min-h-[300px]' : ''} ${fontClass}`;
const contentClass = `bg-surface/50 border border-gold/20 rounded-lg p-4 ${this.activeService ? 'min-h-[300px]' : ''} ${fontClass}`;
const tabHtml = `
<h3 class="font-bold text-red mb-2">${this.t.services.title}</h3>
@@ -96,7 +96,7 @@ export class ServiceView {
class="service-tab text-sm transition-colors
${this.activeService === s.id
? 'border border-gold bg-gold/10 text-navy font-bold rounded-lg px-3 py-1'
: 'border border-gold/20 bg-white/50 text-navy hover:border-gold/50 rounded-lg px-3 py-1'}"
: 'border border-gold/20 bg-surface/50 text-navy hover:border-gold/50 rounded-lg px-3 py-1'}"
data-service="${s.id}"
>${s.name}</button>
`).join('')}
@@ -134,7 +134,7 @@ export class ServiceView {
if (id === serviceId) {
t.className = 'service-tab text-sm transition-colors border border-gold bg-gold/10 text-navy font-bold rounded-lg px-3 py-1';
} else {
t.className = 'service-tab text-sm transition-colors border border-gold/20 bg-white/50 text-navy hover:border-gold/50 rounded-lg px-3 py-1';
t.className = 'service-tab text-sm transition-colors border border-gold/20 bg-surface/50 text-navy hover:border-gold/50 rounded-lg px-3 py-1';
}
});
this.loadService(service);
+30 -5
View File
@@ -29,6 +29,7 @@ interface Settings {
language: string;
cuFont: string;
fontSize: number;
theme: string;
defaultBibleVersion: string;
showVerseNumbers: boolean;
verseNewLine: boolean;
@@ -42,6 +43,7 @@ const DEFAULT_SETTINGS: Settings = {
language: 'en',
cuFont: 'Ponomar',
fontSize: 16,
theme: 'default',
defaultBibleVersion: 'kjv',
showVerseNumbers: true,
verseNewLine: false,
@@ -123,7 +125,7 @@ export class SettingsView {
<!-- Language -->
<div class="mb-6">
<label class="block text-sm font-bold text-navy mb-2">${t.settings.language}</label>
<select id="language" class="w-full border border-gold/30 rounded p-2 bg-white text-navy">
<select id="language" class="w-full border border-gold/30 rounded p-2 bg-surface text-navy">
${LANGUAGES.map(l => `
<option value="${l.code}" ${l.code === this.settings.language ? 'selected' : ''}>
${l.name} (${l.local})
@@ -132,12 +134,25 @@ export class SettingsView {
</select>
</div>
<!-- Theme -->
<div class="mb-6">
<h3 class="text-lg font-bold text-red mb-3">${t.settings.theme}</h3>
<div class="mb-3">
<select id="theme" class="w-full border border-gold/30 rounded p-2 bg-surface text-navy">
<option value="default" ${this.settings.theme === 'default' ? 'selected' : ''}>${t.settings.themeDefault}</option>
<option value="dark" ${this.settings.theme === 'dark' ? 'selected' : ''}>${t.settings.themeDark}</option>
<option value="sepia" ${this.settings.theme === 'sepia' ? 'selected' : ''}>${t.settings.themeSepia}</option>
<option value="hc" ${this.settings.theme === 'hc' ? 'selected' : ''}>${t.settings.themeHC}</option>
</select>
</div>
</div>
<!-- Font Settings -->
<div class="mb-6">
<h3 class="text-lg font-bold text-red mb-3">${t.settings.fontSettings}</h3>
<div class="mb-3">
<select id="cu-font" class="w-full border border-gold/30 rounded p-2 bg-white text-navy">
<select id="cu-font" class="w-full border border-gold/30 rounded p-2 bg-surface text-navy">
${CU_FONTS.map(f => `
<option value="${f.id}" ${f.id === this.settings.cuFont ? 'selected' : ''}>
${f.id === '' ? t.settings.systemFont : f.name}
@@ -187,12 +202,12 @@ export class SettingsView {
<div class="flex-1">
<label class="block text-xs font-bold text-navy mb-1">${t.settings.latitude}</label>
<input type="number" id="latitude" step="0.0001" value="${this.settings.latitude}"
class="w-full border border-gold/30 rounded p-2 bg-white text-navy text-sm">
class="w-full border border-gold/30 rounded p-2 bg-surface text-navy text-sm">
</div>
<div class="flex-1">
<label class="block text-xs font-bold text-navy mb-1">${t.settings.longitude}</label>
<input type="number" id="longitude" step="0.0001" value="${this.settings.longitude}"
class="w-full border border-gold/30 rounded p-2 bg-white text-navy text-sm">
class="w-full border border-gold/30 rounded p-2 bg-surface text-navy text-sm">
</div>
</div>
<div class="flex items-center gap-2">
@@ -209,7 +224,7 @@ export class SettingsView {
<div class="mb-3">
<label class="block text-sm font-bold text-navy mb-1">${t.settings.defaultTranslation}</label>
<select id="default-bible" class="w-full border border-gold/30 rounded p-2 bg-white text-navy">
<select id="default-bible" class="w-full border border-gold/30 rounded p-2 bg-surface text-navy">
<optgroup label="English">
<option value="kjv" ${this.settings.defaultBibleVersion === 'kjv' ? 'selected' : ''}>KJV (${t.settings.bibleComments.fullBible})</option>
<option value="brenton" ${this.settings.defaultBibleVersion === 'brenton' ? 'selected' : ''}>Brenton LXX (${t.settings.bibleComments.otOnly})</option>
@@ -355,6 +370,16 @@ export class SettingsView {
saveSettings(this.settings);
});
document.getElementById('theme')?.addEventListener('change', (e) => {
this.settings.theme = (e.target as HTMLSelectElement).value;
saveSettings(this.settings);
document.documentElement.className = 'theme-' + this.settings.theme;
const chromeColor = getComputedStyle(document.documentElement).getPropertyValue('--clr-browser-chrome').trim();
if (chromeColor) {
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', chromeColor);
}
});
document.getElementById('default-bible')?.addEventListener('change', (e) => {
this.settings.defaultBibleVersion = (e.target as HTMLSelectElement).value;
saveSettings(this.settings);