diff --git a/frontend/src/components/KioskZoneBlock.tsx b/frontend/src/components/KioskZoneBlock.tsx
index ff75738..4a3092c 100644
--- a/frontend/src/components/KioskZoneBlock.tsx
+++ b/frontend/src/components/KioskZoneBlock.tsx
@@ -32,38 +32,31 @@ interface Props {
}
// One tile per ROOM/ZONE — same idea as the Dashboard's ZoneCard, not one tile
-// per device. A room with both a TRV and an AC unit shows both in one tile, via
-// the exact same rad-row/ac-block markup+CSS ZoneCard.tsx uses, so the kiosk
-// visually matches the staff dashboard (per user correction, 2026-08-26, to an
-// earlier per-device-tile version of this file).
+// per device (per user correction, 2026-08-26). Border colour is the room's
+// live state, same st-* classes/vars ZoneCard uses, so a tile reads the same
+// way on the kiosk as on the staff dashboard. Layout differs from ZoneCard
+// deliberately (user, 2026-08-26): a room only ever has one AC unit, so it's
+// pinned top-right, leaving the lower half for a row of up to 3 TRVs (setpoint
+// stacked below the icon rather than beside it — more legible at a glance/touch
+// size than the dashboard's compact side-by-side rad-row).
export default function KioskZoneBlock({ zone, liveDevices, selectionMode, selected, onTap }: Props) {
const trvs = liveDevices
.filter(d => d.device_type === 'shelly_trv')
.sort((a, b) => locationRank(a.location) - locationRank(b.location))
+ .slice(0, 3)
const ac = liveDevices.find(d => KIOSK_AC_DEVICE_TYPES.includes(d.device_type))
const acOn = ac?.power_state === 'on'
const ModeIcon = MODE_ICONS[ac?.current_mode || ''] || Fan
const fanLevel = ac ? (MHI_FAN_SPEEDS as readonly string[]).indexOf(ac.fan_speed || '') + 1 : 0
return (
-
+
{selectionMode && (
{selected && }
)}
-
{zone.name}
-
-
- {trvs.length > 0 && (
-
- {trvs.slice(0, 3).map(d => (
-
-
- {fmtTemp(d.current_target_temp)}
-
- ))}
-
- )}
+
+
{zone.name}
{ac && (
{ac.remote_locked &&
}
@@ -76,9 +69,20 @@ export default function KioskZoneBlock({ zone, liveDevices, selectionMode, selec
{fmtTemp(ac.current_target_temp)} / {fmtTemp(ac.current_room_temp)}
)}
-
- {trvs.length === 0 && !ac &&
No live devices}
+
+ {trvs.length > 0 ? (
+
+ {trvs.map(d => (
+
+
+ {fmtTemp(d.current_target_temp)}
+
+ ))}
+
+ ) : !ac ? (
+
No live devices
+ ) : null}
)
}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index dbb4b40..a74ff5b 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -456,15 +456,23 @@ table.data tr:last-child td { border-bottom: none; }
.kiosk-grid-wrap { flex: 1; overflow-y: auto; padding: 16px; }
.kiosk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
-/* One tile per zone, reusing ZoneCard's own rad-row/ac-block/fan-bar classes
- (defined above under "Zone grid / cards") so a room's TRV + AC read
- identically here and on the Dashboard — just the outer tile is kiosk-sized. */
+/* One tile per zone. Border colour = the room's live state, same st-* vars
+ ZoneCard uses (see "Zone grid / cards" above) — a tile reads the same way
+ here as on the Dashboard. Top area: zone name + the (at most one) AC block,
+ pinned top-right via space-between. Bottom half: a row of up to 3 TRVs,
+ pushed down by the outer flex's space-between — the "lower half" layout
+ the dashboard's cards don't need since staff view is denser/smaller text. */
.kiosk-block {
position: relative; cursor: pointer; background: var(--card-bg);
- border: 1px solid var(--card-border); border-radius: var(--radius);
- box-shadow: var(--shadow-sm); padding: 12px; min-height: 96px;
- display: flex; flex-direction: column; gap: 8px; user-select: none;
+ border: 1px solid var(--card-border); border-left: 4px solid var(--st-vacant); border-radius: var(--radius);
+ box-shadow: var(--shadow-sm); padding: 12px; min-height: 112px;
+ display: flex; flex-direction: column; justify-content: space-between; gap: 10px; user-select: none;
}
+.kiosk-block.st-vacant { border-left-color: var(--st-vacant); }
+.kiosk-block.st-booked { border-left-color: var(--st-booked); }
+.kiosk-block.st-heating_up { border-left-color: var(--st-heating-up); }
+.kiosk-block.st-occupied { border-left-color: var(--st-occupied); }
+.kiosk-block.st-cooling_down { border-left-color: var(--st-cooling-down); }
.kiosk-block.selected { outline: 2px solid var(--gold); outline-offset: -2px; }
.kiosk-block-check {
position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; border-radius: 5px;
@@ -472,8 +480,12 @@ table.data tr:last-child td { border-bottom: none; }
color: var(--navy); background: var(--card-bg);
}
.kiosk-block.selected .kiosk-block-check { background: var(--gold); border-color: var(--gold); color: var(--navy); }
-.kiosk-block-zone { font-size: 13px; font-weight: 700; color: var(--text-dark); padding-right: 22px; }
-.kiosk-block-body { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex: 1; }
+.kiosk-block-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; padding-right: 24px; }
+.kiosk-block-zone { font-size: 13px; font-weight: 700; color: var(--text-dark); }
+
+.kiosk-trv-row { display: flex; gap: 16px; }
+.kiosk-trv { display: flex; flex-direction: column; align-items: center; gap: 3px; font-size: 11.5px; font-weight: 600; color: var(--text-mid); }
+.kiosk-trv.heating { color: var(--danger); }
.kiosk-bulk-bar {
position: sticky; bottom: 0; display: flex; align-items: center; gap: 12px;