diff --git a/extension/audio-options.css b/extension/audio-options.css index 7c3dc61..e680ca8 100644 --- a/extension/audio-options.css +++ b/extension/audio-options.css @@ -25,6 +25,22 @@ --text-muted: var(--text-secondary); --border: var(--border-strong); --radius: 8px; + color-scheme: dark; +} + +html.theme-light { + --bg-base: oklch(0.965 0.012 105); + --surface: oklch(0.995 0.006 100); + --surface-deep: oklch(0.935 0.018 110); + --surface-alt: oklch(0.91 0.022 115); + --border-soft: oklch(0.28 0.035 140 / 0.10); + --border-strong: oklch(0.73 0.035 125); + --text-primary: oklch(0.21 0.03 140); + --text-secondary: oklch(0.43 0.03 135); + --accent-green: oklch(0.52 0.13 155); + --accent-green-hover: oklch(0.45 0.12 155); + --text-on-green: oklch(0.98 0.005 100); + color-scheme: light; } * { @@ -100,6 +116,10 @@ h2 { box-shadow: 0 2px 8px rgba(0,0,0,0.15); } +html.theme-light .panel { + box-shadow: 0 12px 30px oklch(0.22 0.035 140 / 0.09); +} + .muted-panel { color: var(--text-muted); } @@ -142,12 +162,12 @@ h2 { transition: border-color 0.2s, box-shadow 0.2s; } .preset-card:hover { - border-color: rgba(86, 174, 108, 0.4); + border-color: color-mix(in oklch, var(--accent), transparent 60%); } .preset-card:has(input:checked) { border-color: var(--accent); - box-shadow: 0 0 0 1px rgba(86, 174, 108, 0.35); + box-shadow: 0 0 0 1px color-mix(in oklch, var(--accent), transparent 65%); } .preset-card input { @@ -158,8 +178,8 @@ h2 { display: grid; gap: 12px; padding: 16px; - background: rgba(16, 25, 14, 0.58); - border: 1px solid rgba(163, 162, 148, 0.16); + background: var(--surface-deep); + border: 1px solid var(--border-soft); border-radius: var(--radius); transition: opacity 0.3s; } diff --git a/extension/audio-options.html b/extension/audio-options.html index e88401a..86181c8 100644 --- a/extension/audio-options.html +++ b/extension/audio-options.html @@ -3,6 +3,7 @@ KoalaSync Audio Settings + diff --git a/extension/popup.html b/extension/popup.html index 5b877f3..d097899 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -40,6 +40,10 @@ --success: var(--success-green); --warning: var(--warning-amber); --error: var(--danger); + --status-success-text: oklch(0.84 0.12 140); + --status-warning-text: oklch(0.86 0.13 80); + --status-error-text: oklch(0.78 0.13 25); + --status-neutral-text: oklch(0.76 0.02 110); --radius: 12px; --star: var(--warning); color-scheme: dark; @@ -62,6 +66,10 @@ --danger: oklch(0.57 0.17 25); --text-on-green: oklch(0.16 0.03 140); --text-on-warm: oklch(0.18 0.03 45); + --status-success-text: oklch(0.39 0.13 140); + --status-warning-text: oklch(0.43 0.12 75); + --status-error-text: oklch(0.46 0.16 25); + --status-neutral-text: oklch(0.43 0.02 120); color-scheme: light; } @@ -316,6 +324,11 @@ animation: pulseGreen 2s infinite ease-in-out; } .status-offline { background: var(--error); } + .status-connecting { + background: var(--warning); + box-shadow: 0 0 8px color-mix(in oklch, var(--warning), transparent 30%); + } + .status-idle { background: var(--status-neutral-text); } @keyframes pulseGreen { 0% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--success), transparent 30%); } @@ -337,12 +350,12 @@ } .badge-ready { background: color-mix(in oklch, var(--success), transparent 85%); - color: var(--success); + color: var(--status-success-text); border: 1px solid color-mix(in oklch, var(--success), transparent 70%); } .badge-loading { background: color-mix(in oklch, var(--warning), transparent 85%); - color: var(--warning); + color: var(--status-warning-text); border: 1px solid color-mix(in oklch, var(--warning), transparent 70%); } .lobby-peer-item { @@ -364,15 +377,48 @@ overflow-y: auto; font-size: 10px; font-family: monospace; - background: #000; + background: var(--surface-deep); padding: 8px; border-radius: 8px; - color: #ccc; + color: var(--text-secondary); } .log-entry { margin-bottom: 4px; } - .log-error { color: var(--error); } - .log-success { color: var(--success); } + .log-error { color: var(--status-error-text); } + .log-success { color: var(--status-success-text); } + + .conn-ping { + min-width: 48px; + min-height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 7px; + border: 1px solid var(--border-soft); + border-radius: 999px; + background: var(--surface-deep); + color: var(--status-neutral-text); + font-family: ui-monospace, "SFMono-Regular", Consolas, monospace; + font-size: 10px; + font-weight: 700; + line-height: 1; + } + .conn-ping:empty { display: none; } + .conn-ping-good { + border-color: color-mix(in oklch, var(--success), transparent 70%); + background: color-mix(in oklch, var(--success), var(--surface-deep) 88%); + color: var(--status-success-text); + } + .conn-ping-warning { + border-color: color-mix(in oklch, var(--warning), transparent 62%); + background: color-mix(in oklch, var(--warning), var(--surface-deep) 86%); + color: var(--status-warning-text); + } + .conn-ping-bad { + border-color: color-mix(in oklch, var(--error), transparent 62%); + background: color-mix(in oklch, var(--error), var(--surface-deep) 87%); + color: var(--status-error-text); + } /* Invite Link */ .invite-box { @@ -509,7 +555,7 @@ width: 14px; left: 3px; bottom: 3px; - background-color: #a3a294; + background-color: var(--status-neutral-text); transition: .3s; border-radius: 50%; } @@ -521,47 +567,38 @@ background-color: white; } .popup-footer { - margin-top: 18px; - padding: 12px; - border: 1px solid var(--border-soft); - border-radius: var(--radius); - background: color-mix(in oklch, var(--card), var(--accent) 5%); - } - .footer-support { + margin-top: 16px; + padding: 11px 2px 2px; + border-top: 1px solid var(--border-soft); display: flex; align-items: center; justify-content: space-between; - gap: 12px; - padding-bottom: 10px; - border-bottom: 1px solid var(--border-soft); - } - .footer-prompt { - margin: 0; - color: var(--text); - font-size: 11px; - font-weight: 600; - line-height: 1.35; + gap: 10px; } .footer-review { flex: none; - min-height: 30px; - padding: 7px 10px; - border-radius: 8px; - background: var(--warning); - color: var(--text-on-warm) !important; - font-size: 11px; - font-weight: 800; + min-height: 26px; + display: inline-flex; + align-items: center; + padding: 4px 8px; + border: 1px solid color-mix(in oklch, var(--warning), transparent 68%); + border-radius: 7px; + background: color-mix(in oklch, var(--warning), transparent 91%); + color: var(--status-warning-text) !important; + font-size: 10px; + font-weight: 700; } .footer-links { display: flex; - justify-content: center; - gap: 6px; - padding-top: 8px; + align-items: center; + gap: 4px; } .footer-links a { - min-height: 28px; - padding: 6px 9px; - border-radius: 7px; + min-height: 26px; + display: inline-flex; + align-items: center; + padding: 4px 7px; + border-radius: 6px; font-size: 10px; font-weight: 600; } @@ -721,6 +758,51 @@ text-align: left; text-align-last: left; } + .theme-picker { + position: relative; + display: grid; + grid-template-columns: repeat(3, 1fr); + width: 154px; + padding: 3px; + border: 1px solid var(--border-strong); + border-radius: 10px; + background: var(--surface-deep); + } + .theme-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; + } + .theme-choice { + min-width: 0; + min-height: 32px; + margin: 0; + padding: 0 4px; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--text-muted); + font-family: inherit; + font-size: 9px; + font-weight: 700; + line-height: 1; + cursor: pointer; + transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease; + } + .theme-choice:hover { + color: var(--text); + } + .theme-choice.active { + background: var(--surface-alt); + color: var(--text); + box-shadow: 0 1px 5px oklch(0.08 0.02 150 / 0.32), inset 0 0 0 1px var(--border-soft); + } .settings-row .toggle-switch { justify-self: end; margin: 0 2px 0 0; @@ -846,11 +928,10 @@ background-color: white; } html.theme-light .popup-footer { - border-color: var(--border-soft); - box-shadow: 0 8px 24px oklch(0.22 0.035 140 / 0.07); + border-top-color: var(--border-soft); } html.theme-light .tab-btn.active { - color: #fff; + color: var(--text-on-green); } .feature-hint { display: inline-block; @@ -1059,13 +1140,16 @@
- - - + + + +
@@ -1175,7 +1259,7 @@ Disconnected - +
@@ -1212,14 +1296,11 @@ diff --git a/extension/popup.js b/extension/popup.js index 467f57e..6660e95 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -149,13 +149,28 @@ function localizeThemeSelector(locale) { const systemEl = document.getElementById('themeOptionSystem'); const darkEl = document.getElementById('themeOptionDark'); const lightEl = document.getElementById('themeOptionLight'); + const systemButton = document.getElementById('themeChoiceSystem'); + const darkButton = document.getElementById('themeChoiceDark'); + const lightButton = document.getElementById('themeChoiceLight'); if (labelEl) labelEl.textContent = label; if (systemEl) systemEl.textContent = system; if (darkEl) darkEl.textContent = dark; if (lightEl) lightEl.textContent = light; + if (systemButton) systemButton.textContent = system; + if (darkButton) darkButton.textContent = dark; + if (lightButton) lightButton.textContent = light; if (elements.themeSelector) elements.themeSelector.setAttribute('aria-label', label); } +function syncThemePicker() { + const activeMode = elements.themeSelector?.value || 'system'; + document.querySelectorAll('.theme-choice').forEach(button => { + const active = button.dataset.themeValue === activeMode; + button.classList.toggle('active', active); + button.setAttribute('aria-pressed', String(active)); + }); +} + async function updateFeatureHints() { const data = await chrome.storage.sync.get(['dismissedHints']); const dismissed = Array.isArray(data.dismissedHints) ? data.dismissedHints : []; @@ -242,6 +257,7 @@ async function init() { if (elements.langSelector) elements.langSelector.value = activeLang; if (elements.themeSelector) elements.themeSelector.value = ['dark', 'light'].includes(localData.themeMode) ? localData.themeMode : 'system'; + syncThemePicker(); let username = localData.username; if (!username) { @@ -555,7 +571,7 @@ function updateLastActionUI(state, peers) { if (pId === localPeerId) return; const pName = (typeof peer === 'object' && peer.username) ? peer.username : pId.substring(0, 4); const isAcked = safeAcks.includes(pId) || pId === state.senderId; - const color = isAcked ? 'var(--success)' : '#4c5142'; + const color = isAcked ? 'var(--success)' : 'var(--border-strong)'; const icon = isAcked ? '✓' : '...'; const avatar = getAvatarForName(pName); @@ -705,7 +721,7 @@ function updatePeerList(peers) { const peerItem = document.createElement('div'); peerItem.className = 'peer-item'; - peerItem.style.cssText = 'position:relative; display:block; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.05);'; + peerItem.style.cssText = 'position:relative; display:block; padding: 8px 0; border-bottom: 1px solid var(--border-soft);'; const header = document.createElement('div'); header.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding-right: 24px;'; @@ -715,7 +731,7 @@ function updatePeerList(peers) { const avatar = getAvatarForName(pUsername || pId); if (pUsername) { const u = document.createElement('span'); - u.style.cssText = 'font-weight:600; color:white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; display: inline-block;'; + u.style.cssText = 'font-weight:600; color:var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; display: inline-block;'; u.textContent = `${avatar} ${pUsername}`; const i = document.createElement('span'); i.style.cssText = 'font-size:10px; opacity:0.6; font-style:italic; white-space: nowrap; flex-shrink: 0;'; @@ -1040,25 +1056,8 @@ function applyConnectionStatus(status) { const idle = status === 'idle'; if (elements.connDot) { - elements.connDot.className = 'status-dot ' + (connected ? 'status-online' : ((connecting || reconnecting) ? 'status-online' : 'status-offline')); - - if (reconnecting) { - elements.connDot.style.background = '#c96736'; - elements.connDot.style.boxShadow = '0 0 8px #c96736'; - } else if (connecting) { - elements.connDot.style.background = '#de7949'; - elements.connDot.style.boxShadow = '0 0 8px #de7949'; - } else if (idle) { - // Neutral grey — ready, not failed. - elements.connDot.style.background = '#bcb7a9'; - elements.connDot.style.boxShadow = 'none'; - } else if (!connected) { - elements.connDot.style.background = '#b94642'; - elements.connDot.style.boxShadow = 'none'; - } else { - elements.connDot.style.background = ''; - elements.connDot.style.boxShadow = ''; - } + const dotStatus = connected ? 'status-online' : ((connecting || reconnecting) ? 'status-connecting' : (idle ? 'status-idle' : 'status-offline')); + elements.connDot.className = `status-dot ${dotStatus}`; } if (elements.connText) { @@ -1095,16 +1094,16 @@ function updatePingDisplay(pingMs) { if (!elements.connPing) return; if (pingMs === null || pingMs === undefined || typeof pingMs !== 'number' || !Number.isFinite(pingMs)) { elements.connPing.textContent = ''; - elements.connPing.style.color = ''; + elements.connPing.className = 'conn-ping'; return; } elements.connPing.textContent = `${Math.round(pingMs)}ms`; - if (pingMs < 50) { - elements.connPing.style.color = '#56ae6c'; + if (pingMs < 75) { + elements.connPing.className = 'conn-ping conn-ping-good'; } else if (pingMs < 150) { - elements.connPing.style.color = '#c96736'; + elements.connPing.className = 'conn-ping conn-ping-warning'; } else { - elements.connPing.style.color = '#b94642'; + elements.connPing.className = 'conn-ping conn-ping-bad'; } } @@ -1122,10 +1121,10 @@ function updateHistory(history) { const actionLabel = item.action.toUpperCase().replace('FORCE_SYNC_', ''); const entry = document.createElement('div'); - entry.style.cssText = 'margin-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 2px;'; + entry.style.cssText = 'margin-bottom: 4px; border-bottom: 1px solid var(--border-soft); padding-bottom: 2px;'; const timeSpan = document.createElement('span'); - timeSpan.style.color = '#6b705e'; + timeSpan.style.color = 'var(--text-muted)'; timeSpan.textContent = `[${time}] `; const actionBold = document.createElement('b'); @@ -1168,7 +1167,7 @@ function updateRoomList(rooms) { rooms.forEach(r => { const item = document.createElement('div'); item.className = 'room-item'; - item.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor:pointer;'; + item.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding: 8px; border-bottom: 1px solid var(--border-soft); cursor:pointer;'; item.dataset.id = r.id; const leftSide = document.createElement('div'); @@ -1307,9 +1306,18 @@ if (elements.themeSelector) { const themeMode = elements.themeSelector.value; window.koalaTheme?.setMode(themeMode); chrome.storage.local.set({ themeMode }); + syncThemePicker(); }); } +document.querySelectorAll('.theme-choice').forEach(button => { + button.addEventListener('click', () => { + if (!elements.themeSelector) return; + elements.themeSelector.value = button.dataset.themeValue; + elements.themeSelector.dispatchEvent(new window.Event('change')); + }); +}); + if (elements.audioSettingsLink) { elements.audioSettingsLink.addEventListener('click', async (event) => { event.preventDefault(); @@ -2343,7 +2351,7 @@ function refreshDebugInfo() { const addSection = (title) => { const div = document.createElement('div'); - div.style.cssText = 'margin: 8px 0 4px 0; border-bottom: 1px solid #2f3625; padding-bottom: 2px; color: var(--accent); font-weight: bold; font-size: 10px; text-transform: uppercase;'; + div.style.cssText = 'margin: 8px 0 4px 0; border-bottom: 1px solid var(--border-soft); padding-bottom: 2px; color: var(--accent); font-weight: bold; font-size: 10px; text-transform: uppercase;'; div.textContent = title; elements.videoDebug.appendChild(div); }; @@ -2352,7 +2360,7 @@ function refreshDebugInfo() { // No video found — show diagnostic info addSection('Video Detection'); const notFound = document.createElement('div'); - notFound.style.cssText = 'color: #b94642; font-weight: 700; margin-bottom: 8px;'; + notFound.style.cssText = 'color: var(--status-error-text); font-weight: 700; margin-bottom: 8px;'; notFound.textContent = 'NO VIDEO ELEMENT FOUND'; elements.videoDebug.appendChild(notFound); @@ -2370,7 +2378,7 @@ function refreshDebugInfo() { } const hint = document.createElement('div'); - hint.style.cssText = 'margin-top: 12px; padding: 8px; background: rgba(222,121,73,0.12); border-left: 3px solid #de7949; border-radius: 4px; font-size: 10px; color: var(--text-muted);'; + hint.style.cssText = 'margin-top: 12px; padding: 8px; background: color-mix(in oklch, var(--warning), transparent 88%); border-left: 3px solid var(--warning); border-radius: 4px; font-size: 10px; color: var(--text);'; hint.textContent = 'Is a video currently playing? The extension only detects