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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
|
||||
+417
-15
@@ -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); }
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
{ k: "align", t: "seg", l: "Alignment", o: ["left", "center", "right"], d: "left" },
|
||||
],
|
||||
render: (c) => `<div class="heading" style="text-align:${esc(c.align)}">
|
||||
<h2 class="h-main" style="font-size:${Number(c.size) || 30}px">${esc(c.text)}</h2>
|
||||
<h2 class="h-main" style="--h-size:${Number(c.size) || 30}px;font-size:${Number(c.size) || 30}px">${esc(c.text)}</h2>
|
||||
${c.subtitle ? `<div class="wt wrap">${esc(c.subtitle)}</div>` : ""}</div>`,
|
||||
};
|
||||
|
||||
@@ -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 `<path d="${d}" class="ef-line ${cls}"/>${w > 1 ? `<path d="${d}" class="ef-flow ${cls}" style="stroke-width:${width.toFixed(1)};animation-duration:${speed.toFixed(2)}s"/>` : ""}`;
|
||||
};
|
||||
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 = "") => `<div class="ef-node ef-${key}"><span class="ef-ico">${Icons.svg(ico)}</span>
|
||||
<span class="ef-val num">${esc(value)}</span><span class="ef-label">${esc(label)}${extra}</span></div>`;
|
||||
const totals = (c.today || []).map((e) => ha.states[e] && `<span class="ef-total"><b class="num">${esc(fmtState(ha.states[e]))}</b> ${esc(friendly(ha.states[e]))}</span>`).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 `<span class="pf-fig num">${esc(n)}<small>${esc(u || "")}</small></span>`; };
|
||||
const node = (key, ico, label, figure, sub) => `<div class="pf-node pf-${key}"><span class="pf-lbl">${Icons.svg(ico)}${esc(label)}</span>${figure}<span class="pf-sub">${esc(sub)}</span></div>`;
|
||||
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) ? `<span class="pf-fig num">${Math.round(lvl)}<small>%</small></span>` : 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] && `<span class="pf-total"><b class="num">${esc(fmtState(ha.states[e]))}</b> ${esc(friendly(ha.states[e]))}</span>`).filter(Boolean).join("");
|
||||
|
||||
return `<div class="energy ${has.battery ? "" : "no-battery"} ${has.solar ? "" : "no-solar"}">
|
||||
<div class="ef-diagram" role="img" aria-label="${esc(t("Solar {0}, grid {1}, home {2}", fmtW(solar), grid >= 0 ? fmtW(imp) : "−" + fmtW(exp), fmtW(home)))}">
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">${lines}</svg>
|
||||
${has.solar ? node("solar", "sun", t("Solar"), fmtW(solar)) : ""}
|
||||
${has.grid ? node("grid", "plug", exp > imp ? t("To the grid") : t("From the grid"), fmtW(exp > imp ? exp : imp)) : ""}
|
||||
${node("home", "home", t("Home"), fmtW(home))}
|
||||
${has.battery ? node("battery", "battery", charge > 1 ? t("Charging") : discharge > 1 ? t("Discharging") : t("Battery"), isFinite(lvl) ? `${Math.round(lvl)}%` : fmtW(Math.abs(batt)), isFinite(lvl) && Math.abs(batt) > 1 ? ` · ${fmtW(Math.abs(batt))}` : "") : ""}
|
||||
</div>
|
||||
${totals ? `<div class="ef-totals">${totals}</div>` : ""}</div>`;
|
||||
return `<div class="pf">
|
||||
<div class="pf-head"><span class="wt">${t("Energy now")}</span></div>
|
||||
<div class="pf-nodes n${nodes.length}">${nodes.join("")}</div>
|
||||
<div class="pf-flows">${flows.length ? flows.map(([k, label, w]) => `<div class="pf-flow pf-${k}"><span class="pf-flow-lbl">${esc(label)}</span><span class="pf-flow-v num">${esc(fmtW(w))}</span><i style="width:${((w / top) * 100).toFixed(1)}%"></i></div>`).join("")
|
||||
: `<div class="pf-none">${t("No power is flowing right now.")}</div>`}</div>
|
||||
${totals ? `<div class="pf-totals">${totals}</div>` : ""}</div>`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 = `<svg class="vac-ring" viewBox="0 0 100 100" aria-hidden="true"><circle cx="50" cy="50" r="${R}" class="track"/>
|
||||
${hasBatt ? `<circle cx="50" cy="50" r="${R}" class="fill ${batt <= 20 ? "low" : ""}" stroke-dasharray="${(C * frac).toFixed(1)} ${C.toFixed(1)}"/>` : ""}</svg>`;
|
||||
|
||||
const stat = (label, value, ico) => `<div class="vac-stat">${Icons.svg(ico)}<span class="vac-stat-v num">${esc(value)}</span><span class="vac-stat-l">${esc(label)}</span></div>`;
|
||||
const stats = [
|
||||
hasBatt ? stat(t("Battery"), `${Math.round(batt)}%`, "battery") : "",
|
||||
@@ -948,9 +940,9 @@
|
||||
${hasBatt ? `<span class="vac-batt">${Icons.svg(charging ? "bolt" : "battery")}<span class="num">${Math.round(batt)}%</span></span>` : ""}</div>
|
||||
<div class="vac-hero">
|
||||
${mapUrl ? `<img class="vac-map" src="${esc(mapUrl)}" ${domainOf(c.map) === "camera" ? `data-cam="${esc(c.map)}" data-refresh="${Number(c.refresh) || 5}"` : ""} alt="${t("Cleaning map")}">`
|
||||
: `<div class="vac-robot ${cleaning ? "is-cleaning" : ""}">${ring}<span class="vac-pulse" aria-hidden="true"></span><span class="vac-robot-ico">${Icons.svg("vacuum")}</span>
|
||||
${charging ? `<span class="vac-charge" aria-hidden="true">${Icons.svg("bolt")}</span>` : ""}</div>
|
||||
<div class="vac-hero-text"><span class="vac-headline">${esc(headline)}</span>${hasBatt ? `<span class="wt">${t("Battery {0}%", Math.round(batt))}</span>` : ""}</div>`}
|
||||
: `<div class="vac-hero-text"><span class="vac-headline">${esc(headline)}</span>
|
||||
${hasBatt ? `<span class="vac-meter ${batt < 20 ? "low" : ""}" role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${Math.round(batt)}" aria-label="${t("Battery")}"><i style="width:${Math.round(batt)}%"></i></span>
|
||||
<span class="wt num">${charging ? t("Battery {0}%, charging", Math.round(batt)) : t("Battery {0}%", Math.round(batt))}</span>` : ""}</div>`}
|
||||
</div>
|
||||
${stats ? `<div class="vac-stats">${stats}</div>` : ""}
|
||||
${speeds.length ? `<div class="seg vac-fan" role="group" aria-label="${t("Suction power")}">${speeds.map((f) => `<button class="wbtn" aria-pressed="${f === a.fan_speed}" data-act="vac-fan" data-e="${e}" data-v="${esc(f)}">${esc(cap(f))}</button>`).join("")}</div>` : ""}
|
||||
@@ -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 `<div class="gt ${open ? "open" : ""} ${moving ? "moving" : ""}">
|
||||
<div class="gt-head"><span class="name">${esc(name)}</span><span class="gt-state">${esc(open ? t("Open") : d === "button" ? "" : st(st0))}</span></div>
|
||||
<button class="gt-btn wbtn" data-act="gate" data-e="${esc(c.entity)}" data-v="${c.confirm === false ? "" : "1"}" aria-label="${esc(`${label}: ${name}`)}">
|
||||
<span class="gt-ring">${Icons.svg(open && c.kind === "door" ? "door-open" : ico)}</span><span class="gt-lbl">${esc(label)}</span></button>
|
||||
<div class="gt-since">${esc(since)}</div></div>`;
|
||||
<div class="gt-head"><span class="gt-ico">${Icons.svg(open && c.kind === "door" ? "door-open" : ico)}</span><span class="name">${esc(name)}</span></div>
|
||||
<div class="gt-now"><span class="gt-big">${esc(d === "button" ? name : open ? t("Open") : st(st0))}</span><span class="gt-since" title="${esc(since)}">${esc(when)}</span></div>
|
||||
<button class="gt-btn wbtn" data-act="gate" data-e="${esc(c.entity)}" data-v="${c.confirm === false ? "" : "1"}" aria-label="${esc(`${label}: ${name}`)}"><span class="gt-lbl">${esc(label)}</span></button></div>`;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user