Skip to content

Commit f1e49aa

Browse files
committed
Add border/background support to widgets
Enable configurable background and border styling for several widgets and wire these into rendering/export and the properties UI. Changes include: - Update datetime, quote_rss, and weather_icon plugins to support bg_color, border_width, border_color and border_radius (CSS render + export drawing using filled_rectangle/rectangle loops). - Add default border/bg props for quote_rss and weather_icon and update datetime defaults. - Enhance PropertiesPanel to expose Border Style controls for multiple widget types and sync border_radius changes to matching shadow widgets (convert/update shadow to rounded_rect when needed). - Update dist/index.html to reference the new main asset bundle. These changes let users style widget backgrounds, borders and corner radius consistently and ensure exported display code draws the same visuals.
1 parent 777acde commit f1e49aa

File tree

5 files changed

+131
-5
lines changed

5 files changed

+131
-5
lines changed

custom_components/esphome_designer/frontend/dist/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
2727
<link rel="icon" href="./assets/favicon-BFR8sXii.png" type="image/x-icon">
28-
<script type="module" crossorigin src="./assets/main-403oi-Ur.js"></script>
28+
<script type="module" crossorigin src="./assets/main-BbQox7np.js"></script>
2929
<link rel="stylesheet" crossorigin href="./assets/main-1OkGAQ6K.css">
3030
</head>
3131

custom_components/esphome_designer/frontend/features/datetime/plugin.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ export default {
119119
font_family: "Roboto",
120120
text_align: "CENTER",
121121
width: 120,
122-
height: 50
122+
height: 50,
123+
bg_color: "transparent",
124+
border_width: 0,
125+
border_color: "theme_auto",
126+
border_radius: 0
123127
},
124128
render,
125129
exportOpenDisplay: (w, { layout, page }) => {
@@ -299,6 +303,15 @@ export default {
299303
lines.push(` it.filled_rectangle(${w.x}, ${w.y}, ${w.width}, ${w.height}, ${bgColorConst});`);
300304
}
301305

306+
// Draw Border if defined
307+
const borderWidth = p.border_width || 0;
308+
if (borderWidth > 0) {
309+
const borderColor = getColorConst(p.border_color || "theme_auto");
310+
for (let i = 0; i < borderWidth; i++) {
311+
lines.push(` it.rectangle(${w.x} + ${i}, ${w.y} + ${i}, ${w.width} - 2 * ${i}, ${w.height} - 2 * ${i}, ${borderColor});`);
312+
}
313+
}
314+
302315
const cond = getConditionCheck(w);
303316
if (cond) lines.push(` ${cond}`);
304317

custom_components/esphome_designer/frontend/features/quote_rss/plugin.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,18 @@
33
*/
44

55
const render = (element, widget, helpers) => {
6-
const props = widget.props || {};
76
const { getColorStyle } = helpers;
7+
const props = widget.props || {};
8+
9+
// Apply Border & Background
10+
if (props.border_width) {
11+
const bColor = getColorStyle(props.border_color || "black");
12+
element.style.border = `${props.border_width}px solid ${bColor}`;
13+
element.style.borderRadius = `${props.border_radius || 0}px`;
14+
}
15+
if (props.bg_color) {
16+
element.style.backgroundColor = getColorStyle(props.bg_color);
17+
}
818

919
const showAuthor = props.show_author !== false;
1020
const quoteFontSize = parseInt(props.quote_font_size || 18, 10);
@@ -233,6 +243,22 @@ const exportDoc = (w, context) => {
233243

234244
lines.push(` // widget:quote_rss id:${w.id} type:quote_rss x:${w.x} y:${w.y} w:${w.width} h:${w.height} color:${colorProp} align:${textAlign} ${getCondProps(w)}`);
235245

246+
// Background fill
247+
const bgColorProp = p.bg_color || p.background_color || "transparent";
248+
if (bgColorProp && bgColorProp !== "transparent") {
249+
const bgColorConst = getColorConst(bgColorProp);
250+
lines.push(` it.filled_rectangle(${w.x}, ${w.y}, ${w.width}, ${w.height}, ${bgColorConst});`);
251+
}
252+
253+
// Draw Border if defined
254+
const borderWidth = p.border_width || 0;
255+
if (borderWidth > 0) {
256+
const borderColor = getColorConst(p.border_color || "theme_auto");
257+
for (let i = 0; i < borderWidth; i++) {
258+
lines.push(` it.rectangle(${w.x} + ${i}, ${w.y} + ${i}, ${w.width} - 2 * ${i}, ${w.height} - 2 * ${i}, ${borderColor});`);
259+
}
260+
}
261+
236262
const cond = getConditionCheck(w);
237263
if (cond) lines.push(` ${cond}`);
238264

@@ -468,7 +494,11 @@ export default {
468494
height: 120,
469495
refresh_interval: "1h",
470496
ha_url: "http://homeassistant.local:8123",
471-
random: true
497+
random: true,
498+
bg_color: "transparent",
499+
border_width: 0,
500+
border_color: "theme_auto",
501+
border_radius: 0
472502
},
473503
render,
474504
onExportGlobals,

custom_components/esphome_designer/frontend/features/weather_icon/plugin.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44

55
const render = (el, widget, { getColorStyle }) => {
66
const props = widget.props || {};
7+
8+
// Apply Border & Background
9+
if (props.border_width) {
10+
const borderColor = getColorStyle(props.border_color || "black");
11+
el.style.border = `${props.border_width}px solid ${borderColor}`;
12+
el.style.borderRadius = `${props.border_radius || 0}px`;
13+
el.style.boxSizing = "border-box";
14+
}
15+
if (props.bg_color) {
16+
el.style.backgroundColor = getColorStyle(props.bg_color);
17+
}
718
let iconCode = "F0595"; // Default
819
let size = props.size || 24;
920
const color = props.color || "theme_auto";
@@ -104,6 +115,15 @@ const exportDoc = (w, context) => {
104115
lines.push(` it.filled_rectangle(${w.x}, ${w.y}, ${w.width}, ${w.height}, ${bgColorConst});`);
105116
}
106117

118+
// Draw Border if defined
119+
const borderWidth = p.border_width || 0;
120+
if (borderWidth > 0) {
121+
const borderColor = getColorConst(p.border_color || "theme_auto");
122+
for (let i = 0; i < borderWidth; i++) {
123+
lines.push(` it.rectangle(${w.x} + ${i}, ${w.y} + ${i}, ${w.width} - 2 * ${i}, ${w.height} - 2 * ${i}, ${borderColor});`);
124+
}
125+
}
126+
107127
const cond = getConditionCheck(w);
108128
if (cond) lines.push(` ${cond}`);
109129

@@ -226,8 +246,12 @@ export default {
226246
size: 48,
227247
color: "theme_auto",
228248
background_color: "transparent",
249+
background_color: "transparent",
229250
weather_entity: "weather.forecast_home",
230-
fit_icon_to_frame: true
251+
fit_icon_to_frame: true,
252+
border_width: 0,
253+
border_color: "theme_auto",
254+
border_radius: 0
231255
},
232256
render,
233257
exportOpenDisplay: (w, { layout, page }) => {

custom_components/esphome_designer/frontend/js/core/properties.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,27 @@ export class PropertiesPanel {
959959
const updateProp = (key, value) => {
960960
const newProps = { ...widget.props, [key]: value };
961961
AppState.updateWidget(widget.id, { props: newProps });
962+
963+
// Sync border radius to shadow if it exists (User Request: mirror corner radius)
964+
if (key === "border_radius" || key === "radius" || key === "corner_radius") {
965+
const page = AppState.getCurrentPage();
966+
if (page && page.widgets) {
967+
const r = parseInt(value, 10) || 0;
968+
const shadowName = (widget.props?.name || widget.type) + " Shadow";
969+
const shadow = page.widgets.find(w =>
970+
(w.props && w.props.name === shadowName) ||
971+
(w.x === (widget.x || 0) + 5 && w.y === (widget.y || 0) + 5 && w.width === widget.width && w.height === widget.height)
972+
);
973+
974+
if (shadow) {
975+
if (shadow.type === "shape_rect" && r > 0) {
976+
AppState.updateWidget(shadow.id, { type: "rounded_rect", props: { ...shadow.props, radius: r } });
977+
} else if (shadow.type === "rounded_rect") {
978+
AppState.updateWidget(shadow.id, { props: { ...shadow.props, radius: r } });
979+
}
980+
}
981+
}
982+
}
962983
};
963984

964985
if (type === "shape_rect" || type === "shape_circle") {
@@ -1262,8 +1283,15 @@ export class PropertiesPanel {
12621283
];
12631284
this.addSelect("Align", props.text_align || "CENTER", alignOptions, (v) => updateProp("text_align", v));
12641285
this.addColorSelector("Background", props.bg_color || "transparent", colors, (v) => updateProp("bg_color", v));
1286+
1287+
this.createSection("Border Style", false);
1288+
this.addLabeledInput("Border Width", "number", props.border_width || 0, (v) => updateProp("border_width", parseInt(v, 10)));
1289+
this.addColorSelector("Border Color", props.border_color || "theme_auto", colors, (v) => updateProp("border_color", v));
1290+
this.addLabeledInput("Corner Radius", "number", props.border_radius || 0, (v) => updateProp("border_radius", parseInt(v, 10)));
12651291
this.addDropShadowButton(this.getContainer(), widget.id);
12661292
this.endSection();
1293+
1294+
this.endSection();
12671295
}
12681296
else if (type === "lvgl_label") {
12691297
this.createSection("Content", true);
@@ -1448,6 +1476,30 @@ export class PropertiesPanel {
14481476
this.addColorSelector("Color", props.color || "black", colors, (v) => updateProp("color", v));
14491477
this.endSection();
14501478
}
1479+
else if (type === "quote_rss") {
1480+
this.createSection("Data Source", true);
1481+
this.addLabeledInput("Feed URL", "text", props.feed_url || "https://www.brainyquote.com/link/quotebr.rss", (v) => updateProp("feed_url", v));
1482+
this.addLabeledInput("Refresh", "text", props.refresh_interval || "1h", (v) => updateProp("refresh_interval", v));
1483+
this.addCheckbox("Randomize", props.random !== false, (v) => updateProp("random", v));
1484+
this.endSection();
1485+
1486+
this.createSection("Appearance", true);
1487+
this.addCheckbox("Show Author", props.show_author !== false, (v) => updateProp("show_author", v));
1488+
this.addCheckbox("Italic Quote", props.italic_quote !== false, (v) => updateProp("italic_quote", v));
1489+
this.addLabeledInput("Quote Font Size", "number", props.quote_font_size || 18, (v) => updateProp("quote_font_size", parseInt(v, 10)));
1490+
this.addLabeledInput("Author Font Size", "number", props.author_font_size || 14, (v) => updateProp("author_font_size", parseInt(v, 10)));
1491+
this.addColorSelector("Color", props.color || "black", colors, (v) => updateProp("color", v));
1492+
this.addColorSelector("Background", props.bg_color || "transparent", colors, (v) => updateProp("bg_color", v));
1493+
1494+
this.createSection("Border Style", false);
1495+
this.addLabeledInput("Border Width", "number", props.border_width || 0, (v) => updateProp("border_width", parseInt(v, 10)));
1496+
this.addColorSelector("Border Color", props.border_color || "theme_auto", colors, (v) => updateProp("border_color", v));
1497+
this.addLabeledInput("Corner Radius", "number", props.border_radius || 0, (v) => updateProp("border_radius", parseInt(v, 10)));
1498+
this.addDropShadowButton(this.getContainer(), widget.id);
1499+
this.endSection();
1500+
1501+
this.endSection();
1502+
}
14511503
else if (type === "ondevice_temperature") {
14521504
this.createSection("Data Source", true);
14531505
this.addLabeledInputWithPicker("Temperature Entity ID", "text", widget.entity_id || "", (v) => {
@@ -1564,8 +1616,15 @@ export class PropertiesPanel {
15641616
});
15651617
this.addColorSelector("Color", props.color || "black", colors, (v) => updateProp("color", v));
15661618
this.addColorSelector("Background", props.bg_color || "transparent", colors, (v) => updateProp("bg_color", v));
1619+
1620+
this.createSection("Border Style", false);
1621+
this.addLabeledInput("Border Width", "number", props.border_width || 0, (v) => updateProp("border_width", parseInt(v, 10)));
1622+
this.addColorSelector("Border Color", props.border_color || "theme_auto", colors, (v) => updateProp("border_color", v));
1623+
this.addLabeledInput("Corner Radius", "number", props.border_radius || 0, (v) => updateProp("border_radius", parseInt(v, 10)));
15671624
this.addDropShadowButton(this.getContainer(), widget.id);
15681625
this.endSection();
1626+
1627+
this.endSection();
15691628
}
15701629
else if (type === "weather_forecast") {
15711630
this.createSection("Data Source", true);

0 commit comments

Comments
 (0)