The one card with a button is the one with a crooked arrow

Four of the charging cards fold from a header that is a single button: the title
at the left, the arrow hard against the right edge. The fifth has a refresh
button in its header, and that button was placed after the toggle — so the arrow
ended up a button's width in from the edge, alone among the five, and the eye
finds it by searching rather than by knowing where it is.

The header now spends its width the way the others do. The heading keeps the
title and stays the drag handle, the refresh button takes the place beside the
edge, and the arrow is its own control at the end of the row. It folds the card
exactly as the heading does, so nothing that worked before stops working; only
the order changed.

The same layout lives in the Phone App's _FoldCard, with the same fault, so the
arrow moves past the action slot there too. All five cards share that widget and
only this one passes an action, which is why the other four look identical
before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-09-02 21:01:24 +02:00
co-authored by Claude Opus 5
parent 2425a8d3d6
commit 197ff73a39
2 changed files with 28 additions and 7 deletions
+9 -2
View File
@@ -461,13 +461,20 @@ class _FoldCard extends StatelessWidget {
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
),
if (badge != null) ...[badge!, const SizedBox(width: 8)],
Icon(open ? Icons.expand_more : Icons.chevron_right,
size: 20, color: DriverVault.muted(context)),
],
),
),
),
if (action != null) action!,
// The fold arrow belongs hard against the right edge, past the
// action, so a card with a header button is not the one whose
// arrow sits somewhere else. It folds the card like the heading.
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onToggle,
child: Icon(open ? Icons.expand_more : Icons.chevron_right,
size: 20, color: DriverVault.muted(context)),
),
],
),
if (open) ...children,
+19 -5
View File
@@ -1687,12 +1687,18 @@ onMounted(async () => {
@dragover.prevent
@drop.prevent="commitCardOrder"
>
<!-- The fold arrow belongs hard against the right edge, where every
other card puts it a card with a header button must not be the
one whose arrow sits somewhere else. So the arrow is its own
control at the end of the row and the refresh button takes the
place beside it; both still fold the card, and the heading is
still the drag handle. -->
<div class="flex items-center justify-between gap-2">
<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 gap-3 text-left"
:class="[canArrangeCards ? 'cursor-grab active:cursor-grabbing' : '', dragCard === 'info' ? 'opacity-50' : '']"
:aria-expanded="isOpen('info')"
@click="toggleCard('info')"
@@ -1700,10 +1706,6 @@ onMounted(async () => {
@dragend="commitCardOrder"
>
<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"
@@ -1714,6 +1716,18 @@ onMounted(async () => {
>
{{ chargerLiveLoading ? t("common.loading") : t("charging.info.refresh") }}
</button>
<button
type="button"
class="flex shrink-0 items-center"
:aria-expanded="isOpen('info')"
:aria-label="t('charging.info.title')"
@click="toggleCard('info')"
>
<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>
</div>
<div v-show="isOpen('info')">