Four cards is three too many to scroll past
The charging column grew a card at a time and now runs well past a screen, so each of the four folds away: connection, control, readings, information. The mechanism is the provider panel's, down to the chevron and the localStorage key — folding is a reading habit of this browser, not an account setting, and one learned gesture should work in both places. The connected badge stays in the connection header while it is folded, since whether the charger is reachable is the thing worth seeing without opening anything. Charger information keeps its Refresh button beside the toggle rather than inside it, a button being no place for another button. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bcc44da735
commit
b48019b08f
@@ -57,6 +57,41 @@ const sessionMetrics = computed(() =>
|
||||
// --- Real OCPP control (Anker Solix), gated by the per-user control mode ---
|
||||
// The demo session card above is presentational; this card drives a real charger
|
||||
// via the control endpoints when the user has picked Own/Proxy CSMS in Settings.
|
||||
// --- Folding the cards ---
|
||||
//
|
||||
// The column runs long once the readings are in it, so every card folds away.
|
||||
// Which ones are folded is kept in localStorage rather than on the profile, the
|
||||
// same way the provider panel keeps its own: it is a per-device reading habit,
|
||||
// not an account setting, and it should survive leaving the tab. Keyed by card,
|
||||
// so a fold outlives switching charger.
|
||||
const COLLAPSED_KEY = "dv_charging_collapsed";
|
||||
|
||||
const collapsed = ref(readCollapsed());
|
||||
|
||||
function readCollapsed() {
|
||||
try {
|
||||
const raw = JSON.parse(localStorage.getItem(COLLAPSED_KEY) || "[]");
|
||||
return Array.isArray(raw) ? raw.filter((id) => typeof id === "string") : [];
|
||||
} catch {
|
||||
return []; // unreadable (hand-edited, or written by an older version)
|
||||
}
|
||||
}
|
||||
|
||||
function isOpen(id) {
|
||||
return !collapsed.value.includes(id);
|
||||
}
|
||||
|
||||
function toggleCard(id) {
|
||||
collapsed.value = isOpen(id)
|
||||
? [...collapsed.value, id]
|
||||
: collapsed.value.filter((k) => k !== id);
|
||||
try {
|
||||
localStorage.setItem(COLLAPSED_KEY, JSON.stringify(collapsed.value));
|
||||
} catch {
|
||||
// A full or blocked store just means the choice lasts this visit only.
|
||||
}
|
||||
}
|
||||
|
||||
const ctlMode = ref("off");
|
||||
const ctlSerial = ref(localStorage.getItem("dv_ctl_serial") || "");
|
||||
const ctl = ref(null); // { connected, status, controlMode, ... }
|
||||
@@ -728,12 +763,27 @@ onMounted(async () => {
|
||||
<!-- Real OCPP control (Own/Proxy CSMS) — the real home-charger control -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-if="ctlActive" class="dh-card p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between gap-3 text-left"
|
||||
:aria-expanded="isOpen('connection')"
|
||||
@click="toggleCard('connection')"
|
||||
>
|
||||
<p class="text-sm font-semibold text-strong">{{ t("charging.control.connectionTitle") }}</p>
|
||||
<span class="dh-badge" :class="ctlConnected ? 'dh-badge-success' : 'dh-badge-warning'">
|
||||
{{ ctlConnected ? t("charging.control.connected") : t("charging.control.disconnected") }}
|
||||
<!-- Whether the charger is reachable is the one thing worth seeing
|
||||
with the card folded, so the badge rides in the header. -->
|
||||
<span class="flex shrink-0 items-center gap-2">
|
||||
<span class="dh-badge" :class="ctlConnected ? 'dh-badge-success' : 'dh-badge-warning'">
|
||||
{{ ctlConnected ? t("charging.control.connected") : t("charging.control.disconnected") }}
|
||||
</span>
|
||||
<svg
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
class="h-4 w-4 shrink-0 text-muted transition-transform" :class="isOpen('connection') ? '' : '-rotate-90'"
|
||||
><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-show="isOpen('connection')">
|
||||
|
||||
<div class="mt-3 flex gap-2">
|
||||
<select v-if="pickingFromList" v-model="ctlSerial" class="dh-input" @change="refreshCtl">
|
||||
@@ -809,12 +859,26 @@ onMounted(async () => {
|
||||
{{ ctl?.detail || t(ctlIsModbus ? "charging.control.modbusHint" : "charging.control.connectHint") }}
|
||||
</p>
|
||||
<p v-if="ctlError" class="mt-2 text-sm text-danger">{{ ctlError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Acting on the charger, once there is a connection to act over. The
|
||||
card above is where that connection is set up. -->
|
||||
<div v-if="ctlActive && ctlConnected" class="dh-card p-4">
|
||||
<p class="text-sm font-semibold text-strong">{{ t("charging.control.title") }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between gap-3 text-left"
|
||||
:aria-expanded="isOpen('control')"
|
||||
@click="toggleCard('control')"
|
||||
>
|
||||
<p class="text-sm font-semibold text-strong">{{ t("charging.control.title") }}</p>
|
||||
<svg
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
class="h-4 w-4 shrink-0 text-muted transition-transform" :class="isOpen('control') ? '' : '-rotate-90'"
|
||||
><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" /></svg>
|
||||
</button>
|
||||
|
||||
<div v-show="isOpen('control')">
|
||||
|
||||
<div class="mt-3 grid grid-cols-2 gap-2">
|
||||
<div class="rounded-control bg-sunken px-3 py-2">
|
||||
@@ -896,13 +960,27 @@ onMounted(async () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- What the charger reports over Modbus. Its own card rather than a
|
||||
tail on the control one: control is for acting on the charger, and
|
||||
this is a long read that pushed the buttons off the screen. -->
|
||||
<div v-if="ctlIsModbus && ctlConnected" class="dh-card p-4">
|
||||
<p class="text-sm font-semibold text-strong">{{ t("charging.modbus.title") }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between gap-3 text-left"
|
||||
:aria-expanded="isOpen('readings')"
|
||||
@click="toggleCard('readings')"
|
||||
>
|
||||
<p class="text-sm font-semibold text-strong">{{ t("charging.modbus.title") }}</p>
|
||||
<svg
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
class="h-4 w-4 shrink-0 text-muted transition-transform" :class="isOpen('readings') ? '' : '-rotate-90'"
|
||||
><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" /></svg>
|
||||
</button>
|
||||
|
||||
<div v-show="isOpen('readings')">
|
||||
<div class="mt-3 flex flex-col gap-2">
|
||||
<section v-if="modbusPhases.length" class="rounded-control bg-sunken p-3">
|
||||
<h4 class="eyebrow">{{ t("charging.modbus.phases") }}</h4>
|
||||
@@ -977,6 +1055,7 @@ onMounted(async () => {
|
||||
<p class="mt-1 text-[11px] text-muted">{{ t("charging.modbus.alarmsHint") }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charger information: everything the record holds about each imported
|
||||
@@ -985,7 +1064,18 @@ onMounted(async () => {
|
||||
card is what fills the column instead of a bare hint. -->
|
||||
<div class="dh-card p-4">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<p class="text-sm font-semibold text-strong">{{ t("charging.info.title") }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="flex grow items-center justify-between gap-3 text-left"
|
||||
:aria-expanded="isOpen('info')"
|
||||
@click="toggleCard('info')"
|
||||
>
|
||||
<p class="text-sm font-semibold text-strong">{{ t("charging.info.title") }}</p>
|
||||
<svg
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
class="h-4 w-4 shrink-0 text-muted transition-transform" :class="isOpen('info') ? '' : '-rotate-90'"
|
||||
><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" /></svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="homeChargers.length"
|
||||
type="button"
|
||||
@@ -997,6 +1087,7 @@ onMounted(async () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-show="isOpen('info')">
|
||||
<!-- The charger picked in the list beside this card, and only it. -->
|
||||
<div v-if="selectedHomeCharger" class="mt-3 rounded-control bg-sunken p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
@@ -1029,6 +1120,7 @@ onMounted(async () => {
|
||||
<!-- Control mode off: say why the card above is missing, here, where
|
||||
there is now something to read it against. -->
|
||||
<p v-if="!ctlActive" class="mt-3 text-xs text-muted">{{ t("charging.stations.noControlHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user