From 4187c8f38d2bcc68012ea9c35bc9479f25c62eba Mon Sep 17 00:00:00 2001 From: veenm Date: Sun, 25 Oct 2026 22:31:00 +0100 Subject: [PATCH] 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 --- README.md | 4 ++ dashboard_studio/DOCS.md | 7 ++- dashboard_studio/config.yaml | 2 +- dashboard_studio/icon.png | Bin 0 -> 21298 bytes dashboard_studio/server/mock_ha.py | 7 +++ dashboard_studio/web/app.js | 22 +++++++-- dashboard_studio/web/favicon.png | Bin 0 -> 9524 bytes dashboard_studio/web/i18n.js | 3 ++ dashboard_studio/web/index.html | 1 + dashboard_studio/web/style.css | 49 ++++++++++++++++--- dashboard_studio/web/widgets.js | 23 ++++++++- tools/make_icon.py | 74 +++++++++++++++++++++++++++++ 12 files changed, 177 insertions(+), 15 deletions(-) create mode 100644 dashboard_studio/icon.png create mode 100644 dashboard_studio/web/favicon.png create mode 100644 tools/make_icon.py diff --git a/README.md b/README.md index d530075..e526f7f 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,7 @@ pip install aiohttp && python3 dashboard_studio/server/main.py ``` Then open http://localhost:8099. The Photos widget also has fake Immich albums in this mode. To try your real Immich server, set `IMMICH_URL` and `IMMICH_API_KEY` before starting the server. + +## App icon + +`dashboard_studio/icon.png` is drawn by `tools/make_icon.py` (needs Pillow): `python3 tools/make_icon.py dashboard_studio/icon.png`. diff --git a/dashboard_studio/DOCS.md b/dashboard_studio/DOCS.md index 1f2803f..41a6d43 100644 --- a/dashboard_studio/DOCS.md +++ b/dashboard_studio/DOCS.md @@ -45,6 +45,10 @@ This card works with the [Afvalwijzer](https://github.com/xirixiz/homeassistant- Bins use the integration's own colors (GFT green, Papier blue, PMD maroon, and so on), and each bin is always named, so the color is never the only clue. Types with no pickup planned are hidden. You can limit the card to a number of pickups, or to pickups within a number of days. +### Clock with weather + +Pick a weather entity in the clock's settings to show the current conditions and temperature under the time. Today's high and low come from Home Assistant's forecast, refreshed every half hour. Tap the weather to see all its details. + ### Media players One card can control several players. With more than one, a row of buttons at the top switches between them, and a small animated equalizer marks the ones that are playing. The card starts on whatever is playing. It shows the album art, title and artist, with previous, play/pause and next buttons and a volume slider. A player that is off gets a **Turn on** button instead. You can hide players that are off from the switcher. @@ -92,8 +96,9 @@ A dashboard can have several tabs, each with its own widgets, for example *Overv - **Add tab** creates a tab and turns on the menu. - Each tab has a name and an icon (an icon name or an emoji). Use the arrows to reorder tabs and the bin to delete one; ⌘Z undoes this. - **Show tabs in a side menu** turns the menu on or off. +- **Menu style:** *Icons only* (the default) shows a slim rail along the left edge, with the icons centered and a marker on the current tab. Hovering over an icon, or reaching it with the keyboard, shows the tab's name. *With names* shows the names next to the icons. -The menu sits on the left on computers, becomes a narrow rail on tablets and while editing, and turns into a tab bar at the bottom on phones. To move a widget to another tab, select it and change **Tab** in the inspector. The menu also shows in kiosk mode. +On phones the menu turns into a tab bar at the bottom, which always shows the names, since a phone has no hover. To move a widget to another tab, select it and change **Tab** in the inspector. The menu also shows in kiosk mode. ## Kiosk mode diff --git a/dashboard_studio/config.yaml b/dashboard_studio/config.yaml index b7859fb..8ea05ab 100644 --- a/dashboard_studio/config.yaml +++ b/dashboard_studio/config.yaml @@ -1,5 +1,5 @@ name: Dashboard Studio -version: "1.5.0" +version: "1.6.0" slug: dashboard_studio description: Drag-and-drop dashboard builder with a free-form grid, live entity widgets and themes. url: https://git.melvanveen.nl/veenm/ha-dashboard-studio diff --git a/dashboard_studio/icon.png b/dashboard_studio/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f72b4699efced15d4c5048ceafb2f887c71553cf GIT binary patch literal 21298 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 literal 0 HcmV?d00001 diff --git a/dashboard_studio/server/mock_ha.py b/dashboard_studio/server/mock_ha.py index b672d70..0b761ef 100644 --- a/dashboard_studio/server/mock_ha.py +++ b/dashboard_studio/server/mock_ha.py @@ -255,6 +255,13 @@ class MockHA: entry = (ws, frozenset(subs)) self.clients.add(entry) await ws.send_json({"id": mid, "type": "result", "success": True, "result": None}) + elif typ == "call_service" and req.get("domain") == "weather" and req.get("service") == "get_forecasts": + ids = (req.get("target") or {}).get("entity_id") or [] + ids = [ids] if isinstance(ids, str) else ids + day = datetime.now().astimezone().replace(hour=12, minute=0, second=0, microsecond=0) + forecast = [{"datetime": (day + timedelta(days=i)).isoformat(), "condition": c, "temperature": hi, "templow": lo} + for i, (c, hi, lo) in enumerate([("partlycloudy", 15, 8), ("rainy", 12, 7), ("sunny", 16, 6)])] + await ws.send_json({"id": mid, "type": "result", "success": True, "result": {"context": {"id": "mock"}, "response": {e: {"forecast": forecast} for e in ids}}}) elif typ == "call_service" and req.get("domain") == "calendar" and req.get("service") == "get_events": ids = (req.get("target") or {}).get("entity_id") or [] ids = [ids] if isinstance(ids, str) else ids diff --git a/dashboard_studio/web/app.js b/dashboard_studio/web/app.js index 8038306..c2fa62c 100644 --- a/dashboard_studio/web/app.js +++ b/dashboard_studio/web/app.js @@ -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) => ``).join("") + - (editMode ? `` : ""); + (editMode ? `` : ""); } function setTab(id) { @@ -626,6 +637,7 @@ ${Icons.names.map((n) => `
${tr("Tabs")}
${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")}
${current.tabs.map((t, i) => `
diff --git a/dashboard_studio/web/favicon.png b/dashboard_studio/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..91ff11cf974edb83608e0f9dcc2e004b82fd090d GIT binary patch literal 9524 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 Dashboard Studio + diff --git a/dashboard_studio/web/style.css b/dashboard_studio/web/style.css index 58e3048..8701c62 100644 --- a/dashboard_studio/web/style.css +++ b/dashboard_studio/web/style.css @@ -352,6 +352,14 @@ body.editing .w:hover .w-resize, body.editing .w.selected .w-resize { opacity: 1 .clock { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; text-align: center; } .clock .time { font-size: clamp(30px, min(38cqh, 22cqw), 96px); font-weight: 700; letter-spacing: -0.03em; line-height: 1; } .clock .date { color: var(--d-text-2); margin-top: 6px; font-size: 15px; } +.clock-time { display: flex; flex-direction: column; align-items: center; } +.clock-wx { display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: 4px 8px; margin-top: 12px; padding: 6px 12px; border-radius: 99px; background: var(--d-fill); max-width: 100%; } +.clock-wx-ico { font-size: 20px; display: flex; color: var(--d-text-2); } +.clock-wx-temp { font-size: 17px; font-weight: 700; } +.clock-wx-text { font-size: 14px; color: var(--d-text-2); } +.clock-hl { font-size: 14px; color: var(--d-text-2); } +.clock-hl:empty { display: none; } +@container (max-height: 150px) { .clock-wx-text, .clock-hl { display: none; } .clock-wx { margin-top: 6px; } } .cam-btn { position: absolute; inset: 0; } .cam { width: 100%; height: 100%; object-fit: cover; display: block; background: #000; } @@ -405,6 +413,30 @@ body.editing .w:hover .w-resize, body.editing .w.selected .w-resize { opacity: 1 .tabnav-item.add { color: var(--d-text-2); margin-top: 4px; } .tabnav-item.add .i { font-size: 20px; } body.kiosk #tabnav { top: 6px; max-height: calc(100vh - 20px); } + +/* Icon-only rail: full height on the left edge, names as tooltips. */ +@media (min-width: 641px) { + #tabnav.icons { + top: 72px; width: 76px; height: calc(100vh - 72px); max-height: none; margin: 0; padding: 12px 10px; + justify-content: center; align-items: center; gap: 10px; overflow: visible; + border: 0; border-right: 1px solid var(--d-separator); border-radius: 0; + background: color-mix(in srgb, var(--d-tile) 72%, transparent); + } + body.kiosk #tabnav.icons { top: 6px; height: calc(100vh - 6px); } + #tabnav.icons .tabnav-item { position: relative; width: 56px; height: 56px; min-height: 0; padding: 0; justify-content: center; border-radius: 16px; transition: background-color .2s, transform .1s; } + #tabnav.icons .tabnav-item:active { transform: scale(0.94); } + #tabnav.icons .tabnav-ico { font-size: 25px; } + #tabnav.icons .tabnav-item[aria-current="page"]::before { content: ""; position: absolute; left: -10px; top: 16px; bottom: 16px; width: 4px; border-radius: 0 3px 3px 0; background: var(--d-accent); } + #tabnav.icons .tabnav-item.add { margin-top: 6px; border: 1.5px dashed var(--d-separator); } + #tabnav.icons .tabnav-label { + position: absolute; left: calc(100% + 14px); top: 50%; z-index: 40; + transform: translate(-4px, -50%); opacity: 0; pointer-events: none; + padding: 6px 10px; border-radius: 9px; background: var(--surface); color: var(--text); + font-size: 13px; font-weight: 600; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); transition: opacity .15s, transform .15s; + } + #tabnav.icons .tabnav-item:hover .tabnav-label, #tabnav.icons .tabnav-item:focus-visible .tabnav-label { opacity: 1; transform: translate(0, -50%); } + .surface-plain #tabnav.icons { border-right-color: transparent; } +} .surface-plain #tabnav { background: transparent; border-color: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; } .i.flip { transform: rotate(180deg); } .tab-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 4px; } @@ -531,14 +563,14 @@ body.kiosk.idle, body.kiosk.idle * { cursor: none !important; } } /* While editing (and on tablets) the menu becomes a compact rail to leave room for the grid. */ @media (min-width: 641px) { - body.editing #tabnav { width: 84px; padding: 6px; margin-left: 10px; } - body.editing .tabnav-item { flex-direction: column; justify-content: center; gap: 3px; padding: 8px 4px; min-height: 60px; text-align: center; } - body.editing .tabnav-label { font-size: 11px; font-weight: 600; max-width: 100%; } + body.editing #tabnav:not(.icons) { width: 84px; padding: 6px; margin-left: 10px; } + body.editing #tabnav:not(.icons) .tabnav-item { flex-direction: column; justify-content: center; gap: 3px; padding: 8px 4px; min-height: 60px; text-align: center; } + body.editing #tabnav:not(.icons) .tabnav-label { font-size: 11px; font-weight: 600; max-width: 100%; } } @media (max-width: 900px) { - #tabnav { width: 84px; padding: 6px; margin-left: 10px; } - .tabnav-item { flex-direction: column; justify-content: center; gap: 3px; padding: 8px 4px; min-height: 60px; text-align: center; } - .tabnav-label { font-size: 11px; font-weight: 600; max-width: 100%; } + #tabnav:not(.icons) { width: 84px; padding: 6px; margin-left: 10px; } + #tabnav:not(.icons) .tabnav-item { flex-direction: column; justify-content: center; gap: 3px; padding: 8px 4px; min-height: 60px; text-align: center; } + #tabnav:not(.icons) .tabnav-label { font-size: 11px; font-weight: 600; max-width: 100%; } #palette { width: 180px; } #inspector { width: 260px; } #palette-list { grid-template-columns: 1fr; } @@ -550,7 +582,10 @@ body.kiosk.idle, body.kiosk.idle * { cursor: none !important; } flex-direction: row; max-height: none; overflow-x: auto; padding: 6px; border-radius: 22px; background: var(--glass); border-color: var(--glass-edge); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); } - body:not(.editing) #tabnav .tabnav-item { flex: 1; min-width: 64px; } + body:not(.editing) #tabnav .tabnav-item { flex: 1; min-width: 64px; flex-direction: column; justify-content: center; gap: 3px; padding: 8px 4px; min-height: 56px; width: auto; height: auto; } + /* A tab bar keeps its names on phones: there's no hover to reveal them. */ + body:not(.editing) #tabnav.icons .tabnav-label { position: static; opacity: 1; transform: none; background: none; box-shadow: none; padding: 0; font-size: 11px; color: inherit; pointer-events: auto; max-width: 100%; } + body:not(.editing) #tabnav.icons .tabnav-item[aria-current="page"]::before { display: none; } body.has-tabs:not(.editing) #canvas { padding-bottom: 96px; } body.editing #tabnav { position: static; width: auto; flex-direction: row; overflow-x: auto; max-height: none; margin: 4px 10px; } body.editing #stage { flex-direction: column; align-items: stretch; } diff --git a/dashboard_studio/web/widgets.js b/dashboard_studio/web/widgets.js index ff1a150..41201fe 100644 --- a/dashboard_studio/web/widgets.js +++ b/dashboard_studio/web/widgets.js @@ -330,8 +330,29 @@ { k: "h24", t: "check", l: "24-hour time", d: true }, { k: "seconds", t: "check", l: "Show seconds", d: false }, { k: "date", t: "check", l: "Show date", d: true }, + { k: "weather", t: "entity", l: "Weather (optional)", domains: ["weather"] }, + { k: "forecast", t: "check", l: "Show today’s high and low", d: true }, ], - render: (c) => `
${clockHTML(c.h24, c.seconds, c.date)}
`, + entities: (c) => (c.weather ? [c.weather] : []), + render(c, { ha }) { + const s = c.weather && ha.states[c.weather]; + const wx = s ? `` : ""; + return `
${clockHTML(c.h24, c.seconds, c.date)}
${wx}
`; + }, + async after(el, c, ctx) { + if (!c.weather || c.forecast === false || !ctx.ha.states[c.weather]) return; + const cache = ctx.cache; + if (cache.key !== c.weather || Date.now() - cache.at > 30 * 60 * 1000) { + cache.key = c.weather; cache.at = Date.now(); + cache.loading = ctx.ha.forecast(c.weather, "daily").then((f) => (cache.today = f[0])).catch(() => (cache.today = null)); + } + await cache.loading; + const hl = el.querySelector("[data-hl]"); + const f = cache.today; + if (hl && f && f.temperature != null) hl.textContent = f.templow != null ? t("High {0}° · Low {1}°", Math.round(f.temperature), Math.round(f.templow)) : t("High {0}°", Math.round(f.temperature)); + }, }; function clockHTML(h24, sec, date) { diff --git a/tools/make_icon.py b/tools/make_icon.py new file mode 100644 index 0000000..cf9b11e --- /dev/null +++ b/tools/make_icon.py @@ -0,0 +1,74 @@ +"""Render the Dashboard Studio app icon: a dashboard with one lamp-lit tile.""" +import sys +from PIL import Image, ImageDraw, ImageFilter + +S = 1024 # draw large, downscale for anti-aliasing + + +def vgradient(w, h, top, bottom): + g = Image.new("RGBA", (w, h)) + px = g.load() + for y in range(h): + t = y / (h - 1) + c = tuple(int(top[i] + (bottom[i] - top[i]) * t) for i in range(3)) + (255,) + for x in range(w): + px[x, y] = c + return g + + +def rounded_mask(size, box, r): + m = Image.new("L", size, 0) + ImageDraw.Draw(m).rounded_rectangle(box, r, fill=255) + return m + + +img = Image.new("RGBA", (S, S), (0, 0, 0, 0)) +# background squircle +bg = vgradient(S, S, (34, 44, 78), (14, 17, 30)) +img.paste(bg, (0, 0), rounded_mask((S, S), (0, 0, S - 1, S - 1), 230)) + +pad, gap = 150, 56 +left = (pad, pad, 492, S - pad) # tall lit tile +rt = (492 + gap, pad, S - pad, 492 - gap // 2) # graph tile +rb = (492 + gap, 492 + gap // 2, S - pad, S - pad) # switch tile + +# warm glow behind the lit tile +glow = Image.new("RGBA", (S, S), (0, 0, 0, 0)) +ImageDraw.Draw(glow).rounded_rectangle(left, 64, fill=(255, 196, 92, 170)) +img = Image.alpha_composite(img, glow.filter(ImageFilter.GaussianBlur(60))) + +# lit tile with a warm gradient +lit = vgradient(left[2] - left[0], left[3] - left[1], (255, 236, 190), (255, 196, 96)) +img.paste(lit, left[:2], rounded_mask(lit.size, (0, 0, lit.size[0] - 1, lit.size[1] - 1), 64)) + +# translucent shapes go on a layer that is blended in, not pasted over +layer = Image.new("RGBA", (S, S), (0, 0, 0, 0)) +ld = ImageDraw.Draw(layer) +for box in (rt, rb): + ld.rounded_rectangle(box, 64, fill=(255, 255, 255, 26), outline=(255, 255, 255, 40), width=4) +cx = left[0] + 100 +ld.ellipse((cx - 52, left[1] + 48, cx + 52, left[1] + 152), fill=(255, 255, 255, 140)) +bar = (left[0] + 48, left[3] - 128, left[2] - 48, left[3] - 64) +ld.rounded_rectangle(bar, 32, fill=(122, 82, 16, 50)) +img = Image.alpha_composite(img, layer) + +d = ImageDraw.Draw(img) +# lamp in the badge and the filled part of the brightness bar +d.ellipse((cx - 20, left[1] + 80, cx + 20, left[1] + 120), fill=(122, 82, 16, 255)) +d.rounded_rectangle((bar[0], bar[1], bar[0] + int((bar[2] - bar[0]) * 0.68), bar[3]), 32, fill=(140, 94, 20, 255)) + +# graph tile: a line in the accent blue +pts = [(rt[0] + 44, rt[3] - 70), (rt[0] + 110, rt[3] - 120), (rt[0] + 170, rt[3] - 96), (rt[0] + 240, rt[3] - 170), (rt[2] - 44, rt[1] + 70)] +d.line(pts, fill=(90, 170, 255, 255), width=22, joint="curve") +for p in (pts[0], pts[-1]): + d.ellipse((p[0] - 11, p[1] - 11, p[0] + 11, p[1] + 11), fill=(90, 170, 255, 255)) + +# switch tile: an "on" switch +sw = (rb[0] + 50, (rb[1] + rb[3]) // 2 - 46, rb[2] - 50, (rb[1] + rb[3]) // 2 + 46) +d.rounded_rectangle(sw, 46, fill=(48, 209, 88, 255)) +d.ellipse((sw[2] - 86, sw[1] + 6, sw[2] - 6, sw[3] - 6), fill=(255, 255, 255, 255)) + +out = sys.argv[1] +img.resize((256, 256), Image.LANCZOS).save(out) +img.resize((128, 128), Image.LANCZOS).save(out.replace(".png", "-128.png")) +print("saved", out)