Fix 18 UX issues: confirmations, undo, drawer nav, empty states, and polish
Build and push image / build (push) Successful in 54s
Build and push image / build (push) Successful in 54s
Comprehensive UX audit covering modals, drawers, dashboard, and inventory. Key changes: confirmation steps before destructive actions, undo via toast for consume/gone/checkout, back-navigation across entity drawers, optional ratings, discrete item count field, audit progress bar, and sortable column affordance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import { getToday, getStoredTimezone } from "../tz.js";
|
||||
import { fmt, TYPE_GLYPHS } from "../format.js";
|
||||
import { Btn, Pill, Icon } from "./primitives/index.js";
|
||||
import { remainingShort } from "../stats.js";
|
||||
import { useExitAnimation } from "../hooks/useExitAnimation.js";
|
||||
import { useFocusTrap } from "../hooks/useFocusTrap.js";
|
||||
|
||||
export function BrandDetail({
|
||||
brand,
|
||||
@@ -14,6 +16,8 @@ export function BrandDetail({
|
||||
onDelete,
|
||||
onSelectSku,
|
||||
onSelectItem,
|
||||
backLabel,
|
||||
onBack,
|
||||
}: {
|
||||
brand: Brand;
|
||||
data: Bootstrap;
|
||||
@@ -22,6 +26,8 @@ export function BrandDetail({
|
||||
onDelete: () => void;
|
||||
onSelectSku: (p: Product) => void;
|
||||
onSelectItem: (i: Item) => void;
|
||||
backLabel?: string;
|
||||
onBack?: () => void;
|
||||
}) {
|
||||
const products = data.products.filter((p) => p.brandId === brand.id);
|
||||
const strainMap = new Map(data.strains.map((s) => [s.id, s]));
|
||||
@@ -47,13 +53,16 @@ export function BrandDetail({
|
||||
const todayStr = getToday(getStoredTimezone());
|
||||
const tz = getStoredTimezone();
|
||||
|
||||
const { closing, triggerClose } = useExitAnimation(220, onClose);
|
||||
const trapRef = useFocusTrap<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
if (e.key === "Escape") triggerClose();
|
||||
};
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onClose]);
|
||||
}, [triggerClose]);
|
||||
|
||||
const statCards: [string, React.ReactNode][] = [
|
||||
["SKUs", String(products.length)],
|
||||
@@ -64,6 +73,9 @@ export function BrandDetail({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={trapRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
@@ -71,16 +83,16 @@ export function BrandDetail({
|
||||
zIndex: 50,
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
animation: "backdrop-in 200ms ease-out",
|
||||
animation: closing ? "backdrop-out 220ms ease-in forwards" : "backdrop-in 200ms ease-out",
|
||||
}}
|
||||
onClick={onClose}
|
||||
onClick={triggerClose}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: "min(720px, 100vw)",
|
||||
height: "100%",
|
||||
animation: "drawer-in 250ms ease-out",
|
||||
animation: closing ? "drawer-out 220ms ease-in forwards" : "drawer-in 250ms ease-out",
|
||||
background: "var(--bg)",
|
||||
borderLeft: "1px solid var(--line)",
|
||||
overflow: "auto",
|
||||
@@ -92,14 +104,34 @@ export function BrandDetail({
|
||||
padding: "20px 32px",
|
||||
borderBottom: "1px solid var(--line)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "column",
|
||||
gap: onBack ? 8 : 0,
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
background: "var(--bg)",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{onBack && backLabel && (
|
||||
<button
|
||||
onClick={onBack}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
background: "none",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
fontSize: 12,
|
||||
color: "var(--sage)",
|
||||
cursor: "pointer",
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
>
|
||||
<span style={{ transform: "scaleX(-1)", display: "inline-flex" }}><Icon name="arrow" size={12} /></span> Back to {backLabel}
|
||||
</button>
|
||||
)}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>
|
||||
Brand
|
||||
</div>
|
||||
@@ -110,9 +142,11 @@ export function BrandDetail({
|
||||
icon="bin"
|
||||
disabled={hasItems}
|
||||
onClick={onDelete}
|
||||
title={hasItems ? "Cannot delete — has inventory items" : undefined}
|
||||
style={hasItems ? { opacity: 0.3, cursor: "not-allowed" } : undefined}
|
||||
/>
|
||||
<Btn variant="ghost" icon="close" onClick={onClose} />
|
||||
<Btn variant="ghost" icon="close" onClick={triggerClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user