Polish light theme surfaces and add Jobs tray settings
Release rolling / release (push) Successful in 8m23s
Release rolling / release (push) Successful in 8m23s
Expand light-mode CSS for dashboard, details, terminals, job/pull progress, restoring boot card, and settings chrome. Add a Jobs tray settings tab for dismiss timing and reconnect feedback volume; auto terminal themes follow UI color mode when saved.
This commit is contained in:
@@ -148,8 +148,40 @@ manager.on('connect', (conn) => {
|
||||
);
|
||||
}
|
||||
});
|
||||
/** Throttle reconnect toasts — retries every 5s forever; don't spam the UI */
|
||||
/** Throttle reconnect notices (standard mode only) */
|
||||
let lastReconnectToastAt = 0;
|
||||
|
||||
/**
|
||||
* Jobs tray + reconnect feedback prefs (Settings → Jobs tray).
|
||||
* Falls back to minimal defaults if ops shell is not ready.
|
||||
*/
|
||||
function jobsTrayPrefs() {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.peardockOps?.getJobsTrayPrefs) {
|
||||
return window.peardockOps.getJobsTrayPrefs();
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.__peardockSettings) {
|
||||
const s = window.__peardockSettings;
|
||||
return {
|
||||
reconnectFeedback: s.reconnectFeedback || 'minimal',
|
||||
reconnectNotifySuccess: s.reconnectNotifySuccess !== false,
|
||||
reconnectNotifyFailure: s.reconnectNotifyFailure !== false,
|
||||
launchConnectNotify: s.launchConnectNotify !== false,
|
||||
jobActivityInTray: s.jobActivityInTray !== false,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return {
|
||||
reconnectFeedback: 'minimal',
|
||||
reconnectNotifySuccess: true,
|
||||
reconnectNotifyFailure: true,
|
||||
launchConnectNotify: true,
|
||||
jobActivityInTray: true,
|
||||
};
|
||||
}
|
||||
|
||||
manager.on('reconnecting', ({ id, attempt, maxAttempts, delayMs }) => {
|
||||
const secs = Math.max(1, Math.round((delayMs || 5000) / 1000));
|
||||
const label =
|
||||
@@ -157,18 +189,39 @@ manager.on('reconnecting', ({ id, attempt, maxAttempts, delayMs }) => {
|
||||
(id ? `${String(id).slice(0, 8)}…` : 'peer');
|
||||
const attemptLabel =
|
||||
maxAttempts != null ? `${attempt}/${maxAttempts}` : String(attempt);
|
||||
const prefs = jobsTrayPrefs();
|
||||
const mode = prefs.reconnectFeedback || 'minimal';
|
||||
|
||||
// Fleet / list always update; drawer + bell depend on feedback level
|
||||
if (mode === 'standard') {
|
||||
updateStatusIndicator(
|
||||
`Reconnecting to ${label} (attempt ${attemptLabel})… next try in ${secs}s`
|
||||
);
|
||||
}
|
||||
if (id && connections[id]) {
|
||||
connections[id].healthStatus = 'connecting';
|
||||
updateConnectionStatus(id, false);
|
||||
updateConnectionDisplay(id);
|
||||
}
|
||||
refreshFleetIfVisible();
|
||||
|
||||
if (mode === 'silent' || typeof showAlert !== 'function') return;
|
||||
|
||||
const now = Date.now();
|
||||
// First attempt + at most once per minute thereafter
|
||||
if (typeof showAlert === 'function' && (attempt === 1 || now - lastReconnectToastAt > 60_000)) {
|
||||
if (mode === 'minimal') {
|
||||
// One quiet bell on first loss only — no periodic spam, no job tray panel
|
||||
if (attempt === 1) {
|
||||
showAlert(
|
||||
'warning',
|
||||
`Connection lost — retrying ${label} in the background…`,
|
||||
{ toast: false, badge: true }
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// standard: first attempt + at most once per minute thereafter
|
||||
if (attempt === 1 || now - lastReconnectToastAt > 60_000) {
|
||||
lastReconnectToastAt = now;
|
||||
showAlert(
|
||||
'warning',
|
||||
@@ -184,10 +237,15 @@ manager.on('reconnected', ({ id, connection } = {}) => {
|
||||
const conn = connection || (id && manager.connections.get(id)) || manager.active;
|
||||
if (conn) bindLivePeer(conn);
|
||||
refreshFleetIfVisible();
|
||||
if (typeof showAlert === 'function') {
|
||||
const prefs = jobsTrayPrefs();
|
||||
if (
|
||||
prefs.reconnectFeedback !== 'silent' &&
|
||||
prefs.reconnectNotifySuccess !== false &&
|
||||
typeof showAlert === 'function'
|
||||
) {
|
||||
const label =
|
||||
(id && connections[id] && peerDisplayName(connections[id], id)) || 'peardock server';
|
||||
showAlert('success', `Reconnected to ${label}`);
|
||||
showAlert('success', `Reconnected to ${label}`, { toast: false });
|
||||
}
|
||||
if (hasActiveConnection()) {
|
||||
hideWelcomePage();
|
||||
@@ -207,13 +265,19 @@ manager.on('reconnect-failed', ({ id, attempts, maxAttempts } = {}) => {
|
||||
updateConnectionDisplay(id);
|
||||
}
|
||||
refreshFleetIfVisible();
|
||||
if (typeof showAlert === 'function') {
|
||||
const prefs = jobsTrayPrefs();
|
||||
if (
|
||||
prefs.reconnectFeedback !== 'silent' &&
|
||||
prefs.reconnectNotifyFailure !== false &&
|
||||
typeof showAlert === 'function'
|
||||
) {
|
||||
const n = attempts != null && maxAttempts != null ? `${attempts}/${maxAttempts}` : attempts;
|
||||
showAlert(
|
||||
'danger',
|
||||
n
|
||||
? `Could not reconnect after ${n} tries. Host stays offline in Fleet — use Retry connect.`
|
||||
: 'Could not reconnect. Host stays offline in Fleet — use Retry connect.'
|
||||
? `Could not reconnect after ${n} tries. Host stays offline in Fleet — use Reconnect.`
|
||||
: 'Could not reconnect. Host stays offline in Fleet — use Reconnect.',
|
||||
{ toast: false, badge: true }
|
||||
);
|
||||
}
|
||||
updateHealthBadge(null);
|
||||
@@ -6961,16 +7025,52 @@ let detailsTerminalTheme = 'dark';
|
||||
|
||||
const DETAILS_TERMINAL_THEMES = {
|
||||
dark: {
|
||||
background: '#000000',
|
||||
foreground: '#ffffff',
|
||||
cursor: '#ffffff',
|
||||
selectionBackground: '#4d4d4d',
|
||||
background: '#0b0f14',
|
||||
foreground: '#e6edf3',
|
||||
cursor: '#34d399',
|
||||
cursorAccent: '#0b0f14',
|
||||
selectionBackground: 'rgba(52, 211, 153, 0.35)',
|
||||
selectionForeground: '#f4f7fb',
|
||||
black: '#0b0f14',
|
||||
red: '#f87171',
|
||||
green: '#4ade80',
|
||||
yellow: '#fbbf24',
|
||||
blue: '#38bdf8',
|
||||
magenta: '#c084fc',
|
||||
cyan: '#2dd4bf',
|
||||
white: '#e6edf3',
|
||||
brightBlack: '#64748b',
|
||||
brightRed: '#fca5a5',
|
||||
brightGreen: '#86efac',
|
||||
brightYellow: '#fde68a',
|
||||
brightBlue: '#7dd3fc',
|
||||
brightMagenta: '#d8b4fe',
|
||||
brightCyan: '#5eead4',
|
||||
brightWhite: '#f8fafc',
|
||||
},
|
||||
light: {
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
cursor: '#000000',
|
||||
selectionBackground: '#b3d4fc',
|
||||
foreground: '#0f172a',
|
||||
cursor: '#0f766e',
|
||||
cursorAccent: '#ffffff',
|
||||
selectionBackground: 'rgba(15, 118, 110, 0.22)',
|
||||
selectionForeground: '#0f172a',
|
||||
black: '#0f172a',
|
||||
red: '#b91c1c',
|
||||
green: '#15803d',
|
||||
yellow: '#b45309',
|
||||
blue: '#1d4ed8',
|
||||
magenta: '#6d28d9',
|
||||
cyan: '#0e7490',
|
||||
white: '#e2e8f0',
|
||||
brightBlack: '#64748b',
|
||||
brightRed: '#dc2626',
|
||||
brightGreen: '#16a34a',
|
||||
brightYellow: '#d97706',
|
||||
brightBlue: '#2563eb',
|
||||
brightMagenta: '#7c3aed',
|
||||
brightCyan: '#0891b2',
|
||||
brightWhite: '#0f172a',
|
||||
},
|
||||
'solarized-dark': {
|
||||
background: '#002b36',
|
||||
@@ -7103,8 +7203,13 @@ async function initDetailsTerminalNow(containerId) {
|
||||
terminalContainer.style.minHeight = '320px';
|
||||
terminalContainer.innerHTML = '';
|
||||
|
||||
// Always re-resolve so light UI never starts a dark (invisible) palette
|
||||
const themeKey = resolveDetailsTerminalThemeKey(detailsTerminalTheme);
|
||||
detailsTerminalTheme = themeKey;
|
||||
const theme =
|
||||
DETAILS_TERMINAL_THEMES[detailsTerminalTheme] || defaultXtermOptions().theme;
|
||||
DETAILS_TERMINAL_THEMES[themeKey] ||
|
||||
DETAILS_TERMINAL_THEMES.light ||
|
||||
defaultXtermOptions().theme;
|
||||
|
||||
const xterm = new TerminalCtor(
|
||||
defaultXtermOptions({
|
||||
@@ -7117,6 +7222,13 @@ async function initDetailsTerminalNow(containerId) {
|
||||
xterm.loadAddon(fitAddon);
|
||||
xterm.open(terminalContainer);
|
||||
|
||||
// Sync toolbar select + re-assert light palette after open
|
||||
try {
|
||||
applyDetailsTerminalTheme(themeKey);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const sendResize = (cols, rows) => {
|
||||
if (!manager.active?.connected || !cols || !rows) return;
|
||||
if (myGen !== detailsTerminalGen) return;
|
||||
@@ -7251,11 +7363,65 @@ function updateDetailsTerminalFontSizeDisplay() {
|
||||
if (display) display.textContent = String(detailsTerminalFontSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a concrete xterm palette key for the details terminal.
|
||||
* When UI is light and the saved pref is auto/dark, use light so text stays readable.
|
||||
* @param {string} [theme]
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolveDetailsTerminalThemeKey(theme) {
|
||||
const uiLight = document.documentElement?.dataset?.theme === 'light';
|
||||
let key = theme;
|
||||
if (!key || key === 'auto') {
|
||||
try {
|
||||
const pref = window.__peardockSettings?.terminalTheme;
|
||||
if (pref && pref !== 'auto' && DETAILS_TERMINAL_THEMES[pref]) key = pref;
|
||||
else key = uiLight ? 'light' : 'dark';
|
||||
} catch {
|
||||
key = uiLight ? 'light' : 'dark';
|
||||
}
|
||||
}
|
||||
// UI light + dark palette = invisible light-on-white; force light unless a specialty theme
|
||||
if (
|
||||
uiLight &&
|
||||
(key === 'dark' || key === 'auto') &&
|
||||
DETAILS_TERMINAL_THEMES.light
|
||||
) {
|
||||
key = 'light';
|
||||
}
|
||||
if (!DETAILS_TERMINAL_THEMES[key]) key = uiLight ? 'light' : 'dark';
|
||||
return key;
|
||||
}
|
||||
|
||||
function applyDetailsTerminalTheme(theme) {
|
||||
detailsTerminalTheme = theme;
|
||||
const key = resolveDetailsTerminalThemeKey(theme);
|
||||
detailsTerminalTheme = key;
|
||||
const palette = DETAILS_TERMINAL_THEMES[key] || DETAILS_TERMINAL_THEMES.dark;
|
||||
if (detailsTerminalSession?.xterm) {
|
||||
detailsTerminalSession.xterm.options.theme =
|
||||
DETAILS_TERMINAL_THEMES[theme] || DETAILS_TERMINAL_THEMES.dark;
|
||||
// Prefer options.theme; also set via setOption for older xterm builds
|
||||
try {
|
||||
detailsTerminalSession.xterm.options.theme = palette;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
detailsTerminalSession.xterm.setOption?.('theme', palette);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
// Force a redraw so glyph colors update immediately
|
||||
try {
|
||||
detailsTerminalSession.xterm.refresh?.(
|
||||
0,
|
||||
detailsTerminalSession.xterm.rows - 1
|
||||
);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
const terminalThemeSelect = document.getElementById('terminal-theme-select');
|
||||
if (terminalThemeSelect && [...terminalThemeSelect.options].some((o) => o.value === key)) {
|
||||
terminalThemeSelect.value = key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7698,12 +7864,37 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
if (terminalThemeSelect) {
|
||||
// Seed from settings (auto follows UI color mode)
|
||||
try {
|
||||
const s = window.__peardockSettings || {};
|
||||
const ui = document.documentElement?.dataset?.theme || 'dark';
|
||||
let pref = s.terminalTheme;
|
||||
if (pref === 'auto' || !pref) pref = ui === 'light' ? 'light' : 'dark';
|
||||
if (pref && DETAILS_TERMINAL_THEMES[pref]) {
|
||||
applyDetailsTerminalTheme(pref);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if ([...terminalThemeSelect.options].some((o) => o.value === detailsTerminalTheme)) {
|
||||
terminalThemeSelect.value = detailsTerminalTheme;
|
||||
}
|
||||
terminalThemeSelect.addEventListener('change', (e) => {
|
||||
applyDetailsTerminalTheme(e.target.value);
|
||||
});
|
||||
}
|
||||
updateDetailsTerminalFontSizeDisplay();
|
||||
|
||||
// Chain details-terminal into global theme apply (Settings save / UI color mode)
|
||||
try {
|
||||
const prev = window.__peardockApplyTerminalThemes;
|
||||
window.__peardockApplyTerminalThemes = (termTheme, dockerTheme) => {
|
||||
if (typeof prev === 'function') prev(termTheme, dockerTheme);
|
||||
if (termTheme) applyDetailsTerminalTheme(termTheme);
|
||||
};
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -9934,10 +10125,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const detail = err?.message ? `: ${err.message}` : '';
|
||||
const msg = `Launch: could not connect to ${label}${detail}`;
|
||||
console.warn(`[WARN] ${msg}`);
|
||||
if (typeof showAlert === 'function') {
|
||||
// Tray / bell only — do not hold or spam large toasts during boot
|
||||
const prefs = jobsTrayPrefs();
|
||||
if (prefs.launchConnectNotify === false || typeof showAlert !== 'function') return;
|
||||
// Bell only — do not hold boot or open the Jobs tray
|
||||
showAlert('warning', msg, { toast: false, badge: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+89
-15
@@ -2678,6 +2678,9 @@ services:
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" class="nav-link" data-settings-tab="behavior" role="tab">Behavior</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" class="nav-link" data-settings-tab="jobs" role="tab">Jobs tray</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" class="nav-link" data-settings-tab="notifications" role="tab">Notifications</button>
|
||||
</li>
|
||||
@@ -2787,20 +2790,6 @@ services:
|
||||
<input type="number" id="settings-refresh" class="form-control bg-dark text-white" min="0" max="120" value="10">
|
||||
<div class="form-text">0 disables. Polls containers, images, host, tunnels, etc.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="settings-job-dismiss">Job tray auto-dismiss (ms)</label>
|
||||
<input type="number" id="settings-job-dismiss" class="form-control bg-dark text-white" min="0" max="30000" step="500" value="3000">
|
||||
<div class="form-text">How long successful job cards stay visible. 0 = keep until next job.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section mt-4">
|
||||
<h3>Connection recovery</h3>
|
||||
<p class="small text-muted mb-3">
|
||||
When a saved peer drops, the client redials every 5 seconds. Configure how many tries before
|
||||
giving up and marking the host offline (failed). Offline hosts stay visible in Fleet.
|
||||
</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="settings-reconnect-max">Max reconnect tries</label>
|
||||
<select id="settings-reconnect-max" class="form-select bg-dark text-white">
|
||||
@@ -2814,7 +2803,7 @@ services:
|
||||
<option value="7">7 tries</option>
|
||||
<option value="8" selected>8 tries (max)</option>
|
||||
</select>
|
||||
<div class="form-text">After the limit, reconnect stops until you retry manually. Cap is 8.</div>
|
||||
<div class="form-text">After the limit, auto-retry stops until you use Fleet → Reconnect. Cap is 8. Feedback volume is under <strong>Jobs tray</strong>.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2844,6 +2833,88 @@ services:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jobs tray -->
|
||||
<div class="settings-panel hidden" id="settings-panel-jobs" data-settings-panel="jobs">
|
||||
<div class="settings-section">
|
||||
<h3>Bottom job drawer</h3>
|
||||
<p class="small text-muted mb-3">
|
||||
Long Docker ops (deploy, pull, compose) always use the floating Jobs tray bottom-right.
|
||||
Configure how long cards stay open and whether short status lines open the tray.
|
||||
</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="settings-job-dismiss">Success auto-dismiss (ms)</label>
|
||||
<input type="number" id="settings-job-dismiss" class="form-control bg-dark text-white" min="0" max="60000" step="500" value="3000">
|
||||
<div class="form-text">How long a successful job card stays. <code>0</code> = keep until the next job.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="settings-job-error-dismiss">Error auto-dismiss (ms)</label>
|
||||
<input type="number" id="settings-job-error-dismiss" class="form-control bg-dark text-white" min="0" max="120000" step="500" value="0">
|
||||
<div class="form-text">Failed cards stay by default. Set a delay to auto-hide errors too.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="settings-job-activity">Short status activities</label>
|
||||
<select id="settings-job-activity" class="form-select bg-dark text-white">
|
||||
<option value="1" selected>Show in Jobs tray (default)</option>
|
||||
<option value="0">Bell only (no floating drawer)</option>
|
||||
</select>
|
||||
<div class="form-text">Prune, delete, rename, and similar one-line progress.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="settings-job-bell">Also log job results to bell</label>
|
||||
<select id="settings-job-bell" class="form-select bg-dark text-white">
|
||||
<option value="0" selected>No (tray only)</option>
|
||||
<option value="1">Yes — copy success/failure into notifications</option>
|
||||
</select>
|
||||
<div class="form-text">Useful if you auto-dismiss the tray quickly and want history.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section mt-4">
|
||||
<h3>Connection recovery feedback</h3>
|
||||
<p class="small text-muted mb-3">
|
||||
When a peer drops, the client still redials in the background (see Behavior → max tries).
|
||||
These options only control how noisy that process is — Fleet status always updates.
|
||||
</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="settings-reconnect-feedback">Reconnect feedback level</label>
|
||||
<select id="settings-reconnect-feedback" class="form-select bg-dark text-white">
|
||||
<option value="silent">Silent — background only, no tray or bell</option>
|
||||
<option value="minimal" selected>Minimal — no huge tray; bell on first loss & final failure</option>
|
||||
<option value="standard">Standard — Jobs tray “Reconnecting…” + periodic notices</option>
|
||||
</select>
|
||||
<div class="form-text">Minimal is recommended: retries stay quiet while you work.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="settings-reconnect-notify-success">Notify when reconnected</label>
|
||||
<select id="settings-reconnect-notify-success" class="form-select bg-dark text-white">
|
||||
<option value="1" selected>Yes</option>
|
||||
<option value="0">No</option>
|
||||
</select>
|
||||
<div class="form-text">Ignored when feedback is Silent.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="settings-reconnect-notify-failure">Notify when max tries fail</label>
|
||||
<select id="settings-reconnect-notify-failure" class="form-select bg-dark text-white">
|
||||
<option value="1" selected>Yes</option>
|
||||
<option value="0">No</option>
|
||||
</select>
|
||||
<div class="form-text">Ignored when feedback is Silent.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="settings-launch-connect-notify">Notify launch connect failures</label>
|
||||
<select id="settings-launch-connect-notify" class="form-select bg-dark text-white">
|
||||
<option value="1" selected>Yes — bell for each peer that fails at startup</option>
|
||||
<option value="0">No — fail quietly (Fleet still shows offline)</option>
|
||||
</select>
|
||||
<div class="form-text">Launch never blocks on offline peers; this only controls the bell.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifications -->
|
||||
<div class="settings-panel hidden" id="settings-panel-notifications" data-settings-panel="notifications">
|
||||
<div class="settings-section">
|
||||
@@ -3022,16 +3093,19 @@ services:
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="settings-term-theme">Container terminal theme</label>
|
||||
<select id="settings-term-theme" class="form-select bg-dark text-white">
|
||||
<option value="auto" selected>Auto (match UI color mode)</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="monokai">Monokai</option>
|
||||
<option value="solarized-dark">Solarized dark</option>
|
||||
<option value="solarized-light">Solarized light</option>
|
||||
</select>
|
||||
<div class="form-text">Auto tracks Appearance → Color mode (including light).</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="settings-docker-term-theme">Docker CLI terminal theme</label>
|
||||
<select id="settings-docker-term-theme" class="form-select bg-dark text-white">
|
||||
<option value="auto" selected>Auto (match UI color mode)</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="monokai">Monokai</option>
|
||||
|
||||
+72
-13
@@ -18,14 +18,34 @@ let dockerTerminalSession = null
|
||||
let dockerTerminalFontSize = 14
|
||||
let dockerTerminalTheme = 'dark'
|
||||
|
||||
const dockerTerminalThemes = {
|
||||
dark: defaultXtermOptions().theme,
|
||||
light: {
|
||||
const LIGHT_XTERM_THEME = {
|
||||
background: '#ffffff',
|
||||
foreground: '#0f172a',
|
||||
cursor: '#0f172a',
|
||||
selectionBackground: '#b3d4fc',
|
||||
},
|
||||
cursor: '#0f766e',
|
||||
cursorAccent: '#ffffff',
|
||||
selectionBackground: 'rgba(15, 118, 110, 0.22)',
|
||||
selectionForeground: '#0f172a',
|
||||
black: '#0f172a',
|
||||
red: '#b91c1c',
|
||||
green: '#15803d',
|
||||
yellow: '#b45309',
|
||||
blue: '#1d4ed8',
|
||||
magenta: '#6d28d9',
|
||||
cyan: '#0e7490',
|
||||
white: '#e2e8f0',
|
||||
brightBlack: '#64748b',
|
||||
brightRed: '#dc2626',
|
||||
brightGreen: '#16a34a',
|
||||
brightYellow: '#d97706',
|
||||
brightBlue: '#2563eb',
|
||||
brightMagenta: '#7c3aed',
|
||||
brightCyan: '#0891b2',
|
||||
brightWhite: '#0f172a',
|
||||
}
|
||||
|
||||
const dockerTerminalThemes = {
|
||||
dark: defaultXtermOptions().theme,
|
||||
light: LIGHT_XTERM_THEME,
|
||||
'solarized-dark': {
|
||||
background: '#002b36',
|
||||
foreground: '#839496',
|
||||
@@ -170,10 +190,12 @@ function startDockerTerminal(connectionId, peer) {
|
||||
return
|
||||
}
|
||||
|
||||
const themeKey = resolveDockerTerminalThemeKey(dockerTerminalTheme)
|
||||
dockerTerminalTheme = themeKey
|
||||
const xterm = new Terminal(
|
||||
defaultXtermOptions({
|
||||
fontSize: dockerTerminalFontSize,
|
||||
theme: dockerTerminalThemes[dockerTerminalTheme] || dockerTerminalThemes.dark,
|
||||
theme: dockerTerminalThemes[themeKey] || dockerTerminalThemes.light || dockerTerminalThemes.dark,
|
||||
convertEol: true,
|
||||
})
|
||||
)
|
||||
@@ -371,7 +393,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
})
|
||||
const themeSelect = document.getElementById('docker-terminal-theme-select')
|
||||
try {
|
||||
const pref = window.__peardockSettings?.dockerTerminalTheme
|
||||
const s = window.__peardockSettings || {}
|
||||
const ui = document.documentElement?.dataset?.theme || 'dark'
|
||||
let pref = s.dockerTerminalTheme
|
||||
if (pref === 'auto' || !pref) pref = ui === 'light' ? 'light' : 'dark'
|
||||
if (pref && dockerTerminalThemes[pref]) {
|
||||
dockerTerminalTheme = pref
|
||||
}
|
||||
@@ -391,18 +416,52 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateDockerTerminalFontSizeDisplay()
|
||||
})
|
||||
|
||||
function resolveDockerTerminalThemeKey(theme) {
|
||||
const uiLight = document.documentElement?.dataset?.theme === 'light'
|
||||
let key = theme
|
||||
if (!key || key === 'auto') {
|
||||
try {
|
||||
const pref = window.__peardockSettings?.dockerTerminalTheme
|
||||
if (pref && pref !== 'auto' && dockerTerminalThemes[pref]) key = pref
|
||||
else key = uiLight ? 'light' : 'dark'
|
||||
} catch {
|
||||
key = uiLight ? 'light' : 'dark'
|
||||
}
|
||||
}
|
||||
if (uiLight && (key === 'dark' || key === 'auto')) key = 'light'
|
||||
if (!dockerTerminalThemes[key]) key = uiLight ? 'light' : 'dark'
|
||||
return key
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const prev = window.__peardockApplyTerminalThemes
|
||||
window.__peardockApplyTerminalThemes = (termTheme, dockerTheme) => {
|
||||
if (typeof prev === 'function') prev(termTheme, dockerTheme)
|
||||
if (dockerTheme && dockerTerminalThemes[dockerTheme]) {
|
||||
dockerTerminalTheme = dockerTheme
|
||||
const key = resolveDockerTerminalThemeKey(dockerTheme)
|
||||
if (key && dockerTerminalThemes[key]) {
|
||||
dockerTerminalTheme = key
|
||||
const palette = dockerTerminalThemes[key] || dockerTerminalThemes.dark
|
||||
if (dockerTerminalSession?.xterm) {
|
||||
dockerTerminalSession.xterm.options.theme =
|
||||
dockerTerminalThemes[dockerTerminalTheme] || dockerTerminalThemes.dark
|
||||
try {
|
||||
dockerTerminalSession.xterm.options.theme = palette
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
dockerTerminalSession.xterm.setOption?.('theme', palette)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
dockerTerminalSession.xterm.refresh?.(0, dockerTerminalSession.xterm.rows - 1)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
const themeSelect = document.getElementById('docker-terminal-theme-select')
|
||||
if (themeSelect) themeSelect.value = dockerTheme
|
||||
if (themeSelect && [...themeSelect.options].some((o) => o.value === key)) {
|
||||
themeSelect.value = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+80
-15
@@ -26,15 +26,35 @@ let activeContainerId = null
|
||||
let modalTerminalFontSize = 14
|
||||
let modalTerminalTheme = 'dark'
|
||||
|
||||
const terminalThemes = {
|
||||
dark: defaultXtermOptions().theme,
|
||||
light: {
|
||||
/** Shared light ANSI palette for UI light mode */
|
||||
const LIGHT_XTERM_THEME = {
|
||||
background: '#ffffff',
|
||||
foreground: '#0f172a',
|
||||
cursor: '#0f172a',
|
||||
selectionBackground: '#b3d4fc',
|
||||
cursor: '#0f766e',
|
||||
cursorAccent: '#ffffff',
|
||||
selectionBackground: 'rgba(15, 118, 110, 0.22)',
|
||||
selectionForeground: '#0f172a',
|
||||
},
|
||||
black: '#0f172a',
|
||||
red: '#b91c1c',
|
||||
green: '#15803d',
|
||||
yellow: '#b45309',
|
||||
blue: '#1d4ed8',
|
||||
magenta: '#6d28d9',
|
||||
cyan: '#0e7490',
|
||||
white: '#e2e8f0',
|
||||
brightBlack: '#64748b',
|
||||
brightRed: '#dc2626',
|
||||
brightGreen: '#16a34a',
|
||||
brightYellow: '#d97706',
|
||||
brightBlue: '#2563eb',
|
||||
brightMagenta: '#7c3aed',
|
||||
brightCyan: '#0891b2',
|
||||
brightWhite: '#0f172a',
|
||||
}
|
||||
|
||||
const terminalThemes = {
|
||||
dark: defaultXtermOptions().theme,
|
||||
light: LIGHT_XTERM_THEME,
|
||||
'solarized-dark': {
|
||||
background: '#002b36',
|
||||
foreground: '#839496',
|
||||
@@ -116,10 +136,12 @@ function startTerminal(containerId, containerName) {
|
||||
return
|
||||
}
|
||||
|
||||
const themeKey = resolveModalTerminalThemeKey(modalTerminalTheme)
|
||||
modalTerminalTheme = themeKey
|
||||
const xterm = new Terminal(
|
||||
defaultXtermOptions({
|
||||
fontSize: modalTerminalFontSize,
|
||||
theme: terminalThemes[modalTerminalTheme] || terminalThemes.dark,
|
||||
theme: terminalThemes[themeKey] || terminalThemes.light || terminalThemes.dark,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -269,11 +291,45 @@ function updateModalTerminalFontSizeDisplay() {
|
||||
if (display) display.textContent = String(modalTerminalFontSize)
|
||||
}
|
||||
|
||||
function resolveModalTerminalThemeKey(theme) {
|
||||
const uiLight = document.documentElement?.dataset?.theme === 'light'
|
||||
let key = theme
|
||||
if (!key || key === 'auto') {
|
||||
try {
|
||||
const pref = window.__peardockSettings?.terminalTheme
|
||||
if (pref && pref !== 'auto' && terminalThemes[pref]) key = pref
|
||||
else key = uiLight ? 'light' : 'dark'
|
||||
} catch {
|
||||
key = uiLight ? 'light' : 'dark'
|
||||
}
|
||||
}
|
||||
// Avoid light-on-white: UI light + dark palette → force light
|
||||
if (uiLight && (key === 'dark' || key === 'auto')) key = 'light'
|
||||
if (!terminalThemes[key]) key = uiLight ? 'light' : 'dark'
|
||||
return key
|
||||
}
|
||||
|
||||
function applyModalTerminalTheme(theme) {
|
||||
modalTerminalTheme = theme
|
||||
const t = terminalThemes[theme] || terminalThemes.dark
|
||||
const key = resolveModalTerminalThemeKey(theme)
|
||||
modalTerminalTheme = key
|
||||
const t = terminalThemes[key] || terminalThemes.dark
|
||||
Object.values(terminalSessions).forEach((session) => {
|
||||
if (session.xterm) session.xterm.options.theme = t
|
||||
if (!session.xterm) return
|
||||
try {
|
||||
session.xterm.options.theme = t
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
session.xterm.setOption?.('theme', t)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
session.xterm.refresh?.(0, session.xterm.rows - 1)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -310,9 +366,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (session?.xterm) session.xterm.clear()
|
||||
})
|
||||
const themeSelect = document.getElementById('modal-terminal-theme-select')
|
||||
// Prefer saved settings default
|
||||
// Prefer resolved settings default (auto → light/dark from UI)
|
||||
try {
|
||||
const pref = window.__peardockSettings?.terminalTheme
|
||||
const s = window.__peardockSettings || {}
|
||||
const ui = document.documentElement?.dataset?.theme || 'dark'
|
||||
let pref = s.terminalTheme
|
||||
if (pref === 'auto' || !pref) pref = ui === 'light' ? 'light' : 'dark'
|
||||
if (pref && terminalThemes[pref]) {
|
||||
modalTerminalTheme = pref
|
||||
}
|
||||
@@ -320,7 +379,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// ignore
|
||||
}
|
||||
if (themeSelect) {
|
||||
// Select may list auto; show concrete palette when applying
|
||||
if ([...themeSelect.options].some((o) => o.value === modalTerminalTheme)) {
|
||||
themeSelect.value = modalTerminalTheme
|
||||
}
|
||||
themeSelect.addEventListener('change', (e) => applyModalTerminalTheme(e.target.value))
|
||||
}
|
||||
updateModalTerminalFontSizeDisplay()
|
||||
@@ -328,10 +390,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__peardockApplyTerminalThemes = (termTheme, _dockerTheme) => {
|
||||
if (termTheme && terminalThemes[termTheme]) {
|
||||
applyModalTerminalTheme(termTheme)
|
||||
const key = resolveModalTerminalThemeKey(termTheme)
|
||||
if (key && terminalThemes[key]) {
|
||||
applyModalTerminalTheme(key)
|
||||
const themeSelect = document.getElementById('modal-terminal-theme-select')
|
||||
if (themeSelect) themeSelect.value = termTheme
|
||||
if (themeSelect && [...themeSelect.options].some((o) => o.value === key)) {
|
||||
themeSelect.value = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-6
@@ -102,17 +102,37 @@ export function closeAllModals() {
|
||||
* Activity / progress indicator — routes to the job/event tray (no full-screen spinner).
|
||||
* @param {string} message
|
||||
*/
|
||||
/**
|
||||
* Whether short status activities should open the Jobs tray.
|
||||
* Controlled by Settings → Jobs tray → Short status activities.
|
||||
*/
|
||||
function wantJobActivityInTray() {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.peardockOps?.getJobsTrayPrefs) {
|
||||
return window.peardockOps.getJobsTrayPrefs().jobActivityInTray !== false;
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.__peardockSettings) {
|
||||
return window.__peardockSettings.jobActivityInTray !== false;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function showStatusIndicator(message = 'Processing...') {
|
||||
removeLegacyStatusOverlay();
|
||||
|
||||
if (typeof window !== 'undefined' && window.peardockOps?.showActivity) {
|
||||
window.peardockOps.showActivity(message);
|
||||
if (wantJobActivityInTray() && typeof window !== 'undefined' && window.peardockOps?.showActivity) {
|
||||
const job = window.peardockOps.showActivity(message);
|
||||
if (job) {
|
||||
markFeedbackShown('info', message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
notificationManager.add('info', message, { autoDismiss: false, key: 'status-activity' });
|
||||
notificationManager.add('info', message, { autoDismiss: false, key: 'status-activity', badge: false });
|
||||
markFeedbackShown('info', message);
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -123,13 +143,15 @@ export function showStatusIndicator(message = 'Processing...') {
|
||||
* @param {string} message
|
||||
*/
|
||||
export function updateStatusIndicator(message) {
|
||||
if (typeof window !== 'undefined' && window.peardockOps?.updateActivity) {
|
||||
window.peardockOps.updateActivity(message);
|
||||
if (wantJobActivityInTray() && typeof window !== 'undefined' && window.peardockOps?.updateActivity) {
|
||||
const job = window.peardockOps.updateActivity(message);
|
||||
if (job) {
|
||||
markFeedbackShown('info', message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
notificationManager.add('info', message, { autoDismiss: false, key: 'status-activity' });
|
||||
notificationManager.add('info', message, { autoDismiss: false, key: 'status-activity', badge: false });
|
||||
markFeedbackShown('info', message);
|
||||
} catch {
|
||||
// ignore
|
||||
|
||||
@@ -38,6 +38,7 @@ const REQUIRED_IDS = [
|
||||
'settings-view',
|
||||
'settings-panel-peers',
|
||||
'settings-panel-backup',
|
||||
'settings-panel-jobs',
|
||||
'settings-backup-history',
|
||||
'settings-backup-download-btn',
|
||||
'connection-list',
|
||||
@@ -49,6 +50,10 @@ const REQUIRED_IDS = [
|
||||
'settings-accent',
|
||||
'settings-reduce-motion',
|
||||
'settings-reconnect-max',
|
||||
'settings-job-dismiss',
|
||||
'settings-job-error-dismiss',
|
||||
'settings-job-activity',
|
||||
'settings-reconnect-feedback',
|
||||
'settings-open-shortcuts',
|
||||
'fleet-view',
|
||||
'fleet-cards',
|
||||
@@ -60,6 +65,7 @@ const REQUIRED_SNIPPETS = [
|
||||
'data-view="registry"',
|
||||
'data-settings-tab="peers"',
|
||||
'data-settings-tab="backup"',
|
||||
'data-settings-tab="jobs"',
|
||||
'ENABLE_HOLESAIL',
|
||||
'GitOps',
|
||||
'collapse-sidebar-btn',
|
||||
@@ -71,6 +77,7 @@ const REQUIRED_SNIPPETS = [
|
||||
'Deploy the container',
|
||||
'Updates',
|
||||
'Check updates',
|
||||
'Reconnect feedback level',
|
||||
]
|
||||
|
||||
test('index.html retains critical view landmarks', (t) => {
|
||||
|
||||
+250
-18
@@ -80,15 +80,40 @@ export const DEFAULT_SETTINGS = {
|
||||
confirmDestructive: true,
|
||||
refreshSeconds: 10,
|
||||
sidebarCollapsed: false,
|
||||
/** Successful multi-step / activity job cards auto-hide after this many ms (0 = keep). */
|
||||
jobSuccessDismissMs: 3000,
|
||||
/**
|
||||
* Failed job cards auto-hide after this many ms.
|
||||
* 0 = keep until the next job or manual dismiss.
|
||||
*/
|
||||
jobErrorDismissMs: 0,
|
||||
/**
|
||||
* Lightweight status activities (prune, delete, short ops) use the bottom Jobs tray.
|
||||
* When false, those messages go only to the notification bell (no floating drawer).
|
||||
*/
|
||||
jobActivityInTray: true,
|
||||
/**
|
||||
* Whether completed job outcomes also write to the notification bell history.
|
||||
* Jobs tray remains the primary surface either way.
|
||||
*/
|
||||
jobBellOnComplete: false,
|
||||
badgeMode: 'alerts', // alerts | all | none
|
||||
forceToast: false,
|
||||
openTunnelBrowser: true,
|
||||
offerLocalTunnelAfterCreate: true,
|
||||
defaultTunnelSecure: true,
|
||||
defaultTunnelProtocol: 'tcp',
|
||||
terminalTheme: 'dark',
|
||||
dockerTerminalTheme: 'dark',
|
||||
/**
|
||||
* Container / modal exec terminal theme.
|
||||
* `auto` follows the resolved UI color mode (light/dark).
|
||||
* @type {'auto' | 'dark' | 'light' | 'monokai' | 'solarized-dark' | 'solarized-light'}
|
||||
*/
|
||||
terminalTheme: 'auto',
|
||||
/**
|
||||
* Docker CLI terminal theme (`auto` follows UI color mode).
|
||||
* @type {'auto' | 'dark' | 'light' | 'monokai' | 'solarized-dark' | 'solarized-light'}
|
||||
*/
|
||||
dockerTerminalTheme: 'auto',
|
||||
showFirstConnectTip: true,
|
||||
settingsTab: 'appearance',
|
||||
/**
|
||||
@@ -97,6 +122,20 @@ export const DEFAULT_SETTINGS = {
|
||||
* @type {number}
|
||||
*/
|
||||
reconnectMaxAttempts: 8,
|
||||
/**
|
||||
* How loud connection recovery is.
|
||||
* - silent: background only (Fleet / peer list); no job tray, no bell
|
||||
* - minimal: no job-tray “Reconnecting…” panel; bell on first loss + final failure only
|
||||
* - standard: job-tray activity + periodic reconnect notices (legacy)
|
||||
* @type {'silent' | 'minimal' | 'standard'}
|
||||
*/
|
||||
reconnectFeedback: 'minimal',
|
||||
/** Bell when a peer comes back online (ignored when reconnectFeedback is silent). */
|
||||
reconnectNotifySuccess: true,
|
||||
/** Bell when max reconnect tries are exhausted (ignored when silent). */
|
||||
reconnectNotifyFailure: true,
|
||||
/** Bell for each saved peer that fails during launch restore. */
|
||||
launchConnectNotify: true,
|
||||
// Track G — appearance / motion
|
||||
/**
|
||||
* UI color theme. Dark is default.
|
||||
@@ -115,6 +154,66 @@ export const DEFAULT_SETTINGS = {
|
||||
hiddenContainerLabels: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} raw
|
||||
* @returns {'silent' | 'minimal' | 'standard'}
|
||||
*/
|
||||
export function normalizeReconnectFeedback(raw) {
|
||||
const v = String(raw || '').toLowerCase()
|
||||
if (v === 'silent' || v === 'standard') return v
|
||||
return 'minimal'
|
||||
}
|
||||
|
||||
const TERMINAL_THEME_PREFS = new Set([
|
||||
'auto',
|
||||
'dark',
|
||||
'light',
|
||||
'monokai',
|
||||
'solarized-dark',
|
||||
'solarized-light',
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {unknown} raw
|
||||
* @returns {'auto' | 'dark' | 'light' | 'monokai' | 'solarized-dark' | 'solarized-light'}
|
||||
*/
|
||||
export function normalizeTerminalThemePref(raw) {
|
||||
const v = String(raw || '').toLowerCase()
|
||||
if (TERMINAL_THEME_PREFS.has(v)) return /** @type {any} */ (v)
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve terminal palette from preference + UI color mode.
|
||||
* @param {unknown} pref — auto | dark | light | monokai | …
|
||||
* @param {'dark' | 'light' | string} [uiResolved]
|
||||
* @returns {string} concrete theme key (never `auto`)
|
||||
*/
|
||||
export function resolveTerminalTheme(pref, uiResolved) {
|
||||
const p = normalizeTerminalThemePref(pref)
|
||||
if (p !== 'auto') return p
|
||||
const ui = uiResolved === 'light' || uiResolved === 'dark' ? uiResolved : resolveTheme(loadSettings().theme)
|
||||
return ui === 'light' ? 'light' : 'dark'
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot of Jobs tray + connection-feedback prefs (for app.js / uiUtils).
|
||||
* @param {object} [s]
|
||||
*/
|
||||
export function getJobsTrayPrefs(s) {
|
||||
const settings = s || loadSettings()
|
||||
return {
|
||||
jobSuccessDismissMs: Math.max(0, Number(settings.jobSuccessDismissMs) || 0),
|
||||
jobErrorDismissMs: Math.max(0, Number(settings.jobErrorDismissMs) || 0),
|
||||
jobActivityInTray: settings.jobActivityInTray !== false,
|
||||
jobBellOnComplete: settings.jobBellOnComplete === true,
|
||||
reconnectFeedback: normalizeReconnectFeedback(settings.reconnectFeedback),
|
||||
reconnectNotifySuccess: settings.reconnectNotifySuccess !== false,
|
||||
reconnectNotifyFailure: settings.reconnectNotifyFailure !== false,
|
||||
launchConnectNotify: settings.launchConnectNotify !== false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize hidden-container label filters from settings / form.
|
||||
* @param {unknown} raw
|
||||
@@ -227,6 +326,21 @@ export function applyColorTheme(preference) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Keep xterm palettes in sync when terminal prefs are `auto` (or UI just flipped)
|
||||
try {
|
||||
const s =
|
||||
typeof window !== 'undefined' && window.__peardockSettings
|
||||
? window.__peardockSettings
|
||||
: loadSettings()
|
||||
const term = resolveTerminalTheme(s.terminalTheme ?? 'auto', resolved)
|
||||
const docker = resolveTerminalTheme(s.dockerTerminalTheme ?? 'auto', resolved)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__peardockApplyTerminalThemes?.(term, docker)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
@@ -245,6 +359,35 @@ export function loadSettings() {
|
||||
raw.hiddenContainerLabels ?? DEFAULT_SETTINGS.hiddenContainerLabels
|
||||
)
|
||||
merged.theme = normalizeThemePreference(raw.theme ?? DEFAULT_SETTINGS.theme)
|
||||
merged.terminalTheme = normalizeTerminalThemePref(
|
||||
raw.terminalTheme ?? DEFAULT_SETTINGS.terminalTheme
|
||||
)
|
||||
merged.dockerTerminalTheme = normalizeTerminalThemePref(
|
||||
raw.dockerTerminalTheme ?? DEFAULT_SETTINGS.dockerTerminalTheme
|
||||
)
|
||||
merged.reconnectFeedback = normalizeReconnectFeedback(
|
||||
raw.reconnectFeedback ?? DEFAULT_SETTINGS.reconnectFeedback
|
||||
)
|
||||
// Coerce booleans that may be missing from older saves
|
||||
merged.jobActivityInTray = raw.jobActivityInTray !== false
|
||||
merged.jobBellOnComplete = raw.jobBellOnComplete === true
|
||||
merged.reconnectNotifySuccess =
|
||||
raw.reconnectNotifySuccess === undefined
|
||||
? DEFAULT_SETTINGS.reconnectNotifySuccess
|
||||
: raw.reconnectNotifySuccess !== false
|
||||
merged.reconnectNotifyFailure =
|
||||
raw.reconnectNotifyFailure === undefined
|
||||
? DEFAULT_SETTINGS.reconnectNotifyFailure
|
||||
: raw.reconnectNotifyFailure !== false
|
||||
merged.launchConnectNotify =
|
||||
raw.launchConnectNotify === undefined
|
||||
? DEFAULT_SETTINGS.launchConnectNotify
|
||||
: raw.launchConnectNotify !== false
|
||||
const errMs = Number(raw.jobErrorDismissMs)
|
||||
merged.jobErrorDismissMs =
|
||||
raw.jobErrorDismissMs === undefined || !Number.isFinite(errMs) || errMs < 0
|
||||
? DEFAULT_SETTINGS.jobErrorDismissMs
|
||||
: Math.floor(errMs)
|
||||
// Reconnect: 0 = unlimited; 1–8 finite (legacy missing key → default 8)
|
||||
if (raw.reconnectMaxAttempts === undefined || raw.reconnectMaxAttempts === null) {
|
||||
merged.reconnectMaxAttempts = DEFAULT_SETTINGS.reconnectMaxAttempts
|
||||
@@ -278,6 +421,15 @@ export function applySettings(s = loadSettings()) {
|
||||
applyColorTheme(s.theme || 'dark')
|
||||
startListAutoRefresh(s.refreshSeconds)
|
||||
|
||||
// Fast path for uiUtils / reconnect handlers (avoid re-parsing localStorage every event)
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__peardockSettings = { ...s, ...getJobsTrayPrefs(s) }
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Push reconnect policy into ConnectionManager (0 = unlimited, 1–8 finite)
|
||||
try {
|
||||
const max = Number(s.reconnectMaxAttempts)
|
||||
@@ -312,8 +464,16 @@ export function applySettings(s = loadSettings()) {
|
||||
// Terminal theme defaults (if terminals expose hooks)
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__peardockSettings = s
|
||||
window.__peardockApplyTerminalThemes?.(s.terminalTheme, s.dockerTerminalTheme)
|
||||
// Preserve jobs-tray prefs merge from earlier — do not clobber with bare `s`
|
||||
window.__peardockSettings = {
|
||||
...(window.__peardockSettings || {}),
|
||||
...s,
|
||||
...getJobsTrayPrefs(s),
|
||||
}
|
||||
const ui = resolveTheme(s.theme || 'dark')
|
||||
const term = resolveTerminalTheme(s.terminalTheme ?? 'auto', ui)
|
||||
const docker = resolveTerminalTheme(s.dockerTerminalTheme ?? 'auto', ui)
|
||||
window.__peardockApplyTerminalThemes?.(term, docker)
|
||||
// Let the main app re-filter container lists when hide-label rules change
|
||||
window.__peardockOnSettingsChanged?.(s)
|
||||
}
|
||||
@@ -386,14 +546,20 @@ let jobRemoveTimer = null
|
||||
const JOB_EXIT_ANIM_MS = 380
|
||||
|
||||
function jobSuccessDismissMs() {
|
||||
const n = Number(loadSettings().jobSuccessDismissMs)
|
||||
const n = Number(getJobsTrayPrefs().jobSuccessDismissMs)
|
||||
if (!Number.isFinite(n) || n < 0) return 3000
|
||||
return n
|
||||
}
|
||||
|
||||
function jobErrorDismissMs() {
|
||||
const n = Number(getJobsTrayPrefs().jobErrorDismissMs)
|
||||
if (!Number.isFinite(n) || n < 0) return 0
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* Show live job panel. On success, auto-hide after 3s with exit animation.
|
||||
* Errors stay until the next job or manual clear.
|
||||
* Show live job panel. On success, auto-hide after configured delay.
|
||||
* Errors auto-hide only when jobErrorDismissMs > 0.
|
||||
* @param {import('../client/jobs.js').Job|object} job
|
||||
*/
|
||||
export function showJob(job) {
|
||||
@@ -422,9 +588,8 @@ export function showJob(job) {
|
||||
const logEl = drawer.querySelector('.job-log')
|
||||
|
||||
if (job.status === 'success') {
|
||||
scheduleJobDrawerDismiss(drawer)
|
||||
scheduleJobDrawerDismiss(drawer, jobSuccessDismissMs())
|
||||
} else if (job.status === 'error') {
|
||||
// Keep error visible; ensure not mid-leave
|
||||
if (jobDismissTimer) {
|
||||
clearTimeout(jobDismissTimer)
|
||||
jobDismissTimer = null
|
||||
@@ -437,18 +602,20 @@ export function showJob(job) {
|
||||
logEl.scrollTop = logEl.scrollHeight
|
||||
})
|
||||
}
|
||||
const errMs = jobErrorDismissMs()
|
||||
if (errMs > 0) scheduleJobDrawerDismiss(drawer, errMs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} drawer
|
||||
* @param {number} ms
|
||||
*/
|
||||
function scheduleJobDrawerDismiss(drawer) {
|
||||
function scheduleJobDrawerDismiss(drawer, ms) {
|
||||
if (jobDismissTimer) clearTimeout(jobDismissTimer)
|
||||
if (jobRemoveTimer) clearTimeout(jobRemoveTimer)
|
||||
|
||||
const ms = jobSuccessDismissMs()
|
||||
// 0 = keep success tray until the next job
|
||||
// 0 = keep tray until the next job
|
||||
if (!ms || ms <= 0) return
|
||||
|
||||
jobDismissTimer = setTimeout(() => {
|
||||
@@ -503,6 +670,10 @@ function hybridPullOwnsTray() {
|
||||
export function showActivity(message) {
|
||||
// Never clobber the hybrid pull/push progress card with a minified "Pulling… %" activity job
|
||||
if (hybridPullOwnsTray()) return null
|
||||
if (!getJobsTrayPrefs().jobActivityInTray) {
|
||||
// Quiet mode: status lines stay in the bell only (caller may also use showAlert)
|
||||
return null
|
||||
}
|
||||
const job = jobsShowActivity(message)
|
||||
showJob(job)
|
||||
return job
|
||||
@@ -510,6 +681,7 @@ export function showActivity(message) {
|
||||
|
||||
export function updateActivity(message) {
|
||||
if (hybridPullOwnsTray()) return null
|
||||
if (!getJobsTrayPrefs().jobActivityInTray) return null
|
||||
const job = jobsUpdateActivity(message)
|
||||
if (job) showJob(job)
|
||||
return job
|
||||
@@ -1885,6 +2057,9 @@ export function readSettingsForm() {
|
||||
confirmDestructive: document.getElementById('settings-confirm')?.value !== '0',
|
||||
refreshSeconds: Number(document.getElementById('settings-refresh')?.value) || 0,
|
||||
jobSuccessDismissMs: Number(document.getElementById('settings-job-dismiss')?.value) || 0,
|
||||
jobErrorDismissMs: Number(document.getElementById('settings-job-error-dismiss')?.value) || 0,
|
||||
jobActivityInTray: document.getElementById('settings-job-activity')?.value !== '0',
|
||||
jobBellOnComplete: document.getElementById('settings-job-bell')?.value === '1',
|
||||
badgeMode: document.getElementById('settings-badge-mode')?.value || 'alerts',
|
||||
forceToast: document.getElementById('settings-force-toast')?.value === '1',
|
||||
openTunnelBrowser: document.getElementById('settings-tunnel-open-browser')?.value !== '0',
|
||||
@@ -1893,8 +2068,12 @@ export function readSettingsForm() {
|
||||
defaultTunnelSecure: document.getElementById('settings-tunnel-secure')?.value !== '0',
|
||||
defaultTunnelProtocol:
|
||||
document.getElementById('settings-tunnel-protocol')?.value === 'udp' ? 'udp' : 'tcp',
|
||||
terminalTheme: document.getElementById('settings-term-theme')?.value || 'dark',
|
||||
dockerTerminalTheme: document.getElementById('settings-docker-term-theme')?.value || 'dark',
|
||||
terminalTheme: normalizeTerminalThemePref(
|
||||
document.getElementById('settings-term-theme')?.value
|
||||
),
|
||||
dockerTerminalTheme: normalizeTerminalThemePref(
|
||||
document.getElementById('settings-docker-term-theme')?.value
|
||||
),
|
||||
showFirstConnectTip: document.getElementById('settings-first-connect')?.checked !== false,
|
||||
templateListUrls: getTemplateListUrls(),
|
||||
hiddenContainerLabels: normalizeHiddenContainerLabels(hiddenLabelFiltersDraft),
|
||||
@@ -1903,6 +2082,14 @@ export function readSettingsForm() {
|
||||
if (!Number.isFinite(raw) || raw <= 0) return 0
|
||||
return Math.min(8, Math.max(1, Math.floor(raw)))
|
||||
})(),
|
||||
reconnectFeedback: normalizeReconnectFeedback(
|
||||
document.getElementById('settings-reconnect-feedback')?.value
|
||||
),
|
||||
reconnectNotifySuccess:
|
||||
document.getElementById('settings-reconnect-notify-success')?.value !== '0',
|
||||
reconnectNotifyFailure:
|
||||
document.getElementById('settings-reconnect-notify-failure')?.value !== '0',
|
||||
launchConnectNotify: document.getElementById('settings-launch-connect-notify')?.value !== '0',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2166,20 +2353,42 @@ export function loadSettingsView(forceTab) {
|
||||
setSelectValue('settings-confirm', s.confirmDestructive === false ? '0' : '1')
|
||||
setSelectValue('settings-refresh', s.refreshSeconds ?? 10)
|
||||
setSelectValue('settings-job-dismiss', s.jobSuccessDismissMs ?? 3000)
|
||||
setSelectValue('settings-job-error-dismiss', s.jobErrorDismissMs ?? 0)
|
||||
setSelectValue('settings-job-activity', s.jobActivityInTray === false ? '0' : '1')
|
||||
setSelectValue('settings-job-bell', s.jobBellOnComplete ? '1' : '0')
|
||||
setSelectValue(
|
||||
'settings-reconnect-max',
|
||||
s.reconnectMaxAttempts === 0 || s.reconnectMaxAttempts == null
|
||||
? '0'
|
||||
: String(Math.min(8, Math.max(1, Number(s.reconnectMaxAttempts) || 8)))
|
||||
)
|
||||
setSelectValue(
|
||||
'settings-reconnect-feedback',
|
||||
normalizeReconnectFeedback(s.reconnectFeedback)
|
||||
)
|
||||
setSelectValue(
|
||||
'settings-reconnect-notify-success',
|
||||
s.reconnectNotifySuccess === false ? '0' : '1'
|
||||
)
|
||||
setSelectValue(
|
||||
'settings-reconnect-notify-failure',
|
||||
s.reconnectNotifyFailure === false ? '0' : '1'
|
||||
)
|
||||
setSelectValue(
|
||||
'settings-launch-connect-notify',
|
||||
s.launchConnectNotify === false ? '0' : '1'
|
||||
)
|
||||
setSelectValue('settings-badge-mode', s.badgeMode || 'alerts')
|
||||
setSelectValue('settings-force-toast', s.forceToast ? '1' : '0')
|
||||
setSelectValue('settings-tunnel-open-browser', s.openTunnelBrowser === false ? '0' : '1')
|
||||
setSelectValue('settings-tunnel-offer-local', s.offerLocalTunnelAfterCreate === false ? '0' : '1')
|
||||
setSelectValue('settings-tunnel-secure', s.defaultTunnelSecure === false ? '0' : '1')
|
||||
setSelectValue('settings-tunnel-protocol', s.defaultTunnelProtocol || 'tcp')
|
||||
setSelectValue('settings-term-theme', s.terminalTheme || 'dark')
|
||||
setSelectValue('settings-docker-term-theme', s.dockerTerminalTheme || 'dark')
|
||||
setSelectValue('settings-term-theme', normalizeTerminalThemePref(s.terminalTheme))
|
||||
setSelectValue(
|
||||
'settings-docker-term-theme',
|
||||
normalizeTerminalThemePref(s.dockerTerminalTheme)
|
||||
)
|
||||
setCheckbox('settings-first-connect', s.showFirstConnectTip !== false)
|
||||
|
||||
hiddenLabelFiltersDraft = normalizeHiddenContainerLabels(s.hiddenContainerLabels)
|
||||
@@ -2618,9 +2827,21 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
showSettingsTab(btn.getAttribute('data-settings-tab'))
|
||||
})
|
||||
|
||||
// Live theme preview (persists on Save all)
|
||||
// Live theme preview — full Save all still writes localStorage (persists across restarts)
|
||||
document.getElementById('settings-theme')?.addEventListener('change', (e) => {
|
||||
applyColorTheme(e.target?.value || 'dark')
|
||||
const pref = e.target?.value || 'dark'
|
||||
applyColorTheme(pref)
|
||||
// Keep in-memory snapshot in sync so terminal auto themes resolve correctly before save
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__peardockSettings = {
|
||||
...(window.__peardockSettings || loadSettings()),
|
||||
theme: normalizeThemePreference(pref),
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
document.getElementById('settings-accent')?.addEventListener('change', (e) => {
|
||||
const accent = String(e.target?.value || 'teal')
|
||||
@@ -2872,6 +3093,15 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
for (const s of job.steps || []) {
|
||||
if (s.error) markFeedbackShown('danger', s.error)
|
||||
}
|
||||
// Optional: also log completion into the notification bell
|
||||
if (getJobsTrayPrefs().jobBellOnComplete && typeof showAlert === 'function') {
|
||||
showAlert(type, msg, {
|
||||
toast: false,
|
||||
job: false,
|
||||
force: true,
|
||||
badge: type === 'danger',
|
||||
})
|
||||
}
|
||||
}
|
||||
// Keep activity history panel live if open
|
||||
const panel = document.getElementById('activity-panel')
|
||||
@@ -2985,6 +3215,8 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
applyColorTheme,
|
||||
resolveTheme,
|
||||
normalizeThemePreference,
|
||||
normalizeReconnectFeedback,
|
||||
getJobsTrayPrefs,
|
||||
shouldShowFirstConnectTip,
|
||||
showSettingsTab,
|
||||
DEFAULT_SETTINGS,
|
||||
|
||||
+5
-8
@@ -963,6 +963,7 @@
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Save control only — no full-width bar / gradient strip */
|
||||
#settings-view .settings-footer {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
@@ -970,14 +971,10 @@
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0;
|
||||
padding: 0.85rem 0 0.15rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(12, 16, 22, 0.55) 0%,
|
||||
rgba(12, 16, 22, 0.96) 40%,
|
||||
rgba(12, 16, 22, 1) 100%
|
||||
);
|
||||
padding: 0.65rem 0 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#settings-view .settings-footer .settings-save-btn {
|
||||
|
||||
@@ -1046,3 +1046,967 @@ html[data-theme='light'] #sidebar .sidebar-footer,
|
||||
html[data-theme='light'] #sidebar .peer-list-fade {
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(255, 255, 255, 0.95) 40%);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
Dashboard — modern.css hard-codes dark gradients on .dash-card / actions
|
||||
(light tokens alone do not reach those backgrounds)
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
html[data-theme='light'] #dashboard-view,
|
||||
html[data-theme='light'] #dashboard-view .dashboard-layout.dash {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-kicker {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-subtitle {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* KPI chips — force paper surface over dark gradient */
|
||||
html[data-theme='light'] .dash-kpi {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-kpi--link:hover,
|
||||
html[data-theme='light'] .dash-kpi:hover {
|
||||
border-color: rgba(15, 118, 110, 0.28) !important;
|
||||
box-shadow: var(--shadow-md), 0 0 0 1px rgba(15, 118, 110, 0.08) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-kpi--link:focus-visible {
|
||||
border-color: var(--accent-primary) !important;
|
||||
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.18), var(--shadow-md) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-kpi-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-kpi-value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-icon-btn {
|
||||
background: #ffffff !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-secondary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-icon-btn:hover {
|
||||
color: var(--accent-primary);
|
||||
border-color: rgba(15, 118, 110, 0.35) !important;
|
||||
background: rgba(15, 118, 110, 0.06) !important;
|
||||
}
|
||||
|
||||
/* Deploy chip keeps brand gradient; deepen shadow for light surfaces */
|
||||
html[data-theme='light'] .dash-chip-btn {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
box-shadow: 0 6px 18px rgba(15, 118, 110, 0.28);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-chip-btn:hover {
|
||||
box-shadow: 0 8px 22px rgba(15, 118, 110, 0.36);
|
||||
}
|
||||
|
||||
/* Engine / Host / Disk / Live events panels */
|
||||
html[data-theme='light'] .dash-card {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-card-head {
|
||||
border-bottom: 1px solid var(--border-color) !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-card-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-card-title i {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-card-body {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-empty {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-metric {
|
||||
border-bottom-color: rgba(15, 23, 42, 0.08) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-metric-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-metric-value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-disk-row {
|
||||
border-bottom-color: rgba(15, 23, 42, 0.08) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-disk-top span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-disk-top strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-disk-bar {
|
||||
background: rgba(15, 23, 42, 0.08) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] #dashboard-view .events-timeline .event-row {
|
||||
border-bottom-color: rgba(15, 23, 42, 0.08) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] #dashboard-view .events-timeline .event-row:hover {
|
||||
background: rgba(15, 23, 42, 0.04) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-skel-line {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(15, 23, 42, 0.05),
|
||||
rgba(15, 23, 42, 0.1),
|
||||
rgba(15, 23, 42, 0.05)
|
||||
) !important;
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
/* Quick actions dock */
|
||||
html[data-theme='light'] .dash-actions {
|
||||
background: rgba(255, 255, 255, 0.92) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: var(--shadow-sm);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-actions-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-actions-label i {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-action {
|
||||
background: #ffffff !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-action:hover {
|
||||
color: var(--text-primary) !important;
|
||||
border-color: rgba(15, 118, 110, 0.35) !important;
|
||||
background: rgba(15, 118, 110, 0.08) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-action--warn {
|
||||
border-color: rgba(180, 83, 9, 0.28) !important;
|
||||
color: #b45309 !important;
|
||||
background: #fffbeb !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-action--warn:hover {
|
||||
border-color: rgba(180, 83, 9, 0.45) !important;
|
||||
background: #fef3c7 !important;
|
||||
color: #92400e !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-action--danger {
|
||||
border-color: rgba(185, 28, 28, 0.28) !important;
|
||||
color: #b91c1c !important;
|
||||
background: #fef2f2 !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .dash-action--danger:hover {
|
||||
border-color: rgba(185, 28, 28, 0.45) !important;
|
||||
background: #fee2e2 !important;
|
||||
color: #991b1b !important;
|
||||
}
|
||||
|
||||
/* Swarm disabled nav tip — light paper surface */
|
||||
html[data-theme='light'] .pd-nav-tip {
|
||||
background:
|
||||
linear-gradient(165deg, rgba(15, 118, 110, 0.08) 0%, transparent 42%),
|
||||
#ffffff !important;
|
||||
border-color: rgba(15, 118, 110, 0.22) !important;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(15, 23, 42, 0.04),
|
||||
0 18px 48px rgba(15, 23, 42, 0.12),
|
||||
0 0 24px rgba(15, 118, 110, 0.06) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip-arrow {
|
||||
background: #ffffff !important;
|
||||
border-color: rgba(15, 118, 110, 0.22) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip-head {
|
||||
border-bottom-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip-head-text,
|
||||
html[data-theme='light'] .pd-nav-tip-body,
|
||||
html[data-theme='light'] .pd-nav-tip p,
|
||||
html[data-theme='light'] .pd-nav-tip li {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip-label {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip-steps {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip-code {
|
||||
background: #f1f5f9 !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: #0f766e !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip-note {
|
||||
border-top-color: var(--border-color) !important;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip-note code {
|
||||
background: rgba(99, 102, 241, 0.1) !important;
|
||||
color: #4338ca !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-nav-tip code,
|
||||
html[data-theme='light'] .pd-nav-tip pre,
|
||||
html[data-theme='light'] .pd-nav-tip kbd {
|
||||
background: #f1f5f9 !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* Content shell when #content uses elevated panels */
|
||||
html[data-theme='light'] #content {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Toolbar chips / filter groups on list pages */
|
||||
html[data-theme='light'] .btn-group .btn.bg-dark,
|
||||
html[data-theme='light'] .toolbar .btn.bg-dark {
|
||||
background: #ffffff !important;
|
||||
color: var(--text-secondary) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
/* Settings save control — no bar; button only */
|
||||
html[data-theme='light'] #settings-view .settings-footer {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] #settings-view .settings-footer .settings-save-btn {
|
||||
box-shadow: 0 6px 18px rgba(15, 118, 110, 0.22);
|
||||
}
|
||||
|
||||
html[data-theme='light'] #settings-view .settings-footer .settings-save-btn:hover,
|
||||
html[data-theme='light'] #settings-view .settings-footer .settings-save-btn:focus {
|
||||
box-shadow: 0 8px 22px rgba(15, 118, 110, 0.32);
|
||||
}
|
||||
|
||||
html[data-theme='light'] #settings-view .settings-section .form-text {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
/* ─── “Updated …” refresh stamp (top-right of list headers) ─── */
|
||||
html[data-theme='light'] .pd-refresh-stamp {
|
||||
color: var(--text-muted) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
background: rgba(255, 255, 255, 0.92) !important;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-refresh-stamp time {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pd-refresh-stamp i {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* ─── Terminal chrome (xterm palette is set in JS; host chrome follows UI) ─── */
|
||||
html[data-theme='light'] .terminal-toolbar,
|
||||
html[data-theme='light'] .modal-content > .terminal-toolbar,
|
||||
html[data-theme='light'] #terminal-modal .header {
|
||||
background: #f8fafc !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .terminal-toolbar .btn,
|
||||
html[data-theme='light'] .terminal-toolbar .form-select,
|
||||
html[data-theme='light'] .terminal-toolbar label,
|
||||
html[data-theme='light'] #terminal-modal .header .btn {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/*
|
||||
* Do NOT force pure white on the xterm host without matching JS theme —
|
||||
* that produced light-on-white (invisible) when the dropdown still said Dark.
|
||||
* Host stays neutral; xterm canvas owns the cell colors.
|
||||
*/
|
||||
html[data-theme='light'] .xterm-host,
|
||||
html[data-theme='light'] .terminal-xterm-container,
|
||||
html[data-theme='light'] #container-terminal-xterm,
|
||||
html[data-theme='light'] #terminal-container,
|
||||
html[data-theme='light'] #details-terminal,
|
||||
html[data-theme='light'] #docker-terminal {
|
||||
border-color: var(--border-color) !important;
|
||||
/* Match light xterm bg so letterboxing isn't a dark strip */
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .xterm .xterm-viewport {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .xterm .xterm-screen {
|
||||
/* ensure canvas sits on white while light palette paints glyphs */
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] #terminal-modal {
|
||||
background: #ffffff !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
Container details overview (IDENTITY / RUNTIME / NETWORK panels)
|
||||
Hard-coded rgba(0,0,0,0.18) sections read as muddy gray on light UI
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
html[data-theme='light'] #overview-pane .detail-section-card.detail-section-card--flat,
|
||||
html[data-theme='light'] #overview-pane .detail-overview-card {
|
||||
background: #ffffff !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-overview-hero {
|
||||
border-bottom-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-overview-name {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-overview-section {
|
||||
background: #f8fafc !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: none !important;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-overview-section--soft {
|
||||
background: #f1f5f9 !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-overview-section-title {
|
||||
border-bottom-color: var(--border-color) !important;
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-overview-section-title i {
|
||||
color: var(--accent-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-kv {
|
||||
border-bottom-color: rgba(15, 23, 42, 0.08) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-kv-label,
|
||||
html[data-theme='light'] .detail-kv .key {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-kv-value,
|
||||
html[data-theme='light'] .detail-kv .value {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-metric-chip,
|
||||
html[data-theme='light'] .detail-stat-card,
|
||||
html[data-theme='light'] #container-details-view .stat-card {
|
||||
background: #ffffff !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-metric-label,
|
||||
html[data-theme='light'] .detail-stat-label,
|
||||
html[data-theme='light'] #container-details-view .stat-card-label {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-metric-value,
|
||||
html[data-theme='light'] .detail-stat-value,
|
||||
html[data-theme='light'] #container-details-view .stat-card-value {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-pill {
|
||||
background: #f1f5f9 !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-overview-command {
|
||||
background: #f1f5f9 !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-command-line {
|
||||
color: var(--accent-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-overview-empty {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-chip,
|
||||
html[data-theme='light'] .detail-kv-value .detail-chip {
|
||||
background: #e2e8f0 !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .detail-timeline-meta,
|
||||
html[data-theme='light'] .detail-timeline {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ─── Jobs tray (full component polish) ─── */
|
||||
html[data-theme='light'] #job-drawer .job-panel,
|
||||
html[data-theme='light'] .job-panel {
|
||||
background: linear-gradient(165deg, #ffffff 0%, #f8fafc 100%) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel--running {
|
||||
border-color: rgba(15, 118, 110, 0.35) !important;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(15, 118, 110, 0.08),
|
||||
var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel--success {
|
||||
border-color: rgba(21, 128, 61, 0.3) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel--error {
|
||||
border-color: rgba(185, 28, 28, 0.3) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel-title strong,
|
||||
html[data-theme='light'] .job-panel-header > strong {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel-subtitle {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-steps-chip {
|
||||
color: var(--text-muted) !important;
|
||||
background: #f1f5f9 !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel--running .job-steps-chip {
|
||||
color: #0f766e !important;
|
||||
border-color: rgba(15, 118, 110, 0.3) !important;
|
||||
background: rgba(15, 118, 110, 0.08) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-kind-icon {
|
||||
background: rgba(15, 118, 110, 0.1) !important;
|
||||
border-color: rgba(15, 118, 110, 0.25) !important;
|
||||
color: var(--accent-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel--success .job-kind-icon {
|
||||
background: rgba(21, 128, 61, 0.1) !important;
|
||||
border-color: rgba(21, 128, 61, 0.3) !important;
|
||||
color: #15803d !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel--error .job-kind-icon {
|
||||
background: rgba(185, 28, 28, 0.1) !important;
|
||||
border-color: rgba(185, 28, 28, 0.3) !important;
|
||||
color: #b91c1c !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-step {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-step--active {
|
||||
background: rgba(15, 118, 110, 0.07) !important;
|
||||
box-shadow: inset 2px 0 0 var(--accent-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-step--success .job-step-label {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-step--error {
|
||||
background: rgba(185, 28, 28, 0.06) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-step-label {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-step-error {
|
||||
color: #b91c1c !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-step-detail {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log,
|
||||
html[data-theme='light'] .job-panel .job-log {
|
||||
background: #f8fafc !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-status-badge {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-spinner-ring {
|
||||
border-color: rgba(15, 118, 110, 0.18) !important;
|
||||
border-top-color: var(--accent-primary) !important;
|
||||
border-right-color: rgba(15, 118, 110, 0.45) !important;
|
||||
box-shadow: 0 0 8px rgba(15, 118, 110, 0.15) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-spinner::after {
|
||||
background: var(--accent-primary) !important;
|
||||
box-shadow: 0 0 6px rgba(15, 118, 110, 0.35) !important;
|
||||
}
|
||||
|
||||
/* ─── Hybrid pull/push progress card (was hard-coded dark slate) ─── */
|
||||
html[data-theme='light'] .pull-progress {
|
||||
background: #f1f5f9 !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress-phase {
|
||||
color: var(--accent-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress--extracting .pull-progress-phase {
|
||||
color: #0369a1 !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress--complete .pull-progress-phase {
|
||||
color: #15803d !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress--error .pull-progress-phase {
|
||||
color: #b91c1c !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress-meta {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress-pct {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress-bar {
|
||||
background: rgba(15, 23, 42, 0.1) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress-bar-fill {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(15, 118, 110, 0.75),
|
||||
var(--accent-primary, #0f766e)
|
||||
) !important;
|
||||
box-shadow: 0 0 8px rgba(15, 118, 110, 0.25) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress--extracting .pull-progress-bar-fill {
|
||||
background: linear-gradient(90deg, rgba(3, 105, 161, 0.7), #0284c7) !important;
|
||||
box-shadow: 0 0 8px rgba(3, 105, 161, 0.22) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress--complete .pull-progress-bar-fill {
|
||||
background: linear-gradient(90deg, rgba(21, 128, 61, 0.75), #16a34a) !important;
|
||||
box-shadow: 0 0 8px rgba(21, 128, 61, 0.2) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress--error .pull-progress-bar-fill {
|
||||
background: linear-gradient(90deg, rgba(185, 28, 28, 0.75), #dc2626) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-pip {
|
||||
background: rgba(15, 23, 42, 0.12) !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.06) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-pip--idle {
|
||||
background: rgba(15, 23, 42, 0.08) !important;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-pip--wait {
|
||||
background: rgba(100, 116, 139, 0.35) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-pip--download {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(15, 118, 110, 0.55),
|
||||
var(--accent-primary, #0f766e)
|
||||
) !important;
|
||||
box-shadow: 0 0 5px rgba(15, 118, 110, 0.28) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-pip--extract {
|
||||
background: linear-gradient(90deg, rgba(3, 105, 161, 0.55), #0284c7) !important;
|
||||
box-shadow: 0 0 5px rgba(3, 105, 161, 0.25) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-pip--done {
|
||||
background: linear-gradient(90deg, rgba(21, 128, 61, 0.65), #16a34a) !important;
|
||||
box-shadow: 0 0 4px rgba(21, 128, 61, 0.2) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-pip--error {
|
||||
background: linear-gradient(90deg, rgba(185, 28, 28, 0.7), #dc2626) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-id {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-bar {
|
||||
background: rgba(15, 23, 42, 0.1) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-bar-fill {
|
||||
background: rgba(15, 118, 110, 0.9) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer--waiting .pull-layer-bar-fill {
|
||||
background: rgba(100, 116, 139, 0.7) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer--extracting .pull-layer-bar-fill,
|
||||
html[data-theme='light'] .pull-layer--downloaded .pull-layer-bar-fill {
|
||||
background: rgba(2, 132, 199, 0.9) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer--error .pull-layer-bar-fill {
|
||||
background: rgba(220, 38, 38, 0.9) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer--waiting .pull-layer-bar--indet .pull-layer-bar-fill {
|
||||
background: rgba(100, 116, 139, 0.65) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-layer-meta {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress-waiting {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress-waiting--empty {
|
||||
color: transparent !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress-activity {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress--complete .pull-progress-activity {
|
||||
color: #15803d !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .pull-progress--error .pull-progress-activity {
|
||||
color: #b91c1c !important;
|
||||
}
|
||||
|
||||
/* Live log disclosure under the job tray */
|
||||
html[data-theme='light'] .job-log-details > summary,
|
||||
html[data-theme='light'] .job-log-summary {
|
||||
color: var(--text-secondary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
background: #f8fafc !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log-details > summary:hover,
|
||||
html[data-theme='light'] .job-log-summary:hover {
|
||||
color: var(--text-primary) !important;
|
||||
border-color: rgba(15, 118, 110, 0.35) !important;
|
||||
background: rgba(15, 118, 110, 0.06) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log-follow-hint {
|
||||
color: #0f766e !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log-follow-hint.is-paused {
|
||||
color: #b45309 !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log,
|
||||
html[data-theme='light'] .job-panel .job-log {
|
||||
background: #f8fafc !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log-line {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log-ts {
|
||||
color: var(--text-faint) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log--error {
|
||||
color: #b91c1c !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log--warn,
|
||||
html[data-theme='light'] .job-log--warning {
|
||||
color: #b45309 !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-log-empty {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
/* RUNNING badge on job header (Bootstrap badge.bg-* often stays dark-teal) */
|
||||
html[data-theme='light'] .job-panel .badge.bg-info,
|
||||
html[data-theme='light'] .job-status-badge.bg-info {
|
||||
background: rgba(3, 105, 161, 0.12) !important;
|
||||
color: #0369a1 !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel .badge.bg-success {
|
||||
background: rgba(21, 128, 61, 0.12) !important;
|
||||
color: #15803d !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel .badge.bg-danger {
|
||||
background: rgba(185, 28, 28, 0.12) !important;
|
||||
color: #b91c1c !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .job-panel .badge.bg-secondary {
|
||||
background: #e2e8f0 !important;
|
||||
color: #475569 !important;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
Restoring connections (boot) — modern.css uses a hard-coded dark card
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
html[data-theme='light'] #restoring-page {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-card {
|
||||
background:
|
||||
radial-gradient(520px 220px at 50% -10%, rgba(15, 118, 110, 0.12), transparent 58%),
|
||||
radial-gradient(420px 180px at 100% 100%, rgba(3, 105, 161, 0.06), transparent 50%),
|
||||
linear-gradient(168deg, #ffffff 0%, #f8fafc 48%, #f1f5f9 100%) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow:
|
||||
0 28px 64px rgba(15, 23, 42, 0.1),
|
||||
0 0 0 1px rgba(15, 118, 110, 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9) !important;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-card::before {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(15, 118, 110, 0.45) 35%,
|
||||
rgba(13, 148, 136, 0.3) 65%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-orb-glow {
|
||||
background: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
rgba(15, 118, 110, 0.18) 0%,
|
||||
transparent 68%
|
||||
) !important;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(15, 118, 110, 0.16),
|
||||
0 0 28px rgba(15, 118, 110, 0.14) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-orb::after {
|
||||
background: rgba(255, 255, 255, 0.92) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(15, 23, 42, 0.06) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-orb .restoring-spinner .job-spinner-ring,
|
||||
html[data-theme='light'] .restoring-orb .job-spinner--xl .job-spinner-ring {
|
||||
border-color: rgba(15, 118, 110, 0.18) !important;
|
||||
border-top-color: var(--accent-primary) !important;
|
||||
border-right-color: rgba(15, 118, 110, 0.5) !important;
|
||||
box-shadow: 0 0 12px rgba(15, 118, 110, 0.2) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-orb .job-spinner--xl::after,
|
||||
html[data-theme='light'] .restoring-orb .restoring-spinner::after {
|
||||
background: var(--accent-primary) !important;
|
||||
box-shadow: 0 0 8px rgba(15, 118, 110, 0.4) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-kicker {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-title,
|
||||
html[data-theme='light'] #restoring-page h1.restoring-title,
|
||||
html[data-theme='light'] #restoring-page h1 {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-lead {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-status-pill {
|
||||
background: #ffffff !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-status-dot {
|
||||
background: var(--accent-primary) !important;
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(15, 118, 110, 0.16),
|
||||
0 0 10px rgba(15, 118, 110, 0.35) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-status-text,
|
||||
html[data-theme='light'] #restoring-status {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-progress-track {
|
||||
background: rgba(15, 23, 42, 0.08) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-progress-bar {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--accent-secondary),
|
||||
var(--accent-primary)
|
||||
) !important;
|
||||
box-shadow: 0 0 10px rgba(15, 118, 110, 0.35) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-progress-meta {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-progress-meta #restoring-progress-count {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .restoring-progress-hint {
|
||||
color: var(--text-faint) !important;
|
||||
}
|
||||
|
||||
/* Match reduced-motion pulse colors for light accents */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
html[data-theme='light'] .restoring-status-dot {
|
||||
animation: restoring-dot-pulse-light 1.4s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes restoring-dot-pulse-light {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(15, 118, 110, 0.16),
|
||||
0 0 10px rgba(15, 118, 110, 0.35);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
box-shadow:
|
||||
0 0 0 5px rgba(15, 118, 110, 0.1),
|
||||
0 0 14px rgba(15, 118, 110, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
/* Welcome empty-state card (same boot family) */
|
||||
html[data-theme='light'] #welcome-page,
|
||||
html[data-theme='light'] .welcome-card {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme='light'] #welcome-page h1 {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user