/* Vooice — shared visual primitives.
Style language: bone paper, ink line-art, signal-orange, sage-connected.
Metaphor: the call as a physical object. Type as architecture.
*/
const VC_TOKENS = {
paper: '#F4EFE6',
paper2: '#ECE5D6',
paper3: '#E2D9C5',
ink: '#0E0E0F',
ink2: '#2A2724',
ink3: '#6A655C',
line: '#1B1A18',
lineSoft: 'rgba(14,14,15,0.10)',
signal: '#F04E23',
signalSoft: '#FCE3D8',
sage: '#7A8A6E',
sageSoft: '#DDE3D2',
butter: '#E8C36A',
};
// ── VLogo ─ logo mark with variants (controlled by Tweak: logoMark) ──────────
function VLogo({ size = 22, color = '#0E0E0F', dot = '#F04E23', variant }) {
const v = variant || (window.__vcLogoMark || 'halfdisc');
if (v === 'wordmark') return null; // wordmark-only mode: no glyph
if (v === 'phone') {
return (
);
}
if (v === 'sound') {
// concentric arcs — a sound emanating
return (
);
}
if (v === 'aperture') {
// V-as-aperture: triangle nesting circle, dot bottom
return (
);
}
// default — halfdisc
return (
);
}
// ── Wordmark with mark ────────────────────────────────────────────────────────
function VWordmark({ size = 16, color = '#0E0E0F', dot = '#F04E23' }) {
return (
vooice
);
}
// ── Mono label / system text ─────────────────────────────────────────────────
function Mono({ children, color, size = 10, weight = 500, style = {}, upper = true, className }) {
return (
{children}
);
}
// ── Tag / pill ───────────────────────────────────────────────────────────────
function Tag({ children, tone = 'ink', style = {}, className }) {
const tones = {
ink: { bg: 'transparent', fg: VC_TOKENS.ink, bd: VC_TOKENS.ink },
paper: { bg: VC_TOKENS.paper2, fg: VC_TOKENS.ink, bd: 'transparent' },
signal: { bg: VC_TOKENS.signal, fg: VC_TOKENS.paper, bd: 'transparent' },
signalSoft: { bg: VC_TOKENS.signalSoft, fg: VC_TOKENS.signal, bd: 'transparent' },
sage: { bg: VC_TOKENS.sage, fg: VC_TOKENS.paper, bd: 'transparent' },
sageSoft: { bg: VC_TOKENS.sageSoft, fg: '#3F4E33', bd: 'transparent' },
butter: { bg: VC_TOKENS.butter, fg: VC_TOKENS.ink, bd: 'transparent' },
ghost: { bg: 'transparent', fg: VC_TOKENS.ink3, bd: VC_TOKENS.lineSoft },
};
const t = tones[tone] || tones.ink;
return (
{children}
);
}
// ── Live waveform — physical "voice as architecture" ─────────────────────────
// renders a series of vertical bars whose heights breathe according to phase + a seeded amplitude.
function Waveform({
bars = 64, height = 80, color = VC_TOKENS.ink, accent = VC_TOKENS.signal,
active = true, speaker = 'vooice', // 'vooice' | 'callee' | 'silent'
width = '100%', barWidth = 3, gap = 2, seed = 1,
}) {
const [t, setT] = React.useState(0);
React.useEffect(() => {
if (!active) return;
let raf;
const tick = () => { setT(p => p + 0.06); raf = requestAnimationFrame(tick); };
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [active]);
const total = bars;
const items = [];
for (let i = 0; i < total; i++) {
// pseudo-random per-bar amplitude + phase, modulated by speaker
const r = Math.abs(Math.sin((i + 1) * 12.9898 * seed) * 43758.5453) % 1;
const phase = i * 0.18 + r * 6.28;
let amp = (Math.sin(t + phase) + 1) / 2; // 0..1
amp = amp * (0.5 + r * 0.6);
if (speaker === 'silent') amp *= 0.05;
if (speaker === 'callee') amp *= 0.55;
const h = Math.max(2, amp * height);
const isAccent = speaker === 'vooice' && (i % 7 === 0);
items.push(
);
}
return (
{items}
);
}
// ── Concentric ring — "ringing" indicator ────────────────────────────────────
function RingingRing({ size = 24, color = '#F04E23' }) {
return (
);
}
// ── Stamp — letterpress-style accent box ─────────────────────────────────────
function Stamp({ children, color = '#F04E23', style = {} }) {
return (
{children}
);
}
// ── Hairline label row — "FIELD · value" key-value, used a lot ───────────────
function FieldRow({ label, children, align = 'space-between' }) {
return (
{label}
{children}
);
}
// ── Tape strip — used as data ribbons / call timeline ────────────────────────
function TapeStrip({ ticks = 40, color = VC_TOKENS.ink, height = 28, label }) {
return (
{label &&
{label}}
{Array.from({length: ticks}).map((_,i) => (
))}
);
}
// ── Card surface — universal panel ───────────────────────────────────────────
function Surface({ children, padding = 24, bg = VC_TOKENS.paper, bordered = true, style = {} }) {
return (
{children}
);
}
// ── Section header ───────────────────────────────────────────────────────────
function SectionHead({ eyebrow, title, action }) {
return (
{eyebrow &&
{eyebrow}}
{title}
{action}
);
}
// ── Avatar — typographic, no images ──────────────────────────────────────────
function Avatar({ name, size = 36, bg = VC_TOKENS.ink, fg = VC_TOKENS.paper }) {
const initials = (name || '?').split(' ').slice(0,2).map(w => w[0]).join('').toUpperCase();
return (
{initials}
);
}
// ── Big number metric ────────────────────────────────────────────────────────
function Metric({ value, unit, label }) {
return (
{value}
{unit && {unit}}
{label}
);
}
// ── Phone glyph (thin line-art only — no emoji, no glowing icons) ────────────
function PhoneGlyph({ size = 14, color = 'currentColor', strokeWidth = 1.5 }) {
return (
);
}
function ArrowGlyph({ size = 14, dir = 'right', color = 'currentColor' }) {
const rot = { right: 0, left: 180, up: -90, down: 90 }[dir] || 0;
return (
);
}
// ── Striped placeholder for imagery (we don't have any) ──────────────────────
function StripedPlaceholder({ width = '100%', height = 200, label, color = VC_TOKENS.ink, bg = VC_TOKENS.paper2 }) {
const stripe = `repeating-linear-gradient(135deg, ${bg} 0 12px, ${VC_TOKENS.paper3} 12px 13px)`;
return (
{label}
);
}
// Expose to other Babel scripts
// ── AppHeader ──────────────────────────────────────────────────────────────
// Unified top nav for in-app screens. Use across Dashboard, OutboundCall,
// CallDetail, ContactsMemory, Reminders, AuditLog, Pricing.
// `current` highlights the active route key. `dark` flips palette for dark
// canvases (Reminders).
const APP_NAV = [
{ l: 'Home', k: 'home' },
{ l: 'Calls', k: 'outbound-call' },
{ l: 'Reminders', k: 'reminders' },
{ l: 'Memory', k: 'contacts-memory' },
{ l: 'Audit', k: 'audit' },
];
const APP_SUBNAV = [
{ l: 'Pricing', k: 'pricing' },
{ l: 'Landing', k: 'marketing-hero' },
];
function AppHeader({ onNavigate, current, crumbs = [], dark = false, status }) {
const fg = dark ? VC_TOKENS.paper : VC_TOKENS.ink;
const fgMute = dark ? '#807a72' : VC_TOKENS.ink3;
const bg = dark ? VC_TOKENS.ink : VC_TOKENS.paper;
const lineCol = dark ? '#2a2724' : VC_TOKENS.line;
const lineSoft = dark ? '#2a2724' : VC_TOKENS.lineSoft;
// Hide the inline nav and collapse it under a hamburger on phone-sized
// viewports. Using matchMedia keeps the header self-contained — no need to
// thread vw through every screen.
const [isMobile, setIsMobile] = React.useState(
typeof window !== 'undefined' && window.matchMedia('(max-width: 720px)').matches
);
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
const mq = window.matchMedia('(max-width: 720px)');
const update = () => setIsMobile(mq.matches);
if (mq.addEventListener) mq.addEventListener('change', update);
else mq.addListener(update);
return () => {
if (mq.removeEventListener) mq.removeEventListener('change', update);
else mq.removeListener(update);
};
}, []);
React.useEffect(() => { if (!isMobile) setOpen(false); }, [isMobile]);
const go = (k) => { setOpen(false); onNavigate && onNavigate(k); };
return (
go('home')} style={{ cursor:'pointer' }}>
{!isMobile && (
<>
{crumbs.length > 0 && (
<>
{crumbs.map((c, i) => {
const last = i === crumbs.length - 1;
const item = typeof c === 'string' ? { label: c } : c;
return (
item.k && go(item.k)}
style={{ cursor: item.k ? 'pointer' : 'default' }}
>
{item.label}
{!last && /}
);
})}
>
)}
>
)}
{!isMobile && (
<>
{APP_SUBNAV.map(it => (
go(it.k)} style={{ cursor:'pointer' }}>
{it.l.toUpperCase()}
))}
{status && (
{status}
)}
+1 (415) 555‑0188
>
)}
{isMobile && (
)}
{isMobile && open && (
{[...APP_NAV, ...APP_SUBNAV].map(it => {
const active = it.k === current;
return (
go(it.k)} role="menuitem" style={{
padding: '14px 0', borderBottom: `1px solid ${lineSoft}`,
cursor: 'pointer', display: 'flex',
justifyContent: 'space-between', alignItems: 'center',
}}>
{it.l.toUpperCase()}
{active && }
);
})}
{crumbs.length > 0 && (
{crumbs.map((c, i) => {
const last = i === crumbs.length - 1;
const item = typeof c === 'string' ? { label: c } : c;
return (
item.k && go(item.k)} style={{ cursor: item.k ? 'pointer' : 'default' }}>
{item.label}
{!last && /}
);
})}
)}
{status &&
{status}
}
)}
);
}
// Expose to other Babel scripts
Object.assign(window, {
VC_TOKENS, VLogo, VWordmark, Mono, Tag, Waveform, RingingRing, Stamp,
FieldRow, TapeStrip, Surface, SectionHead, Avatar, Metric, PhoneGlyph, ArrowGlyph,
StripedPlaceholder, AppHeader,
});