fix(extension): unify status UX and theme design

This commit is contained in:
Timo
2026-07-12 11:10:15 +02:00
parent 9d6a3650c4
commit 104602edec
4 changed files with 202 additions and 92 deletions
+24 -4
View File
@@ -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;
}
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>KoalaSync Audio Settings</title>
<script src="theme-init.js"></script>
<link rel="stylesheet" href="audio-options.css">
</head>
<body>
+129 -48
View File
@@ -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 @@
</div>
<div class="settings-row">
<label id="themeLabel" for="themeSelector" title="Follow the system theme or choose a fixed appearance">Appearance</label>
<span class="settings-select">
<select id="themeSelector" class="settings-control" aria-label="Appearance">
<div class="theme-picker" role="group" aria-labelledby="themeLabel">
<select id="themeSelector" aria-label="Appearance" tabindex="-1">
<option id="themeOptionSystem" value="system">System</option>
<option id="themeOptionDark" value="dark">Dark</option>
<option id="themeOptionLight" value="light">Light</option>
</select>
</span>
<button id="themeChoiceSystem" class="theme-choice" type="button" data-theme-value="system">System</button>
<button id="themeChoiceDark" class="theme-choice" type="button" data-theme-value="dark">Dark</button>
<button id="themeChoiceLight" class="theme-choice" type="button" data-theme-value="light">Light</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>
@@ -1175,7 +1259,7 @@
<span id="connText">Disconnected</span>
<span id="connInfo" data-i18n-title="STATUS_IDLE_TOOLTIP" title="KoalaSync is ready. Join or create a room to connect and start syncing." style="display:none; cursor:help; flex:none; width:15px; height:15px; line-height:14px; text-align:center; border-radius:50%; border:1px solid var(--text-muted); color:var(--text-muted); font-size:10px; font-weight:700; font-style:normal;">i</span>
</span>
<span id="connPing" style="font-size:11px; font-family:monospace; font-weight:600; opacity:0.8;"></span>
<span id="connPing" class="conn-ping"></span>
<button id="retryBtn" class="secondary" style="display:none; width: auto; padding: 4px 8px; font-size: 10px; margin: 0;" title="Attempt to reconnect to the server" data-i18n="BTN_RETRY" data-i18n-title="BTN_RETRY_TOOLTIP">RETRY</button>
<button id="copyLogs" class="btn secondary" style="width: auto; padding: 4px 10px; font-size: 11px; margin: 0;" title="Copy logs to clipboard for sharing" data-i18n="BTN_COPY_LOGS" data-i18n-title="BTN_COPY_LOGS_TOOLTIP">Copy Logs</button>
</div>
@@ -1212,14 +1296,11 @@
</div>
<div class="popup-footer">
<div class="footer-support">
<p class="footer-prompt" data-i18n="FOOTER_SUPPORT_PROMPT">Enjoying KoalaSync? Leave a review!</p>
<a id="settingsReviewLink" class="footer-review" href="#" target="_blank" data-i18n="FOOTER_REVIEW">★ Rate us</a>
</div>
<nav class="footer-links" aria-label="KoalaSync links">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" data-i18n="LABEL_GITHUB">GitHub</a>
<a id="settingsSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">Support</a>
</nav>
<a id="settingsReviewLink" class="footer-review" href="#" target="_blank" data-i18n="FOOTER_REVIEW">★ Rate us</a>
</div>
</main>
+48 -40
View File
@@ -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 <video> elements. Ensure media is actively loaded on the page.';
elements.videoDebug.appendChild(hint);
return;
@@ -2378,12 +2386,12 @@ function refreshDebugInfo() {
// Video found — full debug
addSection('Playback');
addField('State', state.paused ? 'PAUSED' : 'PLAYING', state.paused ? 'var(--text-muted)' : '#56ae6c');
addField('State', state.paused ? 'PAUSED' : 'PLAYING', state.paused ? 'var(--text-muted)' : 'var(--status-success-text)');
addField('Time', `${state.currentTime.toFixed(2)}s / ${(state.duration || 0).toFixed(2)}s`);
addField('ReadyState', `${state.readyState} (${state.readyStateLabel || '?'})`,
state.readyState >= 3 ? '#56ae6c' : '#de7949');
state.readyState >= 3 ? 'var(--status-success-text)' : 'var(--status-warning-text)');
addField('Network', `${state.networkState} (${state.networkStateLabel || '?'})`,
state.networkState === 1 ? '#56ae6c' : state.networkState === 3 ? '#b94642' : 'var(--text-muted)');
state.networkState === 1 ? 'var(--status-success-text)' : state.networkState === 3 ? 'var(--status-error-text)' : 'var(--text-muted)');
addField('Buffered', state.buffered || '?');
if (state.nativeCurrentTime != null || state.nativeDuration != null) {
addField('Native Time', `${state.nativeCurrentTime ?? '?'}s / ${state.nativeDuration ?? '?'}s`);
@@ -2407,18 +2415,18 @@ function refreshDebugInfo() {
addSection('Dimensions');
const dimsOk = state.videoWidth > 0 && state.videoHeight > 0;
addField('Resolution', `${state.videoWidth}x${state.videoHeight}`, dimsOk ? '#56ae6c' : '#b94642');
addField('Resolution', `${state.videoWidth}x${state.videoHeight}`, dimsOk ? 'var(--status-success-text)' : 'var(--status-error-text)');
if (!dimsOk) {
const dimHint = document.createElement('div');
dimHint.style.cssText = 'color: #de7949; font-size: 9px; margin: 2px 0 6px 12px;';
dimHint.style.cssText = 'color: var(--status-warning-text); font-size: 9px; margin: 2px 0 6px 12px;';
dimHint.textContent = '0x0 = video element hidden or not yet loaded';
elements.videoDebug.appendChild(dimHint);
}
if (state.error) {
addSection('Media Error');
addField('Code', String(state.error.code), '#b94642');
addField('Message', state.error.message || '?', '#b94642');
addField('Code', String(state.error.code), 'var(--status-error-text)');
addField('Message', state.error.message || '?', 'var(--status-error-text)');
}
addSection('Detection');