Add per-widget border option (style, thickness, palette color)
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:
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -88,6 +88,7 @@ function initTasksDialog() {
|
||||
|
||||
document.getElementById('tasks-preview-refresh').addEventListener('click', loadTasksPreview);
|
||||
loadTasksPreview();
|
||||
initBorderFields();
|
||||
}
|
||||
|
||||
function closeTasksDialog() {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user