diff --git a/client/src/pages/DashboardPage.test.tsx b/client/src/pages/DashboardPage.test.tsx
index 88f93cc49..cb838d62a 100644
--- a/client/src/pages/DashboardPage.test.tsx
+++ b/client/src/pages/DashboardPage.test.tsx
@@ -1000,4 +1000,59 @@ describe('DashboardPage', () => {
expect(within(grid()).queryByText('Paris Adventure')).not.toBeInTheDocument();
});
});
+
+ // Two people reported in a row that they could not find how to unarchive or
+ // rename a trip. The controls sat at opacity 0 until the card was hovered, and
+ // the archive button carried the same icon whether it would archive or restore.
+ describe('FE-PAGE-DASH-036: the cover controls are findable', () => {
+ const activeOnly = (trips: unknown[]) =>
+ server.use(
+ http.get('/api/trips', ({ request }) => {
+ const url = new URL(request.url);
+ if (url.searchParams.get('archived')) return HttpResponse.json({ trips: [] });
+ return HttpResponse.json({ trips });
+ }),
+ );
+
+ it('names every action on the card, so none of them is icon-only guesswork', async () => {
+ activeOnly([buildTrip({ title: 'Lisbon 2025', start_date: '2025-05-01', end_date: '2025-05-08' })]);
+ const user = userEvent.setup();
+ render();
+
+ await waitFor(() => expect(screen.getAllByText('Lisbon 2025').length).toBeGreaterThan(0));
+ await user.click(screen.getByText('Completed'));
+ const card = (await screen.findAllByText('Lisbon 2025'))
+ .map(n => n.closest('.trip-card'))
+ .find(Boolean) as HTMLElement;
+ for (const label of ['Edit', 'Duplicate', 'Archive', 'Delete']) {
+ const button = card.querySelector(`[aria-label="${label}"]`) as HTMLElement;
+ expect(button).toBeTruthy();
+ // A tooltip as well as the screen-reader label — the icons alone did not read.
+ expect(button.getAttribute('title')).toBe(label);
+ }
+ });
+
+ it('swaps the archive icon for a restore one once the trip is archived', async () => {
+ const archived = buildTrip({ title: 'Old Rome Trip', start_date: '2024-01-01', end_date: '2024-01-07', is_archived: 1 });
+ server.use(
+ http.get('/api/trips', ({ request }) => {
+ const url = new URL(request.url);
+ return HttpResponse.json({ trips: url.searchParams.get('archived') ? [archived] : [] });
+ }),
+ );
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByText('Archived'));
+ await waitFor(() => expect(screen.getByText('Old Rome Trip')).toBeInTheDocument());
+
+ const card = screen.getByText('Old Rome Trip').closest('.trip-card') as HTMLElement;
+ const restore = card.querySelector('[aria-label="Restore"]') as HTMLElement;
+ expect(restore).toBeTruthy();
+ // lucide stamps the icon name on the svg, which is how the archive and the
+ // restore variant tell themselves apart.
+ expect(restore.querySelector('svg')?.getAttribute('class')).toContain('archive-restore');
+ expect(card.querySelector('[aria-label="Archive"]')).toBeNull();
+ });
+ });
});
diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx
index f1613eb9c..dad6a303c 100644
--- a/client/src/pages/DashboardPage.tsx
+++ b/client/src/pages/DashboardPage.tsx
@@ -15,7 +15,7 @@ import {
MS_PER_DAY, daysUntil, getTripStatus,
} from './dashboard/dashboardModel'
import {
- Plus, Edit2, Trash2, Archive, Copy, ArrowRight, MapPin,
+ Plus, Edit2, Trash2, Archive, ArchiveRestore, Copy, ArrowRight, MapPin,
Plane, Hotel, Utensils, Clock, RefreshCw, ArrowRightLeft, Calendar,
LayoutGrid, List, Ticket, X, CalendarPlus, ParkingSquare,
} from 'lucide-react'
@@ -304,6 +304,7 @@ function BoardingPassHero({ trip, bundle, locale, onOpen, onEdit, onCopy, onArch
const heroPlugins = usePluginStore(s => s.plugins).filter(p => p.type === 'widget' && p.slot === 'hero')
const stop = (e: React.MouseEvent, fn: () => void) => { e.stopPropagation(); fn() }
const status = getTripStatus(trip)
+ const archiveLabel = trip.is_archived ? t('dashboard.restore') : t('dashboard.archive')
const start = splitDate(trip.start_date, locale)
const end = splitDate(trip.end_date, locale)
@@ -404,10 +405,10 @@ function BoardingPassHero({ trip, bundle, locale, onOpen, onEdit, onCopy, onArch
{badge}
-
-
-
-
+
+
+
+
@@ -535,6 +536,7 @@ function TripCard({ trip, locale, badges, onOpen, onEdit, onCopy, onArchive, onD
}): React.ReactElement {
const { t } = useTranslation()
const status = getTripStatus(trip)
+ const archiveLabel = trip.is_archived ? t('dashboard.restore') : t('dashboard.archive')
const start = splitDate(trip.start_date, locale)
const end = splitDate(trip.end_date, locale)
const until = daysUntil(trip.start_date)
@@ -557,10 +559,10 @@ function TripCard({ trip, locale, badges, onOpen, onEdit, onCopy, onArchive, onD
: }
{statusLabel}
-
-
-
-
+
+
+
+
{trip.title}
diff --git a/client/src/styles/dashboard.css b/client/src/styles/dashboard.css
index 9e509b44a..1e0dcea34 100644
--- a/client/src/styles/dashboard.css
+++ b/client/src/styles/dashboard.css
@@ -191,11 +191,12 @@
.trek-dash .hero-top { display: flex; justify-content: space-between; align-items: start; }
.trek-dash .hero-badge {
display: inline-flex; align-items: center; gap: 8px;
- background: oklch(1 0 0 / .14);
+ background: oklch(0.22 0.01 260 / .5);
backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.4);
- border: 1px solid oklch(1 0 0 / .2); padding: 8px 14px 8px 12px;
+ border: 1px solid oklch(1 0 0 / .32); padding: 8px 14px 8px 12px;
border-radius: 999px; font-size: 12px; font-weight: 500;
letter-spacing: 0.06em; text-transform: uppercase;
+ box-shadow: 0 2px 8px oklch(0 0 0 / .26);
}
.trek-dash .hero-badge .pulse {
width: 7px; height: 7px; border-radius: 50%;
@@ -208,15 +209,18 @@
100% { box-shadow: 0 0 0 0 oklch(0.84 0.18 85 / 0); }
}
.trek-dash .hero-tools { display: flex; gap: 8px; }
+/* Same story as .trip-action-btn: a dark scrim instead of a white .14 fill, so
+ the tools read against a bright hero photo too. */
.trek-dash .hero-tool {
- width: 38px; height: 38px; border-radius: 50%;
- background: oklch(1 0 0 / .14);
+ width: 40px; height: 40px; border-radius: 50%;
+ background: oklch(0.22 0.01 260 / .5);
backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.4);
- border: 1px solid oklch(1 0 0 / .2); color: #fff;
+ border: 1px solid oklch(1 0 0 / .32); color: #fff;
+ box-shadow: 0 2px 8px oklch(0 0 0 / .26);
display: grid; place-items: center; transition: background .15s, transform .12s;
}
-.trek-dash .hero-tool:hover { background: oklch(1 0 0 / .26); transform: scale(1.05); }
-.trek-dash .hero-tool svg { width: 16px; height: 16px; }
+.trek-dash .hero-tool:hover { background: oklch(0.22 0.01 260 / .78); transform: scale(1.05); }
+.trek-dash .hero-tool svg { width: 17px; height: 17px; }
.trek-dash .hero-title-block { align-self: end; padding-bottom: 24px; }
.trek-dash .hero-eyebrow {
display: inline-flex; align-items: center; gap: 10px;
@@ -435,26 +439,36 @@
.trek-dash .trip-cover img { width: 100%; height: 100%; object-fit: cover; transition: transform .6s cubic-bezier(.2,.7,.2,1); }
.trek-dash .trip-card:hover .trip-cover img { transform: scale(1.04); }
.trek-dash .trip-cover::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, oklch(0 0 0 / 0) 40%, oklch(0 0 0 / .55) 100%); }
+/* Shares the dark scrim of .trip-action-btn so both overlays on the cover read
+ as one set, and the label survives a bright photo behind it. */
.trek-dash .trip-status {
position: absolute; top: 16px; left: 16px; z-index: 1;
display: inline-flex; align-items: center; gap: 7px; padding: 6px 11px 6px 9px;
border-radius: 999px; font-size: 11.5px; font-weight: 500; letter-spacing: 0.02em;
- background: oklch(1 0 0 / .18); backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.4);
- border: 1px solid oklch(1 0 0 / .22); color: #fff; text-transform: uppercase;
+ background: oklch(0.22 0.01 260 / .58); backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.4);
+ border: 1px solid oklch(1 0 0 / .34); color: #fff; text-transform: uppercase;
+ box-shadow: 0 2px 8px oklch(0 0 0 / .28);
}
.trek-dash .trip-status .indicator { width: 6px; height: 6px; border-radius: 50%; background: oklch(0.84 0.16 145); }
.trek-dash .trip-status.completed .indicator { background: oklch(0.75 0.02 220); }
.trek-dash .trip-status.upcoming .indicator { background: oklch(0.84 0.18 85); }
.trek-dash .trip-status.idea .indicator { background: oklch(0.78 0.14 280); }
-.trek-dash .trip-actions { position: absolute; top: 14px; right: 14px; z-index: 1; display: flex; gap: 6px; opacity: 0; transition: opacity .2s; }
+/* The actions used to sit at opacity 0 until the card was hovered, over a white
+ .18 glass fill. Two people in a row reported they never found the archive and
+ rename buttons — invisible at rest, and on a light cover the white-on-white
+ fill hid them even while hovering. They stay on the card now, and the fill is
+ a dark scrim so the white icons carry against any photo. */
+.trek-dash .trip-actions { position: absolute; top: 14px; right: 14px; z-index: 1; display: flex; gap: 6px; opacity: .92; transition: opacity .2s; }
.trek-dash .trip-card:hover .trip-actions { opacity: 1; }
.trek-dash .trip-action-btn {
- width: 36px; height: 36px; border-radius: 50%;
- background: oklch(1 0 0 / .18); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
- border: 1px solid oklch(1 0 0 / .22); color: #fff; display: grid; place-items: center; transition: background .15s;
+ width: 38px; height: 38px; border-radius: 50%;
+ background: oklch(0.22 0.01 260 / .58); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
+ border: 1px solid oklch(1 0 0 / .34); color: #fff; display: grid; place-items: center;
+ box-shadow: 0 2px 8px oklch(0 0 0 / .28);
+ transition: background .15s, transform .12s;
}
-.trek-dash .trip-action-btn:hover { background: oklch(1 0 0 / .3); }
-.trek-dash .trip-action-btn svg { width: 16px; height: 16px; }
+.trek-dash .trip-action-btn:hover { background: oklch(0.22 0.01 260 / .82); transform: scale(1.06); }
+.trek-dash .trip-action-btn svg { width: 17px; height: 17px; }
.trek-dash .trip-cover-content { position: absolute; left: 18px; right: 18px; bottom: 16px; z-index: 1; color: #fff; }
.trek-dash .trip-name { font-size: 26px; font-weight: 600; letter-spacing: -0.025em; line-height: 1.05; margin: 0; text-shadow: 0 1px 7px oklch(0 0 0 / .3), 0 1px 2px oklch(0 0 0 / .38); }
.trek-dash .trip-where { margin-top: 4px; font-size: 13px; opacity: .85; display: flex; align-items: center; gap: 6px; }
@@ -686,8 +700,8 @@
.trek-dash .trips { grid-template-columns: 1fr; gap: 16px; margin-bottom: 28px; }
.trek-dash .add-trip-card { min-height: 180px; }
- /* Touch devices have no hover — keep the edit/copy/archive/delete actions
- visible at all times instead of revealing them on hover. */
+ /* Touch devices have no hover state to lift the actions to full strength, so
+ skip the resting dim entirely. */
.trek-dash .trip-actions { opacity: 1; }
/* Compact list row on mobile — keeps the list view distinct from the grid. The