diff --git a/extension/locales/de.json b/extension/locales/de.json index 91e5136..6507be7 100644 --- a/extension/locales/de.json +++ b/extension/locales/de.json @@ -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", diff --git a/extension/locales/en.json b/extension/locales/en.json index ed99f04..25f5c04 100644 --- a/extension/locales/en.json +++ b/extension/locales/en.json @@ -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", diff --git a/extension/locales/es.json b/extension/locales/es.json index b36643a..624e419 100644 --- a/extension/locales/es.json +++ b/extension/locales/es.json @@ -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", diff --git a/extension/locales/fr.json b/extension/locales/fr.json index 13a9192..67e0408 100644 --- a/extension/locales/fr.json +++ b/extension/locales/fr.json @@ -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", diff --git a/extension/locales/it.json b/extension/locales/it.json index 303524f..bb72c95 100644 --- a/extension/locales/it.json +++ b/extension/locales/it.json @@ -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", diff --git a/extension/locales/ja.json b/extension/locales/ja.json index 44ccc8e..70162a8 100644 --- a/extension/locales/ja.json +++ b/extension/locales/ja.json @@ -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": "オーディオ処理", diff --git a/extension/locales/ko.json b/extension/locales/ko.json index b0787c1..d40a320 100644 --- a/extension/locales/ko.json +++ b/extension/locales/ko.json @@ -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": "오디오 처리", diff --git a/extension/locales/nl.json b/extension/locales/nl.json index 73316f7..dd2a201 100644 --- a/extension/locales/nl.json +++ b/extension/locales/nl.json @@ -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", diff --git a/extension/locales/pl.json b/extension/locales/pl.json index 47c45b0..dde3e3d 100644 --- a/extension/locales/pl.json +++ b/extension/locales/pl.json @@ -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", diff --git a/extension/locales/pt-BR.json b/extension/locales/pt-BR.json index 37c2fa0..1d07782 100644 --- a/extension/locales/pt-BR.json +++ b/extension/locales/pt-BR.json @@ -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", diff --git a/extension/locales/pt.json b/extension/locales/pt.json index 623cdba..b360401 100644 --- a/extension/locales/pt.json +++ b/extension/locales/pt.json @@ -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", diff --git a/extension/locales/ru.json b/extension/locales/ru.json index 48ce4fc..7aa0e35 100644 --- a/extension/locales/ru.json +++ b/extension/locales/ru.json @@ -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": "Обработка звука", diff --git a/extension/locales/tr.json b/extension/locales/tr.json index 74bae81..b5b29da 100644 --- a/extension/locales/tr.json +++ b/extension/locales/tr.json @@ -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", diff --git a/extension/locales/uk.json b/extension/locales/uk.json index cf9f97f..22aa7d5 100644 --- a/extension/locales/uk.json +++ b/extension/locales/uk.json @@ -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": "Обробка аудіо", diff --git a/extension/locales/zh.json b/extension/locales/zh.json index 5ee51a5..dda1b53 100644 --- a/extension/locales/zh.json +++ b/extension/locales/zh.json @@ -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": "音频处理", diff --git a/extension/popup.html b/extension/popup.html index 3461a8a..6f1b518 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -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 @@ - +
+ +
+ + + + +
+
diff --git a/extension/popup.js b/extension/popup.js index 7d078c6..e1f88a3 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -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(); diff --git a/extension/theme-init.js b/extension/theme-init.js index c9a7b8f..4955a7e 100644 --- a/extension/theme-init.js +++ b/extension/theme-init.js @@ -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); }); })();