/* ============================================================================ MOWDEW LIVE DEMO — shared widgets Detailed papercraft personas (+ circular face avatars), persona headers, flow trackers, record-mark statuses, sync ticker, demo notification bell. The three phones live in mowdew-demo-phones.jsx + mowdew-demo-tabs.jsx; the world state machine lives in Mowdew Live Demo.html. ========================================================================= */ function dmClock(sec) { const m = Math.floor(sec / 60), s = sec % 60; return `${m}:${String(s).padStart(2, '0')}`; } /* ─── Papercraft personas v3 — detailed cut-paper busts, calm poses. Every garment is layered like real paper: occlusion slivers under each overlapping piece, a catch-light on the left paper edge, darker right facets (left-lit). Persona-defining details: Diego wears glasses, a lanyard ID and a watch, tablet under his arm; Marco has his cap with stitching, a shoulder towel, name badge and gripped pool pole; Emma a knit cardigan, scrunchie bun, freckles and a leaf-print mug. Face box unchanged (x57-113 / y22-78) so PaperAvatar crops stay valid. */ function PaperOwner({ size = 118 }) { return ( {/* neck + chin shadow */} {/* torso — leaf polo, broad shoulders */} {/* collar */} {/* lanyard + ID card */} {/* chest pocket + pen */} {/* right arm — cuff, watch, hand with thumb */} {/* left arm + tablet under it */} {/* ears */} {/* head + shade + under-fringe occlusion */} {/* hair — two-tone crop, faded sides */} {/* brows */} {/* glasses + eyes */} {/* nose + smile + blush */} ); } function PaperPro({ size = 118 }) { return ( {/* pool pole — standing in his grip, taped */} {/* neck */} {/* torso — aqua work shirt */} {/* collar + zip + pull */} {/* towel over the left shoulder */} {/* name badge */} {/* left arm + cuff + hand */} {/* right arm gripping the pole */} {/* ears */} {/* head + shade */} {/* brim shadow */} {/* cap — dome, seams, button, droplet patch, brim */} {/* face */} ); } function PaperHome({ size = 118 }) { return ( {/* neck */} {/* tee */} {/* necklace */} {/* cardigan — shawl collar, knit marks, buttons */} {/* arms curve in to the mug + cuffs */} {/* steam + mug with leaf print, both hands w/ thumbs */} {/* ears + hoops */} {/* head + shade + under-fringe occlusion */} {/* hair — sweep, side piece, bun + coral scrunchie, strand, flyaways */} {/* face — brows, lashes, eyes, freckles, nose, smile, blush */} ); } /* ─── PaperAvatar — circular face crop of the same papercraft figures ── */ function PaperAvatar({ who, size = 40, ring = false }) { const P = who === 'owner' ? PaperOwner : who === 'pro' ? PaperPro : PaperHome; const k = size / 56; return (

); } /* ─── Papercraft scene props — one per persona, same cut-paper language: left-lit, darker right facets, soft ground shadow. Sit at the right end of each persona header. ─ */ function PaperOffice({ size = 100 }) { const h = size * 150 / 240; return ( {/* rooftop AC */} {/* parapet */} {/* walls */} {/* front windows — 2 × 3 */} {[48, 72, 96].map(y => ( ))} {/* side windows */} {/* awning + glass door + logo plate */} {/* the crew fleet — same aqua trucks, parked out front */} {[[108, 107, 0.58], [172, 107, 0.58]].map(([tx, ty, s], i) => ( ))} ); } function PaperTruck({ size = 110 }) { const h = size * 130 / 200; return ( {/* bed interior */} {/* tools in the bed */} {/* bed rail */} {/* body + rocker */} {/* cab */} {/* door logo */} {/* front — grill, bumper, headlight */} {/* wheels */} ); } function PaperHouse({ size = 100 }) { const h = size * 150 / 235; return ( {/* chimney */} {/* walls */} {/* roof — front gable + side slope */} {/* door + knob */} {/* windows */} {/* garden — hedge + flower bed */} {/* pool — deck, water, sheen, waves, ladder, float */} ); } /* ─── Persona header — the ROLE is the headline; the name is secondary ─ */ const DEMO_PERSONAS = { owner: { persona: 'Business owner', name: 'Diego Marta', line: 'runs the book', color: 'var(--green-700)', Person: PaperOwner, Prop: PaperOffice }, pro: { persona: 'Pro', name: 'Marco Ruiz', line: 'on the route', color: 'var(--blue-700)', Person: PaperPro, Prop: PaperTruck }, home: { persona: 'Homeowner', name: 'Emma Walsh', line: 'sees it verified', color: 'var(--aqua-700)', Person: PaperHome, Prop: PaperHouse }, }; function PersonaHead({ p }) { const m = DEMO_PERSONAS[p]; const P = m.Person; const Prop = m.Prop; return (

{m.name} · {m.line}
{Prop && }
); } /* ─── DmMark — record-mark status (dot + tracked mono), never a pill ── */ const DM_MARK_C = { blue: ['var(--blue-700)', 'var(--blue-500)'], aqua: ['var(--aqua-700)', 'var(--aqua-500)'], green: ['var(--green-700)', 'var(--green-600)'], yellow:['var(--yellow-700)', 'var(--yellow-500)'], alert: ['var(--alert-700)', 'var(--alert-500)'], ink: ['var(--ink-500)', 'var(--ink-300)'], }; function DmMark({ tone = 'ink', pulse = false, children, style = {} }) { const [c, dot] = DM_MARK_C[tone] || DM_MARK_C.ink; return ( {children} ); } /* reschedule state → mark */ function rqMark(state, viewer) { if (state === 'pending') return viewer === 'owner' ? ['yellow', 'NEEDS YOU', true] : ['yellow', 'PENDING', true]; if (state === 'proposed') return viewer === 'owner' ? ['yellow', 'WAITING', true] : ['yellow', 'NEW TIME', true]; if (state === 'approved') return ['green', 'CONFIRMED', false]; return ['alert', 'DECLINED', false]; } function DmHint({ children }) { return (
{children}
); } /* ─── Flow trackers — light up as the demo progresses ───────────────── */ function FlowTracker({ label, icon, tone, steps, active = true, onClick }) { const toneC = tone === 'aqua' ? 'var(--aqua-700)' : tone === 'blue' ? 'var(--blue-700)' : tone === 'yellow' ? 'var(--yellow-700)' : tone === 'coral' ? 'var(--alert-700)' : tone === 'violet' ? 'var(--chem-violet)' : 'var(--green-700)'; const tone50 = tone === 'aqua' ? 'var(--aqua-50)' : tone === 'blue' ? 'var(--blue-50)' : tone === 'yellow' ? 'var(--yellow-50)' : tone === 'coral' ? 'var(--alert-50)' : tone === 'violet' ? 'var(--chem-50)' : 'var(--green-50)'; return (
{label.toUpperCase()} {steps.map(([lb, done], i) => ( {i > 0 && } {done ? : } {lb} ))}
); } /* ─── Sync ticker — every cross-phone event, newest first ───────────── */ const DEMO_TONE = { blue: { bg: 'var(--blue-50)', fg: 'var(--blue-700)' }, aqua: { bg: 'var(--aqua-50)', fg: 'var(--aqua-700)' }, green: { bg: 'var(--green-50)', fg: 'var(--green-700)' }, yellow:{ bg: 'var(--yellow-50)', fg: 'var(--yellow-700)' }, alert: { bg: 'var(--alert-50)', fg: 'var(--alert-700)' }, }; const DEMO_P_SHORT = { owner: 'Owner', pro: 'Pro', home: 'Home' }; function SyncTicker({ events }) { const list = [...events].reverse(); return (
LIVE SYNC {list.length === 0 ? ( One backend, three phones — interact with any phone and watch the others update. ) : (
{list.map((e) => { const t = DEMO_TONE[e.tone] || DEMO_TONE.green; return ( {e.msg} → {e.to.map(p => DEMO_P_SHORT[p]).join(' + ')} ); })}
)}
); } /* ─── Demo bell + in-phone notification sheet ───────────────────────── */ function DemoBell({ persona, world, act }) { const mine = world.events.filter(e => e.to.includes(persona)); const unread = Math.max(0, mine.length - world.read[persona]); return act.openNotifs(persona)} />; } function DemoNotifSheet({ persona, world, act }) { const mine = [...world.events.filter(e => e.to.includes(persona))].reverse(); return ( act.closeNotifs(persona)} title="Notifications"> {mine.length === 0 ? (
{persona === 'owner' ? : }
All quiet.
When something happens on the other phones, it shows up here.
) : (
{mine.map((e, i) => { const t = DEMO_TONE[e.tone] || DEMO_TONE.green; return (
{e.msg}
{e.at}
); })}
)}
); } /* ─── PaperScene — depth-of-field papercraft diorama behind the phones ── Three depth layers (blurred far → crisp near): paper hills + sun + clouds at the back; Emma's house and a paper tree mid; Marco's Mowdew truck (gap 1) and the pool (gap 2) up front. Coordinates match the 1800-wide demo stage; the phones occlude the middle of each element, which is what sells the depth. Decorative only — aria-hidden. */ function PaperScene() { const sh = (blur = 0) => ({ filter: `${blur ? `blur(${blur}px) ` : ''}drop-shadow(0 6px 8px rgba(27,26,23,.14))` }); return ( ); } /* ─── CrayonHint — hand-drawn note that sits in the gutter BESIDE a phone and points at it. dir='left' = phone is to the LEFT (note in the right gutter); dir='right' = phone is to the RIGHT (note in the left gutter). The note is anchored fully outside the phone so it never overlaps the screen. ─ */ function CrayonHint({ text, tone, idx = 0, dir = 'left', base = 240 }) { const isLeft = dir === 'left'; // phone is to the LEFT of the note const pos = isLeft ? { left: '100%', marginLeft: 8 } : { right: '100%', marginRight: 8 }; const Arrow = ( ); const Note = ( {text} ); return (
{isLeft ? <>{Arrow}{Note} : <>{Note}{Arrow}}
); } Object.assign(window, { dmClock, PaperOwner, PaperPro, PaperHome, PaperAvatar, PaperOffice, PaperTruck, PaperHouse, DEMO_PERSONAS, PersonaHead, CrayonHint, DmMark, rqMark, DmHint, FlowTracker, SyncTicker, DEMO_TONE, DemoBell, DemoNotifSheet, PaperScene, });