/* 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 (
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,
});