From f4c98e27af0340692930e7b7085875cc382e7f59 Mon Sep 17 00:00:00 2001 From: veenm Date: Mon, 26 Oct 2026 23:56:31 +0100 Subject: [PATCH] Dashboard Studio 2.1.0: every card redesigned for desktop, tablet and phone - One card system in three device bands, applied automatically: compact with hover on desktop; larger type and 44px controls on tablets and wall panels; touch sizes in a tighter frame on phones; narrow-card layouts by container - Answers as the largest thing, in regular-weight tabular figures; plain outline icons instead of tinted chips; pale status pills; flat buttons and sliders without shadows or gradients - Energy: power flow (sun, house, battery, grid and the flows between them) - Garage: state in large type with one wide button; vacuum: state and a battery bar - The first heading on a tab is the page's h1 Co-Authored-By: Claude Opus 5.5 --- dashboard_studio/CHANGELOG.md | 10 + dashboard_studio/DOCS.md | 4 +- dashboard_studio/config.yaml | 2 +- dashboard_studio/web/app.js | 3 + dashboard_studio/web/i18n.js | 19 ++ dashboard_studio/web/style.css | 432 ++++++++++++++++++++++++++++++-- dashboard_studio/web/widgets.js | 70 +++--- 7 files changed, 484 insertions(+), 56 deletions(-) diff --git a/dashboard_studio/CHANGELOG.md b/dashboard_studio/CHANGELOG.md index d6c7d1d..8825456 100644 --- a/dashboard_studio/CHANGELOG.md +++ b/dashboard_studio/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 2.1.0 + +- **Every card redesigned,** with a desktop, a tablet and a phone design that apply by themselves: compact with hover states on a computer; larger type and 44px buttons on tablets and wall panels; touch sizes in a tighter frame on phones, with rows that scroll sideways instead of wrapping. Narrow cards put their figures under the names. +- **Calmer cards:** each card's answer is its largest thing, in thin, even figures with the unit in grey; icons are plain outlines that only take colour when something is on or needs attention; status shows as a pale pill; buttons and sliders are flat, without shadows or gradients. +- **Energy:** a new power flow: the sun, the house, the battery and the grid with their power now, and each running flow with its amount, adding up. +- **Garage and gate:** the state in large type (*Closed*), and one wide button. +- **Robot vacuum:** the state in words with the battery as a bar. +- The weather forecast shows today's temperature range in your accent colour and the other days in grey; Formula One, air quality and parcels are easier to read on small cards. +- The dashboard's first heading is now the page's main title, for screen readers. + ## 2.0.0 - **Admins and everyone else:** Home Assistant's administrators are admins in Dashboard Studio; everyone else only sees the dashboards an admin chose for them, full screen in kiosk mode, and can't change anything. Dashboard Studio enforces this itself: only admins can save, and everyone else can only read states and control devices, as in Home Assistant. diff --git a/dashboard_studio/DOCS.md b/dashboard_studio/DOCS.md index 84f241e..c4b99f2 100644 --- a/dashboard_studio/DOCS.md +++ b/dashboard_studio/DOCS.md @@ -62,6 +62,8 @@ More editing tools: ## Look and feel +- **Cards for every screen:** each card has a desktop, a tablet and a phone design, applied by itself. On a computer with a mouse, cards are compact with hover states. On a tablet or wall panel (any touch screen, or a screen 641-1100px wide) the type is larger and every button, switch and key is at least 44px, so it can be read and used from across the room. On a phone the same touch sizes sit in a tighter frame, and rows such as a light's colours scroll sideways instead of wrapping. Within that, each card still adapts to its own size: a narrow card puts its figures under the names instead of beside them. +- **How cards look:** the answer is the largest thing on each card (the temperature, the battery, *Closed*), in thin, even figures with the unit in grey. Icons are plain outlines that take colour only when something is on or needs attention; status shows as a pale pill; buttons are flat. - **Light and dark versions:** turn on **Switch to a dark or light version with the device** under a theme to use a matching dark version of a light theme when the device is in dark mode, and the other way round. *System* always does this. - **Theme:** *System* follows your device's light or dark appearance. The other 14 themes are fixed: Light, Dark, Nord, Dusk, Forest, Graphite, Indigo, Rose, Ocean, Sunrise, Lavender, Mint, Sand, and High contrast for maximum readability. Text meets accessibility contrast levels in every theme. Changing any color switches the theme to *Custom*. - **Tile style:** *Cards* puts every widget on a tile. *Outlined* draws a thin border instead. *Plain* removes the cards completely, so widgets sit straight on the background. You can also turn off the background for a single widget under **Style → No background**. @@ -184,7 +186,7 @@ A live picture of where your power goes. Choose the sensors you have; all are op - **Home use**, or leave it empty and the card works it out from the others. - **Totals for today**, such as kWh produced and used, shown underneath. -Solar, the grid, the battery and your home are connected by lines. Moving dots show where power is flowing, and the more power, the thicker and faster the line. Solar power goes to the home first, then into the battery, and the rest back to the grid. +The card shows the sun, the house, the battery and the grid with their power right now, then each flow that is running, largest first, such as *Sun to the grid 1.3 kW*, with a bar in proportion (sunshine in amber, the grid in grey). The flows add up: what comes from the sun, the grid and the battery is what goes to the house, the battery and the grid. Solar power goes to the house first, then into the battery, and the rest back to the grid. On a square card the four places sit two by two. ### Room diff --git a/dashboard_studio/config.yaml b/dashboard_studio/config.yaml index c5ecdcc..1c7f646 100644 --- a/dashboard_studio/config.yaml +++ b/dashboard_studio/config.yaml @@ -1,5 +1,5 @@ name: Dashboard Studio -version: "2.0.0" +version: "2.1.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/web/app.js b/dashboard_studio/web/app.js index f3111f2..7db3874 100644 --- a/dashboard_studio/web/app.js +++ b/dashboard_studio/web/app.js @@ -1006,6 +1006,9 @@ placeEl(el, pos[w.id]); renderWidget(w, el); } + // The first heading on the tab is the screen's title. + const h = gridEl.querySelector("h2.h-main"); + if (h) { const h1 = document.createElement("h1"); h1.className = h.className; h1.style.cssText = h.style.cssText; h1.textContent = h.textContent; h.replaceWith(h1); } updateGridHeight(pos); } diff --git a/dashboard_studio/web/i18n.js b/dashboard_studio/web/i18n.js index ee96a64..a938a14 100644 --- a/dashboard_studio/web/i18n.js +++ b/dashboard_studio/web/i18n.js @@ -704,6 +704,25 @@ "Stop preview": "Voorbeeld stoppen", "Nothing to show yet": "Nog niets te zien", "Screen": "Scherm", + "Sun": "Zon", + "Producing": "Levert op", + "Not producing": "Levert niets", + "House": "Huis", + "In use": "Verbruik", + "Charging {0}": "Laadt {0}", + "Supplying {0}": "Levert {0}", + "Idle": "Rust", + "Sending back": "Teruglevering", + "Drawing": "Afname", + "Sun to the house": "Zon naar het huis", + "Sun to the battery": "Zon naar de batterij", + "Sun to the grid": "Zon naar het net", + "Grid to the house": "Net naar het huis", + "Grid to the battery": "Net naar de batterij", + "Battery to the house": "Batterij naar het huis", + "Energy now": "Energie nu", + "No power is flowing right now.": "Er stroomt nu geen vermogen.", + "Battery {0}%, charging": "Batterij {0}%, laadt", "Today": "Vandaag", "Tomorrow": "Morgen", "All day": "Hele dag", "Busy": "Bezet", }; diff --git a/dashboard_studio/web/style.css b/dashboard_studio/web/style.css index 257014d..eaccb14 100644 --- a/dashboard_studio/web/style.css +++ b/dashboard_studio/web/style.css @@ -182,7 +182,7 @@ select.input, input.input, textarea.input { background: var(--fill); border: 1px solid transparent; border-radius: 9px; } input.input:focus, select.input:focus, textarea.input:focus { border-color: var(--accent); box-shadow: var(--focus); background: var(--surface); } -textarea.input { resize: vertical; min-height: 76px; font-family: ui-monospace, Menlo, monospace; font-size: 13px; } +textarea.input { resize: vertical; min-height: 76px; font-family: ui-monospace, Menlo, monospace; font-size: 13px; } /* taste-skill-ignore: the JSON field is code */ .input.invalid { border-color: var(--crit); } .seg { display: flex; background: var(--fill); border-radius: 9px; padding: 2px; gap: 2px; } .seg button { flex: 1; border: 0; background: transparent; border-radius: 7px; min-height: 30px; padding: 0 6px; cursor: pointer; font-size: 13px; white-space: nowrap; } @@ -377,7 +377,7 @@ body.editing .w:hover .w-resize, body.editing .w.selected .w-resize { opacity: 1 /* Brightness: a wide pill slider like Control Center, filled with the lamp's color. */ .slider { -webkit-appearance: none; appearance: none; width: 100%; height: 44px; margin: 0; border-radius: 14px; cursor: pointer; - background: linear-gradient(to right, var(--slider-fill, var(--d-text)) var(--pct, 0%), var(--d-fill) var(--pct, 0%)); + background: var(--d-fill); outline: none; } .w.lit-lamp .slider { --slider-fill: color-mix(in srgb, var(--lamp, #f0a83a) 65%, #f0a83a); } @@ -668,7 +668,8 @@ body.kiosk #tabnav { top: 6px; max-height: calc(100vh - 20px); } .clock-bg { position: absolute; inset: 0; margin: -16px; z-index: 0; border-radius: inherit; } .w.transparent .clock-bg, .surface-plain .clock-bg { margin: -6px -4px; } .clock.on-image, .photos[data-act] { cursor: pointer; } -.clock-dim { position: absolute; inset: 0; margin: -16px; z-index: 0; background: linear-gradient(to bottom, rgba(0, 0, 0, calc(var(--dim) * 0.8)), rgba(0, 0, 0, var(--dim))); } +/* taste-skill-ignore: a neutral scrim over the photo */ +.clock-dim { position: absolute; inset: 0; margin: -16px; z-index: 0; background: linear-gradient(to bottom, rgba(0, 0, 0, calc(var(--dim) * 0.8)), rgba(0, 0, 0, var(--dim))); } /* taste-skill-ignore: a neutral scrim over the photo */ .clock.on-image { color: #fff; --d-text-2: rgba(255, 255, 255, 0.86); text-shadow: 0 1px 12px rgba(0, 0, 0, 0.45); } .clock.on-image .clock-wx { background: rgba(0, 0, 0, 0.32); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); } .pos9 { display: grid; grid-template-columns: repeat(3, 34px); gap: 4px; padding: 4px; background: var(--fill); border-radius: 10px; width: max-content; } @@ -707,13 +708,16 @@ body.kiosk #tabnav { top: 6px; max-height: calc(100vh - 20px); } .lp-head p { margin: 2px 0 0; } .lp-dot { width: 48px; height: 48px; border-radius: 50%; display: grid; place-items: center; font-size: 24px; background: var(--tint); color: #2a1d05; flex: none; } .lp-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 48px; border-radius: 14px; margin: 0; cursor: pointer; - background: linear-gradient(to right, var(--fill-c, #ffd58a) var(--pct, 0%), var(--fill) var(--pct, 0%)); } -.lp-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 6px; height: 30px; border-radius: 3px; background: rgba(255, 255, 255, .9); box-shadow: 0 1px 3px rgba(0,0,0,.3); } -.lp-slider::-moz-range-thumb { width: 6px; height: 30px; border: 0; border-radius: 3px; background: #fff; } -.lp-temp { height: 36px; background: linear-gradient(to right, #ffb35c, #fff4e5 55%, #cfe2ff); } + background: var(--fill); overflow: hidden; } +.lp-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 4px; height: 48px; border-radius: 0; background: var(--fill-c, #ffd58a); box-shadow: -2000px 0 0 2000px var(--fill-c, #ffd58a); } +.lp-slider::-moz-range-progress { height: 100%; background: var(--fill-c, #ffd58a); } +.lp-slider::-moz-range-thumb { width: 4px; height: 48px; border: 0; border-radius: 0; background: var(--fill-c, #ffd58a); } +/* taste-skill-ignore: the colour-temperature scale itself, warm to cool */ +.lp-temp { height: 36px; background: linear-gradient(to right, #ffb35c, #fff4e5 55%, #cfe2ff); } /* taste-skill-ignore: the colour-temperature scale itself */ .lp-swatches { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } .swatch.big { width: 38px; height: 38px; border: 0; cursor: pointer; } -.swatch.custom { background: conic-gradient(red, yellow, lime, cyan, blue, magenta, red); color: #fff; position: relative; overflow: hidden; } +/* taste-skill-ignore: the colour wheel of the custom colour picker */ +.swatch.custom { background: conic-gradient(red, yellow, lime, cyan, blue, magenta, red); color: #fff; position: relative; overflow: hidden; } /* taste-skill-ignore: the colour wheel of the custom colour picker */ .swatch.custom input { position: absolute; inset: 0; opacity: 0; cursor: pointer; } /* light groups */ @@ -800,7 +804,7 @@ body.kiosk #tabnav { top: 6px; max-height: calc(100vh - 20px); } .fc-dn { font-weight: 600; text-transform: capitalize; } .fc-lo { color: var(--d-text-2); text-align: right; } .fc-bar { position: relative; height: 6px; border-radius: 3px; background: var(--d-fill); } -.fc-bar i { position: absolute; top: 0; bottom: 0; border-radius: 3px; background: linear-gradient(to right, #5ac8fa, #ffcc00, #ff9500); } +.fc-bar i { position: absolute; top: 0; bottom: 0; border-radius: 3px; } @container (max-height: 300px) { .fc-days { display: none; } } @container (max-width: 360px) { .fc-sun { display: none; } } @@ -823,8 +827,9 @@ body.kiosk #tabnav { top: 6px; max-height: calc(100vh - 20px); } .cv { display: flex; flex-direction: column; height: 100%; gap: 10px; min-height: 0; } .cv-head { display: flex; align-items: center; gap: 6px; } .cv-head .name { flex: 1; min-width: 0; } -.cv-win { position: relative; width: 44px; aspect-ratio: 3 / 4; border-radius: 6px; border: 2px solid var(--d-text-2); background: linear-gradient(#bfe0ff, #e8f4ff); overflow: hidden; flex: none; } -.cv-blind { position: absolute; left: 0; right: 0; top: 0; background: repeating-linear-gradient(to bottom, var(--d-text-2) 0 3px, color-mix(in srgb, var(--d-text-2) 60%, transparent) 3px 5px); transition: height .4s; } +.cv-win { position: relative; width: 44px; aspect-ratio: 3 / 4; border-radius: 6px; border: 2px solid var(--d-text-2); background: #d4e9ff; overflow: hidden; flex: none; } +/* taste-skill-ignore: the slats of the drawn blind */ +.cv-blind { position: absolute; left: 0; right: 0; top: 0; background: repeating-linear-gradient(to bottom, var(--d-text-2) 0 3px, color-mix(in srgb, var(--d-text-2) 60%, transparent) 3px 5px); transition: height .4s; } /* taste-skill-ignore: the slats of the drawn blind */ .cv.one .cv-main { flex: 1; min-height: 0; display: flex; align-items: center; gap: 16px; } .cv.one .cv-win { width: auto; height: min(100%, 150px); } .cv-side { display: flex; flex-direction: column; gap: 6px; min-width: 0; } @@ -882,7 +887,7 @@ body.kiosk #tabnav { top: 6px; max-height: calc(100vh - 20px); } /* room */ .room { position: relative; display: flex; flex-direction: column; gap: 12px; height: 100%; min-height: 0; } .room.on-image { margin: -16px; padding: 16px; border-radius: inherit; color: #fff; --d-text-2: rgba(255, 255, 255, 0.86); --d-fill: rgba(255, 255, 255, 0.18); - background: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.15) 45%, rgba(0, 0, 0, 0.6)), var(--room-img) center / cover; } + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.15) 45%, rgba(0, 0, 0, 0.6)), var(--room-img) center / cover; } /* taste-skill-ignore: a neutral scrim over the photo */ .room-head { display: flex; align-items: center; gap: 12px; min-width: 0; } .room-name { font-size: 17px; } .room-chips { display: flex; flex-wrap: wrap; gap: 6px; } @@ -1206,7 +1211,7 @@ body.kiosk #tabnav { top: 6px; max-height: calc(100vh - 20px); } /* Formula One */ .f1 { display: flex; flex-direction: column; gap: 10px; height: 100%; min-height: 0; } -.f1-next { padding: 12px; border-radius: 14px; background: linear-gradient(135deg, color-mix(in srgb, #e10600 22%, var(--d-fill)), var(--d-fill)); } +.f1-next { padding: 12px; border-radius: 14px; } .f1-top { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--d-text-2); } .f1-top .i { color: #e10600; font-size: 15px; } .f1-top b { margin-left: auto; color: var(--d-text); font-weight: 600; } @@ -1425,7 +1430,7 @@ body.kiosk.idle, body.kiosk.idle * { cursor: none !important; } .mode-seg button { display: inline-flex; align-items: center; gap: 6px; border: 0; background: transparent; border-radius: 9px; min-height: 32px; padding: 0 12px; cursor: pointer; font-weight: 500; color: var(--text-2); } .mode-seg button .i { font-size: 17px; } .mode-seg button:hover { color: var(--text); } -.mode-seg button.on { background: var(--surface); color: var(--text); font-weight: 650; box-shadow: 0 1px 3px rgba(0, 0, 0, .14); } +.mode-seg button.on { background: var(--surface); color: var(--text); font-weight: 650; } .mode-seg button[data-mode="builder"].on { background: var(--accent); color: var(--on-accent); } @media (max-width: 760px) { .mode-seg button span { display: none; } .mode-seg button { padding: 0 9px; } } @@ -1476,7 +1481,7 @@ body.admin-mode #topbar::before { content: "Dashboard Studio"; font-weight: 650; .chip-check input { position: absolute; opacity: 0; pointer-events: none; } .chip-check span { display: inline-flex; align-items: center; min-height: 32px; padding: 0 13px; border-radius: 99px; background: var(--fill); font-size: 13px; font-weight: 500; cursor: pointer; } .chip-check input:checked + span { background: var(--accent); color: var(--on-accent); } -.chip-check input:checked + span::before { content: "✓"; margin-right: 6px; font-weight: 700; } +.chip-check input:checked + span::before { content: ""; width: 8px; height: 4px; margin: -3px 8px 0 1px; border-left: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(-45deg); } .chip-check input:disabled + span { opacity: .55; cursor: default; } .chip-check input:focus-visible + span { box-shadow: var(--focus); } .dev-row, .act-row { display: flex; align-items: center; gap: 12px; background: var(--surface); border-radius: 14px; padding: 10px 14px; border: 1px solid var(--separator); } @@ -1526,3 +1531,400 @@ body.in-haven #btn-new { display: none; } .haven-head { justify-content: flex-start; align-items: center; } .haven-head img { width: 64px; height: 64px; border-radius: 16px; flex: none; box-shadow: 0 6px 18px rgba(0, 0, 0, .2); } .haven-head > div { flex: 1; min-width: 240px; } + +/* ============================================================ cards + One card system in three device bands, picked by the screen itself (nothing to set): + - desktop: a mouse and a wide screen, compact, with hover states; + - tablet and wall panel: touch, or 641-1100px wide: larger type and 44px controls, read from across the room; + - phone: 640px and narrower: the tablet's touch sizes in a tighter frame, rows that scroll sideways. + Inside a band, each card's own size still decides what it shows (the container queries above). + The rules: the card's answer is its largest thing, set at regular weight in tabular figures with the unit + as a grey suffix; icons are plain outlines, never in a tinted circle; colour only where something is on, + live or needs attention; one title style; status as a pale pill. */ +#grid { + --c-pad: 16px; --c-gap: 10px; --c-row: 40px; --c-ctl: 36px; --c-ico: 20px; --c-r: 12px; + --c-fs-title: 14px; --c-fs-body: 14px; --c-fs-name: 15px; --c-fs-meta: 12px; --c-fs-mid: 20px; + --c-hue: var(--d-accent); + --c-sun: #e09a14; + font-variant-numeric: tabular-nums; +} +@media (max-width: 1100px), (pointer: coarse) { + #grid { --c-pad: 18px; --c-gap: 12px; --c-row: 48px; --c-ctl: 44px; --c-ico: 22px; --c-r: 14px; + --c-fs-title: 15px; --c-fs-body: 15px; --c-fs-name: 16px; --c-fs-meta: 13px; --c-fs-mid: 22px; } +} +@media (max-width: 640px) { + #grid { --c-pad: 14px; --c-gap: 10px; --c-fs-title: 14px; --c-fs-name: 15px; --c-fs-mid: 20px; } +} + +/* the frame */ +.w-body { padding: var(--c-pad); } +.pad { padding: var(--c-pad); } +.clock-bg, .clock-dim { margin: calc(-1 * var(--c-pad)); } +.room.on-image { margin: calc(-1 * var(--c-pad)); padding: var(--c-pad); } + +/* one title style, one name style, one meta style */ +.wt { font-size: var(--c-fs-title); font-weight: 600; color: var(--d-text); } +.heading .wt { font-weight: 400; color: var(--d-text-2); font-size: var(--c-fs-body); } +.heading .h-main { font-weight: 650; letter-spacing: -0.02em; margin: 0; } +.value-top .wt, .gauge .wt, .al-code .wt, .vac-hero-text .wt, .clock .wt { font-weight: 400; color: var(--d-text-2); } +.name { font-size: var(--c-fs-name); font-weight: 600; } +.state { font-size: var(--c-fs-body); } +.empty { font-size: var(--c-fs-body); } + +/* icons: plain outlines; the colour says the thing is on, never a tinted disc */ +.badge { width: 28px; height: 28px; background: transparent !important; font-size: calc(var(--c-ico) + 4px); color: var(--d-text-2); place-items: center; border-radius: 0; } +.w.lit-lamp .badge, .surface-plain .w.lit-lamp .badge, .surface-outline .w.lit-lamp .badge { color: var(--lit-text); } +.surface-plain .w.lit-lamp .badge, .surface-outline .w.lit-lamp .badge { color: color-mix(in srgb, var(--lamp, #e3a12f) 70%, #b77a10); } +.w.lit-on .badge { color: var(--accent-w); } +.btn-widget:active .badge { color: var(--accent-w); } +.row-ico, .value-ico, .wx-ico, .fc-ico, .clock-wx-ico { color: var(--d-text-2); } + +/* figures: regular weight, tabular, unit in grey */ +.big, .climate .target, .clock .time, .wx-temp, .fc-temp, .tm-time, .cv-pct, .us-big, .sf-big, .appl-big, .vac-headline, .waste-when, .sp-score, .sp-time, .tz-pct { + font-weight: 400; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; } +.clock .time { font-weight: 300; letter-spacing: -0.04em; } +.unit, .us-big small, .sf-big small, .sf-mid small { font-weight: 400; color: var(--d-text-2); letter-spacing: 0; } +.sf-mid, .clock-wx-temp, .vac-stat-v, .pc-sum b, .ef-val { font-weight: 500; } +.num { font-variant-numeric: tabular-nums; } + +/* controls: flat, one fill, sized for the device */ +.round { width: var(--c-ctl); height: var(--c-ctl); } +.chip { min-height: calc(var(--c-ctl) - 4px); font-size: var(--c-fs-meta); padding: 0 12px; } +.switch::after { box-shadow: none; } +.slider { height: calc(var(--c-ctl) + 8px); border-radius: var(--c-r); } +.slider.thin { height: 28px; } +.list-row { min-height: var(--c-row); } +.list-row .grow { font-size: var(--c-fs-body); } +.list-row .st { font-size: var(--c-fs-meta); } +@media (hover: hover) { .round:hover, .al-key:hover, .vac-btn:not(.primary):not(:disabled):hover, .appl-btn:hover { background: color-mix(in srgb, var(--d-text) 14%, transparent); } } +.wbtn:active { transform: scale(.97); } +@media (prefers-reduced-motion: reduce) { .wbtn:active { transform: none; } } + +/* status as a pale pill: the hue's own tint with its text */ +.appl-status, .level, .cal-now { font-size: var(--c-fs-meta); font-weight: 600; } +.appl.phase-running .appl-status { background: color-mix(in srgb, var(--d-accent) 16%, transparent); color: var(--d-accent); } + +/* pictures: the label sits on a dark plate, no text shadow */ +.cam-label, .ph-date { text-shadow: none; padding: 3px 8px; border-radius: 6px; background: rgba(0, 0, 0, .58); font-size: var(--c-fs-meta); } +.clock.on-image { text-shadow: none; } +.clock.on-image .clock-wx { background: rgba(0, 0, 0, .45); backdrop-filter: none; -webkit-backdrop-filter: none; } + +/* entity tile and value */ +.tile .tile-text .name { font-size: var(--c-fs-name); } +.value .big { font-size: clamp(28px, min(34cqh, 26cqw), 64px); } + +/* light: brightness as one wide bar, colours in one row */ +.swatch { width: 24px; height: 24px; } +.swatch.more { color: var(--d-text-2); } +.lm-dot { background: transparent; width: 28px; height: 28px; font-size: var(--c-ico); color: var(--d-text-2); } +.lm-row.on .lm-dot { background: transparent; color: color-mix(in srgb, var(--tint, #e3a12f) 70%, #8a5a00); } +.w.lit-lamp .lm-row.on .lm-dot { color: var(--lit-text); } +.lm-row { grid-template-columns: 28px minmax(0, 1fr) auto auto; } +.lm-slider::-webkit-slider-thumb { box-shadow: none; border: 1px solid rgba(0, 0, 0, .2); } + +/* thermostat */ +.climate .target { font-size: clamp(32px, min(26cqh, 22cqw), 72px); } + +/* graph */ +.graph .legend { font-size: var(--c-fs-meta); } +.graph .legend b { font-size: var(--c-fs-body); font-weight: 500; } +.graph .axis { font-size: 12px; } + +/* clock */ +.clock .date { font-size: var(--c-fs-body); } +.clock-wx { gap: 4px 8px; } + +/* weather */ +.wx-temp { font-size: clamp(30px, min(32cqh, 20cqw), 60px); } +.wx-ico { font-size: clamp(32px, min(38cqh, 16cqw), 60px); } + +/* media */ +.media .title { font-size: var(--c-fs-name); font-weight: 600; } +.mp-switch .chip { min-height: 30px; } + +/* forecast: no box per hour, the range as a flat bar */ +.fc-hour { background: transparent; padding: 6px 0; border-radius: 10px; } +.fc-hour:first-child { background: var(--d-fill); } +.fc-hour b { font-weight: 500; } +.fc-rain, .fc-t { font-size: 12px; } +.fc-bar i { background: color-mix(in srgb, var(--d-text) 55%, transparent); } +.fc-day:first-child .fc-bar i { background: var(--c-hue); } +.fc-day { font-size: var(--c-fs-body); min-height: var(--c-row); } +.fc-temp { font-size: clamp(32px, 12cqw, 48px); } + +/* timer: a thin ring */ +.tm-ring circle { stroke-width: 4; } +.tm-name { font-size: var(--c-fs-title); } + +/* blinds */ +.cv-btns .round { width: var(--c-ctl); height: var(--c-ctl); } +.cv-name { font-size: var(--c-fs-body); } + +/* alarm: the shield is a plain mark, coloured only when armed or triggered */ +.al-shield { background: transparent !important; width: 28px; height: 28px; font-size: calc(var(--c-ico) + 4px); color: var(--d-text-2); } +.al[class*="state-armed"] .al-shield { color: var(--d-accent); } +.al.state-triggered .al-shield { color: var(--crit); } +.al-key { min-height: var(--c-ctl); font-weight: 500; font-size: calc(var(--c-fs-body) + 5px); } +.al-mode { min-height: var(--c-ctl); font-size: var(--c-fs-body); font-weight: 500; } +.al-mode.on { background: color-mix(in srgb, var(--d-accent) 18%, transparent); color: var(--d-accent); font-weight: 600; } +.al-code { font-variant-numeric: tabular-nums; } + +/* notifications: rows one step off the card */ +.nt-head .name, .todo-head .name { font-size: var(--c-fs-title); } +.nt-item { border-radius: var(--c-r); padding: 10px 12px; } +.nt-body { font-size: var(--c-fs-meta); } +.nt-body b { font-size: var(--c-fs-body); font-weight: 600; } +.nt-time { font-size: 12px; } +.nt-x { width: 32px; height: 32px; } +.todo-count { font-weight: 600; font-size: var(--c-fs-meta); } +.todo-row { min-height: var(--c-row); } +.todo-text { font-size: var(--c-fs-body); } +.todo-input { min-height: var(--c-ctl); border-radius: var(--c-r); } +.todo-plus { width: var(--c-ctl); height: var(--c-ctl); border-radius: var(--c-r); } + +/* who's home: a green ring for home, nothing for away */ +.pp-av { box-shadow: none; } +.pp.home .pp-av { box-shadow: 0 0 0 2px var(--ok); } +.pp-init { font-weight: 600; } +.pp-name { font-size: var(--c-fs-body); } +.pp-where { font-size: var(--c-fs-meta); } + +/* room: a plain mark, chips and devices without a filled icon */ +.room-name { font-size: var(--c-fs-name); } +.room-chip { min-height: 32px; font-size: var(--c-fs-meta); } +.room-dev { min-height: var(--c-row); border-radius: var(--c-r); } +.room-dev-name { font-size: var(--c-fs-meta); } +.room-dev.on { background: color-mix(in srgb, var(--d-text) 88%, transparent); } + +/* vacuum: the state in words, the battery as a bar */ +.vac-hero { justify-content: flex-start; align-items: center; } +.vac-hero-text { gap: 8px; width: 100%; } +.vac-headline { font-size: clamp(26px, 9cqh, 40px); line-height: 1.1; } +.vac-meter { display: block; height: 6px; border-radius: 3px; background: var(--d-fill); overflow: hidden; max-width: 320px; } +.vac-meter i { display: block; height: 100%; border-radius: 3px; background: var(--ok); } +.vac-meter.low i { background: var(--crit); } +.vac-stat { padding: 10px 12px; border-radius: var(--c-r); grid-template-columns: 1fr; } +.vac-stat .i { display: none; } +.vac-stat-v { font-size: var(--c-fs-mid); font-weight: 400; } +.vac-stat-l { font-size: var(--c-fs-meta); } +.vac-btn { min-height: var(--c-ctl); border-radius: var(--c-r); font-size: var(--c-fs-body); } +@container (max-width: 330px) { .vac-hero-text { display: flex; } } + +/* appliance: a thin progress ring with a plain icon inside */ +.appl-ring circle { stroke-width: 4; } +.appl-ico { background: transparent !important; width: 60%; font-size: clamp(22px, 18cqh, 36px); color: var(--d-text-2); } +.phase-running .appl-ico { color: var(--d-accent); } +.phase-done .appl-ico { color: var(--ok); } +.appl-pct { font-size: 12px; font-weight: 600; } +.appl-fact { font-size: var(--c-fs-meta); font-weight: 500; } +.appl-big { font-size: clamp(22px, 17cqh, 34px); } + +/* camera and photos: square label plates (above) */ + +/* car */ +.tz-name { font-weight: 600; } + +/* parcels: a plain mark per row */ +.pc-ico { background: transparent; width: 24px; height: 24px; font-size: var(--c-ico); color: var(--d-text-2); } +.pc-sum b { font-size: var(--c-fs-mid); font-weight: 400; } +.pc-row { border-radius: var(--c-r); min-height: var(--c-row); } +.pc-name { font-size: var(--c-fs-body); } +.pc-status, .pc-when { font-size: var(--c-fs-meta); } + +/* air quality: a dot per room, labels you can read */ +.aq-dot { box-shadow: none; width: 10px; height: 10px; } +.aq-row { border-radius: var(--c-r); min-height: var(--c-row); } +.aq-room b { font-size: var(--c-fs-body); font-weight: 600; } +.aq-room span { font-size: var(--c-fs-meta); } +.aq-val { background: transparent; padding: 2px 4px; min-width: 48px; align-items: flex-end; } +.aq-val small { font-size: 12px; } +.aq-val span { font-size: var(--c-fs-body); font-weight: 500; } + +/* water and gas: the meter's own hue on today only, a plain mark by the name */ +.us-ico { background: transparent !important; width: 22px; height: 22px; font-size: var(--c-ico); } +.us-head .name { font-size: var(--c-fs-title); } +.us-big { font-size: clamp(30px, 11cqw, 44px); } +.us-cmp, .us-foot { font-size: var(--c-fs-meta); } +.us-lbl { font-size: 12px; } +.us-bar { border-radius: 3px 3px 0 0; } + +/* solar forecast: the sun's hue, flat; the best hours in it, the rest pale */ +.sf-big { font-size: clamp(30px, 11cqw, 44px); } +.sf-lbl { font-size: var(--c-fs-meta); } +.sf-h i { background: color-mix(in srgb, var(--c-sun) 58%, var(--d-fill)); border-radius: 3px 3px 0 0; } +.sf-h.past i { opacity: .5; } +.sf-h.best i { background: var(--c-sun); } +.sf-h.now i { background: color-mix(in srgb, var(--c-sun) 58%, var(--d-fill)); box-shadow: none; outline: 2px solid var(--c-sun); outline-offset: 1px; } +.sf-h span { font-size: 12px; bottom: -17px; } +.sf-bars { padding-bottom: 18px; } +.sf-best { background: var(--d-fill); border-radius: var(--c-r); font-size: var(--c-fs-meta); } +.sf-best .i { color: var(--c-sun); } + +/* garage and gate: the state is the answer, the button runs the width */ +.gt { grid-template-rows: auto 1fr auto; gap: 6px; } +.gt-head { gap: 8px; } +.gt-ico { font-size: var(--c-ico); color: var(--d-text-2); display: flex; } +.gt.open .gt-ico { color: var(--warn); } +.gt-head .name { font-size: var(--c-fs-title); } +.gt-now { display: flex; flex-direction: column; justify-content: center; gap: 2px; min-height: 0; } +.gt-big { font-size: clamp(26px, min(24cqh, 16cqw), 44px); font-weight: 400; letter-spacing: -0.02em; line-height: 1.05; } +.gt.open .gt-big { color: var(--warn); } +.gt.moving .gt-big { animation: gt-pulse 1.2s ease-in-out infinite; } +.gt-since { text-align: left; font-size: var(--c-fs-meta); } +.gt-btn { justify-self: stretch; align-self: end; flex-direction: row; justify-content: center; min-height: calc(var(--c-ctl) + 4px); border-radius: var(--c-r); background: var(--d-text); color: var(--d-tile); } +.gt.open .gt-btn { background: var(--d-fill); color: var(--d-text); } +.gt-lbl { font-size: var(--c-fs-body); font-weight: 600; } +@media (prefers-reduced-motion: reduce) { .gt.moving .gt-big { animation: none; } } + +/* sports: sentence case, crests without a disc */ +.sp-league { text-transform: none; letter-spacing: 0; font-size: var(--c-fs-meta); } +.sp-badge { background: transparent; box-shadow: inset 0 0 0 1.5px var(--d-separator); font-weight: 600; } +.sp-team > span:last-child { font-size: var(--c-fs-body); } +.sp-sub, .sp-extra { font-size: var(--c-fs-meta); } + +/* Formula One: the next race on one flat step, no gradient */ +.f1-next { background: var(--d-fill); border-radius: var(--c-r); } +.f1-race { font-weight: 600; font-size: var(--c-fs-mid); } +.f1-place, .f1-top, .f1-sessions { font-size: var(--c-fs-meta); } +.f1-name span { font-size: 12px; } +.f1-name b { font-size: var(--c-fs-body); font-weight: 600; } +.f1-tabs button { font-size: var(--c-fs-meta); min-height: 30px; } +.f1-tabs button[aria-selected="true"] { box-shadow: none; } +.f1-pos { font-weight: 500; } + +/* calendar */ +.cal-sum { font-size: var(--c-fs-body); } +.cal-when, .cal-sub { font-size: var(--c-fs-meta); } +.cal-day { font-size: var(--c-fs-meta); font-weight: 600; } + +/* waste: the bin colours are the bins' own, kept small */ +.waste-when { font-size: clamp(22px, 9cqw, 32px); } +.waste-types { font-size: var(--c-fs-body); } +.waste-day { font-size: var(--c-fs-body); font-weight: 500; } + +/* swipe and scroll containers: dots and arrows as plain floating controls */ +.sw-dots, .hs-nav { backdrop-filter: none; -webkit-backdrop-filter: none; background: var(--surface); } + +/* energy: the power flow */ +.pf { display: flex; flex-direction: column; gap: 12px; height: 100%; min-height: 0; } +.pf-head { display: flex; align-items: baseline; justify-content: space-between; } +.pf-nodes { display: grid; grid-template-columns: repeat(var(--n, 4), minmax(0, 1fr)); gap: 12px 10px; } +.pf-nodes.n2 { --n: 2; } .pf-nodes.n3 { --n: 3; } +.pf-node { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.pf-lbl { display: flex; align-items: center; gap: 6px; font-size: var(--c-fs-meta); color: var(--d-text-2); white-space: nowrap; } +.pf-lbl .i { font-size: 16px; flex: none; } +.pf-sun .pf-lbl .i { color: var(--c-sun); } +.pf-fig { font-size: clamp(22px, 7.5cqw, 32px); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; white-space: nowrap; } +.pf-fig small { font-size: .5em; color: var(--d-text-2); margin-left: 3px; letter-spacing: 0; } +.pf-sub { font-size: var(--c-fs-meta); color: var(--d-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.pf-flows { display: flex; flex-direction: column; gap: 6px; min-height: 0; overflow-y: auto; } +.pf-flow { display: grid; grid-template-columns: minmax(0, 1fr) auto; row-gap: 4px; align-items: baseline; font-size: var(--c-fs-body); } +.pf-flow-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pf-flow-v { font-weight: 500; } +.pf-flow i { grid-column: 1 / -1; display: block; height: 4px; border-radius: 2px; background: var(--d-text); min-width: 4px; } +.pf-flow.pf-sun i { background: var(--c-sun); } +.pf-flow.pf-grid i { background: color-mix(in srgb, var(--d-text) 35%, transparent); } +.pf-none { font-size: var(--c-fs-body); color: var(--d-text-2); } +.pf-totals { margin-top: auto; display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: var(--c-fs-meta); color: var(--d-text-2); } +.pf-total b { color: var(--d-text); font-weight: 500; font-size: var(--c-fs-body); } +/* a square card: the four places as two by two */ +@container (max-width: 520px) { .pf-nodes.n4, .pf-nodes.n3 { --n: 2; } } +/* not much height: the flows say what the sublines would */ +@container (max-height: 440px) { .pf-sub { display: none; } .pf-nodes { row-gap: 8px; } } +@container (max-height: 260px) { .pf-flows { display: none; } } + +/* ---------------- the tablet and wall panel band */ +@media (max-width: 1100px), (pointer: coarse) { + .swatch { width: 30px; height: 30px; } + .swatches { gap: 10px; } + .todo-del { opacity: .6; width: var(--c-ctl); height: var(--c-ctl); } + .fc-hour { flex-basis: 58px; } + .cv-btns { gap: 8px; } + .nt-x { width: var(--c-ctl); height: var(--c-ctl); } + .mp-switch .chip { min-height: 38px; } + .f1-tabs button { min-height: 38px; } +} + +/* ---------------- the phone band */ +@media (max-width: 640px) { + /* colours and hours run in one row you can swipe, so the card keeps its height */ + .swatches { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; } + .swatches::-webkit-scrollbar { display: none; } + .vac-btn span { display: none; } + .vac-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .al-keys { gap: 8px; } + .pf-nodes { gap: 12px; } + .room-devices { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .appl-facts { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; } + .an-form { grid-template-columns: 1fr; } +} + +/* round controls: an outline, filled only while pressed or when they are the main action */ +.round { background: transparent; box-shadow: inset 0 0 0 1.5px var(--d-separator); } +.round.primary { box-shadow: none; } +.appl-btn, .nt-x { background: transparent; box-shadow: inset 0 0 0 1.5px var(--d-separator); } +.nt-x { box-shadow: none; } +.swatch { box-shadow: none; border: 1px solid rgba(0, 0, 0, .12); box-sizing: border-box; } +/* sliders: a flat track with a flat fill (the thumb paints the filled part), no gradient */ +.slider { background: var(--d-fill); overflow: hidden; } +.slider::-webkit-slider-thumb, .lm-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 4px; height: 100px; border: 0; border-radius: 0; + background: var(--slider-fill, var(--d-text)); box-shadow: -2000px 0 0 2000px var(--slider-fill, var(--d-text)); } +.slider::-moz-range-thumb { width: 4px; height: 100px; border: 0; border-radius: 0; background: var(--slider-fill, var(--d-text)); } +.slider::-moz-range-progress { height: 100%; background: var(--slider-fill, var(--d-text)); } +.lm-slider { height: 10px; border-radius: 5px; } +/* the night dimmer only exists while it dims */ +body:not([style*="--night-dim: 0."]):not([style*="--night-dim: 1"]) #night-dim { display: none; } +/* a heading keeps its set size, but a phone's title stays phone-sized */ +@media (max-width: 640px) { .heading .h-main { font-size: min(var(--h-size, 24px), 24px) !important; } } +/* outlines and separators as borders, so nothing at rest carries a shadow */ +.round { box-shadow: none; border: 1.5px solid var(--d-separator); box-sizing: border-box; } +.round.primary { border-color: transparent; } +.appl-btn { box-shadow: none; border: 1.5px solid var(--d-separator); box-sizing: border-box; } +.list-row + .list-row { box-shadow: none; border-top: 1px solid var(--d-separator); } +#grid .seg button[aria-pressed="true"], #grid .f1-tabs button[aria-selected="true"] { box-shadow: none; } +.hs-nav { box-shadow: none; border: 1px solid var(--separator); } +.swatch.more, .w.lit-lamp .swatch.more { background: transparent; border-color: color-mix(in srgb, currentColor 30%, transparent); } +/* each slider knows its track height, and its fill-painting thumb is exactly that tall */ +.slider { --trk: calc(var(--c-ctl) + 8px); height: var(--trk); } +.slider.thin { --trk: 28px; } +.lm-slider, .cv-slider { --trk: 10px; } +.slider::-webkit-slider-thumb, .lm-slider::-webkit-slider-thumb { height: var(--trk); } +.slider::-moz-range-thumb { height: var(--trk); } +/* solar: today's hours in the sun's colour, the hours gone faded */ +.sf-h i, .sf-h.now i { background: var(--c-sun); } +.sf-h.past i { opacity: .35; } +.sf-h.best i { background: var(--c-sun); } +.sf-h:not(.best) i { opacity: .7; } +.sf-h.past:not(.best) i { opacity: .3; } +/* light: the brightness bar never gives way; the colours stay one row you can swipe */ +.light .slider { flex: none; } +.swatches { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; } +.swatches::-webkit-scrollbar { display: none; } +.sf-h:not(.best) i { opacity: 1; } +.sf-h.past:not(.best) i, .sf-h.past i { opacity: .35; } + +/* narrow cards: a tablet's four-column card is about 250px, so names get the width and figures go underneath */ +@container (max-width: 340px) { + .aq-row { grid-template-columns: auto minmax(0, 1fr); row-gap: 4px; } + .aq-vals { grid-column: 2; justify-content: flex-start; gap: 12px; } + .aq-val { flex-direction: row; align-items: baseline; gap: 4px; min-width: 0; padding: 0; } + .aq-val small { display: inline; } + .pc-row { flex-wrap: wrap; row-gap: 2px; } + .pc-when { flex-basis: 100%; text-align: left; padding-left: 34px; } + .cv-head { flex-wrap: wrap; } + .cv-head .name { flex-basis: 100%; } + .cv-row { flex-wrap: wrap; row-gap: 8px; } + .cv-row-main { flex-basis: calc(100% - 42px); } + .cv-btns { margin-left: 42px; } + .nt-item { flex-wrap: wrap; } +} +/* very narrow (half a phone): blinds keep only what each row needs, the timer's figure fits its ring */ +@container (max-width: 240px) { + .cv-head .chip, .cv-head .wbtn:not(.name) { display: none; } + .cv-row .cv-win { display: none; } + .cv-row-main { flex-basis: 100%; } + .cv-btns { margin-left: 0; width: 100%; justify-content: space-between; } + .cv-btns .round { width: 40px; height: 40px; } + .climate .round { width: 38px; height: 38px; } +} +.tm-time { font-size: clamp(20px, min(14cqh, 15cqw), 44px); } diff --git a/dashboard_studio/web/widgets.js b/dashboard_studio/web/widgets.js index 2f7fb58..21f0ec9 100644 --- a/dashboard_studio/web/widgets.js +++ b/dashboard_studio/web/widgets.js @@ -109,7 +109,7 @@ { k: "align", t: "seg", l: "Alignment", o: ["left", "center", "right"], d: "left" }, ], render: (c) => `
-

${esc(c.text)}

+

${esc(c.text)}

${c.subtitle ? `
${esc(c.subtitle)}
` : ""}
`, }; @@ -800,33 +800,30 @@ const has = { solar: !!c.solar, grid: !!c.grid, battery: !!c.battery }; const lvl = Number(S("batteryLevel")?.state); - const flow = (on, d, w, cls) => { - if (!on) return ""; - const speed = Math.max(0.5, Math.min(5, 2400 / (w + 150))); // seconds per cycle: more power, faster dots - const width = w > 1 ? Math.max(1.5, Math.min(5, 1.2 + Math.log10(w + 1))) : 1; - return `${w > 1 ? `` : ""}`; - }; - const lines = [ - flow(has.solar, "M50 14 Q86 14 86 50", sToH, "solar"), - flow(has.solar && has.grid, "M50 14 Q14 14 14 50", sToG, "solar"), - flow(has.solar && has.battery, "M50 14 L50 86", sToB, "solar"), - flow(has.grid, "M14 50 L86 50", gToH, "grid"), - flow(has.grid && has.battery, "M14 50 Q14 86 50 86", gToB, "grid"), - flow(has.battery, "M50 86 Q86 86 86 50", bToH, "battery"), - ].join(""); - const node = (key, ico, label, value, extra = "") => `
${Icons.svg(ico)} - ${esc(value)}${esc(label)}${extra}
`; - const totals = (c.today || []).map((e) => ha.states[e] && `${esc(fmtState(ha.states[e]))} ${esc(friendly(ha.states[e]))}`).filter(Boolean).join(""); + // Where the power is going now: the four places in one row, then each flow that runs, largest first. + // They balance: sun + grid in + battery out = house + battery in + grid out. + const fig = (w) => { const [n, u] = fmtW(w).split(" "); return `${esc(n)}${esc(u || "")}`; }; + const node = (key, ico, label, figure, sub) => `
${Icons.svg(ico)}${esc(label)}${figure}${esc(sub)}
`; + const nodes = [ + has.solar ? node("sun", "sun", t("Sun"), fig(solar), solar > 1 ? t("Producing") : t("Not producing")) : "", + node("home", "home", t("House"), fig(home), t("In use")), + has.battery ? node("battery", "battery", t("Battery"), isFinite(lvl) ? `${Math.round(lvl)}%` : fig(Math.abs(batt)), + charge > 1 ? t("Charging {0}", fmtW(charge)) : discharge > 1 ? t("Supplying {0}", fmtW(discharge)) : t("Idle")) : "", + has.grid ? node("grid", "plug", t("Grid"), fig(exp > imp ? exp : imp), exp > imp ? t("Sending back") : imp > 1 ? t("Drawing") : t("Idle")) : "", + ].filter(Boolean); + const flows = [ + ["sun", t("Sun to the house"), sToH], ["sun", t("Sun to the battery"), sToB], ["sun", t("Sun to the grid"), sToG], + ["grid", t("Grid to the house"), gToH], ["grid", t("Grid to the battery"), gToB], ["battery", t("Battery to the house"), bToH], + ].filter(([, , w]) => w > 1).sort((a, b) => b[2] - a[2]); + const top = Math.max(1, ...flows.map(([, , w]) => w)); + const totals = (c.today || []).map((e) => ha.states[e] && `${esc(fmtState(ha.states[e]))} ${esc(friendly(ha.states[e]))}`).filter(Boolean).join(""); - return `
- - ${totals ? `
${totals}
` : ""}
`; + return `
+
${t("Energy now")}
+
${nodes.join("")}
+
${flows.length ? flows.map(([k, label, w]) => `
${esc(label)}${esc(fmtW(w))}
`).join("") + : `
${t("No power is flowing right now.")}
`}
+ ${totals ? `
${totals}
` : ""}
`; }, }; @@ -927,11 +924,6 @@ const mapUrl = c.map ? (domainOf(c.map) === "image" ? ha.states[c.map]?.attributes.entity_picture : `api/camera/${encodeURIComponent(c.map)}?t=${Date.now()}`) : ""; const speeds = c.fan !== false && Array.isArray(a.fan_speed_list) ? a.fan_speed_list : []; - // battery ring around the robot - const R = 46, C = 2 * Math.PI * R, frac = hasBatt ? Math.max(0, Math.min(1, batt / 100)) : 0; - const ring = ``; - const stat = (label, value, ico) => `
${Icons.svg(ico)}${esc(value)}${esc(label)}
`; const stats = [ hasBatt ? stat(t("Battery"), `${Math.round(batt)}%`, "battery") : "", @@ -948,9 +940,9 @@ ${hasBatt ? `${Icons.svg(charging ? "bolt" : "battery")}${Math.round(batt)}%` : ""}
${mapUrl ? `${t(` - : `
${ring}${Icons.svg("vacuum")} - ${charging ? `` : ""}
-
${esc(headline)}${hasBatt ? `${t("Battery {0}%", Math.round(batt))}` : ""}
`} + : `
${esc(headline)} + ${hasBatt ? ` + ${charging ? t("Battery {0}%, charging", Math.round(batt)) : t("Battery {0}%", Math.round(batt))}` : ""}
`}
${stats ? `
${stats}
` : ""} ${speeds.length ? `
${speeds.map((f) => ``).join("")}
` : ""} @@ -1512,11 +1504,11 @@ const by = changedBy(s, ha); const since = `${open ? t("Open") : st(st0)} · ${relTime(s.last_changed)}${by ? ` · ${t("by {0}", by)}` : ""}`; const ico = { garage: "garage", gate: "gate", door: "door" }[c.kind] || "garage"; + const when = `${relTime(s.last_changed)}${by ? ` · ${t("by {0}", by)}` : ""}`; return `
-
${esc(name)}${esc(open ? t("Open") : d === "button" ? "" : st(st0))}
- -
${esc(since)}
`; +
${Icons.svg(open && c.kind === "door" ? "door-open" : ico)}${esc(name)}
+
${esc(d === "button" ? name : open ? t("Open") : st(st0))}${esc(when)}
+ `; }, };