/* =========================================================== Shared UI components =========================================================== */ /* ---- line icons (simple, single-stroke) ---- */ const ICONS = { search: "M21 21l-4.3-4.3M11 19a8 8 0 110-16 8 8 0 010 16z", cart: "M3 4h2l2.4 12.2a2 2 0 002 1.6h8.7a2 2 0 002-1.6L23 8H6M9 22a1 1 0 100-2 1 1 0 000 2zm9 0a1 1 0 100-2 1 1 0 000 2z", quote: "M8 3h8a2 2 0 012 2v16l-6-3-6 3V5a2 2 0 012-2zM9 8h6M9 12h6", menu: "M3 6h18M3 12h18M3 18h18", close: "M6 6l12 12M18 6L6 18", chevron: "M9 6l6 6-6 6", chevdown: "M6 9l6 6 6-6", phone: "M5 4h3l2 5-2 1a12 12 0 006 6l1-2 5 2v3a2 2 0 01-2 2A16 16 0 013 6a2 2 0 012-2z", check: "M20 6L9 17l-5-5", plus: "M12 5v14M5 12h14", minus: "M5 12h14", arrow: "M5 12h14M13 6l6 6-6 6", pin: "M12 21s-7-6.2-7-11a7 7 0 1114 0c0 4.8-7 11-7 11zm0-8.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z", truck: "M3 6h11v9H3zM14 9h4l3 3v3h-7zM7 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm10 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z", clock: "M12 7v5l3 2M12 21a9 9 0 110-18 9 9 0 010 18z", shield: "M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6z", mail: "M3 5h18v14H3zM3 6l9 7 9-7", star: "M12 3l2.7 5.5 6 .9-4.3 4.2 1 6-5.4-2.8L6.6 19.6l1-6L3.3 9.4l6-.9z", spark: "M12 3v6M12 15v6M3 12h6M15 12h6", filter: "M3 5h18M6 12h12M10 19h4", user: "M12 12a4 4 0 100-8 4 4 0 000 8zm-7 8a7 7 0 0114 0z", lock: "M6 11h12v9H6zM8 11V8a4 4 0 018 0v3", wrench: "M14 7a3.5 3.5 0 01-4.6 4.6L5 16l3 3 4.4-4.4A3.5 3.5 0 0017 10l-1.5-1.5", }; function Icon({ name, size = 20, stroke = 2, className, style }) { return React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round", className, style, "aria-hidden": "true" }, React.createElement("path", { d: ICONS[name] || "" })); } /* ---- Logo: real shift-lever mark + wordmark ---- */ function Logo({ size = 34, light = false }) { return React.createElement(Link, { to: "/", className: "logo", "aria-label": "D&W Truck — home" }, React.createElement("span", { className: "logo-mark", style: { width: size, height: size } }, React.createElement("img", { src: "assets/logo.png", width: size, height: size, alt: "D&W Truck", style: { width: size, height: size, objectFit: "contain", display: "block" } }) ), React.createElement("span", { className: "logo-text", style: { color: light ? "#fff" : "var(--ink)" } }, React.createElement("strong", null, "DWTruck"), React.createElement("span", { className: "logo-sub" }, "SOLUTIONS") ) ); } /* ---- scroll reveal ---- */ function useReveal() { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; if (!("IntersectionObserver" in window) || window.matchMedia("(prefers-reduced-motion: reduce)").matches) { el.classList.add("in"); return; } const io = new IntersectionObserver((ents) => { ents.forEach(e => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }); }, { threshold: 0.08, rootMargin: "0px 0px -6% 0px" }); io.observe(el); const t = setTimeout(() => { if (el && !el.classList.contains("in")) { const r = el.getBoundingClientRect(); if (r.top < window.innerHeight + 80) el.classList.add("in"); } }, 400); return () => { io.disconnect(); clearTimeout(t); }; }, []); return ref; } function Reveal({ children, delay = 0, className = "", as = "div", style = {} }) { const ref = useReveal(); return React.createElement(as, { ref, className: "reveal " + className, style: { transitionDelay: delay + "ms", ...style } }, children); } /* ---- placeholder image box ---- */ function PH({ label, ratio = "4/3", dark = false, className = "", style = {} }) { return React.createElement("div", { className: "ph " + (dark ? "ph-dark " : "") + className, "data-label": label, style: { aspectRatio: ratio, ...style } }); } /* ---- real image with graceful placeholder fallback ---- */ function Img({ src, alt, ratio = "4/3", className = "", phLabel = "part photo" }) { const [err, setErr] = useState(false); if (!src || err) return React.createElement(PH, { label: phLabel, ratio, className }); return React.createElement("img", { src, alt: alt || "", loading: "lazy", className: "img " + className, style: { aspectRatio: ratio, width: "100%", height: "100%", objectFit: "cover", display: "block" }, onError: () => setErr(true) }); } /* ---- price block ---- */ function Price({ variant, size = "md" }) { if (variant.price === null || variant.price === undefined) return React.createElement(Link, { to: "/quote", className: "price-call price-quote-link", onClick: (e) => e.stopPropagation() }, "Request a quote →"); return React.createElement("span", { className: "price price-" + size }, variant.compare_at_price ? React.createElement("span", { className: "price-was" }, money(variant.compare_at_price)) : null, React.createElement("span", { className: "price-now" }, money(variant.price)), variant.core_charge ? React.createElement("span", { className: "price-core" }, "+ " + money(variant.core_charge) + " core") : null ); } /* ---- Add to quote button (used everywhere) ---- */ function AddToQuoteBtn({ product, variant, size = "sm", block = false, label = "Add to quote" }) { const quote = useQuote(); const toast = useToast(); const onClick = (e) => { e.preventDefault(); e.stopPropagation(); quote.addItem({ variant_id: variant.id, sku: variant.sku, name: product.name, product_slug: product.slug, price: variant.price, core_charge: variant.core_charge, image: product.images[0], attributes: variant.attributes, part_numbers: variant.part_numbers }); toast.push("Added to quote", { icon: "quote", action: { to: "/quote", label: "View quote" } }); }; return React.createElement("button", { className: "btn btn-ghost btn-" + size + (block ? " btn-block" : "") + " atq", onClick }, React.createElement(Icon, { name: "quote", size: 16 }), label); } /* ---- Product card ---- */ function ProductCard({ product }) { const v = product.variants[0]; const { navigate } = useRouter(); const orderable = product.is_online_orderable && v.price !== null; return React.createElement("article", { className: "card pcard", tabIndex: 0, onClick: () => navigate("/p/" + product.slug), onKeyDown: (e) => { if (e.key === "Enter") navigate("/p/" + product.slug); } }, React.createElement("div", { className: "pcard-media" }, React.createElement(Img, { src: (product.images && product.images[0]) ? product.images[0].url : "", alt: product.name, ratio: "4/3" }), React.createElement("div", { className: "pcard-badges" }, product.is_reman ? React.createElement("span", { className: "badge badge-reman" }, "Reman") : React.createElement("span", { className: "badge badge-oem" }, "New"), !orderable ? React.createElement("span", { className: "badge badge-call" }, "Call") : null ) ), React.createElement("div", { className: "pcard-body" }, React.createElement("div", { className: "pcard-group" }, product.grouping_label), React.createElement("h3", { className: "pcard-title" }, product.name), React.createElement("div", { className: "pcard-sku mono" }, v.sku), React.createElement("div", { className: "pcard-foot" }, React.createElement(Price, { variant: v, size: "sm" }), React.createElement(AddToQuoteBtn, { product, variant: v, label: "Quote" }) ) ) ); } /* ---- breadcrumbs ---- */ function Crumbs({ items }) { return React.createElement("nav", { className: "crumbs", "aria-label": "Breadcrumb" }, items.map((it, i) => React.createElement(React.Fragment, { key: i }, i > 0 ? React.createElement(Icon, { name: "chevron", size: 13, className: "crumb-sep" }) : null, it.to ? React.createElement(Link, { to: it.to, className: "crumb" }, it.label) : React.createElement("span", { className: "crumb crumb-cur" }, it.label) )) ); } /* ---- qty stepper ---- */ function Stepper({ value, onChange, min = 1 }) { return React.createElement("div", { className: "stepper" }, React.createElement("button", { onClick: () => onChange(Math.max(min, value - 1)), "aria-label": "Decrease" }, React.createElement(Icon, { name: "minus", size: 15 })), React.createElement("input", { type: "text", value, inputMode: "numeric", onChange: e => { const n = parseInt(e.target.value.replace(/\D/g, "")) || min; onChange(Math.max(min, n)); }, "aria-label": "Quantity" }), React.createElement("button", { onClick: () => onChange(value + 1), "aria-label": "Increase" }, React.createElement(Icon, { name: "plus", size: 15 })) ); } /* ---- skeleton grid ---- */ function SkeletonCard() { return React.createElement("div", { className: "card pcard" }, React.createElement("div", { className: "sk", style: { aspectRatio: "4/3" } }), React.createElement("div", { className: "pcard-body" }, React.createElement("div", { className: "sk", style: { height: 10, width: "40%", marginBottom: 10 } }), React.createElement("div", { className: "sk", style: { height: 16, width: "85%", marginBottom: 8 } }), React.createElement("div", { className: "sk", style: { height: 12, width: "55%" } }) ) ); } Object.assign(window, { Icon, Logo, useReveal, Reveal, PH, Img, Price, AddToQuoteBtn, ProductCard, Crumbs, Stepper, SkeletonCard });