/* global React */ // Shared primitives + cross-file state for DAVA'S prototype. const { useState, useEffect, useRef, useSyncExternalStore } = React; /* ─────────────────────────── PLACEHOLDER ──────────────────────────── */ function Placeholder({ label, ratio = "4/5", tone = "bone", corner, ring, fill = false, style, children }) { const cls = `ph ${tone}${fill ? " fill" : ""}`; const aspect = fill ? undefined : ratio; // Si label apunta a una foto real de producto, intenta mostrar la imagen // Acepta "assets/productos/XX.jpg", "/assets/productos/XX.jpg", o cualquier ruta absoluta a una imagen // PERO ignora el PLACEHOLDER genérico (que no existe en disco) para evitar 404s const looksLikePhoto = label && typeof label === "string" && /assets\/productos\/[A-Za-z0-9_-]+\.(jpg|jpeg|png|webp)/i.test(label) && !/PLACEHOLDER/i.test(label); const [imgFailed, setImgFailed] = useState(false); if (looksLikePhoto && !imgFailed) { // Normaliza la ruta: quita el "/" inicial si lo tiene para que sea relativa const src = label.replace(/^\//, ""); return (
{corner setImgFailed(true)} style={{ position: fill ? "absolute" : "relative", inset: fill ? 0 : "auto", width: "100%", height: "100%", objectFit: "contain", padding: "2%", display: "block", background: "var(--bone, #F5F0E6)" }} /> {corner ?
{corner}
: null} {children}
); } // Fallback: placeholder original (cuadros decorativos para fotos del taller, hero, etc.) return (
{label ?
{label}
: null} {ring ?
{ring}
: null} {corner ?
{corner}
: null} {children}
); } /* ─────────────────────────── EYEBROW ──────────────────────────── */ function Eyebrow({ children, onDark = false, accent = "var(--accent)" }) { return (
{children}
); } /* ─────────────────────────── HEART ICON ──────────────────────────── */ function HeartIcon({ filled = false, size = 14, stroke = "currentColor" }) { return ( ); } /* ─────────────────────────── ARROW ──────────────────────────── */ function Arr({ size = 12 }) { return ( ); } /* ─────────────────────────── SCROLL REVEAL HOOK ──────────────────────────── */ function useReveal() { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el || typeof IntersectionObserver === "undefined") return; if (!el.classList.contains("reveal")) el.classList.add("reveal"); const io = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("is-in"); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: "0px 0px -40px 0px" } ); io.observe(el); return () => io.disconnect(); }, []); return ref; } function Reveal({ as: Tag = "div", className = "", delay = 0, children, ...rest }) { const ref = useReveal(); return ( {children} ); } /* ─────────────────────────── WISHLIST STORE (window-shared) ──────────────────────────── */ const WL_KEY = "davas.wishlist.v1"; const wlStore = (() => { const listeners = new Set(); let state = []; try { const raw = localStorage.getItem(WL_KEY); if (raw) state = JSON.parse(raw); } catch {} const persist = () => { try { localStorage.setItem(WL_KEY, JSON.stringify(state)); } catch {} }; return { getSnapshot: () => state, subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); }, add: (p) => { if (!state.find((x) => x.id === p.id)) { state = [...state, p]; persist(); listeners.forEach((l) => l()); } }, remove: (id) => { state = state.filter((x) => x.id !== id); persist(); listeners.forEach((l) => l()); }, toggle: (p) => { const has = state.find((x) => x.id === p.id); if (has) { state = state.filter((x) => x.id !== p.id); } else { state = [...state, p]; } persist(); listeners.forEach((l) => l()); }, has: (id) => !!state.find((x) => x.id === id), clear: () => { state = []; persist(); listeners.forEach((l) => l()); }, }; })(); function useWishlist() { const items = useSyncExternalStore(wlStore.subscribe, wlStore.getSnapshot, wlStore.getSnapshot); return { items, count: items.length, add: wlStore.add, remove: wlStore.remove, toggle: wlStore.toggle, has: (id) => !!items.find((x) => x.id === id), clear: wlStore.clear, }; } /* ─────────────────────────── UI STORE (drawers, modals) ──────────────────────────── */ const uiStore = (() => { const listeners = new Set(); let state = { wlOpen: false, modal: null }; return { getSnapshot: () => state, subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); }, set: (patch) => { state = { ...state, ...patch }; listeners.forEach((l) => l()); }, }; })(); function useUI() { const s = useSyncExternalStore(uiStore.subscribe, uiStore.getSnapshot, uiStore.getSnapshot); return { ...s, openWl: () => uiStore.set({ wlOpen: true }), closeWl: () => uiStore.set({ wlOpen: false }), openModal: (p) => uiStore.set({ modal: p }), closeModal: () => uiStore.set({ modal: null }), }; } /* ─────────────────────────── WHATSAPP HELPER ──────────────────────────── */ function waLink(text) { return `https://wa.me/573005260637?text=${encodeURIComponent(text)}`; } function waMessageForWishlist(items) { if (!items.length) return "Hola DAVA'S, quiero ver opciones."; const lines = items.map((p, i) => `${i + 1}. ${p.model || p.name}${p.pattern ? ` · patrón ${p.pattern}` : ""}${p.price ? ` · $${p.price}` : ""}`).join("\n"); return `Hola DAVA'S, quiero pedir estas piezas:\n\n${lines}\n\n¿Me confirman disponibilidad y tiempos?`; } Object.assign(window, { Placeholder, Eyebrow, HeartIcon, Arr, useReveal, Reveal, useWishlist, wlStore, useUI, uiStore, waLink, waMessageForWishlist, });