Add per-widget border option (style, thickness, palette color)
Build and push server image / test (push) Has been cancelled
Build and push server image / build-and-push (push) Has been cancelled
Build and push server image / deploy (push) Has been cancelled

A Widget-level property (border_style/border_thickness/border_color_index),
not a per-type config field, since every widget type can have one -- drawn
once centrally in device.py's _render_widgets before compositing, using
an exact panel palette color so it never dithers. Styles: solid, dashed,
dotted, and a fancy double-line picture-frame-mat look. Configurable from
a shared "Border" card in every widget's gear-icon dialog.
This commit is contained in:
2026-07-27 19:51:04 +00:00
parent eb7127718b
commit b15747a604
30 changed files with 503 additions and 19 deletions
+1
View File
@@ -364,6 +364,7 @@ input:focus, select:focus {
.calendar-user-name { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); }
.calendar-row { flex-wrap: wrap; }
.calendar-color-picker { display: inline-flex; align-items: center; gap: 5px; margin-left: 8px; }
.border-color-picker { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 4px; }
.color-swatch {
width: 20px; height: 20px; padding: 0; margin: 0;
border: 2px solid var(--border); border-radius: 5px;
@@ -30,6 +30,7 @@ function initBatteryDialog() {
document.getElementById('battery-preview-refresh').addEventListener('click', loadBatteryPreview);
loadBatteryPreview();
initBorderFields();
}
function closeBatteryDialog() {
+48
View File
@@ -0,0 +1,48 @@
// Shared "Border" card (models.Widget.border_style/border_thickness/
// border_color_index, _widget_border_fields.html) -- present on every
// widget type's dialog regardless of widget_type, so this is one shared
// init function each widget_dialog_<type>.js's init<Type>Dialog() calls,
// rather than 8 copies of the same slider/swatch/save wiring. Not a
// page-load script by itself -- frame_layout.js loads it unconditionally
// (like every other widget_dialog_*.js) since which dialog is open, and
// therefore which init<Type>Dialog() calls initBorderFields(), varies.
function initBorderFields() {
const styleSelect = document.getElementById('border_style');
if (!styleSelect) return; // dialog fragment didn't render the border card -- shouldn't happen
const thickness = document.getElementById('border_thickness');
const thicknessValue = document.getElementById('border_thickness_value');
thickness.addEventListener('input', (e) => {
thicknessValue.textContent = `${e.target.value}px`;
});
const colorIndexInput = document.getElementById('border_color_index');
document.querySelectorAll('#border-color-picker .color-swatch').forEach((btn) => {
btn.addEventListener('click', () => {
document.querySelectorAll('#border-color-picker .color-swatch').forEach((el) => el.classList.remove('selected'));
btn.classList.add('selected');
colorIndexInput.value = btn.dataset.index;
});
});
document.getElementById('border-config-form').addEventListener('submit', async (e) => {
e.preventDefault();
const body = JSON.stringify({
border_style: styleSelect.value,
border_thickness: Number(thickness.value),
border_color_index: Number(colorIndexInput.value),
});
try {
const resp = await fetch(`${window.FRAME_API}/border`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Border saved.');
} catch (err) {
showStatus(false, err.message);
}
});
}
@@ -197,6 +197,7 @@ function initCalendarDialog() {
document.getElementById('calendar-preview-refresh').addEventListener('click', loadCalendarPreview);
loadCalendarPreview();
initBorderFields();
}
function closeCalendarDialog() {
@@ -113,6 +113,7 @@ function initPhotosDialog() {
// Slow poll: picks up real changes (new photo displayed, queue edited
// from elsewhere) without a manual refresh. Skipped mid-drag.
photosPollTimer = setInterval(loadQueue, 10000);
initBorderFields();
}
function closePhotosDialog() {
@@ -55,6 +55,7 @@ function initStaticDialog() {
document.getElementById('static-preview-refresh').addEventListener('click', loadStaticPreview);
loadStaticPreview();
initBorderFields();
}
function closeStaticDialog() {
+1
View File
@@ -88,6 +88,7 @@ function initTasksDialog() {
document.getElementById('tasks-preview-refresh').addEventListener('click', loadTasksPreview);
loadTasksPreview();
initBorderFields();
}
function closeTasksDialog() {
+1
View File
@@ -130,6 +130,7 @@ function initTextDialog() {
document.getElementById('text-preview-refresh').addEventListener('click', loadTextPreview);
loadTextPreview();
initBorderFields();
}
function closeTextDialog() {
@@ -151,6 +151,7 @@ function initWeatherDialog() {
document.getElementById('weather-preview-refresh').addEventListener('click', () => loadWeatherPreview(true));
loadWeatherPreview(false);
initBorderFields();
}
function closeWeatherDialog() {
@@ -96,6 +96,7 @@ function initWhiteboardDialog() {
// whiteboard's `force` param).
document.getElementById('whiteboard-preview-refresh').addEventListener('click', () => loadWhiteboardPreview(true));
loadWhiteboardPreview(false);
initBorderFields();
// --- file picker (Browse...) ---
const browseToggle = document.getElementById('whiteboard-browse-toggle');