Fix 15 UX friction points across modals, navigation, and accessibility
Build and push image / build (push) Successful in 49s
Build and push image / build (push) Successful in 49s
Addresses bulk operation safety (confirmation for consume/gone), drawer action hierarchy, exit animations, sidebar action distinction, toast dismissibility and ARIA, retry affordance, sort direction toggle, sequential audit queue from dashboard, mobile nav separation, price label disambiguation, grouped-view sort consistency, footer context hints, bin deletion feedback, segmented control ARIA, and drawer focus trapping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import { TYPES, helpers } from "../types.js";
|
||||
import { getToday, getStoredTimezone } from "../tz.js";
|
||||
import { fmt, TYPE_GLYPHS } from "../format.js";
|
||||
import { Btn, Pill, Icon } from "./primitives/index.js";
|
||||
import { useExitAnimation } from "../hooks/useExitAnimation.js";
|
||||
import { useFocusTrap } from "../hooks/useFocusTrap.js";
|
||||
|
||||
// Right-side drawer for an inventory instance. Shows the asset id and
|
||||
// product context up top, then per-batch fields (price, THC, weight),
|
||||
@@ -40,14 +42,16 @@ export function ProductDetail({
|
||||
|
||||
const isActive = item.status === "active";
|
||||
const isCheckedOut = item.status === "checked-out";
|
||||
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]);
|
||||
|
||||
// Sibling instances of the same product (excluding this one) — useful for
|
||||
// seeing previous purchases of the same SKU.
|
||||
@@ -101,6 +105,9 @@ export function ProductDetail({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={trapRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
@@ -108,16 +115,16 @@ export function ProductDetail({
|
||||
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",
|
||||
@@ -140,32 +147,47 @@ export function ProductDetail({
|
||||
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>
|
||||
Inventory · <span className="mono">{item.assetId}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "flex-end" }}>
|
||||
{isActive && (
|
||||
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
|
||||
{isActive && overdue && (
|
||||
<Btn variant="sage" icon="search" onClick={() => onAudit(item)}>
|
||||
Audit
|
||||
</Btn>
|
||||
)}
|
||||
{isActive && !overdue && (
|
||||
<Btn variant="secondary" icon="pocket" onClick={() => onCheckout(item)}>
|
||||
Check out
|
||||
</Btn>
|
||||
)}
|
||||
{isCheckedOut && (
|
||||
<Btn variant="sage" icon="pocket" onClick={() => onCheckin(item)}>
|
||||
Check in
|
||||
</Btn>
|
||||
)}
|
||||
<div style={{ width: 1, height: 20, background: "var(--line)", margin: "0 2px" }} />
|
||||
{isActive && !overdue && (
|
||||
<Btn variant="ghost" icon="search" onClick={() => onAudit(item)}>
|
||||
Audit
|
||||
</Btn>
|
||||
)}
|
||||
{isActive && overdue && (
|
||||
<Btn variant="ghost" icon="pocket" onClick={() => onCheckout(item)}>
|
||||
Check out
|
||||
</Btn>
|
||||
)}
|
||||
{isActive && (
|
||||
<Btn variant="ghost" icon="check" onClick={() => onAudit(item)}>
|
||||
Audit
|
||||
</Btn>
|
||||
)}
|
||||
{isCheckedOut && (
|
||||
<Btn variant="sage" icon="check" onClick={() => onCheckin(item)}>
|
||||
Check in
|
||||
{(isActive || isCheckedOut) && (
|
||||
<Btn variant="ghost" icon="leaf" onClick={() => onConsume(item)}>
|
||||
Consume
|
||||
</Btn>
|
||||
)}
|
||||
{(isActive || isCheckedOut) && (
|
||||
<Btn variant="secondary" icon="check" onClick={() => onConsume(item)}>
|
||||
Mark consumed
|
||||
<Btn variant="ghost" icon="bin" onClick={() => onMarkGone(item)}>
|
||||
Gone
|
||||
</Btn>
|
||||
)}
|
||||
{(isActive || isCheckedOut) && (
|
||||
<Btn variant="ghost" icon="bin" onClick={() => onMarkGone(item)} />
|
||||
)}
|
||||
<Btn variant="ghost" icon="edit" onClick={() => onEdit(item)} />
|
||||
<Btn variant="ghost" icon="close" onClick={onClose} />
|
||||
<Btn variant="ghost" icon="edit" onClick={() => onEdit(item)}>
|
||||
Edit
|
||||
</Btn>
|
||||
<Btn variant="ghost" icon="close" onClick={triggerClose} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user