/* SAPL Garment Library — app state & composition */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "cardVariant": "Gallery", "gridStyle": "Mixed", "cardSize": 250, "accent": "#9a7322" }/*EDITMODE-END*/; const STOPWORDS = ["the", "for", "and", "with", "something", "edit", "in", "a", "of"]; function matchesQuery(g, q) { if (!q.trim()) return true; const hay = [ g.style, g.season, g.category, g.dna, g.fabric, g.silhouette, g.value, g.team, g.craft.join(" "), g.colours.map((c) => c.name).join(" "), g.sample ? "sample" : "", g.pending ? "review pending" : "", ].join(" ").toLowerCase(); const tokens = q.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.includes(t)); if (!tokens.length) return true; return tokens.some((t) => hay.includes(t) || (t.endsWith("s") && hay.includes(t.slice(0, -1)))); } function isSimilar(g, ref) { if (g.id === ref.id) return true; return g.dna === ref.dna || g.fabric === ref.fabric || g.silhouette === ref.silhouette || g.value === ref.value; } function App() { const { GARMENTS, DNA_MOODS, FACETS, BOARDS, SEARCH_SEEDS } = window.GL_DATA; const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [loading, setLoading] = React.useState(true); const [activeDna, setActiveDna] = React.useState(null); const [facetSel, setFacetSel] = React.useState({}); const [appliedQuery, setAppliedQuery] = React.useState(""); const [similarTo, setSimilarTo] = React.useState(null); const [searchOpen, setSearchOpen] = React.useState(false); const [searchDraft, setSearchDraft] = React.useState(""); const [selection, setSelection] = React.useState([]); const [boardsOpen, setBoardsOpen] = React.useState(false); const [uploadOpen, setUploadOpen] = React.useState(false); const [pinned, setPinned] = React.useState({}); const [toast, setToast] = React.useState(null); React.useEffect(() => { const id = setTimeout(() => setLoading(false), 900); return () => clearTimeout(id); }, []); React.useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setSearchOpen(true); } if (e.key === "Escape") { setSearchOpen(false); setBoardsOpen(false); setUploadOpen(false); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const showToast = (msg) => { setToast(msg); setTimeout(() => setToast((m) => (m === msg ? null : m)), 2600); }; const reshimmer = () => { setLoading(true); setTimeout(() => setLoading(false), 450); }; /* ---- filtering ---- */ const passesFacets = (g) => FACETS.every((f) => { const sel = facetSel[f.key] || []; if (!sel.length) return true; const v = f.get(g); return Array.isArray(v) ? v.some((x) => sel.includes(x)) : sel.includes(v); }); const filtered = GARMENTS.filter( (g) => (!activeDna || g.dna === activeDna) && passesFacets(g) && matchesQuery(g, appliedQuery) && (!similarTo || isSimilar(g, similarTo)) ); const draftCount = GARMENTS.filter((g) => matchesQuery(g, searchDraft)).length; const dnaCounts = {}; GARMENTS.forEach((g) => { dnaCounts[g.dna] = (dnaCounts[g.dna] || 0) + 1; }); const dominant = Object.entries(dnaCounts).sort((a, b) => b[1] - a[1])[0][0]; const pendingCount = GARMENTS.filter((g) => g.pending).length; const facetOptions = (f) => { const m = new Map(); GARMENTS.forEach((g) => { const v = f.get(g); (Array.isArray(v) ? v : [v]).forEach((x) => m.set(x, (m.get(x) || 0) + 1)); }); return [...m.entries()].sort().map(([value, count]) => ({ value, count })); }; const toggleFacet = (key, value) => setFacetSel((s) => { const cur = s[key] || []; return { ...s, [key]: cur.includes(value) ? cur.filter((v) => v !== value) : [...cur, value] }; }); const pills = []; FACETS.forEach((f) => (facetSel[f.key] || []).forEach((v) => pills.push({ key: f.key, value: v, label: v, type: "facet" }))); if (appliedQuery) pills.push({ key: "q", value: appliedQuery, label: "“" + appliedQuery + "”", type: "query" }); if (similarTo) pills.push({ key: "sim", value: similarTo.id, label: "Similar to " + similarTo.style, type: "similar" }); const removePill = (p) => { if (p.type === "facet") toggleFacet(p.key, p.value); if (p.type === "query") setAppliedQuery(""); if (p.type === "similar") setSimilarTo(null); }; const clearAll = () => { setFacetSel({}); setAppliedQuery(""); setSimilarTo(null); setActiveDna(null); }; /* ---- actions ---- */ const toggleSelect = (g) => setSelection((s) => (s.includes(g.id) ? s.filter((id) => id !== g.id) : [...s, g.id])); const saveOne = (g) => { setSelection((s) => (s.includes(g.id) ? s : [...s, g.id])); }; const addToBoard = (board) => { const name = board ? board.name : "Untitled board"; if (board) setPinned((p) => ({ ...p, [board.id]: [...new Set([...(p[board.id] || []), ...selection])] })); showToast(selection.length + (selection.length === 1 ? " piece pinned to " : " pieces pinned to ") + name); setSelection([]); }; const findSimilar = (g) => { clearAll(); setSimilarTo(g); reshimmer(); showToast("Pieces cut from the same cloth as " + g.style); }; const commitSearch = () => { setAppliedQuery(searchDraft.trim()); setSearchOpen(false); setSimilarTo(null); reshimmer(); }; /* ---- layout ---- */ const size = t.cardSize; const gridStyleObj = { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(min(" + size + "px, 100%), 1fr))", gridAutoFlow: "dense", gap: "28px 22px", }; return (