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
i
-
+
@@ -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