Add light mode theme setting and remove offline banner
Release rolling / release (push) Has been cancelled
Release rolling / release (push) Has been cancelled
Dark remains default; users can pick light or match system under Settings → Appearance. Drop the full-height connection-lost banner that stretched across the window.
This commit is contained in:
+118
-22
@@ -98,6 +98,12 @@ export const DEFAULT_SETTINGS = {
|
||||
*/
|
||||
reconnectMaxAttempts: 8,
|
||||
// Track G — appearance / motion
|
||||
/**
|
||||
* UI color theme. Dark is default.
|
||||
* `system` follows prefers-color-scheme.
|
||||
* @type {'dark' | 'light' | 'system'}
|
||||
*/
|
||||
theme: 'dark',
|
||||
reduceMotion: false,
|
||||
accent: 'teal', // teal | cyan | violet
|
||||
showRefreshStamp: true,
|
||||
@@ -131,6 +137,99 @@ export function normalizeHiddenContainerLabels(raw) {
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize theme preference from settings.
|
||||
* @param {unknown} raw
|
||||
* @returns {'dark' | 'light' | 'system'}
|
||||
*/
|
||||
export function normalizeThemePreference(raw) {
|
||||
const v = String(raw || '').toLowerCase()
|
||||
if (v === 'light' || v === 'system') return v
|
||||
return 'dark'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve stored preference to an applied theme (dark | light).
|
||||
* @param {'dark' | 'light' | 'system' | string | undefined} preference
|
||||
* @returns {'dark' | 'light'}
|
||||
*/
|
||||
export function resolveTheme(preference) {
|
||||
const pref = normalizeThemePreference(preference)
|
||||
if (pref === 'light') return 'light'
|
||||
if (pref === 'system') {
|
||||
try {
|
||||
if (typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: light)').matches) {
|
||||
return 'light'
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return 'dark'
|
||||
}
|
||||
return 'dark'
|
||||
}
|
||||
|
||||
/** @type {MediaQueryList | null} */
|
||||
let systemThemeMql = null
|
||||
/** @type {((this: MediaQueryList, ev: MediaQueryListEvent) => void) | null} */
|
||||
let systemThemeHandler = null
|
||||
|
||||
/**
|
||||
* Apply resolved color theme to document (html + body data-theme).
|
||||
* @param {'dark' | 'light' | 'system' | string | undefined} preference
|
||||
* @returns {'dark' | 'light'}
|
||||
*/
|
||||
export function applyColorTheme(preference) {
|
||||
const pref = normalizeThemePreference(preference)
|
||||
const resolved = resolveTheme(pref)
|
||||
try {
|
||||
const root = document.documentElement
|
||||
root.dataset.theme = resolved
|
||||
root.dataset.themePref = pref
|
||||
if (document.body) {
|
||||
document.body.dataset.theme = resolved
|
||||
document.body.dataset.themePref = pref
|
||||
}
|
||||
// Browser chrome / PWA status bar
|
||||
const meta = document.querySelector('meta[name="theme-color"]')
|
||||
if (meta) {
|
||||
meta.setAttribute('content', resolved === 'light' ? '#f3f5f8' : '#0a0c10')
|
||||
}
|
||||
const tile = document.querySelector('meta[name="msapplication-TileColor"]')
|
||||
if (tile) {
|
||||
tile.setAttribute('content', resolved === 'light' ? '#f3f5f8' : '#0f1117')
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Follow OS when preference is system
|
||||
try {
|
||||
if (systemThemeMql && systemThemeHandler) {
|
||||
systemThemeMql.removeEventListener?.('change', systemThemeHandler)
|
||||
// Safari < 14
|
||||
systemThemeMql.removeListener?.(systemThemeHandler)
|
||||
}
|
||||
systemThemeMql = null
|
||||
systemThemeHandler = null
|
||||
if (pref === 'system' && typeof matchMedia === 'function') {
|
||||
systemThemeMql = matchMedia('(prefers-color-scheme: light)')
|
||||
systemThemeHandler = () => {
|
||||
applyColorTheme('system')
|
||||
}
|
||||
if (systemThemeMql.addEventListener) {
|
||||
systemThemeMql.addEventListener('change', systemThemeHandler)
|
||||
} else if (systemThemeMql.addListener) {
|
||||
systemThemeMql.addListener(systemThemeHandler)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
export function loadSettings() {
|
||||
try {
|
||||
const raw = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}')
|
||||
@@ -145,6 +244,7 @@ export function loadSettings() {
|
||||
merged.hiddenContainerLabels = normalizeHiddenContainerLabels(
|
||||
raw.hiddenContainerLabels ?? DEFAULT_SETTINGS.hiddenContainerLabels
|
||||
)
|
||||
merged.theme = normalizeThemePreference(raw.theme ?? DEFAULT_SETTINGS.theme)
|
||||
// Reconnect: 0 = unlimited; 1–8 finite (legacy missing key → default 8)
|
||||
if (raw.reconnectMaxAttempts === undefined || raw.reconnectMaxAttempts === null) {
|
||||
merged.reconnectMaxAttempts = DEFAULT_SETTINGS.reconnectMaxAttempts
|
||||
@@ -175,6 +275,7 @@ export function applySettings(s = loadSettings()) {
|
||||
document.body.dataset.accent = s.accent || 'teal'
|
||||
document.body.dataset.reduceMotion = s.reduceMotion ? '1' : '0'
|
||||
document.body.dataset.refreshStamp = s.showRefreshStamp === false ? '0' : '1'
|
||||
applyColorTheme(s.theme || 'dark')
|
||||
startListAutoRefresh(s.refreshSeconds)
|
||||
|
||||
// Push reconnect policy into ConnectionManager (0 = unlimited, 1–8 finite)
|
||||
@@ -263,14 +364,6 @@ export function startListAutoRefresh(seconds) {
|
||||
}, sec * 1000)
|
||||
}
|
||||
|
||||
export function setOfflineBanner(visible, text) {
|
||||
const el = document.getElementById('offline-banner')
|
||||
const t = document.getElementById('offline-banner-text')
|
||||
if (!el) return
|
||||
el.classList.toggle('visible', Boolean(visible))
|
||||
if (t && text) t.textContent = text
|
||||
}
|
||||
|
||||
export function updateActivePeerChip(conn) {
|
||||
const label = document.getElementById('active-peer-label')
|
||||
const dot = document.getElementById('active-peer-dot')
|
||||
@@ -1781,9 +1874,11 @@ export function readSettingsForm() {
|
||||
: 'comfortable'
|
||||
const accentRaw = document.getElementById('settings-accent')?.value || 'teal'
|
||||
const accent = ['teal', 'cyan', 'violet'].includes(accentRaw) ? accentRaw : 'teal'
|
||||
const theme = normalizeThemePreference(document.getElementById('settings-theme')?.value)
|
||||
return {
|
||||
density,
|
||||
accent,
|
||||
theme,
|
||||
reduceMotion: document.getElementById('settings-reduce-motion')?.value === '1',
|
||||
showRefreshStamp: document.getElementById('settings-refresh-stamp')?.value !== '0',
|
||||
sidebarCollapsed: document.getElementById('settings-sidebar')?.value === 'collapsed',
|
||||
@@ -2063,6 +2158,7 @@ async function runRestorePackage(pkg, opts = {}) {
|
||||
export function loadSettingsView(forceTab) {
|
||||
const s = loadSettings()
|
||||
setSelectValue('settings-density', s.density || 'comfortable')
|
||||
setSelectValue('settings-theme', normalizeThemePreference(s.theme))
|
||||
setSelectValue('settings-accent', s.accent || 'teal')
|
||||
setSelectValue('settings-reduce-motion', s.reduceMotion ? '1' : '0')
|
||||
setSelectValue('settings-refresh-stamp', s.showRefreshStamp === false ? '0' : '1')
|
||||
@@ -2450,10 +2546,6 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
})
|
||||
.catch((err) => console.warn('[ops] track-g-ux failed to load', err))
|
||||
|
||||
document.getElementById('offline-banner-dismiss')?.addEventListener('click', () => {
|
||||
setOfflineBanner(false)
|
||||
})
|
||||
|
||||
document.getElementById('smart-net-resuggest')?.addEventListener('click', async () => {
|
||||
try {
|
||||
const ipam = await suggestNetwork()
|
||||
@@ -2526,6 +2618,17 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
showSettingsTab(btn.getAttribute('data-settings-tab'))
|
||||
})
|
||||
|
||||
// Live theme preview (persists on Save all)
|
||||
document.getElementById('settings-theme')?.addEventListener('change', (e) => {
|
||||
applyColorTheme(e.target?.value || 'dark')
|
||||
})
|
||||
document.getElementById('settings-accent')?.addEventListener('change', (e) => {
|
||||
const accent = String(e.target?.value || 'teal')
|
||||
if (['teal', 'cyan', 'violet'].includes(accent)) {
|
||||
document.body.dataset.accent = accent
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('settings-save-btn')?.addEventListener('click', (e) => {
|
||||
const btn = e.currentTarget
|
||||
if (btn?.classList.contains('is-saving') || btn?.classList.contains('is-saved')) return
|
||||
@@ -2819,25 +2922,15 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
warmSnapshot()
|
||||
refreshSwarmNavState().catch(() => {})
|
||||
updateActivePeerChip(conn)
|
||||
setOfflineBanner(false)
|
||||
})
|
||||
manager.on('disconnect', () => {
|
||||
clearSnapshotCache()
|
||||
updateActivePeerChip(manager.active)
|
||||
if (!manager.active?.connected) {
|
||||
setOfflineBanner(true, 'Connection lost — reconnecting…')
|
||||
setSwarmNavEnabled(false, SWARM_DISABLED_NAV_TITLE)
|
||||
}
|
||||
})
|
||||
manager.on('reconnecting', ({ attempt, delayMs }) => {
|
||||
const secs = Math.max(1, Math.round((delayMs || 5000) / 1000))
|
||||
setOfflineBanner(
|
||||
true,
|
||||
`Connection lost — retrying every ${secs}s (attempt ${attempt})…`
|
||||
)
|
||||
})
|
||||
manager.on('reconnected', () => {
|
||||
setOfflineBanner(false)
|
||||
warmSnapshot()
|
||||
refreshSwarmNavState().catch(() => {})
|
||||
})
|
||||
@@ -2889,6 +2982,9 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
loadSettings,
|
||||
saveSettings,
|
||||
applySettings,
|
||||
applyColorTheme,
|
||||
resolveTheme,
|
||||
normalizeThemePreference,
|
||||
shouldShowFirstConnectTip,
|
||||
showSettingsTab,
|
||||
DEFAULT_SETTINGS,
|
||||
|
||||
-17
@@ -18,23 +18,6 @@
|
||||
padding: 0.75rem 0.85rem 0.2rem;
|
||||
}
|
||||
|
||||
#offline-banner {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: linear-gradient(90deg, #7c2d12, #9a3412);
|
||||
color: #fff7ed;
|
||||
font-size: 0.9rem;
|
||||
z-index: 2000;
|
||||
}
|
||||
#offline-banner.visible {
|
||||
display: flex;
|
||||
}
|
||||
#offline-banner .btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.pd-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
+1048
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user