Fix 15 UX friction points across modals, navigation, and accessibility
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:
2026-05-08 10:44:19 -04:00
parent 3bdf857099
commit 00a76a10d7
16 changed files with 356 additions and 78 deletions
+44 -22
View File
@@ -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>