/* ============================================================================
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 (
);
}
function PaperPro({ size = 118 }) {
return (
);
}
function PaperHome({ size = 118 }) {
return (
);
}
/* ─── 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 (
);
}
function PaperTruck({ size = 110 }) {
const h = size * 130 / 200;
return (
);
}
function PaperHouse({ size = 100 }) {
const h = size * 150 / 235;
return (
);
}
/* ─── 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.persona}
{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 (
)}
);
}
/* ─── 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 (