Merge pull request #95: Prevent model status from overlapping the footer

This commit is contained in:
D-Ogi committed 2026-09-16 18:10:32 +02:00
commit 08504163a4
3 files changed
+84 -22

No files matched your search

+58 -13
View File
@@ -24,11 +24,23 @@ failures = []
def check_sidebar_layout(window):
"""Measure actual rendered controls and tooltip bounds across all themes/languages."""
window.resize(800, 600)
def check_theme_layout(window, width, height):
"""Measure controls, tooltips and status-bar bounds across themes and languages."""
window.resize(width, height)
window.evaluate_js("window.appInstance.modelsOpen = true")
deadline = time.monotonic() + 10
while not window.evaluate_js("document.querySelector('.model-dialog').getBoundingClientRect().height > 0"):
assert time.monotonic() < deadline, "Model dialog did not become visible"
time.sleep(.05)
window.evaluate_js("""window.sidebarProbe = null; (async () => {
const app = window.appInstance, rows = [];
// Measure final theme styles, not an intermediate animated color.
const motion = document.createElement('style');
motion.textContent = '*, *::before, *::after { transition: none !important; animation: none !important; }';
document.head.appendChild(motion);
const originalGpu = app.systemInfo.gpu, originalStatus = app.models.status;
app.systemInfo.gpu = 'NVIDIA GeForce RTX 4090 Laptop GPU';
app.models.status = 'downloading';
for (const language of app.availableLanguages) {
app.t = await loadLanguage(language.id);
await Alpine.nextTick();
@@ -70,17 +82,48 @@ def check_sidebar_layout(window):
return (Math.max(a, b) + .05) / (Math.min(a, b) + .05);
};
const background = getComputedStyle(panel).backgroundColor;
const textContrast = [...panel.querySelectorAll('h2, p')]
.map(node => contrast(getComputedStyle(node).color, background));
const buttonContrast = [...panel.querySelectorAll('button')]
// Hidden error/progress messages are not rendered; WebKit may
// retain their previous computed colors until they become visible.
const visible = node => node.getBoundingClientRect().height > 0
&& node.getClientRects().length && node.textContent.trim();
const textStyles = [...panel.querySelectorAll('h2, p')].filter(visible)
.map(node => ({text: node.textContent, color: getComputedStyle(node).color}));
const textContrast = textStyles.map(style => contrast(style.color, background));
const buttonContrast = [...panel.querySelectorAll('button')].filter(visible)
.map(node => { const style = getComputedStyle(node);
return contrast(style.color, style.backgroundColor); });
rows.push({theme: theme.id, language: language.id,
const gpu = document.querySelector('[x-text^="systemInfo.gpu"]');
const footer = gpu.parentElement;
const button = document.querySelector('#models-button');
const frame = document.querySelector('main').getBoundingClientRect();
const contains = (outer, inner) => inner.left >= outer.left - 1
&& inner.right <= outer.right + 1 && inner.top >= outer.top - 1
&& inner.bottom <= outer.bottom + 1;
const statusBounds = footer.getBoundingClientRect();
const buttonBounds = button.getBoundingClientRect();
const gpuBounds = gpu.getBoundingClientRect();
const overlap = Math.min(buttonBounds.right, gpuBounds.right)
- Math.max(buttonBounds.left, gpuBounds.left) > 1
&& Math.min(buttonBounds.bottom, gpuBounds.bottom)
- Math.max(buttonBounds.top, gpuBounds.top) > 1;
const statusVisible = contains(frame, statusBounds)
&& contains(statusBounds, buttonBounds) && contains(statusBounds, gpuBounds)
&& !overlap && footer.scrollWidth <= footer.clientWidth + 1
&& gpu.scrollWidth <= gpu.clientWidth + 1
&& button.scrollWidth <= button.clientWidth + 1;
rows.push({theme: theme.id, language: language.id, statusVisible,
statusGeometry: {frame: frame.toJSON(), footer: statusBounds.toJSON(),
button: buttonBounds.toJSON(), gpu: gpuBounds.toJSON(), overlap,
scroll: footer.scrollWidth, client: footer.clientWidth},
contrast: Math.min(...textContrast, ...buttonContrast),
modelColors: {background, textStyles, isOpen: app.modelsOpen, display: getComputedStyle(panel.parentElement).display},
client: aside.clientWidth, scroll: aside.scrollWidth,
outside, hintsInside: hints.every(Boolean)});
}
}
app.systemInfo.gpu = originalGpu;
app.models.status = originalStatus;
motion.remove();
return rows;
})().then(rows => { window.sidebarProbe = {rows}; })
.catch(error => { window.sidebarProbe = {error: String(error)}; });""")
@@ -97,8 +140,9 @@ def check_sidebar_layout(window):
for row in results:
assert row['scroll'] <= row['client'] + 1, row
assert not row['outside'] and row['hintsInside'], row
assert row['contrast'] >= 4.5, row
print('THEME PASS', len(results), 'theme/language combinations; minimum model-panel contrast',
assert row['contrast'] is not None and row['contrast'] >= 4.5, row
assert row['statusVisible'], row
print('THEME PASS', f'{width}x{height}', len(results), 'theme/language combinations; minimum model-panel contrast',
round(min(row['contrast'] for row in results), 2), flush=True)
@@ -139,7 +183,8 @@ def exercise(window, api):
break
time.sleep(.1)
assert window.evaluate_js("window.savedProbe?.lang") == 'en'
check_sidebar_layout(window)
check_theme_layout(window, 800, 600)
check_theme_layout(window, 1000, 800)
print('DESKTOP PASS', json.dumps(state), flush=True)
except BaseException as exc:
failures.append(repr(exc))
@@ -158,9 +203,9 @@ def watchdog():
threading.Thread(target=watchdog, daemon=True).start()
# WebKit can defer network page loading for an entirely hidden native window.
# Exercise its normal visible startup on the macOS CI desktop.
remwmgui.main(exercise, hidden=sys.platform != "darwin")
# Native engines can defer viewport sizing for an entirely hidden window.
# Measure the real visible desktop layout (Linux CI supplies Xvfb).
remwmgui.main(exercise)
finished.set()
check_config.cleanup()
if failures:
+7 -9
View File
@@ -277,7 +277,7 @@
</aside>
<!-- RIGHT CONTENT (Preview & Logs) -->
<div class="flex-1 min-w-[400px] flex flex-col relative bg-black/40 backdrop-blur-sm overflow-hidden">
<div class="flex-1 min-w-0 flex flex-col relative bg-black/40 backdrop-blur-sm overflow-hidden">
<!-- MARQUEE BAR -->
<div class="bg-neon-pink text-black font-bold text-xs py-1 overflow-hidden whitespace-nowrap border-b border-neon-pink">
@@ -341,8 +341,8 @@
</div>
<!-- STATUS BAR -->
<div class="bg-black/60 backdrop-blur-md border-t border-white/10 p-2 px-4 flex justify-between items-center text-[10px] font-mono text-gray-500 uppercase">
<div class="flex gap-4">
<div class="status-bar bg-black/60 backdrop-blur-md border-t border-white/10 p-2 px-4 text-[10px] font-mono text-gray-500 uppercase">
<div class="status-details">
<span class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full" :class="systemInfo.cuda ? 'bg-neon-green' : 'bg-red-500'"></span>
<span x-text="t.status_bar?.cuda || 'CUDA'"></span>: <span x-text="systemInfo.cuda ? (t.status_bar?.on || 'ON') : (t.status_bar?.off || 'OFF')"></span>
@@ -353,7 +353,10 @@
</span>
<span><span x-text="t.status_bar?.ram || 'RAM'"></span>: <span x-text="systemInfo.ram + '%'"></span></span>
</div>
<div x-text="systemInfo.gpu || (t.status_bar?.cpu_mode || 'CPU Mode')"></div>
<div class="status-device" x-text="systemInfo.gpu || (t.status_bar?.cpu_mode || 'CPU Mode')"></div>
<button id="models-button" @click="modelsOpen = true" class="glass-panel px-3 py-2 text-xs text-neon-cyan rounded">
Models: <span x-text="models.status"></span>
</button>
</div>
<!-- PROGRESS OVERLAY (Visible when processing) -->
@@ -466,11 +469,6 @@
</div>
</main>
<div class="fixed bottom-3 right-3 z-50">
<button id="models-button" @click="modelsOpen = true" class="glass-panel px-3 py-2 text-xs text-neon-cyan rounded">
Models: <span x-text="models.status"></span>
</button>
</div>
<div x-show="bridgeError" class="fixed top-2 left-2 right-2 z-50 bg-card border border-error p-3 text-sm" x-text="bridgeError"></div>
<div x-show="modelsOpen" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-6">
<section class="glass-panel model-dialog w-full max-w-xl p-6 rounded-xl" role="dialog" aria-label="AI models">
+19
View File
@@ -1094,3 +1094,22 @@ body .glass-panel.model-dialog h2 { color: var(--model-accent) !important; }
color: var(--model-ink) !important;
border: 1px solid var(--model-accent) !important;
}
/* Keep model readiness inside the status bar, including inset window themes. */
.status-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
flex-shrink: 0;
}
.status-details {
display: flex;
flex: 1 1 100%;
flex-wrap: wrap;
gap: 4px 16px;
}
.status-details > span { max-width: 100%; flex-wrap: wrap; }
.status-device { flex: 1 1 160px; min-width: 0; overflow-wrap: anywhere; }
#models-button { flex: 0 0 auto; max-width: 100%; margin-left: auto; }