Dashboard Studio 1.6.0: app icon, clock weather, icon-only tab rail

- App store icon (icon.png) and favicon, drawn by tools/make_icon.py
- Clock can show a weather entity with today's high and low from
  weather.get_forecasts
- Side menu gets an icon-only rail style (now the default): full height,
  centered icons, active marker, names as hover/focus tooltips; phones
  keep a labelled bottom tab bar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 22:31:00 +01:00
parent 9e7e7beaa0
commit 4187c8f38d
12 changed files with 177 additions and 15 deletions
+17 -5
View File
@@ -97,6 +97,15 @@
});
return res?.response || {};
}
/** Today's and the coming days' forecast, via weather.get_forecasts with return_response. */
async forecast(entityId, type = "daily") {
await this.ready;
const res = await this.send({
type: "call_service", domain: "weather", service: "get_forecasts",
target: { entity_id: entityId }, service_data: { type }, return_response: true,
});
return res?.response?.[entityId]?.forecast || [];
}
async history(entityIds, hours) {
await this.ready;
const res = await this.send({
@@ -134,7 +143,7 @@
const DEFAULT_KIOSK = { saver: 0, exitCorner: "top-right", hideCursor: true };
const newTab = (name, icon = "home") => ({ id: uid(), name, icon, widgets: [] });
const newDashboard = (name) => ({ id: uid(), name, theme: { ...DEFAULT_THEME }, grid: { ...DEFAULT_GRID }, kiosk: { ...DEFAULT_KIOSK }, nav: { show: false }, tabs: [newTab(tr("Overview"))] });
const newDashboard = (name) => ({ id: uid(), name, theme: { ...DEFAULT_THEME }, grid: { ...DEFAULT_GRID }, kiosk: { ...DEFAULT_KIOSK }, nav: { show: false, style: "icons" }, tabs: [newTab(tr("Overview"))] });
// Bring dashboards saved by older versions up to the current shape.
function migrate(d) {
@@ -147,7 +156,7 @@
d.theme = { ...DEFAULT_THEME, ...t };
d.grid = { ...DEFAULT_GRID, ...(d.grid || {}) };
d.kiosk = { ...DEFAULT_KIOSK, ...(d.kiosk || {}) };
d.nav = { show: false, ...(d.nav || {}) };
d.nav = { show: false, style: "icons", ...(d.nav || {}) };
if (!Array.isArray(d.tabs) || !d.tabs.length) d.tabs = [{ ...newTab(tr("Overview")), widgets: d.widgets || [] }];
delete d.widgets;
return d;
@@ -169,8 +178,8 @@
const numeric = (e) => !isNaN(Number(ha.states[e].state));
put("heading", 0, 0, 8, 1, { text: tr("Home"), subtitle: tr("Everything at a glance") });
put("clock", 8, 0, 4, 3, {});
const weather = first("weather")[0];
put("clock", 8, 0, 4, 3, { weather: weather || "" });
if (weather) put("weather", 0, 1, 4, 2, { entity: weather });
const lights = first("light", 3);
if (lights[0]) put("light", 4, 1, 4, 2, { entity: lights[0] });
@@ -394,9 +403,11 @@
navEl.hidden = !show;
if (!show) return;
const active = tab().id;
navEl.innerHTML = current.tabs.map((t) => `<button class="tabnav-item" data-tab="${t.id}" ${t.id === active ? 'aria-current="page"' : ""}>
// "icons": a slim rail; names appear as a tooltip on hover or keyboard focus.
navEl.classList.toggle("icons", current.nav.style !== "labels");
navEl.innerHTML = current.tabs.map((t) => `<button class="tabnav-item" data-tab="${t.id}" aria-label="${esc(t.name)}" ${t.id === active ? 'aria-current="page"' : ""}>
<span class="tabnav-ico">${Icons.has(t.icon) ? Icons.svg(t.icon) : t.icon ? `<span class="glyph">${esc(t.icon)}</span>` : Icons.svg("tile")}</span><span class="tabnav-label">${esc(t.name)}</span></button>`).join("") +
(editMode ? `<button class="tabnav-item add" data-tabop="add">${Icons.svg("plus")}<span class="tabnav-label">${tr("Add tab")}</span></button>` : "");
(editMode ? `<button class="tabnav-item add" data-tabop="add" aria-label="${tr("Add tab")}">${Icons.svg("plus")}<span class="tabnav-label">${tr("Add tab")}</span></button>` : "");
}
function setTab(id) {
@@ -626,6 +637,7 @@
<datalist id="dl-icons">${Icons.names.map((n) => `<option value="${n}">`).join("")}</datalist>
<div class="section"><div class="section-title">${tr("Tabs")}</div>
${fieldHTML({ k: "show", t: "check", l: "Show tabs in a side menu" }, current.nav.show, "nav")}
${fieldHTML({ k: "style", t: "seg", l: "Menu style", o: ["icons", "labels"] }, current.nav.style || "icons", "nav")}
<div class="tab-list">${current.tabs.map((t, i) => `<div class="tab-item">
<span class="icon-prev" aria-hidden="true">${Icons.has(t.icon) ? Icons.svg(t.icon) : esc(t.icon || "")}</span>
<input class="input" type="text" data-path="tab" data-tab="${t.id}" data-k="name" value="${esc(t.name)}" aria-label="${tr("Tab name")}">