The cards drag too, held by their headings
Same arrangement the provider panel gives its readings, applied to the four charging cards: drag one and the column reorders live under the pointer, the card being dragged goes half-transparent, the one it is over takes a ring, and the rail's lock holds the lot still. Two things differ from the readings row, both because these are four different things rather than four of one. A card is placed with the CSS order property instead of by moving markup, so each keeps its own template and its own v-if. And the handle is the card's heading rather than the whole card — a card that was draggable everywhere would fight the current-limit slider and the address fields for the pointer. Saved on the profile as charger_card_order, beside the tab order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1418a566fd
commit
3c64d6e84c
@@ -43,7 +43,8 @@ type userRecord struct {
|
|||||||
CarOrder json.RawMessage `json:"car_order"`
|
CarOrder json.RawMessage `json:"car_order"`
|
||||||
// The charging page's tab arrangement, stored the same way and for the same
|
// The charging page's tab arrangement, stored the same way and for the same
|
||||||
// reason.
|
// reason.
|
||||||
ChargerTabOrder json.RawMessage `json:"charger_tab_order"`
|
ChargerTabOrder json.RawMessage `json:"charger_tab_order"`
|
||||||
|
ChargerCardOrder json.RawMessage `json:"charger_card_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// carOrder decodes the stored garage arrangement, treating anything unexpected
|
// carOrder decodes the stored garage arrangement, treating anything unexpected
|
||||||
@@ -52,6 +53,8 @@ func (rec userRecord) carOrder() []string { return decodeStringList(rec.CarOrder
|
|||||||
|
|
||||||
func (rec userRecord) chargerTabOrder() []string { return decodeStringList(rec.ChargerTabOrder) }
|
func (rec userRecord) chargerTabOrder() []string { return decodeStringList(rec.ChargerTabOrder) }
|
||||||
|
|
||||||
|
func (rec userRecord) chargerCardOrder() []string { return decodeStringList(rec.ChargerCardOrder) }
|
||||||
|
|
||||||
// decodeStringList reads a PocketBase json field that holds a list of strings,
|
// decodeStringList reads a PocketBase json field that holds a list of strings,
|
||||||
// treating anything unexpected as empty rather than failing the whole read.
|
// treating anything unexpected as empty rather than failing the whole read.
|
||||||
func decodeStringList(raw json.RawMessage) []string {
|
func decodeStringList(raw json.RawMessage) []string {
|
||||||
@@ -82,9 +85,10 @@ func (rec userRecord) toModel() models.User {
|
|||||||
Role: orDefault(rec.Role, "user"),
|
Role: orDefault(rec.Role, "user"),
|
||||||
Created: rec.Created,
|
Created: rec.Created,
|
||||||
|
|
||||||
Organization: rec.Organization,
|
Organization: rec.Organization,
|
||||||
CarOrder: rec.carOrder(),
|
CarOrder: rec.carOrder(),
|
||||||
ChargerTabOrder: rec.chargerTabOrder(),
|
ChargerTabOrder: rec.chargerTabOrder(),
|
||||||
|
ChargerCardOrder: rec.chargerCardOrder(),
|
||||||
}
|
}
|
||||||
if t := parsePBDate(rec.DeletionRequestedAt); !t.IsZero() {
|
if t := parsePBDate(rec.DeletionRequestedAt); !t.IsZero() {
|
||||||
u.DeletionRequestedAt = &t
|
u.DeletionRequestedAt = &t
|
||||||
@@ -133,16 +137,17 @@ func (s *Server) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type updateMeRequest struct {
|
type updateMeRequest struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Bio *string `json:"bio"`
|
Bio *string `json:"bio"`
|
||||||
Theme *string `json:"theme"`
|
Theme *string `json:"theme"`
|
||||||
Locale *string `json:"locale"`
|
Locale *string `json:"locale"`
|
||||||
DateFormat *string `json:"dateFormat"`
|
DateFormat *string `json:"dateFormat"`
|
||||||
Currency *string `json:"currency"`
|
Currency *string `json:"currency"`
|
||||||
FontSize *string `json:"fontSize"`
|
FontSize *string `json:"fontSize"`
|
||||||
DragLocked *bool `json:"dragLocked"`
|
DragLocked *bool `json:"dragLocked"`
|
||||||
CarOrder *[]string `json:"carOrder"`
|
CarOrder *[]string `json:"carOrder"`
|
||||||
ChargerTabOrder *[]string `json:"chargerTabOrder"`
|
ChargerTabOrder *[]string `json:"chargerTabOrder"`
|
||||||
|
ChargerCardOrder *[]string `json:"chargerCardOrder"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// maxCarOrder bounds the stored arrangement. listCars fetches at most 200 owned
|
// maxCarOrder bounds the stored arrangement. listCars fetches at most 200 owned
|
||||||
@@ -168,6 +173,10 @@ func normalizeChargerTabOrder(in []string) ([]string, error) {
|
|||||||
return normalizeOrder("chargerTabOrder", in, maxChargerTabOrder)
|
return normalizeOrder("chargerTabOrder", in, maxChargerTabOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeChargerCardOrder(in []string) ([]string, error) {
|
||||||
|
return normalizeOrder("chargerCardOrder", in, maxChargerTabOrder)
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeOrder(field string, in []string, max int) ([]string, error) {
|
func normalizeOrder(field string, in []string, max int) ([]string, error) {
|
||||||
if len(in) > max {
|
if len(in) > max {
|
||||||
return nil, fmt.Errorf("%s is too long (max %d)", field, max)
|
return nil, fmt.Errorf("%s is too long (max %d)", field, max)
|
||||||
@@ -284,6 +293,14 @@ func (s *Server) handleUpdateMe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
payload["charger_tab_order"] = keys
|
payload["charger_tab_order"] = keys
|
||||||
}
|
}
|
||||||
|
if in.ChargerCardOrder != nil {
|
||||||
|
keys, err := normalizeChargerCardOrder(*in.ChargerCardOrder)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload["charger_card_order"] = keys
|
||||||
|
}
|
||||||
|
|
||||||
var rec userRecord
|
var rec userRecord
|
||||||
if err := s.pb.Update(r.Context(), s.usersCollection(), claims.ID, payload, &rec); err != nil {
|
if err := s.pb.Update(r.Context(), s.usersCollection(), claims.ID, payload, &rec); err != nil {
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ var collectionsSchema = map[string][]fieldDef{
|
|||||||
// The charging page's tab order. Per user like the garage order, and for
|
// The charging page's tab order. Per user like the garage order, and for
|
||||||
// the same reason: it is this person's arrangement of their own page.
|
// the same reason: it is this person's arrangement of their own page.
|
||||||
fJSON("charger_tab_order", 2000),
|
fJSON("charger_tab_order", 2000),
|
||||||
|
// The charging page's card order, stored the same way as its tab order.
|
||||||
|
fJSON("charger_card_order", 2000),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -495,8 +495,9 @@ type User struct {
|
|||||||
// CarOrder is the garage arrangement: car ids in the order this user dragged
|
// CarOrder is the garage arrangement: car ids in the order this user dragged
|
||||||
// them into. The car list is already returned in this order, so a client only
|
// them into. The car list is already returned in this order, so a client only
|
||||||
// needs it to send an updated arrangement back.
|
// needs it to send an updated arrangement back.
|
||||||
CarOrder []string `json:"carOrder"`
|
CarOrder []string `json:"carOrder"`
|
||||||
ChargerTabOrder []string `json:"chargerTabOrder"`
|
ChargerTabOrder []string `json:"chargerTabOrder"`
|
||||||
|
ChargerCardOrder []string `json:"chargerCardOrder"`
|
||||||
|
|
||||||
// Non-empty while an account-deletion request is pending its cooldown.
|
// Non-empty while an account-deletion request is pending its cooldown.
|
||||||
DeletionRequestedAt *time.Time `json:"deletionRequestedAt,omitempty"`
|
DeletionRequestedAt *time.Time `json:"deletionRequestedAt,omitempty"`
|
||||||
|
|||||||
@@ -108,6 +108,9 @@
|
|||||||
"boost": "Boost denne session",
|
"boost": "Boost denne session",
|
||||||
"modbusHint": "Indtast laderens adresse på dette netværk og gem den. Laderen skal være tændt og på samme netværk som DriverVault."
|
"modbusHint": "Indtast laderens adresse på dette netværk og gem den. Laderen skal være tændt og på samme netværk som DriverVault."
|
||||||
},
|
},
|
||||||
|
"cards": {
|
||||||
|
"dragHint": "Træk et kort i dets overskrift for at ændre rækkefølgen i kolonnen."
|
||||||
|
},
|
||||||
"modbus": {
|
"modbus": {
|
||||||
"title": "Laderaflæsninger",
|
"title": "Laderaflæsninger",
|
||||||
"phases": "Pr. fase",
|
"phases": "Pr. fase",
|
||||||
|
|||||||
@@ -94,6 +94,9 @@
|
|||||||
"boost": "Boost this session",
|
"boost": "Boost this session",
|
||||||
"modbusHint": "Enter the charger's address on this network and save it. The charger must be powered on and on the same network as DriverVault."
|
"modbusHint": "Enter the charger's address on this network and save it. The charger must be powered on and on the same network as DriverVault."
|
||||||
},
|
},
|
||||||
|
"cards": {
|
||||||
|
"dragHint": "Drag a card by its heading to rearrange the column."
|
||||||
|
},
|
||||||
"modbus": {
|
"modbus": {
|
||||||
"title": "Charger readings",
|
"title": "Charger readings",
|
||||||
"phases": "Per phase",
|
"phases": "Per phase",
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
"boost": "Boost w tej sesji",
|
"boost": "Boost w tej sesji",
|
||||||
"modbusHint": "Podaj adres ładowarki w tej sieci i zapisz go. Ładowarka musi być włączona i w tej samej sieci co DriverVault."
|
"modbusHint": "Podaj adres ładowarki w tej sieci i zapisz go. Ładowarka musi być włączona i w tej samej sieci co DriverVault."
|
||||||
},
|
},
|
||||||
|
"cards": {
|
||||||
|
"dragHint": "Przeciągnij kartę za nagłówek, aby zmienić kolejność w kolumnie."
|
||||||
|
},
|
||||||
"modbus": {
|
"modbus": {
|
||||||
"title": "Odczyty ładowarki",
|
"title": "Odczyty ładowarki",
|
||||||
"phases": "Na fazę",
|
"phases": "Na fazę",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const prefs = reactive({
|
|||||||
// view because it arrives with the profile anyway, and the bar has to render
|
// view because it arrives with the profile anyway, and the bar has to render
|
||||||
// in the right order on the first paint.
|
// in the right order on the first paint.
|
||||||
chargerTabOrder: [],
|
chargerTabOrder: [],
|
||||||
|
chargerCardOrder: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const FONT_SCALE = { small: "93.75%", medium: "100%", large: "112.5%" };
|
const FONT_SCALE = { small: "93.75%", medium: "100%", large: "112.5%" };
|
||||||
@@ -54,6 +55,7 @@ export function applyProfilePrefs(profile) {
|
|||||||
prefs.fontSize = profile.fontSize || "medium";
|
prefs.fontSize = profile.fontSize || "medium";
|
||||||
prefs.dragLocked = !!profile.dragLocked;
|
prefs.dragLocked = !!profile.dragLocked;
|
||||||
prefs.chargerTabOrder = Array.isArray(profile.chargerTabOrder) ? profile.chargerTabOrder : [];
|
prefs.chargerTabOrder = Array.isArray(profile.chargerTabOrder) ? profile.chargerTabOrder : [];
|
||||||
|
prefs.chargerCardOrder = Array.isArray(profile.chargerCardOrder) ? profile.chargerCardOrder : [];
|
||||||
applyTheme();
|
applyTheme();
|
||||||
applyFontSize();
|
applyFontSize();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,88 @@ const sessionMetrics = computed(() =>
|
|||||||
// --- Real OCPP control (Anker Solix), gated by the per-user control mode ---
|
// --- 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
|
// 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.
|
// via the control endpoints when the user has picked Own/Proxy CSMS in Settings.
|
||||||
|
// --- Arranging the cards ---
|
||||||
|
//
|
||||||
|
// The four cards drag into any order, like the provider panel's readings, and
|
||||||
|
// the arrangement is saved on the profile beside the tab order. The column is a
|
||||||
|
// flex box, so a card is placed with the CSS order property rather than by
|
||||||
|
// moving markup: these are four different things, not four of one thing.
|
||||||
|
//
|
||||||
|
// The handle is the card's own header. Making the whole card draggable would
|
||||||
|
// fight the controls inside it — a range slider and two text fields cannot
|
||||||
|
// share a pointer with a native drag.
|
||||||
|
const ALL_CHARGER_CARDS = ["control", "connection", "readings", "info"];
|
||||||
|
|
||||||
|
const cardKeys = ref([...ALL_CHARGER_CARDS]);
|
||||||
|
watch(
|
||||||
|
() => prefs.chargerCardOrder,
|
||||||
|
(order) => {
|
||||||
|
const arranged = [];
|
||||||
|
for (const key of order || []) {
|
||||||
|
if (ALL_CHARGER_CARDS.includes(key) && !arranged.includes(key)) arranged.push(key);
|
||||||
|
}
|
||||||
|
cardKeys.value = [...arranged, ...ALL_CHARGER_CARDS.filter((k) => !arranged.includes(k))];
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const canArrangeCards = computed(() => !prefs.dragLocked);
|
||||||
|
const dragCard = ref(""); // card being dragged
|
||||||
|
const dropCard = ref(""); // card it is currently hovering over
|
||||||
|
const cardOrderError = ref("");
|
||||||
|
let cardsMoved = false; // the column changed during this drag and isn't saved yet
|
||||||
|
|
||||||
|
// Where this card sits in the column.
|
||||||
|
function cardOrder(key) {
|
||||||
|
const i = cardKeys.value.indexOf(key);
|
||||||
|
return i < 0 ? ALL_CHARGER_CARDS.length : i; // unarranged falls to the end, not the top
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCardDragStart(key, e) {
|
||||||
|
dragCard.value = key;
|
||||||
|
cardsMoved = false;
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
// Firefox only starts a drag once something is on the transfer.
|
||||||
|
e.dataTransfer.setData("text/plain", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder live as the pointer crosses cards, so the column shows the
|
||||||
|
// arrangement you are about to get. dragenter fires again for every element
|
||||||
|
// inside the same card, so only a genuinely new one moves anything.
|
||||||
|
function onCardDragEnter(key) {
|
||||||
|
if (!dragCard.value || key === dragCard.value || dropCard.value === key) return;
|
||||||
|
dropCard.value = key;
|
||||||
|
const list = cardKeys.value;
|
||||||
|
const from = list.indexOf(dragCard.value);
|
||||||
|
const to = list.indexOf(key);
|
||||||
|
if (from < 0 || to < 0) return;
|
||||||
|
list.splice(to, 0, ...list.splice(from, 1));
|
||||||
|
cardsMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save whatever the column now shows. Called from both drop and dragend: a card
|
||||||
|
// released beside the column never produces a drop.
|
||||||
|
async function commitCardOrder() {
|
||||||
|
dragCard.value = "";
|
||||||
|
dropCard.value = "";
|
||||||
|
if (!cardsMoved) return;
|
||||||
|
cardsMoved = false;
|
||||||
|
cardOrderError.value = "";
|
||||||
|
const arranged = [...cardKeys.value];
|
||||||
|
try {
|
||||||
|
await api.updateMe({ chargerCardOrder: arranged });
|
||||||
|
prefs.chargerCardOrder = arranged;
|
||||||
|
} catch (e) {
|
||||||
|
// The arrangement didn't stick; say so and put the stored one back rather
|
||||||
|
// than leaving the column showing an order the server does not have.
|
||||||
|
cardOrderError.value = e.message;
|
||||||
|
cardKeys.value = [
|
||||||
|
...prefs.chargerCardOrder.filter((k) => ALL_CHARGER_CARDS.includes(k)),
|
||||||
|
...ALL_CHARGER_CARDS.filter((k) => !prefs.chargerCardOrder.includes(k)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Folding the cards ---
|
// --- Folding the cards ---
|
||||||
//
|
//
|
||||||
// The column runs long once the readings are in it, so every card folds away.
|
// The column runs long once the readings are in it, so every card folds away.
|
||||||
@@ -853,15 +935,35 @@ onMounted(async () => {
|
|||||||
<div v-show="chargerTab === 'home'" class="grid gap-6 lg:grid-cols-[1fr_360px] lg:items-start">
|
<div v-show="chargerTab === 'home'" class="grid gap-6 lg:grid-cols-[1fr_360px] lg:items-start">
|
||||||
<!-- Real OCPP control (Own/Proxy CSMS) — the real home-charger control -->
|
<!-- Real OCPP control (Own/Proxy CSMS) — the real home-charger control -->
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
|
<p
|
||||||
|
v-if="cardOrderError"
|
||||||
|
class="rounded-control bg-danger-soft px-4 py-3 text-sm font-medium text-danger"
|
||||||
|
style="order: -1"
|
||||||
|
>
|
||||||
|
{{ cardOrderError }}
|
||||||
|
</p>
|
||||||
<!-- Acting on the charger — first, because it is what the page is
|
<!-- Acting on the charger — first, because it is what the page is
|
||||||
opened for. It appears only once there is a connection to act
|
opened for. It appears only once there is a connection to act
|
||||||
over, which the card below is where you set up. -->
|
over, which the card below is where you set up. -->
|
||||||
<div v-if="ctlActive && ctlConnected" class="dh-card p-4">
|
<div
|
||||||
|
v-if="ctlActive && ctlConnected"
|
||||||
|
class="dh-card p-4 transition-shadow duration-150"
|
||||||
|
:class="dropCard === 'control' ? 'ring-2 ring-accent' : ''"
|
||||||
|
:style="{ order: cardOrder('control') }"
|
||||||
|
@dragenter.prevent="onCardDragEnter('control')"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="commitCardOrder"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
:draggable="canArrangeCards"
|
||||||
|
:title="canArrangeCards ? t('charging.cards.dragHint') : ''"
|
||||||
class="flex w-full items-center justify-between gap-3 text-left"
|
class="flex w-full items-center justify-between gap-3 text-left"
|
||||||
|
:class="[canArrangeCards ? 'cursor-grab active:cursor-grabbing' : '', dragCard === 'control' ? 'opacity-50' : '']"
|
||||||
:aria-expanded="isOpen('control')"
|
:aria-expanded="isOpen('control')"
|
||||||
@click="toggleCard('control')"
|
@click="toggleCard('control')"
|
||||||
|
@dragstart="onCardDragStart('control', $event)"
|
||||||
|
@dragend="commitCardOrder"
|
||||||
>
|
>
|
||||||
<p class="text-sm font-semibold text-strong">{{ t("charging.control.title") }}</p>
|
<p class="text-sm font-semibold text-strong">{{ t("charging.control.title") }}</p>
|
||||||
<svg
|
<svg
|
||||||
@@ -957,12 +1059,25 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<!-- Which charger, and how to reach it. Below the controls: it is
|
<!-- Which charger, and how to reach it. Below the controls: it is
|
||||||
touched once and then left alone. -->
|
touched once and then left alone. -->
|
||||||
<div v-if="ctlActive" class="dh-card p-4">
|
<div
|
||||||
|
v-if="ctlActive"
|
||||||
|
class="dh-card p-4 transition-shadow duration-150"
|
||||||
|
:class="dropCard === 'connection' ? 'ring-2 ring-accent' : ''"
|
||||||
|
:style="{ order: cardOrder('connection') }"
|
||||||
|
@dragenter.prevent="onCardDragEnter('connection')"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="commitCardOrder"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
:draggable="canArrangeCards"
|
||||||
|
:title="canArrangeCards ? t('charging.cards.dragHint') : ''"
|
||||||
class="flex w-full items-center justify-between gap-3 text-left"
|
class="flex w-full items-center justify-between gap-3 text-left"
|
||||||
|
:class="[canArrangeCards ? 'cursor-grab active:cursor-grabbing' : '', dragCard === 'connection' ? 'opacity-50' : '']"
|
||||||
:aria-expanded="isOpen('connection')"
|
:aria-expanded="isOpen('connection')"
|
||||||
@click="toggleCard('connection')"
|
@click="toggleCard('connection')"
|
||||||
|
@dragstart="onCardDragStart('connection', $event)"
|
||||||
|
@dragend="commitCardOrder"
|
||||||
>
|
>
|
||||||
<p class="text-sm font-semibold text-strong">{{ t("charging.control.connectionTitle") }}</p>
|
<p class="text-sm font-semibold text-strong">{{ t("charging.control.connectionTitle") }}</p>
|
||||||
<!-- Whether the charger is reachable is the one thing worth seeing
|
<!-- Whether the charger is reachable is the one thing worth seeing
|
||||||
@@ -1060,12 +1175,25 @@ onMounted(async () => {
|
|||||||
<!-- What the charger reports over Modbus. Its own card rather than a
|
<!-- 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
|
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. -->
|
this is a long read that pushed the buttons off the screen. -->
|
||||||
<div v-if="ctlIsModbus && ctlConnected" class="dh-card p-4">
|
<div
|
||||||
|
v-if="ctlIsModbus && ctlConnected"
|
||||||
|
class="dh-card p-4 transition-shadow duration-150"
|
||||||
|
:class="dropCard === 'readings' ? 'ring-2 ring-accent' : ''"
|
||||||
|
:style="{ order: cardOrder('readings') }"
|
||||||
|
@dragenter.prevent="onCardDragEnter('readings')"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="commitCardOrder"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
:draggable="canArrangeCards"
|
||||||
|
:title="canArrangeCards ? t('charging.cards.dragHint') : ''"
|
||||||
class="flex w-full items-center justify-between gap-3 text-left"
|
class="flex w-full items-center justify-between gap-3 text-left"
|
||||||
|
:class="[canArrangeCards ? 'cursor-grab active:cursor-grabbing' : '', dragCard === 'readings' ? 'opacity-50' : '']"
|
||||||
:aria-expanded="isOpen('readings')"
|
:aria-expanded="isOpen('readings')"
|
||||||
@click="toggleCard('readings')"
|
@click="toggleCard('readings')"
|
||||||
|
@dragstart="onCardDragStart('readings', $event)"
|
||||||
|
@dragend="commitCardOrder"
|
||||||
>
|
>
|
||||||
<p class="text-sm font-semibold text-strong">{{ t("charging.modbus.title") }}</p>
|
<p class="text-sm font-semibold text-strong">{{ t("charging.modbus.title") }}</p>
|
||||||
<svg
|
<svg
|
||||||
@@ -1156,13 +1284,25 @@ onMounted(async () => {
|
|||||||
charger. It stands on its own — control needs Own/Proxy CSMS, but
|
charger. It stands on its own — control needs Own/Proxy CSMS, but
|
||||||
what the charger *is* is known either way, so with control off this
|
what the charger *is* is known either way, so with control off this
|
||||||
card is what fills the column instead of a bare hint. -->
|
card is what fills the column instead of a bare hint. -->
|
||||||
<div class="dh-card p-4">
|
<div
|
||||||
|
class="dh-card p-4 transition-shadow duration-150"
|
||||||
|
:class="dropCard === 'info' ? 'ring-2 ring-accent' : ''"
|
||||||
|
:style="{ order: cardOrder('info') }"
|
||||||
|
@dragenter.prevent="onCardDragEnter('info')"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="commitCardOrder"
|
||||||
|
>
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
:draggable="canArrangeCards"
|
||||||
|
:title="canArrangeCards ? t('charging.cards.dragHint') : ''"
|
||||||
class="flex grow items-center justify-between gap-3 text-left"
|
class="flex grow items-center justify-between gap-3 text-left"
|
||||||
|
:class="[canArrangeCards ? 'cursor-grab active:cursor-grabbing' : '', dragCard === 'info' ? 'opacity-50' : '']"
|
||||||
:aria-expanded="isOpen('info')"
|
:aria-expanded="isOpen('info')"
|
||||||
@click="toggleCard('info')"
|
@click="toggleCard('info')"
|
||||||
|
@dragstart="onCardDragStart('info', $event)"
|
||||||
|
@dragend="commitCardOrder"
|
||||||
>
|
>
|
||||||
<p class="text-sm font-semibold text-strong">{{ t("charging.info.title") }}</p>
|
<p class="text-sm font-semibold text-strong">{{ t("charging.info.title") }}</p>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
Reference in New Issue
Block a user