feat: wire Display tab — font size, hover delay, grid columns with immediate apply

This commit is contained in:
Brian Krabach
2026-03-29 23:28:12 -07:00
parent bb21ce7aa9
commit fd4e91bc51
3 changed files with 328 additions and 2 deletions
+57 -2
View File
@@ -937,6 +937,51 @@ function saveDisplaySettings(settings) {
} catch (_) { /* blocked — ok */ } } catch (_) { /* blocked — ok */ }
} }
/**
* Apply display settings to the live DOM.
* Sets --preview-font-size CSS custom property and updates #session-grid
* grid-template-columns based on the gridColumns setting.
* @param {object} ds - display settings object
*/
function applyDisplaySettings(ds) {
// Apply font size as CSS custom property
document.documentElement.style.setProperty('--preview-font-size', ds.fontSize + 'px');
// Apply grid columns
var grid = document.getElementById('session-grid');
if (grid) {
if (ds.gridColumns === 'auto') {
grid.style.removeProperty('grid-template-columns');
} else {
grid.style.gridTemplateColumns = 'repeat(' + ds.gridColumns + ', 1fr)';
}
}
}
/**
* Handle a change event on any Display settings control.
* Reads current values from form elements, saves via saveDisplaySettings,
* and applies via applyDisplaySettings immediately.
*/
function onDisplaySettingChange() {
var ds = loadDisplaySettings();
var fontSizeEl = document.getElementById('setting-font-size');
if (fontSizeEl) ds.fontSize = parseInt(fontSizeEl.value, 10) || ds.fontSize;
var hoverDelayEl = document.getElementById('setting-hover-delay');
if (hoverDelayEl) ds.hoverPreviewDelay = parseInt(hoverDelayEl.value, 10);
var gridColumnsEl = document.getElementById('setting-grid-columns');
if (gridColumnsEl) {
var raw = gridColumnsEl.value;
ds.gridColumns = raw === 'auto' ? 'auto' : parseInt(raw, 10);
}
saveDisplaySettings(ds);
applyDisplaySettings(ds);
}
/** /**
* Open the settings dialog. * Open the settings dialog.
* Sets _settingsOpen, calls dialog.showModal(), removes hidden from backdrop, * Sets _settingsOpen, calls dialog.showModal(), removes hidden from backdrop,
@@ -1126,7 +1171,8 @@ function bindStaticEventListeners() {
if (!tile) return; if (!tile) return;
if (_previewTimer) { clearTimeout(_previewTimer); _previewTimer = null; } if (_previewTimer) { clearTimeout(_previewTimer); _previewTimer = null; }
var name = tile.dataset.session; var name = tile.dataset.session;
_previewTimer = setTimeout(function () { showPreview(name); }, 1500); var delay = loadDisplaySettings().hoverPreviewDelay;
if (delay > 0) _previewTimer = setTimeout(function () { showPreview(name); }, delay);
}, true); // useCapture: true for delegation with mouseenter }, true); // useCapture: true for delegation with mouseenter
gridEl.addEventListener('mouseleave', function (e) { gridEl.addEventListener('mouseleave', function (e) {
@@ -1144,7 +1190,8 @@ function bindStaticEventListeners() {
if (!item) return; if (!item) return;
if (_previewTimer) { clearTimeout(_previewTimer); _previewTimer = null; } if (_previewTimer) { clearTimeout(_previewTimer); _previewTimer = null; }
var name = item.dataset.session; var name = item.dataset.session;
_previewTimer = setTimeout(function () { showPreview(name); }, 1500); var delay = loadDisplaySettings().hoverPreviewDelay;
if (delay > 0) _previewTimer = setTimeout(function () { showPreview(name); }, delay);
}, true); }, true);
sidebarListEl.addEventListener('mouseleave', function (e) { sidebarListEl.addEventListener('mouseleave', function (e) {
@@ -1153,6 +1200,11 @@ function bindStaticEventListeners() {
hidePreview(); hidePreview();
}, true); }, true);
} }
// Display settings — bind change events for immediate apply
on($('setting-font-size'), 'change', onDisplaySettingChange);
on($('setting-hover-delay'), 'change', onDisplaySettingChange);
on($('setting-grid-columns'), 'change', onDisplaySettingChange);
} }
// ─── Test-only helpers ──────────────────────────────────────────────────────── // ─── Test-only helpers ────────────────────────────────────────────────────────
@@ -1169,6 +1221,7 @@ function _setViewMode(mode) {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initDeviceId(); initDeviceId();
applyDisplaySettings(loadDisplaySettings());
document.addEventListener('keydown', trackInteraction); document.addEventListener('keydown', trackInteraction);
document.addEventListener('click', trackInteraction); document.addEventListener('click', trackInteraction);
document.addEventListener('touchstart', trackInteraction); document.addEventListener('touchstart', trackInteraction);
@@ -1233,6 +1286,8 @@ if (typeof module !== 'undefined' && module.exports) {
// Settings // Settings
loadDisplaySettings, loadDisplaySettings,
saveDisplaySettings, saveDisplaySettings,
applyDisplaySettings,
onDisplaySettingChange,
openSettings, openSettings,
closeSettings, closeSettings,
switchSettingsTab, switchSettingsTab,
+271
View File
@@ -845,3 +845,274 @@ def test_exports_switch_settings_tab() -> None:
assert "switchSettingsTab" in exports, ( assert "switchSettingsTab" in exports, (
"module.exports must export switchSettingsTab" "module.exports must export switchSettingsTab"
) )
# ── Display tab wiring (task-8) ────────────────────────────────────────────────
def test_apply_display_settings_function_exists() -> None:
"""applyDisplaySettings function must exist."""
assert "function applyDisplaySettings" in _JS, (
"applyDisplaySettings must be defined in app.js"
)
def test_apply_display_settings_sets_font_size_css_property() -> None:
"""applyDisplaySettings must set --preview-font-size CSS custom property."""
match = re.search(
r"function applyDisplaySettings\s*\(\w+\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
re.DOTALL,
)
assert match, "applyDisplaySettings function not found"
body = match.group(1)
assert "--preview-font-size" in body, (
"applyDisplaySettings must set --preview-font-size CSS property"
)
assert "documentElement" in body, (
"applyDisplaySettings must call setProperty on document.documentElement"
)
def test_apply_display_settings_sets_grid_columns_repeat() -> None:
"""applyDisplaySettings must set repeat(N, 1fr) for numeric grid columns."""
match = re.search(
r"function applyDisplaySettings\s*\(\w+\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
re.DOTALL,
)
assert match, "applyDisplaySettings function not found"
body = match.group(1)
assert "repeat" in body and "1fr" in body, (
"applyDisplaySettings must set repeat(N, 1fr) for numeric grid columns"
)
def test_apply_display_settings_handles_auto_grid_columns() -> None:
"""applyDisplaySettings must handle 'auto' gridColumns by removing inline style."""
match = re.search(
r"function applyDisplaySettings\s*\(\w+\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
re.DOTALL,
)
assert match, "applyDisplaySettings function not found"
body = match.group(1)
assert "auto" in body, (
"applyDisplaySettings must handle 'auto' gridColumns"
)
assert "session-grid" in body or "gridTemplateColumns" in body, (
"applyDisplaySettings must update session-grid or gridTemplateColumns"
)
def test_on_display_setting_change_function_exists() -> None:
"""onDisplaySettingChange function must exist."""
assert "function onDisplaySettingChange" in _JS, (
"onDisplaySettingChange must be defined in app.js"
)
def test_on_display_setting_change_reads_font_size() -> None:
"""onDisplaySettingChange must read from setting-font-size element."""
match = re.search(
r"function onDisplaySettingChange\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
re.DOTALL,
)
assert match, "onDisplaySettingChange function not found"
body = match.group(1)
assert "setting-font-size" in body, (
"onDisplaySettingChange must read from setting-font-size"
)
def test_on_display_setting_change_reads_hover_delay() -> None:
"""onDisplaySettingChange must read from setting-hover-delay element."""
match = re.search(
r"function onDisplaySettingChange\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
re.DOTALL,
)
assert match, "onDisplaySettingChange function not found"
body = match.group(1)
assert "setting-hover-delay" in body, (
"onDisplaySettingChange must read from setting-hover-delay"
)
def test_on_display_setting_change_reads_grid_columns() -> None:
"""onDisplaySettingChange must read from setting-grid-columns element."""
match = re.search(
r"function onDisplaySettingChange\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
re.DOTALL,
)
assert match, "onDisplaySettingChange function not found"
body = match.group(1)
assert "setting-grid-columns" in body, (
"onDisplaySettingChange must read from setting-grid-columns"
)
def test_on_display_setting_change_calls_save_display_settings() -> None:
"""onDisplaySettingChange must call saveDisplaySettings."""
match = re.search(
r"function onDisplaySettingChange\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
re.DOTALL,
)
assert match, "onDisplaySettingChange function not found"
body = match.group(1)
assert "saveDisplaySettings" in body, (
"onDisplaySettingChange must call saveDisplaySettings"
)
def test_on_display_setting_change_calls_apply_display_settings() -> None:
"""onDisplaySettingChange must call applyDisplaySettings."""
match = re.search(
r"function onDisplaySettingChange\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
_JS,
re.DOTALL,
)
assert match, "onDisplaySettingChange function not found"
body = match.group(1)
assert "applyDisplaySettings" in body, (
"onDisplaySettingChange must call applyDisplaySettings"
)
def test_hover_preview_no_hardcoded_1500() -> None:
"""Hover preview delays must not be hardcoded 1500ms — must use loadDisplaySettings."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
# Count occurrences of hardcoded 1500 in the body
hardcoded_count = body.count(", 1500)")
assert hardcoded_count == 0, (
f"Hover preview must not use hardcoded 1500ms delay (found {hardcoded_count} occurrences). "
"Use loadDisplaySettings().hoverPreviewDelay instead."
)
def test_hover_preview_reads_delay_from_settings() -> None:
"""Hover preview must read delay from loadDisplaySettings().hoverPreviewDelay."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
assert "loadDisplaySettings" in body and "hoverPreviewDelay" in body, (
"bindStaticEventListeners hover preview must read delay from loadDisplaySettings().hoverPreviewDelay"
)
def test_hover_preview_skips_when_delay_zero() -> None:
"""Hover preview must check delay > 0 before setting setTimeout."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
# The body must have a conditional guard on the delay value
assert "delay > 0" in body or "> 0" in body, (
"bindStaticEventListeners hover preview must guard: if (delay > 0) before setTimeout"
)
def test_bind_static_event_listeners_binds_font_size_change() -> None:
"""bindStaticEventListeners must bind change event on setting-font-size."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
assert "setting-font-size" in body, (
"bindStaticEventListeners must reference setting-font-size for change binding"
)
assert "onDisplaySettingChange" in body, (
"bindStaticEventListeners must call onDisplaySettingChange on change"
)
def test_bind_static_event_listeners_binds_hover_delay_change() -> None:
"""bindStaticEventListeners must bind change event on setting-hover-delay."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
assert "setting-hover-delay" in body, (
"bindStaticEventListeners must reference setting-hover-delay for change binding"
)
def test_bind_static_event_listeners_binds_grid_columns_change() -> None:
"""bindStaticEventListeners must bind change event on setting-grid-columns."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
assert "setting-grid-columns" in body, (
"bindStaticEventListeners must reference setting-grid-columns for change binding"
)
def test_dom_content_loaded_calls_apply_display_settings() -> None:
"""DOMContentLoaded handler must call applyDisplaySettings(loadDisplaySettings())."""
# Find the DOMContentLoaded handler block
match = re.search(
r"DOMContentLoaded.*?\{(.*?)(?=\}\);?\s*\n// |\}\);\s*$)",
_JS,
re.DOTALL,
)
assert match, "DOMContentLoaded handler not found"
body = match.group(1)
assert "applyDisplaySettings" in body, (
"DOMContentLoaded handler must call applyDisplaySettings(loadDisplaySettings())"
)
assert "loadDisplaySettings" in body, (
"DOMContentLoaded handler must call applyDisplaySettings(loadDisplaySettings())"
)
def test_exports_apply_display_settings() -> None:
"""module.exports must export applyDisplaySettings."""
match = re.search(
r"module\.exports\s*=\s*\{(.*?)\};",
_JS,
re.DOTALL,
)
assert match, "module.exports block not found"
exports = match.group(1)
assert "applyDisplaySettings" in exports, (
"module.exports must export applyDisplaySettings"
)
def test_exports_on_display_setting_change() -> None:
"""module.exports must export onDisplaySettingChange."""
match = re.search(
r"module\.exports\s*=\s*\{(.*?)\};",
_JS,
re.DOTALL,
)
assert match, "module.exports block not found"
exports = match.group(1)
assert "onDisplaySettingChange" in exports, (
"module.exports must export onDisplaySettingChange"
)