design(stops): cohesive card layout, count badges, row-click, merged columns

Tier 1 — Layout consistency:
* Move Stops tab action buttons (Upload Schedule, Add Stop) into the
  filter bar, matching the Locations tab. One muscle memory for
  'where the Add button is'. Removes the page-level StopsHeaderActions
  component.
* Make the card a single cohesive unit. Tabs sit INSIDE the card on
  top with a border-b, content fills the rest. No more 'rounded-b-2xl
  rounded-t-none border-t-0' shape.
* New StopsLocationsTabs: pill-style tabs with icons + count badges +
  sublabel ('269 · 7 cities'). Instant orientation. URL-driven via
  Link so refresh + deep-linking work.

Tier 2 — Easier to scan:
* New StatsStrip under the page header: '269 stops · 7 cities · 4
  states · 4 active · 5 draft · 0 inactive' (different stats for the
  Locations tab). Context before the table loads.
* Stops table collapses 8 columns → 6: drop Brand, merge City+State
  into 'Where', merge Date+Time into 'When' (date + weekday on one
  line, time on the next). 30% more breathing room per row.
* Replace font-mono for dates/times with tabular-nums — same column
  alignment, type family stays consistent.
* Click anywhere on a Stops row to edit. Redundant 'Edit' link is
  gone. Kebab menu still handles Publish/Duplicate/Delete.
* Match Locations' empty state (icon + title + subtitle) on the Stops
  side.

Polish:
* Consistent status pills (Active = emerald, Inactive = stone, Draft
  = amber) in both tabs.
* Tab-switch fade via framer-motion AnimatePresence — subtle y/opacity
  transition between Stops and Locations content.
This commit is contained in:
2026-06-04 15:48:15 +00:00
parent 1feba25ced
commit bc29c70e8b
6 changed files with 550 additions and 290 deletions
+107
View File
@@ -0,0 +1,107 @@
"use client";
import Link from "next/link";
type Props = {
active: "stops" | "locations";
stopCount: number;
locationCount: number;
stopsSublabel?: string; // e.g. "269 · 7 cities"
locationsSublabel?: string; // e.g. "41 · 8 cities"
};
const StopIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
<circle cx="12" cy="10" r="3" />
</svg>
);
const PinIcon = () => (
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
);
export default function StopsLocationsTabs({
active,
stopCount,
locationCount,
stopsSublabel,
locationsSublabel,
}: Props) {
const tabs = [
{
value: "stops" as const,
label: "Stops",
count: stopCount,
sublabel: stopsSublabel,
icon: <StopIcon />,
href: "/admin/stops",
},
{
value: "locations" as const,
label: "Locations",
count: locationCount,
sublabel: locationsSublabel,
icon: <PinIcon />,
href: "/admin/stops?tab=locations",
},
];
return (
<div
className="flex flex-wrap items-stretch gap-1.5 px-3 py-2.5 border-b border-[var(--admin-border)] bg-[var(--admin-card-bg-alt)]"
role="tablist"
aria-label="Stops and Locations tabs"
>
{tabs.map((t) => {
const isActive = t.value === active;
return (
<Link
key={t.value}
href={t.href}
role="tab"
aria-selected={isActive}
className={`
group relative flex items-center gap-2.5 rounded-xl px-3.5 py-2
text-sm font-semibold transition-all duration-200
${isActive
? "bg-white text-emerald-700 border border-emerald-200 shadow-sm"
: "text-stone-600 border border-transparent hover:text-emerald-700 hover:bg-emerald-50/40"
}
`}
>
<span
className={`flex-shrink-0 transition-colors ${isActive ? "text-emerald-600" : "text-stone-400 group-hover:text-emerald-500"}`}
aria-hidden
>
{t.icon}
</span>
<span>{t.label}</span>
<span
className={`
inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded-full px-1.5
text-[11px] font-bold tabular-nums
${isActive
? "bg-emerald-600 text-white"
: "bg-stone-200/70 text-stone-600 group-hover:bg-emerald-100 group-hover:text-emerald-700"
}
`}
>
{t.count}
</span>
{t.sublabel && (
<span
className={`hidden sm:inline text-[11px] font-medium tabular-nums ${isActive ? "text-stone-500" : "text-stone-400"}`}
>
· {t.sublabel}
</span>
)}
</Link>
);
})}
</div>
);
}