Kiosk: dashboard-matching state-colour borders, AC top-right + TRV row layout

- kiosk-block border-left now uses the same st-vacant/booked/heating_up/
  occupied/cooling_down colour scheme as the Dashboard's ZoneCard
- Layout tweak (user, 2026-08-26): a room only ever has one AC unit, so
  it's pinned top-right; lower half is a row of up to 3 TRVs with setpoint
  stacked below the icon rather than beside it (more legible at kiosk touch
  size than the dashboard's compact rad-row)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-26 10:57:31 +00:00
parent b6352cf5f7
commit 65d53d1d41
2 changed files with 44 additions and 28 deletions

View file

@ -32,38 +32,31 @@ interface Props {
} }
// One tile per ROOM/ZONE — same idea as the Dashboard's ZoneCard, not one tile // 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 // per device (per user correction, 2026-08-26). Border colour is the room's
// the exact same rad-row/ac-block markup+CSS ZoneCard.tsx uses, so the kiosk // live state, same st-* classes/vars ZoneCard uses, so a tile reads the same
// visually matches the staff dashboard (per user correction, 2026-08-26, to an // way on the kiosk as on the staff dashboard. Layout differs from ZoneCard
// earlier per-device-tile version of this file). // 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) { export default function KioskZoneBlock({ zone, liveDevices, selectionMode, selected, onTap }: Props) {
const trvs = liveDevices const trvs = liveDevices
.filter(d => d.device_type === 'shelly_trv') .filter(d => d.device_type === 'shelly_trv')
.sort((a, b) => locationRank(a.location) - locationRank(b.location)) .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 ac = liveDevices.find(d => KIOSK_AC_DEVICE_TYPES.includes(d.device_type))
const acOn = ac?.power_state === 'on' const acOn = ac?.power_state === 'on'
const ModeIcon = MODE_ICONS[ac?.current_mode || ''] || Fan const ModeIcon = MODE_ICONS[ac?.current_mode || ''] || Fan
const fanLevel = ac ? (MHI_FAN_SPEEDS as readonly string[]).indexOf(ac.fan_speed || '') + 1 : 0 const fanLevel = ac ? (MHI_FAN_SPEEDS as readonly string[]).indexOf(ac.fan_speed || '') + 1 : 0
return ( return (
<div className={`kiosk-block${selected ? ' selected' : ''}`} onClick={onTap} role="button"> <div className={`kiosk-block st-${zone.room_state}${selected ? ' selected' : ''}`} onClick={onTap} role="button">
{selectionMode && ( {selectionMode && (
<div className="kiosk-block-check">{selected && <Check size={14} strokeWidth={2.5} />}</div> <div className="kiosk-block-check">{selected && <Check size={14} strokeWidth={2.5} />}</div>
)} )}
<div className="kiosk-block-zone">{zone.name}</div>
<div className="kiosk-block-body">
{trvs.length > 0 && (
<div className="rad-row">
{trvs.slice(0, 3).map(d => (
<div key={d.id} className={`rad-icon ${d.heating_active ? 'heating' : 'idle'}`}>
<Heater size={16} strokeWidth={1.75} />
<span>{fmtTemp(d.current_target_temp)}</span>
</div>
))}
</div>
)}
<div className="kiosk-block-top">
<span className="kiosk-block-zone">{zone.name}</span>
{ac && ( {ac && (
<div className={`ac-block${acOn ? '' : ' off'}`}> <div className={`ac-block${acOn ? '' : ' off'}`}>
{ac.remote_locked && <Lock size={12} strokeWidth={1.75} className="ac-lock" />} {ac.remote_locked && <Lock size={12} strokeWidth={1.75} className="ac-lock" />}
@ -76,9 +69,20 @@ export default function KioskZoneBlock({ zone, liveDevices, selectionMode, selec
<div className="ac-temp">{fmtTemp(ac.current_target_temp)} / {fmtTemp(ac.current_room_temp)}</div> <div className="ac-temp">{fmtTemp(ac.current_target_temp)} / {fmtTemp(ac.current_room_temp)}</div>
</div> </div>
)} )}
{trvs.length === 0 && !ac && <span className="muted" style={{ fontSize: 11.5 }}>No live devices</span>}
</div> </div>
{trvs.length > 0 ? (
<div className="kiosk-trv-row">
{trvs.map(d => (
<div key={d.id} className={`kiosk-trv${d.heating_active ? ' heating' : ''}`}>
<Heater size={18} strokeWidth={1.75} />
<span>{fmtTemp(d.current_target_temp)}</span>
</div>
))}
</div>
) : !ac ? (
<span className="muted" style={{ fontSize: 11.5 }}>No live devices</span>
) : null}
</div> </div>
) )
} }

View file

@ -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-wrap { flex: 1; overflow-y: auto; padding: 16px; }
.kiosk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; } .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 /* One tile per zone. Border colour = the room's live state, same st-* vars
(defined above under "Zone grid / cards") so a room's TRV + AC read ZoneCard uses (see "Zone grid / cards" above) — a tile reads the same way
identically here and on the Dashboard — just the outer tile is kiosk-sized. */ 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 { .kiosk-block {
position: relative; cursor: pointer; background: var(--card-bg); position: relative; cursor: pointer; background: var(--card-bg);
border: 1px solid var(--card-border); border-radius: var(--radius); 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: 96px; box-shadow: var(--shadow-sm); padding: 12px; min-height: 112px;
display: flex; flex-direction: column; gap: 8px; user-select: none; 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.selected { outline: 2px solid var(--gold); outline-offset: -2px; }
.kiosk-block-check { .kiosk-block-check {
position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; border-radius: 5px; 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); 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.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-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; padding-right: 24px; }
.kiosk-block-body { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex: 1; } .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 { .kiosk-bulk-bar {
position: sticky; bottom: 0; display: flex; align-items: center; gap: 12px; position: sticky; bottom: 0; display: flex; align-items: center; gap: 12px;