fix(extension): localize theme and appearance tooltips and selector options

This commit is contained in:
Timo
2026-07-12 14:54:00 +02:00
parent c175364f9a
commit b987d13612
18 changed files with 474 additions and 16 deletions
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Wendet Audioeffekte wie Kompression auf die Videowiedergabe an",
"AUDIO_OPEN_SETTINGS": "Öffnen",
"NEW_FEATURE_AUDIO": "Neu: Audio-Verarbeitung — probiere den Kompressor aus!",
"NEW_FEATURE_THEME": "Neu: Themes — wechsle zum klassischen Look oder minimalem Graphit.",
"AUDIO_BACK": "← Zurück",
"AUDIO_PAGE_TITLE": "Audio-Einstellungen",
"AUDIO_MASTER_TOGGLE": "Audio-Verarbeitung",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"NEW_FEATURE_THEME": "New: Themes — switch to the classic look or minimal graphite.",
"AUDIO_BACK": "← Back",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Aplica efectos de audio como compresión a la reproducción de video",
"AUDIO_OPEN_SETTINGS": "Abrir",
"NEW_FEATURE_AUDIO": "Nuevo: Procesamiento de audio — ¡prueba el compresor!",
"NEW_FEATURE_THEME": "Nuevo: Temas — cambia al aspecto clásico o al grafito minimalista.",
"AUDIO_BACK": "← Volver",
"AUDIO_PAGE_TITLE": "Configuración de audio",
"AUDIO_MASTER_TOGGLE": "Procesamiento de audio",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Applique des effets audio comme la compression à la lecture vidéo",
"AUDIO_OPEN_SETTINGS": "Ouvrir",
"NEW_FEATURE_AUDIO": "Nouveau : Traitement audio — essayez le compresseur !",
"NEW_FEATURE_THEME": "Nouveau : Thèmes — passez au look classique ou au graphite minimal.",
"AUDIO_BACK": "← Retour",
"AUDIO_PAGE_TITLE": "Paramètres audio",
"AUDIO_MASTER_TOGGLE": "Traitement audio",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Applica effetti sonori (come il compressore) ai video",
"AUDIO_OPEN_SETTINGS": "Apri",
"NEW_FEATURE_AUDIO": "Novità: Elaborazione Audio — prova il compressore!",
"NEW_FEATURE_THEME": "Nuovo: Temi — passa all'aspetto classico o al grafite minimale.",
"AUDIO_BACK": "← Indietro",
"AUDIO_PAGE_TITLE": "Impostazioni Audio",
"AUDIO_MASTER_TOGGLE": "Elaborazione Audio",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "動画再生に圧縮などのオーディオエフェクトを適用します",
"AUDIO_OPEN_SETTINGS": "開く",
"NEW_FEATURE_AUDIO": "新機能: オーディオ処理 — コンプレッサーを試してみよう!",
"NEW_FEATURE_THEME": "新機能:テーマ — クラシックな外観やミニマルなグラファイトに切り替え。",
"AUDIO_BACK": "← 戻る",
"AUDIO_PAGE_TITLE": "オーディオ設定",
"AUDIO_MASTER_TOGGLE": "オーディオ処理",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "비디오 재생에 압축과 같은 오디오 효과를 적용합니다",
"AUDIO_OPEN_SETTINGS": "열기",
"NEW_FEATURE_AUDIO": "신규: 오디오 처리 — 컴프레서를 사용해보세요!",
"NEW_FEATURE_THEME": "신규: 테마 — 클래식 룩이나 미니멀 그래파이트로 전환하세요.",
"AUDIO_BACK": "← 뒤로",
"AUDIO_PAGE_TITLE": "오디오 설정",
"AUDIO_MASTER_TOGGLE": "오디오 처리",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Past audio-effecten zoals compressie toe op video-afspelen",
"AUDIO_OPEN_SETTINGS": "Openen",
"NEW_FEATURE_AUDIO": "Nieuw: Audioverwerking — probeer de compressor!",
"NEW_FEATURE_THEME": "Nieuw: Thema's — schakel naar de klassieke look of minimaal grafiet.",
"AUDIO_BACK": "← Terug",
"AUDIO_PAGE_TITLE": "Audio-instellingen",
"AUDIO_MASTER_TOGGLE": "Audioverwerking",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Stosuje efekty dźwiękowe, takie jak kompresja, do odtwarzania wideo",
"AUDIO_OPEN_SETTINGS": "Otwórz",
"NEW_FEATURE_AUDIO": "Nowość: Przetwarzanie dźwięku — wypróbuj kompresor!",
"NEW_FEATURE_THEME": "Nowość: Motywy — przełącz na klasyczny wygląd lub minimalny grafit.",
"AUDIO_BACK": "← Wstecz",
"AUDIO_PAGE_TITLE": "Ustawienia dźwięku",
"AUDIO_MASTER_TOGGLE": "Przetwarzanie dźwięku",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Aplica efeitos de áudio (como compressão) aos vídeos",
"AUDIO_OPEN_SETTINGS": "Abrir",
"NEW_FEATURE_AUDIO": "Novo: Processamento de áudio — experimente o compressor!",
"NEW_FEATURE_THEME": "Novo: Temas — mude para o visual clássico ou grafite minimalista.",
"AUDIO_BACK": "← Voltar",
"AUDIO_PAGE_TITLE": "Configurações de áudio",
"AUDIO_MASTER_TOGGLE": "Processamento de áudio",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Aplica efeitos de áudio (como compressão) aos vídeos",
"AUDIO_OPEN_SETTINGS": "Abrir",
"NEW_FEATURE_AUDIO": "Novo: Processamento de áudio — experimente o compressor!",
"NEW_FEATURE_THEME": "Novo: Temas — mude para o visual clássico ou grafite minimalista.",
"AUDIO_BACK": "← Voltar",
"AUDIO_PAGE_TITLE": "Configurações de Áudio",
"AUDIO_MASTER_TOGGLE": "Processamento de Áudio",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Применяет аудиоэффекты, такие как сжатие, к воспроизведению видео",
"AUDIO_OPEN_SETTINGS": "Открыть",
"NEW_FEATURE_AUDIO": "Новое: Обработка звука — попробуйте компрессор!",
"NEW_FEATURE_THEME": "Новое: Темы — переключитесь на классический вид или минимальный графит.",
"AUDIO_BACK": "← Назад",
"AUDIO_PAGE_TITLE": "Настройки звука",
"AUDIO_MASTER_TOGGLE": "Обработка звука",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Video oynatımına sıkıştırma gibi ses efektleri uygular",
"AUDIO_OPEN_SETTINGS": "Aç",
"NEW_FEATURE_AUDIO": "Yeni: Ses işleme — kompresörü deneyin!",
"NEW_FEATURE_THEME": "Yeni: Temalar — klasik görünüme veya minimal grafite geçin.",
"AUDIO_BACK": "← Geri",
"AUDIO_PAGE_TITLE": "Ses ayarları",
"AUDIO_MASTER_TOGGLE": "Ses işleme",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Застосуйте звукові ефекти, такі як стиснення, до відтворення відео",
"AUDIO_OPEN_SETTINGS": "Відкрити",
"NEW_FEATURE_AUDIO": "Новинка: обробка звуку — спробуйте компресор!",
"NEW_FEATURE_THEME": "Нове: Теми — перемкніться на класичний вигляд або мінімальний графіт.",
"AUDIO_BACK": "← Назад",
"AUDIO_PAGE_TITLE": "Параметри звуку",
"AUDIO_MASTER_TOGGLE": "Обробка аудіо",
+1
View File
@@ -209,6 +209,7 @@
"LABEL_AUDIO_PROCESSING_TOOLTIP": "将压缩等音频效果应用于视频播放",
"AUDIO_OPEN_SETTINGS": "打开",
"NEW_FEATURE_AUDIO": "新功能:音频处理 - 尝试压缩器!",
"NEW_FEATURE_THEME": "新功能:主题 — 切换到经典外观或极简石墨风格。",
"AUDIO_BACK": "← 返回",
"AUDIO_PAGE_TITLE": "音频设置",
"AUDIO_MASTER_TOGGLE": "音频处理",
+326 -2
View File
@@ -73,6 +73,113 @@
color-scheme: light;
}
/* ============================================================
THEME: CYBER — the original indigo / slate look, kept for
people who prefer the pre-nature design. Cool navy neutrals
with an electric indigo brand accent and a violet companion.
============================================================ */
html[data-palette="cyber"] {
--bg-base: oklch(0.215 0.042 265);
--surface: oklch(0.285 0.05 265);
--surface-deep: oklch(0.185 0.04 265);
--surface-alt: oklch(0.25 0.048 265);
--border-soft: oklch(0.9 0.03 265 / 0.10);
--border-strong: oklch(0.42 0.05 265);
--text-primary: oklch(0.97 0.012 265);
--text-secondary: oklch(0.74 0.03 265);
--accent-green: oklch(0.59 0.20 275);
--accent-green-hover: oklch(0.69 0.17 275);
--accent-terracotta: oklch(0.64 0.21 320);
--success-green: oklch(0.74 0.17 150);
--warning-amber: oklch(0.79 0.15 75);
--danger: oklch(0.63 0.20 25);
--text-on-green: oklch(0.99 0.006 275);
--text-on-warm: oklch(0.99 0.006 320);
--status-success-text: oklch(0.82 0.15 150);
--status-warning-text: oklch(0.85 0.14 78);
--status-error-text: oklch(0.78 0.16 25);
--status-neutral-text: oklch(0.74 0.03 265);
color-scheme: dark;
}
html[data-palette="cyber"].theme-light {
--bg-base: oklch(0.965 0.01 265);
--surface: oklch(0.995 0.005 265);
--surface-deep: oklch(0.945 0.014 265);
--surface-alt: oklch(0.915 0.02 265);
--border-soft: oklch(0.30 0.05 265 / 0.12);
--border-strong: oklch(0.79 0.03 265);
--text-primary: oklch(0.26 0.05 265);
--text-secondary: oklch(0.48 0.045 265);
--accent-green: oklch(0.53 0.21 275);
--accent-green-hover: oklch(0.46 0.20 275);
--accent-terracotta: oklch(0.55 0.23 320);
--success-green: oklch(0.52 0.16 150);
--warning-amber: oklch(0.62 0.15 72);
--danger: oklch(0.56 0.21 25);
--text-on-green: oklch(0.99 0.005 275);
--text-on-warm: oklch(0.99 0.005 320);
--status-success-text: oklch(0.45 0.15 150);
--status-warning-text: oklch(0.50 0.13 70);
--status-error-text: oklch(0.50 0.18 25);
--status-neutral-text: oklch(0.48 0.045 265);
color-scheme: light;
}
/* ============================================================
THEME: GRAPHITE — a quiet black-and-white minimal system.
Near-neutral cool greys, a monochrome accent (bright on dark,
ink on light); status hues are the only colour, kept muted so
they still read as success / warning / danger.
============================================================ */
html[data-palette="graphite"] {
--bg-base: oklch(0.19 0.004 260);
--surface: oklch(0.255 0.005 260);
--surface-deep: oklch(0.165 0.004 260);
--surface-alt: oklch(0.225 0.005 260);
--border-soft: oklch(0.92 0.008 260 / 0.11);
--border-strong: oklch(0.42 0.006 260);
--text-primary: oklch(0.97 0.003 260);
--text-secondary: oklch(0.72 0.005 260);
--accent-green: oklch(0.93 0.006 260);
--accent-green-hover: oklch(0.99 0.003 260);
--accent-terracotta: oklch(0.80 0.008 260);
--success-green: oklch(0.74 0.10 155);
--warning-amber: oklch(0.80 0.10 80);
--danger: oklch(0.64 0.14 25);
--text-on-green: oklch(0.20 0.004 260);
--text-on-warm: oklch(0.20 0.004 260);
--status-success-text: oklch(0.81 0.09 152);
--status-warning-text: oklch(0.84 0.10 80);
--status-error-text: oklch(0.76 0.12 25);
--status-neutral-text: oklch(0.72 0.005 260);
color-scheme: dark;
}
html[data-palette="graphite"].theme-light {
--bg-base: oklch(0.955 0.002 260);
--surface: oklch(1 0 0);
--surface-deep: oklch(0.928 0.003 260);
--surface-alt: oklch(0.90 0.004 260);
--border-soft: oklch(0.20 0.006 260 / 0.13);
--border-strong: oklch(0.77 0.004 260);
--text-primary: oklch(0.22 0.005 260);
--text-secondary: oklch(0.46 0.006 260);
--accent-green: oklch(0.28 0.006 260);
--accent-green-hover: oklch(0.18 0.005 260);
--accent-terracotta: oklch(0.44 0.008 260);
--success-green: oklch(0.51 0.12 152);
--warning-amber: oklch(0.60 0.12 72);
--danger: oklch(0.55 0.17 25);
--text-on-green: oklch(0.98 0.002 260);
--text-on-warm: oklch(0.98 0.002 260);
--status-success-text: oklch(0.45 0.12 152);
--status-warning-text: oklch(0.49 0.11 70);
--status-error-text: oklch(0.49 0.16 25);
--status-neutral-text: oklch(0.46 0.006 260);
color-scheme: light;
}
body {
width: 360px;
margin: 0;
@@ -94,6 +201,60 @@
radial-gradient(circle at 105% 5%, oklch(0.82 0.12 65 / 0.2), transparent 38%);
}
/* Per-theme ambient glow: each palette carries its own signature
corner wash so the backdrop matches the accent, not eucalyptus. */
html[data-palette="cyber"] body {
background-image:
radial-gradient(circle at 12% -8%, oklch(0.64 0.19 275 / 0.16), transparent 42%),
radial-gradient(circle at 105% 12%, oklch(0.64 0.21 320 / 0.11), transparent 34%);
}
html[data-palette="cyber"].theme-light body {
background-image:
radial-gradient(circle at 8% -5%, oklch(0.60 0.20 275 / 0.16), transparent 40%),
radial-gradient(circle at 105% 5%, oklch(0.62 0.20 320 / 0.14), transparent 38%);
}
html[data-palette="graphite"] body {
background-image:
radial-gradient(circle at 12% -8%, oklch(0.93 0.006 260 / 0.05), transparent 44%);
}
html[data-palette="graphite"].theme-light body {
background-image:
radial-gradient(circle at 12% -8%, oklch(0.20 0.006 260 / 0.045), transparent 46%);
}
/* The Create Room button carries an inline eucalyptus glow; retint
its shadow per palette so the hero action stays on-theme. */
html[data-palette="cyber"] #createRoomBtn,
html[data-palette="cyber"] #syncTabCreateRoomBtn {
box-shadow: 0 4px 12px oklch(0.64 0.19 275 / 0.4) !important;
}
html[data-palette="graphite"] #createRoomBtn,
html[data-palette="graphite"] #syncTabCreateRoomBtn {
box-shadow: 0 4px 12px oklch(0.20 0.006 260 / 0.4) !important;
}
/* Cyber's brand ink is white (for the dark indigo accent), but its
success/warning fills are bright — so the few surfaces that print
text directly on those fills need dark ink to stay legible. */
html[data-palette="cyber"] .toast-success,
html[data-palette="cyber"] .toast-warning,
html[data-palette="cyber"] .copy-success {
color: oklch(0.22 0.03 265) !important;
}
/* Theme cross-fade: enabled only for the ~260ms around a user-initiated
theme/appearance switch (added by popup.js), so colours glide instead
of snapping — without slowing down normal hover/state transitions. */
html.theme-anim,
html.theme-anim * {
transition: background-color 0.24s ease, border-color 0.24s ease,
color 0.24s ease, box-shadow 0.24s ease, fill 0.24s ease !important;
}
@media (prefers-reduced-motion: reduce) {
html.theme-anim,
html.theme-anim * { transition: none !important; }
}
.header-row {
display: flex;
align-items: center;
@@ -216,7 +377,7 @@
input:focus,
select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px oklch(0.68 0.13 150 / 0.18);
box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent), transparent 82%);
}
button.primary {
@@ -495,6 +656,23 @@
.click-leaf-amber i {
background: linear-gradient(135deg, oklch(0.68 0.14 55 / 0.95), oklch(0.55 0.13 40 / 0.85));
}
/* Non-eucalyptus themes retint the burst from their own accent pair so
the particles stay on-theme and, because those vars flip with
light/dark, remain visible against every background. */
html[data-palette="cyber"] .click-leaf i,
html[data-palette="graphite"] .click-leaf i {
background: linear-gradient(135deg, var(--accent), var(--accent-hover));
}
html[data-palette="cyber"] .click-leaf-sage i,
html[data-palette="graphite"] .click-leaf-sage i {
background: linear-gradient(135deg, var(--accent-hover), var(--accent));
}
html[data-palette="cyber"] .click-leaf-amber i {
background: linear-gradient(135deg, var(--accent-terracotta), var(--accent));
}
html[data-palette="graphite"] .click-leaf-amber i {
background: linear-gradient(135deg, var(--text-secondary), var(--accent));
}
@keyframes leafBurst {
0% {
opacity: 0;
@@ -865,6 +1043,109 @@
color: var(--text);
box-shadow: 0 1px 5px oklch(0.08 0.02 150 / 0.32), inset 0 0 0 1px var(--border-soft);
}
/* Theme (colour palette) picker: a full-width row of swatch cards so
each curated scheme previews its own colours before selection. */
.palette-row {
display: block;
padding: 12px 0 4px;
border-bottom: 1px solid var(--border-soft);
}
.palette-row > label {
margin: 0 0 9px;
color: var(--text);
font-size: 12px;
font-weight: 600;
letter-spacing: 0;
text-transform: none;
}
.palette-picker {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.palette-picker select {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.palette-choice {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
min-height: 62px;
margin: 0;
padding: 10px 4px 9px;
border: 1px solid var(--border-strong);
border-radius: 10px;
background: var(--surface-deep);
color: var(--text-muted);
font-family: inherit;
font-size: 11px;
font-weight: 700;
line-height: 1;
cursor: pointer;
transition: border-color 0.18s ease, background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.palette-choice:hover {
color: var(--text);
border-color: var(--accent);
transform: translateY(-1px);
}
.palette-choice.active {
color: var(--text);
border-color: var(--accent);
background: color-mix(in oklch, var(--accent) 10%, var(--surface-deep));
box-shadow: 0 0 0 1px var(--accent), 0 5px 14px oklch(0.08 0.02 260 / 0.28);
}
.palette-swatch {
display: inline-flex;
padding: 3px;
border-radius: 999px;
background: var(--surface-alt);
box-shadow: inset 0 0 0 1px var(--border-soft);
}
.palette-swatch i {
width: 13px;
height: 13px;
border-radius: 50%;
display: block;
box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.22);
}
.palette-swatch i + i {
margin-left: -5px;
}
.palette-labeltext {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.palette-name {
font-size: 11px;
font-weight: 700;
line-height: 1.1;
}
.palette-desc {
font-size: 9px;
font-weight: 600;
line-height: 1.1;
letter-spacing: 0.01em;
color: var(--text-muted);
opacity: 0.85;
}
.palette-choice.active .palette-desc {
color: var(--accent);
opacity: 1;
}
.tab-btn[data-tab="tab-dev"],
#devToolsTabBtn {
color: color-mix(in oklch, var(--text-muted), transparent 18%);
@@ -1147,7 +1428,7 @@
<option value="" data-i18n="OPTION_SELECT_TAB">-- Select a Tab --</option>
</select>
<!-- Hint Tooltip -->
<div id="targetTabHint" style="display: none; position: absolute; top: -25px; right: 0; background: var(--accent); color: var(--text-on-green); padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: bold; pointer-events: none; animation: floatHint 2s ease-in-out infinite; box-shadow: 0 4px 12px rgba(86, 174, 108, 0.4); z-index: 10;" data-i18n="HINT_SELECT_VIDEO">
<div id="targetTabHint" style="display: none; position: absolute; top: -25px; right: 0; background: var(--accent); color: var(--text-on-green); padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: bold; pointer-events: none; animation: floatHint 2s ease-in-out infinite; box-shadow: 0 4px 12px color-mix(in oklch, var(--accent), transparent 60%); z-index: 10;" data-i18n="HINT_SELECT_VIDEO">
Select your video here!
<div style="position: absolute; bottom: -4px; right: 20px; width: 8px; height: 8px; background: var(--accent); transform: rotate(45deg);"></div>
</div>
@@ -1221,6 +1502,49 @@
<button id="themeChoiceLight" class="theme-choice" type="button" data-theme-value="light">Light</button>
</div>
</div>
<div class="palette-row">
<label id="paletteLabel" for="paletteSelector" title="Pick a curated colour scheme for the extension">Theme</label>
<div class="palette-picker" role="group" aria-labelledby="paletteLabel">
<select id="paletteSelector" aria-label="Theme" tabindex="-1">
<option value="eucalyptus">Eucalyptus</option>
<option value="cyber">Cyber</option>
<option value="graphite">Graphite</option>
</select>
<button id="paletteChoiceEucalyptus" class="palette-choice" type="button" data-palette-value="eucalyptus">
<span class="palette-swatch" aria-hidden="true">
<i style="background: oklch(0.73 0.13 155)"></i>
<i style="background: oklch(0.62 0.14 45)"></i>
<i style="background: oklch(0.30 0.035 150)"></i>
</span>
<span class="palette-labeltext">
<span class="palette-name">Eucalyptus</span>
<span class="palette-desc">Nature</span>
</span>
</button>
<button id="paletteChoiceCyber" class="palette-choice" type="button" data-palette-value="cyber">
<span class="palette-swatch" aria-hidden="true">
<i style="background: oklch(0.64 0.19 275)"></i>
<i style="background: oklch(0.64 0.21 320)"></i>
<i style="background: oklch(0.185 0.04 265)"></i>
</span>
<span class="palette-labeltext">
<span class="palette-name">Cyber</span>
<span class="palette-desc">Classic</span>
</span>
</button>
<button id="paletteChoiceGraphite" class="palette-choice" type="button" data-palette-value="graphite">
<span class="palette-swatch" aria-hidden="true">
<i style="background: oklch(0.95 0.004 260)"></i>
<i style="background: oklch(0.55 0.006 260)"></i>
<i style="background: oklch(0.20 0.004 260)"></i>
</span>
<span class="palette-labeltext">
<span class="palette-name">Graphite</span>
<span class="palette-desc">Minimal</span>
</span>
</button>
</div>
</div>
<div class="settings-row">
<label for="langSelector" data-i18n="LABEL_LANGUAGE" data-i18n-title="LABEL_LANGUAGE_TOOLTIP" title="Choose your preferred extension language">App Language</label>
<span class="settings-select">
+114 -11
View File
@@ -65,6 +65,7 @@ const elements = {
cancelLobbyBtn: document.getElementById('cancelLobbyBtn'),
langSelector: document.getElementById('langSelector'),
themeSelector: document.getElementById('themeSelector'),
paletteSelector: document.getElementById('paletteSelector'),
audioSettingsLink: document.getElementById('audioSettingsLink'),
settingsSupportLink: document.getElementById('settingsSupportLink'),
@@ -115,6 +116,12 @@ const FEATURE_HINTS = [
selector: '#audioProcessingLabel',
position: 'beforebegin',
i18nTooltip: 'NEW_FEATURE_AUDIO'
},
{
key: 'theme_picker',
selector: '#paletteLabel',
position: 'afterend',
i18nTooltip: 'NEW_FEATURE_THEME'
}
];
@@ -135,16 +142,23 @@ function openAudioSettingsPage() {
function localizeThemeSelector(locale) {
const labels = {
de: ['Darstellung', 'System', 'Dunkel', 'Hell'], fr: ['Apparence', 'Système', 'Sombre', 'Clair'],
es: ['Apariencia', 'Sistema', 'Oscuro', 'Claro'], it: ['Aspetto', 'Sistema', 'Scuro', 'Chiaro'],
nl: ['Weergave', 'Systeem', 'Donker', 'Licht'], pl: ['Wygląd', 'System', 'Ciemny', 'Jasny'],
pt: ['Aparência', 'Sistema', 'Escuro', 'Claro'], 'pt-BR': ['Aparência', 'Sistema', 'Escuro', 'Claro'],
tr: ['Görünüm', 'Sistem', 'Koyu', 'Açık'], ru: ['Оформление', 'Системная', 'Тёмная', 'Светлая'],
ja: ['外観', 'システム', 'ダーク', 'ライト'], ko: ['테마', '시스템', '다크', '라이트'],
zh: ['外观', '跟随系统', '深色', '浅色'], uk: ['Вигляд', 'Системна', 'Темна', 'Світла'],
en: ['Appearance', 'System', 'Dark', 'Light']
de: ['Darstellung', 'System', 'Dunkel', 'Hell', 'Folge dem System-Design oder wähle ein festes Design'],
fr: ['Apparence', 'Système', 'Sombre', 'Clair', 'Suivre le thème du système ou choisir une apparence fixe'],
es: ['Apariencia', 'Sistema', 'Oscuro', 'Claro', 'Seguir el tema del sistema o elegir una apariencia fija'],
it: ['Aspetto', 'Sistema', 'Scuro', 'Chiaro', 'Segui il tema di sistema o scegli un aspetto fisso'],
nl: ['Weergave', 'Systeem', 'Donker', 'Licht', 'Volg het systeemthema of kies een vaste weergave'],
pl: ['Wygląd', 'System', 'Ciemny', 'Jasny', 'Dostosuj do motywu systemowego lub wybierz stały wygląd'],
pt: ['Aparência', 'Sistema', 'Escuro', 'Claro', 'Seguir o tema do sistema ou escolher uma aparência fixa'],
'pt-BR': ['Aparência', 'Sistema', 'Escuro', 'Claro', 'Seguir o tema do sistema ou escolher uma aparência fixa'],
tr: ['Görünüm', 'Sistem', 'Koyu', 'Açık', 'Sistem temasını takip et veya sabit bir görünüm seç'],
ru: ['Оформление', 'Системная', 'Тёмная', 'Светлая', 'Следовать системной теме или выбрать фиксированное оформление'],
ja: ['外観', 'システム', 'ダーク', 'ライト', 'システムテーマに従うか、固定の外観を選択します'],
ko: ['테마', '시스템', '다크', '라이트', '시스템 테마를 따르거나 고정된 테마를 선택하세요'],
zh: ['外观', '跟随系统', '深色', '浅色', '跟随系统主题或选择固定外观'],
uk: ['Вигляд', 'Системна', 'Темна', 'Світла', 'Дотримуватися системної теми або вибрати фіксоване оформлення'],
en: ['Appearance', 'System', 'Dark', 'Light', 'Follow the system theme or choose a fixed appearance']
};
const [label, system, dark, light] = labels[locale] || labels.en;
const [label, system, dark, light, tooltip] = labels[locale] || labels.en;
const labelEl = document.getElementById('themeLabel');
const systemEl = document.getElementById('themeOptionSystem');
const darkEl = document.getElementById('themeOptionDark');
@@ -152,7 +166,10 @@ function localizeThemeSelector(locale) {
const systemButton = document.getElementById('themeChoiceSystem');
const darkButton = document.getElementById('themeChoiceDark');
const lightButton = document.getElementById('themeChoiceLight');
if (labelEl) labelEl.textContent = label;
if (labelEl) {
labelEl.textContent = label;
if (tooltip) labelEl.setAttribute('title', tooltip);
}
if (systemEl) systemEl.textContent = system;
if (darkEl) darkEl.textContent = dark;
if (lightEl) lightEl.textContent = light;
@@ -160,6 +177,51 @@ function localizeThemeSelector(locale) {
if (darkButton) darkButton.textContent = dark;
if (lightButton) lightButton.textContent = light;
if (elements.themeSelector) elements.themeSelector.setAttribute('aria-label', label);
// The palette scheme names (Eucalyptus / Cyber / Graphite) are brand
// words kept verbatim; only the row label is localized.
const paletteLabels = {
de: ['Farbschema', 'Wähle ein kuratiertes Farbschema für die Erweiterung'],
fr: ['Thème', 'Choisissez une palette de couleurs personnalisée pour l\'extension'],
es: ['Tema', 'Elige una paleta de colores seleccionada para la extensión'],
it: ['Tema', 'Scegli una combinazione di colori curata per l\'estensione'],
nl: ['Thema', 'Kies een samengesteld kleurenschema voor de extensie'],
pl: ['Motyw', 'Wybierz dopasowany schemat kolorów dla rozszerzenia'],
pt: ['Tema', 'Escolha um esquema de cores personalizado para a extensão'],
'pt-BR': ['Tema', 'Escolha um esquema de cores personalizado para a extensão'],
tr: ['Tema', 'Uzantı için özel olarak seçilmiş bir renk şeması seçin'],
ru: ['Тема', 'Выберите цветовую схему для расширения'],
ja: ['テーマ', '拡張機能の厳選された配色を選択します'],
ko: ['테마', '확장 프로그램에 맞는 특별한 색상 테마를 선택하세요'],
zh: ['主题', '为扩展程序选择精心设计的配色方案'],
uk: ['Тема', 'Виберіть колірну схему для розширення'],
en: ['Theme', 'Pick a curated colour scheme for the extension']
};
const [paletteLabel, paletteTooltip] = paletteLabels[locale] || paletteLabels.en;
const paletteLabelEl = document.getElementById('paletteLabel');
if (paletteLabelEl) {
paletteLabelEl.textContent = paletteLabel;
if (paletteTooltip) paletteLabelEl.setAttribute('title', paletteTooltip);
}
if (elements.paletteSelector) elements.paletteSelector.setAttribute('aria-label', paletteLabel);
// Micro-descriptors under each swatch card: [nature, classic, minimal].
const paletteDescs = {
de: ['Natur', 'Klassik', 'Minimal'], fr: ['Nature', 'Classique', 'Minimal'],
es: ['Naturaleza', 'Clásico', 'Minimal'], it: ['Natura', 'Classico', 'Minimal'],
nl: ['Natuur', 'Klassiek', 'Minimaal'], pl: ['Natura', 'Klasyczny', 'Minimal'],
pt: ['Natureza', 'Clássico', 'Minimal'], 'pt-BR': ['Natureza', 'Clássico', 'Minimal'],
tr: ['Doğa', 'Klasik', 'Minimal'], ru: ['Природа', 'Классика', 'Минимал'],
ja: ['ネイチャー', 'クラシック', 'ミニマル'], ko: ['자연', '클래식', '미니멀'],
zh: ['自然', '经典', '极简'], uk: ['Природа', 'Класика', 'Мінімал'],
en: ['Nature', 'Classic', 'Minimal']
};
const [natureDesc, classicDesc, minimalDesc] = paletteDescs[locale] || paletteDescs.en;
const descMap = { eucalyptus: natureDesc, cyber: classicDesc, graphite: minimalDesc };
document.querySelectorAll('.palette-choice').forEach(btn => {
const desc = btn.querySelector('.palette-desc');
if (desc) desc.textContent = descMap[btn.dataset.paletteValue] || desc.textContent;
});
}
function syncThemePicker() {
@@ -171,6 +233,25 @@ function syncThemePicker() {
});
}
function syncPalettePicker() {
const activePalette = elements.paletteSelector?.value || 'eucalyptus';
document.querySelectorAll('.palette-choice').forEach(button => {
const active = button.dataset.paletteValue === activePalette;
button.classList.toggle('active', active);
button.setAttribute('aria-pressed', String(active));
});
}
// Briefly enable global colour transitions so a theme/appearance switch
// cross-fades instead of snapping. Self-clearing; a no-op under reduced motion.
let themeAnimTimer = null;
function flashThemeTransition() {
const root = document.documentElement;
root.classList.add('theme-anim');
if (themeAnimTimer) clearTimeout(themeAnimTimer);
themeAnimTimer = setTimeout(() => root.classList.remove('theme-anim'), 300);
}
async function updateFeatureHints() {
const data = await chrome.storage.sync.get(['dismissedHints']);
const dismissed = Array.isArray(data.dismissedHints) ? data.dismissedHints : [];
@@ -243,7 +324,7 @@ function setRoomRefreshCooldown() {
async function init() {
// Local-only by design — settings and room credentials never come from
// storage.sync (only onboardingComplete + dismissedHints live there).
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab', 'themeMode']);
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab', 'themeMode', 'themePalette']);
let activeLang = localData.locale;
if (!activeLang) {
@@ -257,7 +338,9 @@ async function init() {
if (elements.langSelector) elements.langSelector.value = activeLang;
if (elements.themeSelector) elements.themeSelector.value = ['dark', 'light'].includes(localData.themeMode) ? localData.themeMode : 'system';
if (elements.paletteSelector) elements.paletteSelector.value = ['cyber', 'graphite'].includes(localData.themePalette) ? localData.themePalette : 'eucalyptus';
syncThemePicker();
syncPalettePicker();
let username = localData.username;
if (!username) {
@@ -1304,6 +1387,7 @@ if (elements.autoCopyInvite) {
if (elements.themeSelector) {
elements.themeSelector.addEventListener('change', () => {
const themeMode = elements.themeSelector.value;
flashThemeTransition();
window.koalaTheme?.setMode(themeMode);
chrome.storage.local.set({ themeMode });
syncThemePicker();
@@ -1318,6 +1402,25 @@ document.querySelectorAll('.theme-choice').forEach(button => {
});
});
if (elements.paletteSelector) {
elements.paletteSelector.addEventListener('change', () => {
const themePalette = elements.paletteSelector.value;
flashThemeTransition();
window.koalaTheme?.setPalette(themePalette);
chrome.storage.local.set({ themePalette });
syncPalettePicker();
dismissFeatureHint('theme_picker');
});
}
document.querySelectorAll('.palette-choice').forEach(button => {
button.addEventListener('click', () => {
if (!elements.paletteSelector) return;
elements.paletteSelector.value = button.dataset.paletteValue;
elements.paletteSelector.dispatchEvent(new window.Event('change'));
});
});
if (elements.audioSettingsLink) {
elements.audioSettingsLink.addEventListener('click', async (event) => {
event.preventDefault();
+19 -3
View File
@@ -1,8 +1,11 @@
(() => {
const systemTheme = window.matchMedia('(prefers-color-scheme: light)');
let mode = 'system';
let palette = 'eucalyptus';
const normalize = value => ['system', 'dark', 'light'].includes(value) ? value : 'system';
const normalizePalette = value => ['eucalyptus', 'cyber', 'graphite'].includes(value) ? value : 'eucalyptus';
const apply = value => {
mode = normalize(value);
const light = mode === 'light' || (mode === 'system' && systemTheme.matches);
@@ -11,10 +14,18 @@
document.documentElement.style.colorScheme = light ? 'light' : 'dark';
};
const applyPalette = value => {
palette = normalizePalette(value);
document.documentElement.dataset.palette = palette;
};
apply('system');
applyPalette('eucalyptus');
window.koalaTheme = {
getMode: () => mode,
setMode: apply
setMode: apply,
getPalette: () => palette,
setPalette: applyPalette
};
systemTheme.addEventListener('change', () => {
@@ -22,8 +33,13 @@
});
if (!globalThis.chrome?.storage?.local) return;
chrome.storage.local.get(['themeMode'], data => apply(data.themeMode));
chrome.storage.local.get(['themeMode', 'themePalette'], data => {
apply(data.themeMode);
applyPalette(data.themePalette);
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.themeMode) apply(changes.themeMode.newValue);
if (area !== 'local') return;
if (changes.themeMode) apply(changes.themeMode.newValue);
if (changes.themePalette) applyPalette(changes.themePalette.newValue);
});
})();