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:
parent
85c63b336c
commit
3259009690
5 changed files with 87 additions and 31 deletions
19
app/page.tsx
19
app/page.tsx
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue