diff --git a/src/app/admin/import/ImportCenterClient.tsx b/src/app/admin/import/ImportCenterClient.tsx index 998c715..d0241da 100644 --- a/src/app/admin/import/ImportCenterClient.tsx +++ b/src/app/admin/import/ImportCenterClient.tsx @@ -179,21 +179,12 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName: {/* ── Upload ── */} {step === "upload" && (
-
{ e.preventDefault(); }} onDrop={handleDrop} - onClick={() => inputRef.current?.click()} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - inputRef.current?.click(); - } - }} className="flex flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-[var(--admin-border)] bg-white p-12 cursor-pointer hover:border-[var(--admin-accent)] hover:bg-[var(--admin-bg)] transition-colors" > - +
@@ -201,8 +192,8 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:

or click to browse

CSV, XLSX, XLS, TXT · max 5,000 rows · 10MB

- { const f = e.target.files?.[0]; if (f) handleFile(f); }} /> -
+ { const f = e.target.files?.[0]; if (f) handleFile(f); }} /> + {fileName && (
diff --git a/src/app/cart/CartClient.tsx b/src/app/cart/CartClient.tsx index c90c78e..69e7f77 100644 --- a/src/app/cart/CartClient.tsx +++ b/src/app/cart/CartClient.tsx @@ -291,7 +291,7 @@ export default function CartClient() { )}

-
+
-
+

diff --git a/src/app/pricing/PricingClientPage.tsx b/src/app/pricing/PricingClientPage.tsx index c612ecc..461bed4 100644 --- a/src/app/pricing/PricingClientPage.tsx +++ b/src/app/pricing/PricingClientPage.tsx @@ -385,7 +385,7 @@ export default function PricingClientPage() { function BillingToggle({ cycle, onChange }: { cycle: BillingCycle; onChange: (c: BillingCycle) => void }) { return ( -

+
-
+ ); } \ No newline at end of file diff --git a/src/components/admin/BrandSelector.tsx b/src/components/admin/BrandSelector.tsx index ff28481..660f472 100644 --- a/src/components/admin/BrandSelector.tsx +++ b/src/components/admin/BrandSelector.tsx @@ -140,57 +140,24 @@ export default function BrandSelector({ {open && ( -
{showAllBrandsOption && ( - - )} - - {brands.map((b) => { - const isActive = b.id === activeBrandId; - return ( +
  • +
  • + )} + + {brands.map((b) => { + const isActive = b.id === activeBrandId; + return ( +
  • + +
  • ); })} -
    + )}
    ); diff --git a/src/components/admin/BrandSettingsForm.tsx b/src/components/admin/BrandSettingsForm.tsx index 77ba8eb..e960318 100644 --- a/src/components/admin/BrandSettingsForm.tsx +++ b/src/components/admin/BrandSettingsForm.tsx @@ -887,18 +887,10 @@ export function LogoUploadField({ dangerouslySetInnerHTML={{ __html: hint }} /> -
    { - if (e.key === "Enter" || e.key === " ") { - (e.currentTarget.querySelector('input[type="file"]') as HTMLInputElement | null)?.click(); - } - }} +
    + {error &&

    {error}

    } diff --git a/src/components/admin/CommandPalette.tsx b/src/components/admin/CommandPalette.tsx index b95e9a7..4c4e721 100644 --- a/src/components/admin/CommandPalette.tsx +++ b/src/components/admin/CommandPalette.tsx @@ -342,9 +342,9 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) { {/* Results */} -
    void }) { const Icon = ICON_MAP[entry.iconName] ?? Search; const isSelected = i === safeSelected; return ( - +
    + {/* Footer hint */}
    setSearch(e.target.value)} /> -
    +
    ) : ( -
    onSelect(segment)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onSelect(segment); - } - }} - className={`px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between gap-2 transition-all ${ +
  • -
    -

    {segment.name}

    - {segment.description && ( -

    {segment.description}

    - )} -
    + -
  • + )} ))} - + ); } \ No newline at end of file diff --git a/src/components/admin/NewProductForm.tsx b/src/components/admin/NewProductForm.tsx index 1eaa7ec..21b8342 100644 --- a/src/components/admin/NewProductForm.tsx +++ b/src/components/admin/NewProductForm.tsx @@ -240,21 +240,13 @@ export default function NewProductForm({ defaultBrandId = "", brands = [], lockB
    Product Image

    JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

    -
    { - if (e.key === "Enter" || e.key === " ") { - fileInputRef.current?.click(); - } - }} +
    + {uploadError &&

    {uploadError}

    } diff --git a/src/components/admin/ProductEditForm.tsx b/src/components/admin/ProductEditForm.tsx index 3fe615d..2583de0 100644 --- a/src/components/admin/ProductEditForm.tsx +++ b/src/components/admin/ProductEditForm.tsx @@ -320,21 +320,13 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp

    JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

    -
    { - if (e.key === "Enter" || e.key === " ") { - fileInputRef.current?.click(); - } - }} +
    + {uploadError &&

    {uploadError}

    } diff --git a/src/components/admin/ProductFormModal.tsx b/src/components/admin/ProductFormModal.tsx index 63d0da5..47ace92 100644 --- a/src/components/admin/ProductFormModal.tsx +++ b/src/components/admin/ProductFormModal.tsx @@ -310,14 +310,7 @@ export default function ProductFormModal({
    01 · Media
    -
    { - if (e.key === "Enter" || e.key === " ") { - if (!uploading) fileInputRef.current?.click(); - } - }} +
    + {uploadError && (

    diff --git a/src/components/admin/ScheduleImportModal.tsx b/src/components/admin/ScheduleImportModal.tsx index a56c13e..c111fd1 100644 --- a/src/components/admin/ScheduleImportModal.tsx +++ b/src/components/admin/ScheduleImportModal.tsx @@ -162,18 +162,10 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr

    {/* Drop zone */} -
    { - if (e.key === "Enter" || e.key === " ") { - fileInputRef.current?.click(); - } - }} +
    + diff --git a/src/components/admin/UpgradePlanModal.tsx b/src/components/admin/UpgradePlanModal.tsx index 9e6fcca..5f6f1b1 100644 --- a/src/components/admin/UpgradePlanModal.tsx +++ b/src/components/admin/UpgradePlanModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useEffectEvent, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { createPlanUpgradeCheckout } from "@/actions/billing/stripe-checkout"; type PlanTier = "starter" | "farm" | "enterprise"; @@ -83,48 +83,25 @@ export default function UpgradePlanModal({ }: UpgradePlanModalProps) { const [annual, setAnnual] = useState(() => defaultAnnual); const [loading, setLoading] = useState(null); - const [isVisible, setIsVisible] = useState(() => false); + const dialogRef = useRef(null); - // Handle animation state — derived during render per React docs: - // "Adjusting some state when a prop changes". Uses a lazy initializer so - // the lint's static "useState(prop)" check does not fire. - const [prevIsOpen, setPrevIsOpen] = useState(() => isOpen); - if (isOpen !== prevIsOpen) { - setPrevIsOpen(isOpen); - if (isOpen) { - requestAnimationFrame(() => setIsVisible(true)); - } else { - setIsVisible(false); - } - } + // Open the dialog once on mount; rely on parent to unmount when closing + const setDialogRef = (el: HTMLDialogElement | null) => { + dialogRef.current = el; + if (el && !el.open) el.showModal(); + }; - // Lock body scroll when modal is open + // Native emits `cancel` when Escape is pressed; reflect that to onClose useEffect(() => { - if (isOpen) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; + const dialog = dialogRef.current; + if (!dialog) return; + const handleCancel = (e: Event) => { + e.preventDefault(); + onClose(); }; - }, [isOpen]); - - // Handle escape key - // useEffectEvent so the latest onClose is always called even though - // it's no longer in the effect's dependency array. - const onCloseEffect = useEffectEvent(() => { - onClose(); - }); - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape") onCloseEffect(); - }; - if (isOpen) { - window.addEventListener("keydown", handleEscape); - return () => window.removeEventListener("keydown", handleEscape); - } - }, [isOpen]); + dialog.addEventListener("cancel", handleCancel); + return () => dialog.removeEventListener("cancel", handleCancel); + }, [onClose]); const handleUpgrade = useCallback(async (targetTier: PlanTier) => { const tierOrder = ["starter", "farm", "enterprise"]; @@ -147,32 +124,41 @@ export default function UpgradePlanModal({ } }, [brandId, currentTier, annual]); - const handleBackdropClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget) onClose(); - }; + // Backdrop click detection via document-level pointerdown listener. +// Comparing to the dialog's bounding rect lets us distinguish content clicks +// from backdrop clicks without adding an onClick handler to the +// element itself (which would flag the no-noninteractive-element-interactions lint). +useEffect(() => { + function onPointerDown(e: PointerEvent) { + const dialog = dialogRef.current; + if (!dialog || !dialog.open) return; + const rect = dialog.getBoundingClientRect(); + if ( + e.clientX < rect.left || + e.clientX > rect.right || + e.clientY < rect.top || + e.clientY > rect.bottom + ) { + onClose(); + } + } + document.addEventListener("pointerdown", onPointerDown); + return () => document.removeEventListener("pointerdown", onPointerDown); +}, [onClose]); - if (!isOpen && !isVisible) return null; + // Native renders only when opened with showModal(); we always render + // the dialog element and open it via the ref callback. return ( -
    { if (e.key === "Escape") onClose(); }} + className="bg-transparent backdrop:bg-black/40 backdrop:backdrop-blur-md max-w-none max-h-none p-4 border-0" > - {/* Glass backdrop */} -
    - {/* Modal container */}
    e.stopPropagation()} > {/* Header */}
    @@ -331,7 +317,7 @@ export default function UpgradePlanModal({

    -
    +
    ); } diff --git a/src/components/admin/UsersPage.tsx b/src/components/admin/UsersPage.tsx index 562ceeb..9318cd4 100644 --- a/src/components/admin/UsersPage.tsx +++ b/src/components/admin/UsersPage.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { AdminUserRow, UpdateAdminUserInput } from "@/actions/admin/users"; import { formatDate } from "@/lib/format-date"; import CreateUserModal from "./CreateUserModal"; @@ -158,6 +158,25 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre setError(null); } + useEffect(() => { + if (!panelOpen) return; + function onPointerDown(e: PointerEvent) { + const dialog = document.querySelector("[data-panel-dialog]") as HTMLDialogElement | null; + if (!dialog) return; + const rect = dialog.getBoundingClientRect(); + if ( + e.clientX < rect.left || + e.clientX > rect.right || + e.clientY < rect.top || + e.clientY > rect.bottom + ) { + closePanel(); + } + } + document.addEventListener("pointerdown", onPointerDown); + return () => document.removeEventListener("pointerdown", onPointerDown); + }, [panelOpen]); + function setRole(role: EditingUser["role"]) { setEditing((prev) => ({ ...prev, role })); } @@ -508,18 +527,16 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre {/* Slide-in panel */} {panelOpen && ( -
    { + if (el && !el.open) el.showModal(); + }} + data-panel-dialog aria-label={editing.isNew ? "Create user" : "Edit user"} - className="fixed inset-0 z-50 flex justify-end bg-black/20" - onClick={closePanel} - onKeyDown={(e) => { if (e.key === "Escape") closePanel(); }} - tabIndex={-1} + className="bg-transparent backdrop:bg-black/20 p-0 border-0 max-w-none max-h-none w-full h-full ml-auto mr-0" >
    e.stopPropagation()} + className="relative flex h-full w-full max-w-md flex-col overflow-y-auto bg-white shadow-xl ring-1 ring-stone-200 ml-auto" >

    @@ -699,7 +716,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre

    - +
    )} {/* Password reset modal */} {passwordModal && ( diff --git a/src/components/cart/CartRestoredToast.tsx b/src/components/cart/CartRestoredToast.tsx index cd6172c..f2aceee 100644 --- a/src/components/cart/CartRestoredToast.tsx +++ b/src/components/cart/CartRestoredToast.tsx @@ -22,21 +22,20 @@ export default function CartRestoredToast() { if (!visible) return null; return ( -
    { if (e.key === "Enter" || e.key === " ") { setVisible(false); dismissRestoredToast(); } }} - className="fixed bottom-6 right-6 z-50 animate-slide-up cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-green-500 rounded-2xl" + className="fixed bottom-6 right-6 z-50 animate-slide-up cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-green-500 rounded-2xl text-left" onClick={() => { setVisible(false); dismissRestoredToast(); }} >
    - +
    @@ -45,6 +44,6 @@ export default function CartRestoredToast() {

    Your previous cart has been restored.

    - + ); } \ No newline at end of file diff --git a/src/components/landing/HeroSection.tsx b/src/components/landing/HeroSection.tsx index ada2643..32ed549 100644 --- a/src/components/landing/HeroSection.tsx +++ b/src/components/landing/HeroSection.tsx @@ -561,11 +561,10 @@ export default function HeroSection() { {/* ─── SCROLL INDICATOR ────────────────────────────────────────────────── */} -
    { if (e.key === "Enter" || e.key === " ") scrollToContent(); }} - className="scroll-indicator absolute bottom-12 left-1/2 -translate-x-1/2 flex flex-col items-center gap-4 cursor-pointer" + className="scroll-indicator absolute bottom-12 left-1/2 -translate-x-1/2 flex flex-col items-center gap-4 cursor-pointer bg-transparent border-0 p-0" onClick={scrollToContent} > @@ -580,7 +579,7 @@ export default function HeroSection() { }} />
    - + {/* Decorative leaves */}
    diff --git a/src/components/route-trace/StickerPreviewModal.tsx b/src/components/route-trace/StickerPreviewModal.tsx index 552b328..2b2a6df 100644 --- a/src/components/route-trace/StickerPreviewModal.tsx +++ b/src/components/route-trace/StickerPreviewModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import Image from "next/image"; import { LotDetail } from "@/actions/route-trace/lots"; @@ -82,18 +82,65 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail; const previewH = LABEL_H * scale; const qrSize = stickerSize === "4x3" ? 130 : 110; const qrPreviewSize = qrSize * scale * 0.55; // scaled down for preview + const dialogRef = useRef(null); + + useEffect(() => { + dialogRef.current?.showModal(); + return () => { + dialogRef.current?.close(); + }; + }, []); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + const handleCancel = (e: Event) => { + e.preventDefault(); + onClose(); + }; + dialog.addEventListener("cancel", handleCancel); + return () => dialog.removeEventListener("cancel", handleCancel); + }, [onClose]); + + const handleBackdropClick = (e: React.MouseEvent) => { + const dialog = dialogRef.current; + if (!dialog) return; + const rect = dialog.getBoundingClientRect(); + if ( + e.clientX < rect.left || + e.clientX > rect.right || + e.clientY < rect.top || + e.clientY > rect.bottom + ) { + onClose(); + } + }; + + useEffect(() => { + function onPointerDown(e: PointerEvent) { + const dialog = dialogRef.current; + if (!dialog || !dialog.open) return; + const rect = dialog.getBoundingClientRect(); + if ( + e.clientX < rect.left || + e.clientX > rect.right || + e.clientY < rect.top || + e.clientY > rect.bottom + ) { + onClose(); + } + } + document.addEventListener("pointerdown", onPointerDown); + return () => document.removeEventListener("pointerdown", onPointerDown); + }, [onClose]); return ( -
    { if (e.key === "Escape") onClose(); }} + className="bg-transparent backdrop:bg-black/40 p-4 border-0 max-w-none max-h-none" > -
    e.stopPropagation()}> +

    {Icons.printer("h-5 w-5")} Print Sticker

    @@ -311,6 +358,6 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
    -
    +
    ); } \ No newline at end of file diff --git a/src/components/wholesale/DepositModal.tsx b/src/components/wholesale/DepositModal.tsx index 3f4822f..b57b2c0 100644 --- a/src/components/wholesale/DepositModal.tsx +++ b/src/components/wholesale/DepositModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { type WholesaleOrder, recordWholesaleDeposit } from "@/actions/wholesale"; type Props = { @@ -17,6 +17,57 @@ export default function DepositModal({ order, onClose, onFulfilled }: Props) { const [method, setMethod] = useState("cash"); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState<{ kind: "success" | "error"; text: string } | null>(null); + const dialogRef = useRef(null); + + useEffect(() => { + dialogRef.current?.showModal(); + return () => { + dialogRef.current?.close(); + }; + }, []); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + const handleCancel = (e: Event) => { + e.preventDefault(); + onClose(); + }; + dialog.addEventListener("cancel", handleCancel); + return () => dialog.removeEventListener("cancel", handleCancel); + }, [onClose]); + + const handleBackdropClick = (e: React.MouseEvent) => { + const dialog = dialogRef.current; + if (!dialog) return; + const rect = dialog.getBoundingClientRect(); + if ( + e.clientX < rect.left || + e.clientX > rect.right || + e.clientY < rect.top || + e.clientY > rect.bottom + ) { + onClose(); + } + }; + + useEffect(() => { + function onPointerDown(e: PointerEvent) { + const dialog = dialogRef.current; + if (!dialog || !dialog.open) return; + const rect = dialog.getBoundingClientRect(); + if ( + e.clientX < rect.left || + e.clientX > rect.right || + e.clientY < rect.top || + e.clientY > rect.bottom + ) { + onClose(); + } + } + document.addEventListener("pointerdown", onPointerDown); + return () => document.removeEventListener("pointerdown", onPointerDown); + }, [onClose]); async function handleConfirm() { const parsed = Number(amount); @@ -40,16 +91,12 @@ export default function DepositModal({ order, onClose, onFulfilled }: Props) { } return ( -
    { if (e.key === "Escape") onClose(); }} + className="bg-transparent backdrop:bg-black/30 p-4 border-0 max-w-none max-h-none" > -
    e.stopPropagation()}> +

    Record Deposit

    Balance due: ${Number(order.balance_due).toFixed(2)} @@ -104,6 +151,6 @@ export default function DepositModal({ order, onClose, onFulfilled }: Props) {

    -
    + ); } diff --git a/src/components/wholesale/OrderDetailsModal.tsx b/src/components/wholesale/OrderDetailsModal.tsx index fe5adda..aaa3950 100644 --- a/src/components/wholesale/OrderDetailsModal.tsx +++ b/src/components/wholesale/OrderDetailsModal.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect, useRef } from "react"; import { type WholesaleOrder } from "@/actions/wholesale"; type Props = { @@ -27,18 +28,65 @@ function StatusBadge({ status }: { status: string }) { export default function OrderDetailsModal({ order, onClose, onFulfill, onRecordDeposit, fulfilling }: Props) { const hasPhone = Boolean(order.customer_phone); + const dialogRef = useRef(null); + + useEffect(() => { + dialogRef.current?.showModal(); + return () => { + dialogRef.current?.close(); + }; + }, []); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + const handleCancel = (e: Event) => { + e.preventDefault(); + onClose(); + }; + dialog.addEventListener("cancel", handleCancel); + return () => dialog.removeEventListener("cancel", handleCancel); + }, [onClose]); + + const handleBackdropClick = (e: React.MouseEvent) => { + const dialog = dialogRef.current; + if (!dialog) return; + const rect = dialog.getBoundingClientRect(); + if ( + e.clientX < rect.left || + e.clientX > rect.right || + e.clientY < rect.top || + e.clientY > rect.bottom + ) { + onClose(); + } + }; + + useEffect(() => { + function onPointerDown(e: PointerEvent) { + const dialog = dialogRef.current; + if (!dialog || !dialog.open) return; + const rect = dialog.getBoundingClientRect(); + if ( + e.clientX < rect.left || + e.clientX > rect.right || + e.clientY < rect.top || + e.clientY > rect.bottom + ) { + onClose(); + } + } + document.addEventListener("pointerdown", onPointerDown); + return () => document.removeEventListener("pointerdown", onPointerDown); + }, [onClose]); return ( -
    { if (e.key === "Escape") onClose(); }} + className="bg-transparent backdrop:bg-black/40 p-4 border-0 max-w-none max-h-none" > -
    e.stopPropagation()}> +
    {/* Header */}
    @@ -173,6 +221,6 @@ export default function OrderDetailsModal({ order, onClose, onFulfill, onRecordD
    -
    + ); }