feat: shrink PO/Notes buttons, lift popover stacking, responsive header

- PO (FileText) and Notes (MessageSquare) inline buttons: w-7 h-7 / w-8 h-8
  -> w-6 h-6 with size 11 icons and tighter radius, plus narrower right
  column gap so the smaller buttons don't sit in stale spacing.
- Popover z-index vs next group: add onPopoverChange callback to
  CollectionItemCard so SupplierGroup / AreaGroup can lift their
  section from z-10 to z-30 when *any* popover inside is open. Covers
  both compact (own popover) and detailed (card popover) modes. Active
  <li> in compact mode also lifts to z-30 so popover paints above
  sibling rows in the same list.
- Header categories on small screens: stats + toggle row now
  flex-wraps so chips don't overflow; added xs (480px) breakpoint
  and hid Supplier/Area labels below it to keep tiny phones clean.
  Status filter chip row already scrolls horizontally.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-02 22:42:40 +02:00
parent 85c63b336c
commit 3259009690
5 changed files with 87 additions and 31 deletions

View file

@ -422,9 +422,11 @@ const loadItems = useCallback(async (showLoading = true) => {
</div>
</div>
{/* Stats + Controls bar */}
<div className="px-4 pb-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 min-w-0">
{/* Stats + Controls bar — wraps onto two lines on narrow viewports
so the stat chips don't collide with the toggle groups. On
≥640px everything fits on one line again. */}
<div className="px-4 pb-2 flex flex-wrap items-center justify-between gap-x-2 gap-y-2">
<div className="flex items-center gap-1.5 min-w-0 flex-wrap">
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-[#ebeae5] text-[11px] font-medium text-[#26251e]">
<Package size={12} aria-hidden="true" />
{items.length} item{items.length !== 1 ? 's' : ''}
@ -437,8 +439,9 @@ const loadItems = useCallback(async (showLoading = true) => {
)}
</div>
{/* View toggle */}
<div className="flex items-center gap-1 flex-shrink-0">
{/* View toggle — buttons stay icon-only on the smallest screens
(label hidden at <sm) to keep the row from jumbling. */}
<div className="flex items-center gap-1 flex-shrink-0 flex-wrap">
{/* Group mode toggle */}
<div
className="flex items-center gap-0.5 bg-[rgba(38,37,30,0.06)] rounded-lg p-0.5"
@ -448,24 +451,26 @@ const loadItems = useCallback(async (showLoading = true) => {
<button
onClick={() => setGroupMode('supplier')}
aria-pressed={groupMode === 'supplier'}
aria-label="Group by supplier"
className={cn(
'px-2 py-1 rounded-md text-[11px] font-semibold transition-all flex items-center gap-1',
groupMode === 'supplier' ? 'bg-white text-[#26251e] shadow-sm' : 'text-[rgba(38,37,30,0.6)]'
)}
>
<MapPin size={10} aria-hidden="true" />
Supplier
<span className="hidden xs:inline sm:inline">Supplier</span>
</button>
<button
onClick={() => setGroupMode('area')}
aria-pressed={groupMode === 'area'}
aria-label="Group by area"
className={cn(
'px-2 py-1 rounded-md text-[11px] font-semibold transition-all flex items-center gap-1',
groupMode === 'area' ? 'bg-white text-[#26251e] shadow-sm' : 'text-[rgba(38,37,30,0.6)]'
)}
>
<Layers size={10} aria-hidden="true" />
Area
<span className="hidden xs:inline sm:inline">Area</span>
</button>
</div>

View file

@ -1,5 +1,6 @@
'use client';
import { useState } from 'react';
import { OrderItem } from '@/lib/types';
import { cn } from '@/lib/utils';
import CollectionItemCard from '@/components/CollectionItemCard';
@ -72,13 +73,23 @@ export default function AreaGroup({
const hasCoords = center.latitude !== 0 && center.longitude !== 0;
// Tracks whether any detailed-mode CollectionItemCard inside has
// an open popover. We use this to lift the section's stacking
// context so the popover paints above the next area group.
const [childPopoverOpen, setChildPopoverOpen] = useState(false);
return (
<section
className={cn(
'rounded-2xl overflow-hidden transition-all duration-200 border',
'relative rounded-2xl overflow-hidden transition-all duration-200 border',
color.light,
`border-[rgba(38,37,30,0.1)]`,
`ring-1 ${color.ring}`
`ring-1 ${color.ring}`,
// Lift to z-30 when a popover is open so it can escape this
// section's stacking context and paint above the next area
// group. Sibling sections otherwise share z-10 and render in
// document order, which would hide a popover behind the next.
childPopoverOpen ? 'z-30' : 'z-10'
)}
aria-label={`${label} area group`}
>
@ -218,6 +229,7 @@ export default function AreaGroup({
onProblem={onProblem}
onNavigate={onNavigate}
onShowDetails={onShowDetails}
onPopoverChange={setChildPopoverOpen}
/>
))}
</div>

View file

@ -31,6 +31,10 @@ interface CollectionItemProps {
onNavigate: (lat: number, lng: number, name: string) => void;
// Opens the ItemDetailModal showing PO document + buyer notes
onShowDetails: (item: OrderItem) => void;
// Notifies the parent that a popover has opened/closed so it can
// lift its own stacking layer — the card's z-30 alone can't escape
// the section's stacking context, so the section needs to know.
onPopoverChange?: (open: boolean) => void;
}
function getItemType(item: OrderItem): {
@ -113,6 +117,7 @@ export default function CollectionItemCard({
onProblem,
onNavigate,
onShowDetails,
onPopoverChange,
}: CollectionItemProps) {
const [expanded, setExpanded] = useState(false);
const [showNotesTooltip, setShowNotesTooltip] = useState(false);
@ -122,6 +127,13 @@ export default function CollectionItemCard({
useClickOutside(notesTooltipRef, () => setShowNotesTooltip(false));
useClickOutside(poTooltipRef, () => setShowPoTooltip(false));
// Bubble the open/close state up so the wrapping supplier/area
// group can lift itself above the *next* group in the list.
const popoverOpen = showNotesTooltip || showPoTooltip;
useEffect(() => {
onPopoverChange?.(popoverOpen);
}, [popoverOpen, onPopoverChange]);
const supplier = item.expand?.supplier;
const itemType = getItemType(item);
const coords = parseCoordinates(supplier?.coordinates);
@ -141,8 +153,9 @@ export default function CollectionItemCard({
<article
className={cn(
compact
? 'bg-[#ebeae5] rounded-xl overflow-hidden'
: 'bg-[#e6e5e0] rounded-2xl card-press overflow-hidden border border-[rgba(38,37,30,0.1)]',
? 'bg-[#ebeae5] rounded-xl overflow-visible'
: 'bg-[#e6e5e0] rounded-2xl card-press overflow-visible border border-[rgba(38,37,30,0.1)]',
(showNotesTooltip || showPoTooltip) && 'relative z-30',
item.problem_flag && 'ring-1 ring-[#cf2d56]'
)}
>
@ -254,16 +267,15 @@ export default function CollectionItemCard({
onClick={(e) => { e.stopPropagation(); setShowPoTooltip(v => !v); setShowNotesTooltip(false); }}
aria-label="Show PO details"
aria-expanded={showPoTooltip}
className="w-7 h-7 rounded-lg flex items-center justify-center bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors"
className="w-6 h-6 rounded-md flex items-center justify-center bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors"
>
<FileText size={13} aria-hidden="true" />
<FileText size={11} aria-hidden="true" />
</button>
{/* PO tooltip popover */}
{showPoTooltip && (
<div
ref={poTooltipRef}
className="absolute right-0 z-50 mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
onClick={e => e.stopPropagation()}
className="absolute right-0 top-full z-[60] mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
>
{po ? (
<>
@ -331,16 +343,15 @@ export default function CollectionItemCard({
onClick={(e) => { e.stopPropagation(); setShowNotesTooltip(v => !v); setShowPoTooltip(false); }}
aria-label="Show notes"
aria-expanded={showNotesTooltip}
className="w-7 h-7 rounded-lg flex items-center justify-center bg-[#8a5a02] text-white active:bg-[#6b4501] transition-colors"
className="w-6 h-6 rounded-md flex items-center justify-center bg-[#8a5a02] text-white active:bg-[#6b4501] transition-colors"
>
<MessageSquare size={13} aria-hidden="true" />
<MessageSquare size={11} aria-hidden="true" />
</button>
{/* Notes tooltip popover */}
{showNotesTooltip && (
<div
ref={notesTooltipRef}
className="absolute right-0 z-50 mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
onClick={e => e.stopPropagation()}
className="absolute right-0 top-full z-[60] mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
>
<div className="flex items-center justify-between mb-2">
<p className="text-[11px] font-semibold text-[#26251e] uppercase tracking-wide">Buyer Notes</p>

View file

@ -67,6 +67,12 @@ export default function SupplierGroup({
// Active tooltip: { itemId, type }
const [activeTooltip, setActiveTooltip] = useState<{ itemId: string; type: 'po' | 'notes' } | null>(null);
// Tracks whether a *detailed-mode* card inside this group has its
// own popover open — they live in CollectionItemCard state, so we
// learn about them via the onPopoverChange callback. Combined with
// activeTooltip above, this drives whether the section lifts to z-30
// to paint above the next supplier/area group.
const [childPopoverOpen, setChildPopoverOpen] = useState(false);
const listRef = useRef<HTMLUListElement>(null);
// Close tooltip when clicking outside the list
@ -83,9 +89,16 @@ export default function SupplierGroup({
return (
<section
className={cn(
'relative z-10 bg-[#e6e5e0] rounded-2xl overflow-visible transition-all duration-200 border border-[rgba(38,37,30,0.1)]',
'relative bg-[#e6e5e0] rounded-2xl overflow-visible transition-all duration-200 border border-[rgba(38,37,30,0.1)]',
'border-l-[6px]',
color.border
color.border,
// Bump the section's stacking layer when *any* tooltip inside
// it is open — own compact-list popover OR a detailed-mode
// card popover. Sibling sections all sit at z-10 in document
// order; without this lift, an open popover is occluded by the
// next group. Combining both signals keeps compact + detailed
// modes covered with a single className.
(activeTooltip || childPopoverOpen) ? 'z-30' : 'z-10'
)}
aria-label={`${supplier.suppliername} collection group`}
>
@ -169,7 +182,14 @@ export default function SupplierGroup({
return (
<li
key={item.id}
className="relative flex items-center justify-between px-4 py-3 hover:bg-[#ebeae5] transition-colors"
className={cn(
'relative flex items-center justify-between px-4 py-3 hover:bg-[#ebeae5] transition-colors',
// Lift the row containing the open tooltip so the
// popover paints above the next list item inside
// this same group (which would otherwise occlude
// it under the default z-10 stacking context).
(isPoOpen || isNotesOpen) ? 'z-30' : 'z-10'
)}
>
{/* Left: icon + description */}
<div className="flex items-center gap-2.5 min-w-0 flex-1">
@ -185,7 +205,7 @@ export default function SupplierGroup({
</div>
{/* Right: qty + FileText + Collect/Problem */}
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
<div className="flex items-center gap-1.5 flex-shrink-0 ml-2">
<span className="text-sm font-semibold text-[rgba(38,37,30,0.7)]">{expectedQty}×</span>
{/* PO FileText button */}
@ -197,15 +217,14 @@ export default function SupplierGroup({
}}
aria-label="Show PO details"
aria-expanded={isPoOpen}
className="w-8 h-8 rounded-lg flex items-center justify-center bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors"
className="w-6 h-6 rounded-md flex items-center justify-center bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors"
>
<FileText size={13} aria-hidden="true" />
<FileText size={11} aria-hidden="true" />
</button>
{/* PO tooltip popover */}
{isPoOpen && (
<div
className="absolute right-0 z-50 mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
onClick={e => e.stopPropagation()}
className="absolute right-0 top-full z-[60] mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
>
<div className="flex items-center justify-between mb-2">
<p className="text-[11px] font-semibold text-[#26251e] uppercase tracking-wide">Purchase Order</p>
@ -276,15 +295,14 @@ export default function SupplierGroup({
}}
aria-label="Show notes"
aria-expanded={isNotesOpen}
className="w-8 h-8 rounded-lg flex items-center justify-center bg-[#8a5a02] text-white active:bg-[#6b4501] transition-colors"
className="w-6 h-6 rounded-md flex items-center justify-center bg-[#8a5a02] text-white active:bg-[#6b4501] transition-colors"
>
<MessageSquare size={13} aria-hidden="true" />
<MessageSquare size={11} aria-hidden="true" />
</button>
{/* Notes tooltip popover */}
{isNotesOpen && (
<div
className="absolute right-0 z-50 mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
onClick={e => e.stopPropagation()}
className="absolute right-0 top-full z-[60] mt-1 w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3"
>
<div className="flex items-center justify-between mb-2">
<p className="text-[11px] font-semibold text-[#26251e] uppercase tracking-wide">Buyer Notes</p>
@ -354,6 +372,7 @@ export default function SupplierGroup({
onProblem={onProblem}
onNavigate={onNavigate}
onShowDetails={onShowDetails}
onPopoverChange={setChildPopoverOpen}
/>
))}
</div>

View file

@ -25,6 +25,15 @@ module.exports = {
'18': '4.5rem',
'22': '5.5rem',
},
screens: {
// Tiny phones (320–479px). Default Tailwind starts at `sm`=640px,
// which leaves the 320–414px band — the smallest screens the
// app actually ships to — without an in-between breakpoint.
// `xs` lets us hide labels on those screens while keeping the
// rest of the layout compact, instead of squeezing everything
// into a single cramped row.
xs: '480px',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-out forwards',
'slide-up': 'slideUp 0.3s ease-out forwards',