/* ==========================================================
YAPPY. — shared UI primitives
========================================================== */
/* ---------- Icons: 3px stroke, round caps, forest only ---------- */
const ICON_PATHS = {
leaf: <>>,
person: <>>,
target: <>>,
explore: <>>,
consult: <>>,
decision: <>>,
box: <>>,
shield: <>>,
lock: <>>,
doc: <>>,
support: <>>,
factory: <>>,
calendar: <>>,
clock: <>>,
arrow: <>>,
back: <>>,
plus: <>>,
close: <>>,
menu: <>>,
bag: <>>,
user: <>>,
check: <>>,
alert: <>>,
pause: <>>,
mail: <>>,
};
function Icon({ name, size = 24, stroke = 3, style, className }) {
return (
);
}
/* ---------- Logo: supplied artwork only. Never retyped. ---------- */
function Logo({ variant = 'forest', width = 150, onClick, label = 'Yappy — home' }) {
const src = variant === 'cream' ? 'assets/logo-cream.png' : variant === 'mono' ? 'assets/logo-mono.png' : 'assets/logo-forest.png';
// clear space = 1x full-stop diameter (≈ 6.2% of wordmark width)
const pad = Math.round(width * 0.062);
return (
);
}
function Greenrise({ variant = 'cream', width = 64 }) {
return
;
}
/* ---------- Brand line (always in this form) ---------- */
function BrandLine({ dark = false, size = 14, style }) {
return (
The world is loud. Find your quiet.
);
}
/* ---------- Flowing shape: one per panel, bleeds off edge ---------- */
const FLOW_PATHS = {
a: 'M420 0 C 320 120 470 260 360 380 C 260 490 330 620 500 700 L 700 700 L 700 0 Z',
b: 'M0 520 C 160 430 260 560 400 500 C 540 440 600 330 700 360 L 700 700 L 0 700 Z',
c: 'M700 120 C 540 160 520 300 600 400 C 680 500 560 620 420 700 L 700 700 Z',
d: 'M260 0 C 200 180 380 240 360 400 C 340 560 160 600 220 700 L 700 700 L 700 0 Z',
e: 'M0 0 L 380 0 C 300 140 420 220 340 360 C 260 500 120 520 0 640 Z',
};
function Flow({ shape = 'a', color, style, className }) {
return (
);
}
/* ---------- Full stop motif (max one per layout) ---------- */
function FullStop({ size = 120, style }) {
return ;
}
/* ---------- Tbc / Demo flags ---------- */
function Tbc({ children = 'To be confirmed', solid }) { return {children}; }
function Demo({ children = 'Demo data' }) { return {children}; }
function Price({ value, label = 'Price to be confirmed' }) {
const p = fmtPrice(value);
return p ? {p} : {label};
}
/* ---------- Variant pill ---------- */
function VariantPill({ p, onDark }) {
return {p.pct} Variant*;
}
function SkuName({ p, size = 120, color, dotColor }) {
return (
{p.name}.
);
}
const VARIANT_NOTE = '*Percentages are variant identifiers only. They do not indicate strength, potency, dosage or effect.';
/* ---------- Noise field ---------- */
function NoiseField({ rows = 14, dark = false, size = 15, style, className, fade }) {
const text = React.useMemo(() => {
const out = [];
for (let r = 0; r < rows; r++) {
const start = (r * 7) % NOISE_WORDS.length;
const seq = [...NOISE_WORDS.slice(start), ...NOISE_WORDS.slice(0, start)];
out.push(seq.join(' · ') + ' · ');
}
return out;
}, [rows]);
return (
{text.map((t, i) =>
{t}{t}
)}
);
}
/* ---------- Pack shot (supplied packaging artwork) ---------- */
function Pack({ p, width = 300, rotate = 0, shadow = true, style, className = '' }) {
return (
);
}
/* ---------- Header ---------- */
function Header({ path }) {
const { state, setCartOpen } = useStore();
const [open, setOpen] = React.useState(false);
const items = [['Shop', '/shop'], ['How It Works', '/how-it-works'], ['Free Consultation', '/consult'], ['FAQs', '/faq']];
const count = cartCount(state.cart);
React.useEffect(() => { setOpen(false); }, [path]);
return (
<>
Now introducing BRB. · DND. · OFFLINE. — Find your quiet.
{open && (
{[...items, ['My orders', '/account'], ['Support', '/support']].map(([l, h]) =>
{l})}
)}
>
);
}
/* ---------- Qty stepper ---------- */
function Qty({ value, onChange, dark }) {
const b = { width: 40, height: 40, display: 'grid', placeItems: 'center', borderRadius: '50%' };
return (
{value}
);
}
/* ---------- Cart drawer ---------- */
function CartDrawer() {
const { state, setQty, setCartOpen } = useStore();
const close = () => setCartOpen(false);
const total = cartTotal(state.cart);
return (
<>
>
);
}
/* ---------- Footer ---------- */
function Footer() {
const cols = [
['Shop', [['BRB.', '/product/brb'], ['DND.', '/product/dnd'], ['OFFLINE.', '/product/offline'], ['All products', '/shop']]],
['Guidance', [['How it works', '/how-it-works'], ['Free doctor consultation', '/consult'], ['Our standards', '/standards'], ['FAQs', '/faq']]],
['Help', [['Contact & support', '/support'], ['My orders', '/account'], ['Shipping & cancellation', '/policy/shipping'], ['Refunds', '/policy/refunds']]],
['Legal', [['Privacy policy', '/policy/privacy'], ['Terms & conditions', '/policy/terms'], ['Regulatory & product information', '/policy/regulatory']]],
];
return (
);
}
/* ---------- Accordion ---------- */
function Accordion({ items, defaultOpen = -1 }) {
const [open, setOpen] = React.useState(defaultOpen);
return (
{items.map(([q, a], i) => (
))}
);
}
/* ---------- Page hero (shared info pages) ---------- */
function PageHero({ kicker, title, children, tone = 'cream', flow = 'c' }) {
const dark = tone === 'forest';
return (
{kicker &&
{kicker}
}
{title}
{children}
);
}
function DemoRibbon() {
return null;
}
Object.assign(window, {
Icon, Logo, Greenrise, BrandLine, Flow, FullStop, Tbc, Demo, Price, VariantPill, SkuName, VARIANT_NOTE,
NoiseField, Pack, Header, CartDrawer, Qty, Footer, Accordion, PageHero, DemoRibbon,
});