Skip to content

Commit 286ac14

Browse files
frahlgclaude
andauthored
feat(web): the dashboard takes the iOS app's palette (#1857)
theme.css now carries the phone's colours: a blue-black ground, ink for the interface, one grid hue both ways, solar, a battery that stores in cyan and gives in orange, a violet car, and two states, --live and --alarm. --green and --red now alias those states; energy keeps the -e names. Every use of a red, green or amber name is sorted into energy or state. States (ok, connected, done, failed, blocked, danger buttons, warnings that need action) move to --live or --alarm; warnings that need no action move to ink2. Battery, car and solar flows that sat on the grid names move to their own hue. Prices and savings become ink levels. Text on an accent fill reads --on-accent, now that the accent is ink. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 67d65c6 commit 286ac14

28 files changed

Lines changed: 363 additions & 379 deletions

‎.changeset/ios-palette-web.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ftw": patch
3+
---
4+
5+
The box's dashboard now uses the phone app's colours. The grid is one pink colour both ways, and the sign and the word say whether you buy or sell. The battery is cyan when it stores power and orange when it gives power, and the car is violet. Green now only means live or confirmed, and red only means refused, failed or blocked. Prices are shades of ink, not red and green.

‎web/app.css‎

Lines changed: 41 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ body.ftw-app .icon-btn:hover {
209209
}
210210
body.ftw-app .icon-btn[aria-pressed="true"] {
211211
color: var(--fg);
212-
background: oklch(0.3 0.1 var(--accent-hue) / 0.2);
212+
background: color-mix(in oklab, var(--accent-e) 12%, transparent);
213213
border-color: var(--accent-e);
214214
}
215215
body.ftw-app .icon-btn[aria-pressed="true"]:hover {
@@ -346,7 +346,7 @@ body.ftw-app #overview-cars[hidden] {
346346
display: none !important;
347347
}
348348
body.ftw-app .overview-cars-age {
349-
color: var(--amber);
349+
color: var(--fg-dim);
350350
font-family: var(--mono);
351351
font-size: 11px;
352352
}
@@ -434,15 +434,15 @@ body.ftw-app .overview-health-dot {
434434
width: 7px;
435435
height: 7px;
436436
border-radius: 50%;
437-
background: var(--red-e);
438-
box-shadow: 0 0 0 3px color-mix(in oklch, var(--red-e) 14%, transparent);
437+
background: var(--alarm);
438+
box-shadow: 0 0 0 3px color-mix(in oklch, var(--alarm) 14%, transparent);
439439
}
440440
body.ftw-app .overview-health.is-connected {
441441
color: var(--fg-dim);
442442
}
443443
body.ftw-app .overview-health.is-connected .overview-health-dot {
444-
background: var(--green-e);
445-
box-shadow: 0 0 0 3px color-mix(in oklch, var(--green-e) 14%, transparent);
444+
background: var(--live);
445+
box-shadow: 0 0 0 3px color-mix(in oklch, var(--live) 14%, transparent);
446446
}
447447
body.ftw-app .section-kicker {
448448
margin: 0 0 3px;
@@ -746,10 +746,10 @@ body.ftw-app .val-import { color: var(--red-e); }
746746
body.ftw-app .val-export { color: var(--green-e); }
747747
body.ftw-app .val-generation { color: var(--amber); }
748748
body.ftw-app .val-charging { color: var(--cyan); }
749-
body.ftw-app .val-discharging { color: var(--cyan-dim); }
749+
body.ftw-app .val-discharging { color: var(--energy-gives); }
750750
body.ftw-app .val-neutral { color: var(--fg); }
751-
body.ftw-app .val-ev-charging { color: var(--green-e); }
752-
body.ftw-app .val-ev-connected{ color: var(--amber); }
751+
body.ftw-app .val-ev-charging { color: var(--violet); }
752+
body.ftw-app .val-ev-connected{ color: var(--fg-dim); }
753753

754754
/* Fuse — own row, one box per phase. */
755755
body.ftw-app .fuse-row {
@@ -814,7 +814,7 @@ body.ftw-app .fuse-phase-col .fuse-phase-w {
814814
padding: 0 4px;
815815
}
816816
body.ftw-app .fuse-phase-col .fuse-phase-w.export {
817-
color: var(--cyan);
817+
color: var(--green-e);
818818
}
819819
body.ftw-app .fuse-phase-box .fuse-phase-label {
820820
font-family: var(--mono);
@@ -857,11 +857,9 @@ body.ftw-app .fuse-fill {
857857
background: var(--green-e);
858858
transition: width 0.3s, background 0.3s;
859859
}
860-
body.ftw-app .fuse-phase-fill.warn,
861-
body.ftw-app .fuse-fill.warn { background: var(--amber); }
862860
body.ftw-app .fuse-phase-fill.crit,
863-
body.ftw-app .fuse-fill.crit { background: var(--red-e); }
864-
body.ftw-app .fuse-phase-fill.export { background: var(--cyan); }
861+
body.ftw-app .fuse-fill.crit { background: var(--alarm); }
862+
body.ftw-app .fuse-phase-fill.export { background: var(--green-e); }
865863
body.ftw-app .fuse-bar {
866864
/* Fallback single-bar track (used when the site meter doesn't
867865
report per-phase amperage). Same theme-aware background as the
@@ -1012,7 +1010,7 @@ body.ftw-app .history-toggle button {
10121010
cursor: pointer;
10131011
transition: color 220ms ease;
10141012
}
1015-
body.ftw-app .history-toggle button.active { color: #0a0a0a; }
1013+
body.ftw-app .history-toggle button.active { color: var(--on-accent); }
10161014
body.ftw-app .history-toggle button:not(.active):hover { color: var(--fg); }
10171015
body.ftw-app .history-toggle button:focus-visible {
10181016
outline: 1px solid var(--accent-e);
@@ -1167,7 +1165,7 @@ body.ftw-app .mode-buttons button:hover {
11671165
color: var(--fg);
11681166
}
11691167
body.ftw-app .mode-buttons button.active {
1170-
background: oklch(0.3 0.1 var(--accent-hue) / 0.25);
1168+
background: color-mix(in oklab, var(--accent-e) 14%, transparent);
11711169
border-color: var(--accent-e);
11721170
color: var(--fg);
11731171
}
@@ -1228,7 +1226,7 @@ body.ftw-app .plan-style-steps button:hover {
12281226
color: var(--fg);
12291227
}
12301228
body.ftw-app .plan-style-steps button[aria-checked="true"] {
1231-
background: oklch(0.3 0.1 var(--accent-hue) / 0.35);
1229+
background: color-mix(in oklab, var(--accent-e) 18%, transparent);
12321230
box-shadow: inset 0 0 0 1px var(--accent-e);
12331231
color: var(--fg);
12341232
}
@@ -1271,7 +1269,7 @@ body.ftw-app .plan-style-note .btn-link {
12711269
font-size: 13px;
12721270
}
12731271
body.ftw-app .plan-style-status {
1274-
color: var(--red-e);
1272+
color: var(--alarm);
12751273
}
12761274
/* The numbers describe the plan that is still running until the new one
12771275
lands, so they step back while the box replans. */
@@ -1465,8 +1463,7 @@ body.ftw-app .plan-horizon button {
14651463
body.ftw-app .plan-horizon button:hover { color: var(--fg); }
14661464
body.ftw-app .plan-horizon button.active {
14671465
background: var(--accent-e);
1468-
/* On-accent text is near-black per the shared design system, never white. */
1469-
color: #0a0a0a;
1466+
color: var(--on-accent);
14701467
}
14711468

14721469
body.ftw-app .chart-title-row {
@@ -1637,8 +1634,8 @@ body.ftw-app .live-stat-value {
16371634
}
16381635
body.ftw-app .live-stat-value.is-import { color: var(--red-e); }
16391636
body.ftw-app .live-stat-value.is-export { color: var(--green-e); }
1640-
body.ftw-app .live-stat-value.is-charging { color: var(--accent-e); }
1641-
body.ftw-app .live-stat-value.is-discharging { color: var(--blue-e, #60a5fa); }
1637+
body.ftw-app .live-stat-value.is-charging { color: var(--cyan); }
1638+
body.ftw-app .live-stat-value.is-discharging { color: var(--energy-gives); }
16421639
body.ftw-app .live-stat-value.is-neutral { color: var(--fg-dim); }
16431640

16441641
/* 24 h battery + SoC history block — sits below the live power chart.
@@ -1686,12 +1683,9 @@ body.ftw-app .range-buttons button:not(.active):hover {
16861683
border-color: var(--fg-dim);
16871684
}
16881685
body.ftw-app .range-buttons button.active {
1689-
/* On-accent text per the shared design system is near-black, not white — so the
1690-
amber ON state is readable in both themes and stops looking
1691-
washed-out in light mode where the old oklch(0.15 …) barely
1692-
differed from pure black. */
1686+
/* The ON state is ink with --on-accent text, so it reads in both themes. */
16931687
background: var(--accent-e);
1694-
color: #0a0a0a;
1688+
color: var(--on-accent);
16951689
border-color: var(--accent-e);
16961690
font-weight: 600;
16971691
}
@@ -1831,8 +1825,8 @@ body.ftw-app .plan-state-badge {
18311825
text-transform: uppercase;
18321826
}
18331827
body.ftw-app .plan-state-badge.is-active {
1834-
border-color: color-mix(in oklch, var(--green-e) 55%, var(--line));
1835-
color: var(--green-e);
1828+
border-color: color-mix(in oklab, var(--live) 55%, var(--line));
1829+
color: var(--live);
18361830
}
18371831
body.ftw-app .plan-state-badge.is-warn {
18381832
border-color: color-mix(in oklch, var(--accent-e) 60%, var(--line));
@@ -2204,20 +2198,20 @@ body.ftw-app .btn-add:disabled {
22042198
opacity: 0.75;
22052199
}
22062200
body.ftw-app .btn-remove {
2207-
color: var(--red-e);
2201+
color: var(--alarm);
22082202
flex: 0 0 auto;
22092203
}
22102204
body.ftw-app .btn-remove:hover {
22112205
background: transparent;
2212-
border-color: var(--red-e);
2206+
border-color: var(--alarm);
22132207
}
22142208
body.ftw-app .settings-status {
22152209
color: var(--fg-muted);
22162210
font-family: var(--mono);
22172211
}
2218-
body.ftw-app .settings-status.success { color: var(--green-e); }
2219-
body.ftw-app .settings-status.warning { color: var(--amber); }
2220-
body.ftw-app .settings-status.error { color: var(--red-e); }
2212+
body.ftw-app .settings-status.success { color: var(--live); }
2213+
body.ftw-app .settings-status.warning { color: var(--alarm); }
2214+
body.ftw-app .settings-status.error { color: var(--alarm); }
22212215
body.ftw-app .creds-badge {
22222216
border-radius: 999px;
22232217
font-family: var(--mono);
@@ -2226,9 +2220,9 @@ body.ftw-app .creds-badge {
22262220
text-transform: uppercase;
22272221
}
22282222
body.ftw-app .creds-badge.creds-saved {
2229-
background: color-mix(in srgb, var(--green-e) 10%, transparent);
2230-
border-color: color-mix(in srgb, var(--green-e) 35%, transparent);
2231-
color: var(--green-e);
2223+
background: color-mix(in srgb, var(--live) 10%, transparent);
2224+
border-color: color-mix(in srgb, var(--live) 35%, transparent);
2225+
color: var(--live);
22322226
}
22332227
body.ftw-app .creds-badge.creds-missing {
22342228
background: color-mix(in srgb, var(--accent-e) 10%, transparent);
@@ -2347,8 +2341,8 @@ body.ftw-app .drv-catalog-tag {
23472341
/* Four of thirty-seven have run on customer hardware. That is the difference
23482342
worth showing, so it is the only tag that carries colour. */
23492343
body.ftw-app .drv-catalog-proven {
2350-
border-color: color-mix(in srgb, var(--green-e) 40%, transparent);
2351-
color: var(--green-e);
2344+
border-color: color-mix(in srgb, var(--live) 40%, transparent);
2345+
color: var(--live);
23522346
}
23532347
body.ftw-app .drv-catalog-unproven {
23542348
border-color: color-mix(in srgb, var(--accent-e) 30%, transparent);
@@ -2449,10 +2443,10 @@ body.ftw-app .drv-editor-problem {
24492443
text-align: left;
24502444
}
24512445
body.ftw-app .drv-editor-problem:hover { color: var(--fg); }
2452-
body.ftw-app .drv-editor-error { border-left-color: var(--red-e); }
2446+
body.ftw-app .drv-editor-error { border-left-color: var(--alarm); }
24532447
body.ftw-app .drv-editor-warning { border-left-color: var(--accent-e); }
24542448
body.ftw-app .drv-editor-clean {
2455-
color: var(--green-e);
2449+
color: var(--live);
24562450
font-family: var(--mono);
24572451
font-size: 0.72rem;
24582452
}
@@ -2508,10 +2502,10 @@ body.ftw-app .driver-test-status {
25082502
font-size: 0.75rem;
25092503
}
25102504
body.ftw-app .driver-test-status.ok {
2511-
color: var(--green-e);
2505+
color: var(--live);
25122506
}
25132507
body.ftw-app .driver-test-status.error {
2514-
color: var(--red-e);
2508+
color: var(--alarm);
25152509
}
25162510
body.ftw-app .driver-test-output {
25172511
background: var(--ink-sunken);
@@ -2565,10 +2559,10 @@ body.ftw-app .v2x-command-status {
25652559
min-height: 1em;
25662560
}
25672561
body.ftw-app .v2x-command-status.ok {
2568-
color: var(--green-e);
2562+
color: var(--live);
25692563
}
25702564
body.ftw-app .v2x-command-status.error {
2571-
color: var(--red-e);
2565+
color: var(--alarm);
25722566
}
25732567
@media (max-width: 720px) {
25742568
body.ftw-app .v2x-control-panel {
@@ -2642,7 +2636,7 @@ body.ftw-app .driver-test-empty {
26422636
font-size: 0.78rem;
26432637
}
26442638
body.ftw-app .driver-test-error {
2645-
color: var(--red-e);
2639+
color: var(--alarm);
26462640
margin-top: 8px;
26472641
}
26482642

@@ -2985,9 +2979,6 @@ html[data-theme="light"] body.ftw-app > header .icon-btn,
29852979
html[data-theme="light"] body.ftw-app > header .settings-btn {
29862980
color: var(--fg);
29872981
}
2988-
html[data-theme="light"] body.ftw-app .mode-buttons button.active {
2989-
background: oklch(0.75 0.14 var(--accent-hue) / 0.25);
2990-
}
29912982
html[data-theme="light"] body.ftw-app .status-bar {
29922983
background: var(--ink-sunken);
29932984
}
@@ -3072,7 +3063,7 @@ body.ftw-app .twin-profile-toggle button {
30723063

30733064
body.ftw-app .twin-profile-toggle button.active {
30743065
background: var(--accent-e);
3075-
color: #0a0a0a;
3066+
color: var(--on-accent);
30763067
}
30773068

30783069
body.ftw-app .twin-profile-toggle button:not(.active):hover {

‎web/app.js‎

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -319,31 +319,11 @@
319319
return Math.round(w) + " W";
320320
}
321321

322-
// Fuse bar color — smooth green→yellow→orange→red gradient across
323-
// 0 %→100 %. Anchors land at the named color at each zone
324-
// boundary: 0 %=green, 50 %=yellow, 75 %=orange, 90 %+=red. Between
325-
// them the hue interpolates linearly so a bar at 45 % already reads
326-
// yellow-leaning (h ≈ 100, close to yellow's 95), not solid green —
327-
// matches the "45 % is more towards yellow than green" spec. Oklch
328-
// holds lightness + chroma constant so the transitions don't muddy.
322+
// Fuse bar color. The bar is grid power, so it takes the grid colour, as
323+
// on the phone; from 90 % it takes the alarm, because operators just need
324+
// to see "DANGER" there.
329325
function fuseFillColor(pct) {
330-
var c = Math.max(0, Math.min(100, pct));
331-
var h;
332-
if (c < 50) {
333-
// 0 → 50 : 150 (green) → 95 (yellow)
334-
h = 150 - (c / 50) * (150 - 95);
335-
} else if (c < 75) {
336-
// 50 → 75 : 95 (yellow) → 50 (orange)
337-
h = 95 - ((c - 50) / 25) * (95 - 50);
338-
} else if (c < 90) {
339-
// 75 → 90 : 50 (orange) → 22 (red)
340-
h = 50 - ((c - 75) / 15) * (50 - 22);
341-
} else {
342-
// 90 → 100: solid red. Saturate rather than continue shifting
343-
// hue — operators just need to see "DANGER", not a hue delta.
344-
h = 22;
345-
}
346-
return "oklch(0.78 0.18 " + h.toFixed(1) + ")";
326+
return pct >= 90 ? "var(--alarm)" : "var(--red-e)";
347327
}
348328

349329
// Snap an axis range to "nice" round numbers. Returns { min, max, step }
@@ -3442,7 +3422,7 @@
34423422
startBtn.style.cursor = "pointer";
34433423
startBtn.style.fontWeight = "600";
34443424
startBtn.style.background = "var(--accent-e)";
3445-
startBtn.style.color = "#0a0a0a";
3425+
startBtn.style.color = "var(--on-accent)";
34463426
function renderStartLabel() { startBtn.textContent = "Boost for " + dur.options[dur.selectedIndex].textContent; }
34473427
renderStartLabel();
34483428
dur.addEventListener("change", renderStartLabel);
@@ -3692,7 +3672,7 @@
36923672
startBtn.style.cursor = "pointer";
36933673
startBtn.style.fontWeight = "600";
36943674
startBtn.style.background = "var(--accent-e)";
3695-
startBtn.style.color = "#0a0a0a";
3675+
startBtn.style.color = "var(--on-accent)";
36963676

36973677
var stopBtn = document.createElement("button");
36983678
stopBtn.type = "button";
@@ -4376,7 +4356,7 @@
43764356
dayChips.forEach(function (chip) {
43774357
chip.setAttribute("aria-pressed", chip.on ? "true" : "false");
43784358
chip.style.background = chip.on ? "var(--accent-e)" : "transparent";
4379-
chip.style.color = chip.on ? "#0a0a0a" : "var(--text-dim)";
4359+
chip.style.color = chip.on ? "var(--on-accent)" : "var(--text-dim)";
43804360
chip.style.borderColor = chip.on ? "var(--accent-e)" : "var(--line)";
43814361
});
43824362
}

‎web/car-soc-history.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,16 +138,16 @@ function styles(doc) {
138138
.car-soc-history header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:18px;border-bottom:1px solid var(--line)}
139139
.car-soc-history h2{font-size:1.1rem;margin:0}.car-soc-history h3{font-size:.95rem;margin:0}
140140
.car-soc-history button{font:inherit;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:7px 12px;background:transparent;cursor:pointer}
141-
.car-soc-history button[aria-pressed=true]{background:var(--accent-e);color:#0a0a0a}
141+
.car-soc-history button[aria-pressed=true]{background:var(--accent-e);color:var(--on-accent)}
142142
.car-soc-history button:focus-visible{outline:2px solid var(--accent-e);outline-offset:3px}
143143
.car-soc-history .soc-body{padding:18px}.car-soc-history .soc-ranges{display:flex;gap:8px;margin-bottom:14px}
144144
.car-soc-history .soc-note{font-size:.82rem;color:var(--fg-muted);line-height:1.5;margin:10px 0}
145145
.car-soc-history .soc-source{border-top:1px solid var(--line);padding-top:14px;margin-top:14px}
146146
.car-soc-history .soc-last{font:1.3rem var(--mono);margin:8px 0}
147147
.car-soc-history svg{display:block;width:100%;height:auto;min-height:130px;overflow:visible}
148148
.car-soc-history .soc-grid{stroke:var(--line)}.car-soc-history .soc-axis{font:11px var(--mono);fill:var(--fg-muted)}
149-
.car-soc-history .soc-dot{fill:var(--cyan)}.car-soc-history .soc-dot:focus{stroke:var(--fg);stroke-width:2}
150-
.car-soc-history .soc-envelope{stroke:var(--cyan);stroke-width:2}
149+
.car-soc-history .soc-dot{fill:var(--violet)}.car-soc-history .soc-dot:focus{stroke:var(--fg);stroke-width:2}
150+
.car-soc-history .soc-envelope{stroke:var(--violet);stroke-width:2}
151151
@media(max-width:480px){.car-soc-history .soc-axis{font-size:24px}}
152152
`;
153153
}

‎web/components/energy-flow-readings.js‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ function planetColor(role, watts, thresholdW) {
5151
}
5252
if (role === "pv") return isIdle(watts, thresholdW) ? "var(--fg-muted)" : "var(--amber)";
5353
if (role === "battery") {
54-
return isIdle(watts, thresholdW) ? "var(--cyan)" : watts >= 0 ? "var(--green-e)" : "var(--red-e)";
54+
return !isIdle(watts, thresholdW) && watts < 0 ? "var(--energy-gives)" : "var(--cyan)";
5555
}
56-
if (role === "ev") return isIdle(watts, thresholdW) ? "var(--white-s)" : "var(--green-e)";
56+
if (role === "ev") return isIdle(watts, thresholdW) ? "var(--white-s)" : "var(--violet)";
5757
return "var(--fg)";
5858
}
5959

@@ -88,8 +88,8 @@ export function flowReadingsFromStatus(status, opts) {
8888
{ text: `↑ ${formatEnergyToday(today, "export_wh", false)}`, color: "var(--green-e)", bold: true },
8989
];
9090
const batDailyParts = [
91-
{ text: `↑ ${formatEnergyToday(today, "bat_charged_wh", false)}`, color: "var(--green-e)", bold: true },
92-
{ text: `↓ ${formatEnergyToday(today, "bat_discharged_wh", false)}`, color: "var(--red-e)", bold: true },
91+
{ text: `↑ ${formatEnergyToday(today, "bat_charged_wh", false)}`, color: "var(--cyan)", bold: true },
92+
{ text: `↓ ${formatEnergyToday(today, "bat_discharged_wh", false)}`, color: "var(--energy-gives)", bold: true },
9393
];
9494

9595
const gridW = num(status && status.grid_w);

0 commit comments

Comments
 (0)