WFDASHDRIVER-APP/app/globals.css
SJCoetz 9a99c489c3 feat: add geographic grouping, refactor collection workflow
- Add standardized item display helpers for consistent labeling across components
- Rework PocketBase data fetching into scoped assigned/pool/problem item queries
- Add proximity-based clustering and AreaGroup component for geographic list grouping
- Sync map page tab state with main collections list using localStorage
- Update all existing components to use new data and display patterns
- Add custom map marker, tooltip, and cluster styling
2026-07-07 17:15:42 +02:00

630 lines
No EOL
17 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@tailwind base;
@tailwind components;
@tailwind utilities;
/* =============================================================
Cursor brand bindings — warm cream + warm brown + orange accent
Overrides Tailwind's default orange scale so every `bg-orange-*`
`text-orange-*` `border-orange-*` resolves to Cursor's tokens.
============================================================= */
:root {
--foreground-rgb: 38, 37, 30; /* Cursor Dark #26251e */
--background-rgb: 242, 241, 237; /* Cursor Cream #f2f1ed */
--primary: 245, 78, 0; /* Cursor Orange #f54e00 */
--primary-light: 255, 122, 41; /* Cursor Orange tinted */
--success: 31, 138, 101; /* #1f8a65 */
--warning: 234, 179, 8; /* #eab308 */
--danger: 207, 45, 86; /* #cf2d56 */
--info: 159, 187, 224; /* #9fbbe0 */
--nav-height: 4rem;
/* Cursor surface scale (from DESIGN.md §Color Palette) */
--surface-100: #f7f7f4;
--surface-200: #f2f1ed; /* page bg */
--surface-300: #ebeae5; /* button default */
--surface-400: #e6e5e0; /* card */
--surface-500: #e1e0db;
/* Tailwind orange-50..900 → Cursor warm scale.
This is the global re-skin: every `bg-orange-600` etc. below
resolves to a warm cream/brown, with `--primary` reserved for
the single orange accent role (CTAs, active filter, link). */
--orange-50: #f7f7f4; /* surface 100 */
--orange-100: #ebeae5; /* surface 300 */
--orange-200: #e6e5e0; /* surface 400 */
--orange-300: #d6d4ce; /* cream darken */
--orange-400: #b8b5ac; /* muted warm */
--orange-500: #6b6760; /* secondary text */
--orange-600: #26251e; /* Cursor Dark (identity surface) */
--orange-700: #1a1914; /* hover over dark */
--orange-800: #1a1914;
--orange-900: #0f0e0a;
}
/* Red and green are reserved for status (problem / success). Keep
them at their Tailwind defaults — we just darken the lightest
stops so the bg-* uses don't clash with the cream canvas. */
:root {
--red-50: #fbeaef;
--red-100: #f3c4d2;
--red-500: #cf2d56; /* matches --danger */
--red-600: #b3224a;
--red-700: #8a1b3a;
--green-50: #e6f1ec;
--green-100: #c2dfd0;
--green-500: #1f8a65; /* matches --success */
--green-600: #18714f;
--green-700: #125a3e;
--amber-50: #fcf3d8;
--amber-100: #f6e3a4;
--amber-500: #d49103;
--amber-700: #8a5a02;
--violet-50: #ece8f3;
--violet-100: #d4cae3;
--violet-500: #6c5a99;
--violet-700: #4a3d6b;
}
/* Re-skin greys to warm so text reads as part of the same family
as the cream surface. */
:root {
--gray-50: #f7f7f4;
--gray-100: #ebeae5;
--gray-200: #e6e5e0;
--gray-300: #d6d4ce;
--gray-400: #b8b5ac;
--gray-500: #8a857b;
--gray-600: #6b6760;
--gray-700: #4a4742;
--gray-800: #2e2c28;
--gray-900: #26251e;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
height: 100%;
-webkit-tap-highlight-color: transparent;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
color: rgb(var(--foreground-rgb));
background: rgb(var(--background-rgb));
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
overscroll-behavior-y: none;
-webkit-user-select: none;
user-select: none;
/* Smooth page transitions */
scroll-behavior: smooth;
}
/* Mobile viewport height fix (100vh is wrong on mobile) */
@supports (-webkit-touch-callout: none) {
.h-screen {
height: -webkit-fill-available;
}
.min-h-screen {
min-height: -webkit-fill-available;
}
}
/* Safe area insets for notched phones */
.safe-top {
padding-top: env(safe-area-inset-top);
}
.safe-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
/* Content must account for bottom nav */
.pb-nav {
padding-bottom: calc(var(--nav-height) + env(safe-area-inset-bottom, 0px) + 1rem);
}
/* Hide scrollbar but keep scroll functionality */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Touch action optimization */
.touch-manipulation {
touch-action: manipulation;
}
/* Prevent text selection on interactive elements */
button, a, nav {
-webkit-user-select: none;
user-select: none;
}
/* Allow text selection in content areas */
.selectable {
-webkit-user-select: text;
user-select: text;
}
/* Leaflet container fix */
.leaflet-container {
width: 100%;
height: 100%;
z-index: 1;
font-family: inherit;
}
/* Supplier item-name tooltip — Cursor-harmonized: warm cream surface
with a hairline border and tight radius matching the rest of the
app. Default Leaflet tooltip is white with a hard tip — wrong tone. */
.supplier-tooltip {
background: #e6e5e0 !important;
color: #26251e !important;
border: 1px solid rgba(38, 37, 30, 0.1) !important;
border-radius: 6px !important;
font-family: system-ui, -apple-system, sans-serif !important;
padding: 4px 8px !important;
box-shadow: 0 4px 14px rgba(0,0,0,0.18) !important;
white-space: nowrap !important;
max-width: 240px !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.supplier-tooltip::before {
border-top-color: #e6e5e0 !important;
}
/* ───────────────────────────────────────────────────────────────────
Waypoint pill — the new unified marker shape. Leaflet wraps each
divIcon in a `.leaflet-marker-icon` container that ships with
`text-align: center` and a default white background. We override
so the pill:
• renders the actual pill (no default white box behind it)
• centers on the lat/lng (Leaflet applies the iconAnchor already)
• never gets selected or dragged-text by accident
• keeps its high z-index so it stacks above polylines
─────────────────────────────────────────────────────────────── */
.waypoint-div-icon {
background: transparent !important;
border: none !important;
user-select: none !important;
-webkit-user-select: none !important;
}
/* ──────────────────────────────────────────────────────────────────
Waypoint map marker — consistent circle for every stop.
Three reasons:
1. At 10+ stops pill-shaped icons crowded and collided. A small
identical circle scales gracefully at every density.
2. Driver reads shape — a denser region of dots = more pickups,
not "many different icon types to parse."
3. Cluster count lives in a small corner badge so the silhouette
of "1 stop" vs "9 stops at same supplier" stays identical.
─────────────────────────────────────────────────────────────── */
.waypoint-div-icon {
background: transparent !important;
border: none !important;
user-select: none !important;
-webkit-user-select: none !important;
}
.waypoint-div-icon .waypoint-dot-wrap {
position: relative;
cursor: pointer;
display: block;
pointer-events: auto;
}
/* Soft halo: sits behind the dot, gives it a faint presence on the
map without competing with the dot's silhouette. */
.waypoint-div-icon .waypoint-dot-shadow {
position: absolute;
top: 50%;
left: 50%;
width: 28px;
height: 28px;
margin: -14px 0 0 -14px;
border-radius: 50%;
background: rgba(245, 78, 0, 0.18);
filter: blur(2px);
transition: transform 150ms ease, opacity 150ms ease;
}
/* The dot itself — 26px circle, brand orange fill, cream ring for
contrast on light tiles, subtle dark ring for depth on darks. */
.waypoint-div-icon .waypoint-dot {
position: absolute;
top: 50%;
left: 50%;
width: 26px;
height: 26px;
margin: -13px 0 0 -13px;
border-radius: 50%;
background: #f54e00;
border: 3px solid #f2f1ed;
box-shadow:
0 0 0 1px rgba(38, 37, 30, 0.25),
0 4px 10px rgba(38, 37, 30, 0.35);
transition: transform 150ms ease, box-shadow 150ms ease;
}
/* Cluster count badge — small pill in the top-right corner of the
hit area. Only ever shown on multi-point clusters; for singletons
we leave the slot empty so the silhouette stays minimal. */
.waypoint-div-icon .waypoint-dot-badge {
position: absolute;
top: 0;
right: 0;
min-width: 18px;
height: 18px;
padding: 0 5px;
display: inline-flex;
align-items: center;
justify-content: center;
background: #26251e;
color: #f2f1ed;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.02em;
border-radius: 9999px;
border: 2px solid #f2f1ed;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
pointer-events: none; /* clicks pass through to the dot */
}
/* Singleton name label — sits below the dot, only when there's exactly
one stop here. Hidden when a count badge is present so the dot's
footprint stays small in dense regions. */
.waypoint-div-icon .waypoint-dot-name {
position: absolute;
top: calc(50% + 16px);
left: 50%;
transform: translateX(-50%);
max-width: 140px;
padding: 2px 6px;
background: rgba(38, 37, 30, 0.78);
color: #f2f1ed;
font-family: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 10px;
font-weight: 600;
letter-spacing: -0.005em;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
pointer-events: none;
opacity: 0;
transition: opacity 150ms ease;
}
.waypoint-div-icon:hover .waypoint-dot-name {
opacity: 1;
}
/* Hover state — dot scales up 1.15× and lifts, badge pops slightly. */
.waypoint-div-icon:hover .waypoint-dot,
.waypoint-div-icon:focus-visible .waypoint-dot {
transform: scale(1.15);
box-shadow:
0 0 0 1px rgba(38, 37, 30, 0.3),
0 7px 16px rgba(245, 78, 0, 0.4),
0 4px 8px rgba(38, 37, 30, 0.3);
}
.waypoint-div-icon:hover .waypoint-dot-shadow {
transform: scale(1.4);
opacity: 0.6;
}
/* Keyboard focus: the hit box is the only focusable surface. */
.waypoint-div-icon:focus-visible {
outline: none;
}
.waypoint-div-icon:focus-visible .waypoint-dot {
box-shadow:
0 0 0 3px rgba(245, 78, 0, 0.4),
0 0 0 1px rgba(38, 37, 30, 0.3),
0 4px 10px rgba(38, 37, 30, 0.35);
}
/* Active press: dot squashes slightly to confirm the click. */
.waypoint-div-icon:active .waypoint-dot {
transform: scale(0.95);
}
/* Respect prefers-reduced-motion: drop the easing and transforms
so users sensitive to motion still see the waypoints clearly. */
@media (prefers-reduced-motion: reduce) {
.waypoint-div-icon .waypoint-dot,
.waypoint-div-icon .waypoint-dot-shadow,
.waypoint-div-icon .waypoint-dot-name {
transition: none !important;
}
.waypoint-div-icon:hover .waypoint-dot,
.waypoint-div-icon:focus-visible .waypoint-dot,
.waypoint-div-icon:hover .waypoint-dot-shadow {
transform: none !important;
}
}
/* ========================================
Animations
======================================== */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes scaleIn {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
.animate-slide-up {
animation: slideUp 0.35s cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.animate-slide-down {
animation: slideDown 0.3s ease-out forwards;
}
.animate-scale-in {
animation: scaleIn 0.2s ease-out forwards;
}
/* Stagger animation delay helper */
.stagger-1 { animation-delay: 0.05s; }
.stagger-2 { animation-delay: 0.1s; }
.stagger-3 { animation-delay: 0.15s; }
.stagger-4 { animation-delay: 0.2s; }
.stagger-5 { animation-delay: 0.25s; }
/* ========================================
Card Styles
======================================== */
.card-shadow {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.06),
0 1px 3px rgba(0, 0, 0, 0.1);
}
.card-shadow-md {
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
.card-shadow-lg {
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -4px rgba(0, 0, 0, 0.1);
}
/* Pressed/active state for cards */
.card-press {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.card-press:active {
transform: scale(0.985);
box-shadow:
0 1px 1px rgba(0, 0, 0, 0.06),
0 1px 2px rgba(0, 0, 0, 0.08);
}
/* ========================================
Status Badges
======================================== */
.status-pending { @apply bg-gray-100 text-gray-700 border-gray-300; }
.status-assigned { @apply bg-blue-100 text-blue-700 border-blue-300; }
.status-collected { @apply bg-green-100 text-green-700 border-green-300; }
.status-problem { @apply bg-red-100 text-red-700 border-red-300; }
/* ========================================
Loading Skeleton
======================================== */
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
/* ========================================
Modal Overlay
======================================== */
.modal-overlay {
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
/* ========================================
Input Focus Rings
======================================== */
input:focus,
textarea:focus,
select:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(245, 78, 0, 0.3);
}
/* ========================================
Bottom Sheet (iOS-style modal)
======================================== */
.bottom-sheet {
border-radius: 1rem 1rem 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
}
@media (min-width: 640px) {
.bottom-sheet {
border-radius: 1rem;
}
}
/* ========================================
Custom scrollbar (for when needed)
======================================== */
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25);
}
/* ========================================
Pull-to-refresh visual feedback
======================================== */
.pull-indicator {
transition: transform 0.2s ease-out;
}
/* ========================================
Accessibility: keyboard focus + reduced motion
======================================== */
/* Visible keyboard focus across the whole app. Only paints for
keyboard users (`:focus-visible`), never on tap/click. */
*:focus {
outline: none;
}
*:focus-visible {
outline: 2px solid #f54e00;
outline-offset: 2px;
border-radius: 6px;
}
/* Pressable controls get the focus ring tighter to the control. */
button:focus-visible,
a:focus-visible,
[role="button"]:focus-visible {
outline-offset: 2px;
}
/* On dark surfaces (Cursor Dark #26251e) the orange ring is invisible;
use a white halo instead. */
.bg-orange-600:focus-visible,
button[class*="bg-[#26251e]"]:focus-visible,
button[class*="bg-gray-900"]:focus-visible,
header button:focus-visible,
header input:focus-visible {
outline-color: #ffffff;
box-shadow: 0 0 0 4px rgba(245, 78, 0, 0.35);
}
/* Respect users who opted out of motion. Keep opacity/state
transitions; strip translate/scale/rotate motion. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.skeleton {
animation: none;
background: #ececec;
}
.press-feedback:active,
.press-feedback-sm:active,
.card-press:active {
transform: none;
}
}
/* Screen-reader-only utility for visually hidden labels. */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Live region for toasts: visible label is already present, this
keeps the polite announcer quiet for status updates. */
[role="status"] {
/* No visual change; just a hook for SR. */
}
/* ========================================
Haptic-like press feedback
======================================== */
.press-feedback {
transition: transform 0.1s ease, opacity 0.1s ease;
}
.press-feedback:active {
transform: scale(0.96);
opacity: 0.9;
}
.press-feedback-sm {
transition: transform 0.1s ease, opacity 0.1s ease;
}
.press-feedback-sm:active {
transform: scale(0.98);
opacity: 0.9;
}