feat(admin/stops): editorial redesign + calendar/route view

Reimagine the admin stops surface under a 'Harvest Almanac' editorial
direction. Establishes the type system (Fraunces serif display, Geist
sans, JetBrains Mono numerics) loaded via next/font, and a compact
modal pattern used across the new components.

Stop modal (AddStopModal)
- Compact 2-col grid: City|State, Date|Time, Address|ZIP|Cutoff
- Status replaced with slim segmented control (Draft / Publish Now)
- Tighter inputs (36px), single-line footer with Esc hint
- Auto-focus on City field, reactive submit label

GlassModal
- New 'compact' prop: tighter padding, eyebrow text, no accent bar,
  capped max-height for dense forms

Stop product assignment (StopProductAssignment)
- Reimagine the <select> dropdown as an editorial card-grid picker
- Big Fraunces numeral + small-caps eyebrow showing live count
- Click-to-toggle assign/remove; green check for assigned, red X on
  hover-to-remove intent
- Search + filter chips (All / Available / On this stop) with
  mono counters
- '/' keyboard shortcut focuses search
- Summary footer with 'Remove all' action

Stops page
- New StopsViewClient wrapper with shared search/status filter and
  Calendar/Table view toggle (default = Calendar)
- New StopsCalendarClient: month grid almanac with editorial header,
  color-coded event chips, today highlight, prev/next/Today nav
- Event popover with auto-edge-detection positioning, full stop
  details, Publish/View Route/Edit actions
- Day-route drawer: slides in from right with route spine (numbered
  markers + connecting hairline), 3-card summary (stops/live/brands),
  per-stop Publish/Edit/Duplicate actions
- StopsTableClient refactored to accept hideInternalFilterBar prop
  so the wrapper can own shared filtering

Design tokens
- New utility classes in admin-design-system.css: ha-display,
  ha-eyebrow, ha-field, ha-segment, ha-picker, ha-product-card,
  ha-calendar, ha-event-popover, ha-drawer, ha-route-stop, etc.
- All uses CSS variables (--font-fraunces, --font-geist,
  --font-jetbrains-mono) so the type system cascades
This commit is contained in:
2026-06-04 17:13:33 +00:00
parent df6f4181df
commit bdcaf0f1da
9 changed files with 3104 additions and 377 deletions
+66 -20
View File
@@ -9,9 +9,22 @@ type Props = {
onClose: () => void;
children: React.ReactNode;
maxWidth?: string;
/** Compact mode: tighter padding, smaller title, no accent bar — for dense forms. */
compact?: boolean;
/** Optional eyebrow text rendered above the title in compact mode. */
eyebrow?: string;
};
export default function GlassModal({ title, titleIcon, subtitle, onClose, children, maxWidth = "max-w-lg" }: Props) {
export default function GlassModal({
title,
titleIcon,
subtitle,
onClose,
children,
maxWidth = "max-w-lg",
compact = false,
eyebrow,
}: Props) {
// Lock body scroll
useEffect(() => {
document.body.style.overflow = "hidden";
@@ -33,25 +46,36 @@ export default function GlassModal({ title, titleIcon, subtitle, onClose, childr
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 md:p-8"
className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4 md:p-6"
onClick={handleBackdropClick}
style={{ backgroundColor: "rgba(60, 56, 37, 0.5)" }}
style={{
backgroundColor: "rgba(60, 56, 37, 0.45)",
backdropFilter: "blur(2px)",
WebkitBackdropFilter: "blur(2px)",
}}
>
{/* Modal card - solid white with shadow for high contrast */}
<div
className={`relative w-full ${maxWidth} max-h-[calc(100vh-2rem)] sm:max-h-[calc(100vh-3rem)] md:max-h-[calc(100vh-4rem)] rounded-2xl bg-white shadow-[0_25px_50px_-12px_rgba(0,0,0,0.15)] flex flex-col`}
className={`relative w-full ${maxWidth} ${
compact
? "max-h-[min(640px,calc(100vh-2rem))]"
: "max-h-[calc(100vh-2rem)] sm:max-h-[calc(100vh-3rem)] md:max-h-[calc(100vh-4rem)]"
} rounded-2xl bg-white shadow-[0_25px_50px_-12px_rgba(0,0,0,0.18),0_8px_16px_-4px_rgba(0,0,0,0.05)] flex flex-col overflow-hidden`}
>
{/* Subtle top border accent */}
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-[var(--admin-accent)] to-[var(--admin-accent-hover)] rounded-t-2xl" />
{/* Accent bar — only for non-compact (compact forms have their own internal accent) */}
{!compact && (
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-[var(--admin-accent)] to-[var(--admin-accent-hover)] rounded-t-2xl" />
)}
{/* Header */}
<div
className="flex items-center justify-between px-4 sm:px-6 md:px-8 py-4 sm:py-6 shrink-0"
<div
className={`flex items-center justify-between shrink-0 ${
compact ? "px-5 pt-4 pb-3" : "px-4 sm:px-6 md:px-8 py-4 sm:py-6"
}`}
style={{ borderBottom: "1px solid var(--admin-border-light)" }}
>
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
{titleIcon && (
<div
{titleIcon && !compact && (
<div
className="flex h-8 w-8 sm:h-10 sm:w-10 items-center justify-center rounded-xl shrink-0"
style={{ backgroundColor: "var(--admin-accent-light)" }}
>
@@ -59,21 +83,39 @@ export default function GlassModal({ title, titleIcon, subtitle, onClose, childr
</div>
)}
<div className="min-w-0">
<h2
className="text-lg sm:text-xl font-semibold text-[var(--admin-text-primary)] truncate"
style={{ letterSpacing: "-0.02em" }}
{compact && eyebrow && (
<p className="ha-eyebrow mb-0.5 truncate">{eyebrow}</p>
)}
<h2
className={`${
compact
? "ha-display text-lg truncate"
: "text-lg sm:text-xl font-semibold truncate"
} text-[var(--admin-text-primary)]`}
style={compact ? undefined : { letterSpacing: "-0.02em" }}
>
{title}
</h2>
{subtitle && (
<p className="mt-0.5 text-xs sm:text-sm text-[var(--admin-text-muted)] hidden sm:block">{subtitle}</p>
{subtitle && !compact && (
<p className="mt-0.5 text-xs sm:text-sm text-[var(--admin-text-muted)] hidden sm:block">
{subtitle}
</p>
)}
</div>
</div>
<button
onClick={onClose}
className="flex h-8 w-8 sm:h-9 sm:w-9 items-center justify-center rounded-full transition-all duration-150 shrink-0 ml-2"
style={{ backgroundColor: "var(--admin-bg-subtle)", color: "var(--admin-text-muted)" }}
className={`flex shrink-0 items-center justify-center rounded-full transition-all duration-150 ml-2 ${
compact
? "h-7 w-7 text-[var(--admin-text-muted)] hover:bg-[var(--admin-bg-subtle)] hover:text-[var(--admin-text-primary)]"
: "h-8 w-8 sm:h-9 sm:w-9"
}`}
style={
compact
? undefined
: { backgroundColor: "var(--admin-bg-subtle)", color: "var(--admin-text-muted)" }
}
aria-label="Close modal"
>
<svg className="h-4 w-4 sm:h-5 sm:w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
@@ -81,8 +123,12 @@ export default function GlassModal({ title, titleIcon, subtitle, onClose, childr
</button>
</div>
{/* Content - scrollable */}
<div className="relative px-4 sm:px-6 md:px-8 py-4 sm:py-6 md:py-8 overflow-y-auto flex-1">
{/* Content */}
<div
className={`relative overflow-y-auto flex-1 ${
compact ? "px-5 pb-5 pt-3" : "px-4 sm:px-6 md:px-8 py-4 sm:py-6 md:py-8"
}`}
>
{children}
</div>
</div>