/* global React */ const { useState, useEffect, useRef, useMemo, useCallback } = React; // ============================================================ // SVG ICONS — minimal stroke icons (Lucide-like, redrawn) // ============================================================ const Icon = { Search: (p) => (), Cart: (p) => (), Heart: (p) => (), Menu: (p) => (), X: (p) => (), Plus: (p) => (), Compare: (p) => (), Filter: (p) => (), Bookmark: (p) => (), Arrow: (p) => (), Check: (p) => (), Shield: (p) => (), Plane: (p) => (), }; // ============================================================ // LOGO LOCKUP // ============================================================ function Logo({ onClick }) { return (
INDIE Paper logomark
INDIE PAPER
The right paper changes everything.
); } // ============================================================ // NAV // ============================================================ function Nav({ route, navigate, cartCount = 0, compareCount = 0, onCartClick, authUser, authProfile, onAuthClick, onSignOut, onProfileClick }) { const [scrolled, setScrolled] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); // Simple search function — runs against _INDIE_CATALOG in memory const runSearch = (q) => { setSearchQuery(q); if (!q || q.length < 2) { setSearchResults([]); return; } const catalog = window._INDIE_CATALOG || window.PAPERS || []; const ql = q.toLowerCase(); const results = catalog.filter(p => { return ( p.name.toLowerCase().includes(ql) || (p.brand || '').toLowerCase().includes(ql) || (p.finish || '').toLowerCase().includes(ql) || (p.color || '').toLowerCase().includes(ql) || (p.sku || '').toLowerCase().includes(ql) || String(p.gsm || '').includes(q) || (p.tags || []).some(t => t.toLowerCase().includes(ql)) ); }).slice(0, 8); setSearchResults(results); }; const openSearch = () => { setSearchOpen(true); setTimeout(() => document.getElementById('ip-search-input')?.focus(), 50); }; const closeSearch = () => { setSearchOpen(false); setSearchQuery(''); setSearchResults([]); }; // Close on Escape useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') closeSearch(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openSearch(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 8); window.addEventListener("scroll", onScroll); onScroll(); return () => window.removeEventListener("scroll", onScroll); }, []); return ( ); } // ============================================================ // BRAND STAMP — small circular brand badge // ============================================================ function BrandStamp({ brand, tier }) { const cls = brand === "Fedrigoni" ? "fedrigoni" : brand === "Superb Bamboo" ? "bamboo" : brand === "Gmund" ? "gmund" : "local"; const initial = brand === "Fedrigoni" ? "F" : brand === "Superb Bamboo" ? "竹" : brand === "Gmund" ? "G" : brand === "Arjowiggins" ? "A" : "L"; return ( {initial} {brand} ); } // ============================================================ // PAPER SWATCH — visual placeholder with paper-like texture // drops to image-slot when user fills // ============================================================ function Swatch({ paper, slotId, children }) { // Render as CSS-generated paper texture by default — terra-cotta, ivory, kraft tones. // `image-slot` element underneath will accept user drops later. return (
{children}
); } // CSS-generated paper texture using layered radial gradients + SVG noise function PaperTexture({ tone, grain }) { // Determine grain pattern via SVG turbulence baseFreq const baseFreq = { smooth: "0.65", felt: "0.85", fiber: "1.2", cotton: "0.95", laid: "0.55", kraft: "1.4", embossed: "0.4", tradition: "0.75", }[grain] || "0.85"; const id = useMemo(() => "ptx-" + Math.random().toString(36).slice(2, 9), []); // For dark papers (black), invert noise feel const isDark = tone === "#1A1A18"; const noiseOpacity = isDark ? 0.18 : 0.32; return (
{/* embossed/laid lines for laid finish */} {grain === "laid" && (
)} {/* deckled edge (top-left corner peel) */}
); } // shade utility — darken a hex by N% function shade(hex, percent) { const h = hex.replace("#", ""); const num = parseInt(h, 16); let r = (num >> 16) + Math.round(255 * percent / 100); let g = ((num >> 8) & 0xff) + Math.round(255 * percent / 100); let b = (num & 0xff) + Math.round(255 * percent / 100); r = Math.max(0, Math.min(255, r)); g = Math.max(0, Math.min(255, g)); b = Math.max(0, Math.min(255, b)); return "#" + ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0"); } // ============================================================ // PAPER CARD // ============================================================ function PaperCard({ paper, onOpen, onAddCart, onToggleCompare, isComparing, onRequestSample }) { const gsmOptions = Object.keys(paper.price || {}).filter(k => k !== 'default').sort((a,b) => parseInt(a)-parseInt(b)); const [selectedGsm, setSelectedGsm] = useState(gsmOptions.length === 1 ? gsmOptions[0] : null); const unitPrice = selectedGsm ? (paper.price[selectedGsm] || paper.price_min || 0) : (paper.price_min || 0); const hasVariants = gsmOptions.length > 1; return (
onOpen?.(paper)}>
{paper.eco && Eco · FSC} {paper.brandTier === "premium" && !paper.eco && Premium} {paper.featured && Editor's pick}
Grain
{paper.grain}
Color
{paper.color}
Origin
{paper.origin}
MOQ
{paper.moq} sheets
{paper.brand} {paper.origin}

{paper.name}

A3+ · {paper.finish}
{hasVariants && (
e.stopPropagation()}> {gsmOptions.map(g => ( ))}
)}
A3+ · from / sheet {selectedGsm ? <>Rp {unitPrice.toLocaleString("id-ID")}{selectedGsm}gsm : <>from Rp {(paper.price_min || 0).toLocaleString("id-ID")} }
e.stopPropagation()}>
); } // ============================================================ // FOOTER // ============================================================ function Footer({ navigate }) { return ( ); } // expose Object.assign(window, { React, Icon, Logo, Nav, Footer, BrandStamp, Swatch, PaperTexture, PaperCard, shade, });