/* Brainstorm X — Name Nebula: shared helpers + presentational pieces. Loaded before flow-arena.jsx. Exposes window.BXNebula + window.BXheatFromVotes. */ (function () { const { useState, useEffect, useRef } = React; const DS = window.BrainstormXDesignSystem_f2a458; const { Button, Card, NameCard, KeywordOrb, Badge, Avatar, AvatarStack, IconButton, Input, Chip } = DS; const I = window.BXIcons; const clamp = (v, a, b) => Math.min(b, Math.max(a, v)); const INVITED = (window.BXpeople ? window.BXpeople.length : 3) + 1; window.BXheatFromVotes = function (up = 0, down = 0) { const net = up - down; const hi = Math.ceil(INVITED * 1.5), lo = Math.ceil(INVITED * 0.8); if (net >= hi) return 'on_fire'; if (net >= lo) return 'hot'; if (net >= 1) return 'warm'; if (net <= -hi) return 'frozen'; if (net <= -lo) return 'cold'; return 'neutral'; }; function hash(str) { let h = 0; for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0; return h; } function deriveVoters(n) { const people = window.BXpeople || []; const c = n.counts || {}; const tally = people.map((p) => ({ name: p.name, up: 0, down: 0 })); ['up', 'down'].forEach((type) => { const total = c[type] || 0; for (let i = 0; i < total; i++) tally[i % (people.length || 1)][type]++; }); return tally.filter((t) => t.up || t.down); } // names related by shared keyword / style / wave function relatedIds(name, names) { if (!name) return new Set(); const ks = new Set(name.keywords || []); const ids = new Set(); names.forEach((m) => { if (m.id === name.id) return; const shareKw = (m.keywords || []).some((k) => ks.has(k)); if (shareKw || m.style === name.style || (m.wave != null && m.wave === name.wave)) ids.add(m.id); }); return ids; } function whyText(n, names) { const sameStyle = names.filter((m) => m.style === n.style && (m.counts ? (m.counts.up || 0) > (m.counts.down || 0) : false)).length; if (n.heat === 'on_fire' || n.heat === 'hot') { return `Rising — ${sameStyle > 1 ? `${sameStyle} teammates favour ${n.style.replace('_', ' ')} names` : 'the team keeps voting it up'}.`; } if (n.heat === 'frozen' || n.heat === 'cold') return 'Cooling — the team is moving away from this direction.'; if (n.custom) return 'Added by your team — not yet generated by the AI.'; return 'Undecided — needs a few more votes to find its place.'; } function domainRows(name) { const root = name.toLowerCase().replace(/[^a-z0-9]/g, ''); return [['.com', 0], ['.co.uk', 1], ['.io', 2], ['.app', 3]].map(([tld]) => { const taken = tld === '.com' ? hash(root) % 3 !== 0 : hash(root + tld) % 2 === 0; return { domain: root + tld, available: !taken }; }); } function legalRows(name) { const root = name.replace(/[^A-Za-z0-9]/g, ''); const cands = [`${root}ly`, `${root} Co`, `${root.slice(0, Math.max(3, root.length - 1))}o`]; const states = [ { label: 'No exact mark found', tone: 'success' }, { label: 'Similar mark — review', tone: 'fire' }, { label: 'Possible conflict', tone: 'fire' }, { label: 'Clear in your class', tone: 'success' }, ]; return cands.map((c, i) => ({ name: c, ...states[(hash(c) + i) % states.length] })); } // ---- one-off styles + keyframes -------------------------------------- { const css = ` @keyframes bxFloatY{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}} @keyframes bxFireGlow{0%,100%{box-shadow:0 0 16px 1px rgba(255,90,55,.55), inset 0 0 18px rgba(255,130,50,.32)}50%{box-shadow:0 0 32px 5px rgba(255,60,40,.85), inset 0 0 30px rgba(255,140,45,.55)}} @keyframes bxIceGlow{0%,100%{box-shadow:0 0 18px 1px rgba(120,200,255,.45), inset 0 0 22px rgba(185,232,255,.38)}50%{box-shadow:0 0 28px 4px rgba(150,215,255,.65), inset 0 0 32px rgba(205,242,255,.55)}} @keyframes bxFrost{0%,100%{opacity:.5}50%{opacity:.95}} @keyframes bxPortalPulse{0%,100%{box-shadow:0 0 22px rgba(45,226,230,.4)}50%{box-shadow:0 0 40px rgba(45,226,230,.75)}} @keyframes bxPortalCore{0%{transform:translate(-50%,-50%) scale(0.2);opacity:0}30%{opacity:1}100%{transform:translate(-50%,-50%) scale(2.4);opacity:0}} @keyframes bxSpark{0%{transform:translate(-50%,-50%) scale(0.2);opacity:1}100%{transform:translate(-50%,-50%) scale(2.6);opacity:0}} @keyframes bxSpin{to{transform:rotate(360deg)}} @keyframes bxBannerIn{0%{transform:transl(-50%,0) translateY(-14px);opacity:0}100%{opacity:1}} .bx-nc-mount [aria-label="Shortlist"]:not([data-overlay]){display:none !important} .bx-nc-mount [aria-label="Strong contender"]{display:none !important} .bx-nc-mount [aria-label="Move away from this"]{display:none !important} .bx-scroll{scrollbar-width:thin;scrollbar-color:rgba(160,150,255,0.45) transparent;} .bx-scroll::-webkit-scrollbar{width:9px;height:9px;} .bx-scroll::-webkit-scrollbar-track{background:transparent;margin:10px 0;} .bx-scroll::-webkit-scrollbar-thumb{background:linear-gradient(180deg,var(--neon-violet),var(--neon-cyan));border-radius:999px;border:2px solid transparent;background-clip:padding-box;} .bx-scroll::-webkit-scrollbar-thumb:hover{background:var(--neon-cyan);background-clip:padding-box;}`; let el = document.getElementById('bx-arena-extra'); if (!el) { el = document.createElement('style'); el.id = 'bx-arena-extra'; document.head.appendChild(el); } el.textContent = css; } function Stars({ n = 70 }) { const stars = React.useMemo(() => Array.from({ length: n }, () => ({ top: Math.random() * 100, left: Math.random() * 100, s: Math.random() * 2 + 1, o: Math.random() * 0.6 + 0.15, glow: Math.random() > 0.88, })), [n]); return
{stars.map((s, i) => )}
; } function FireFX() { return (
); } function FrostFX() { return (
); } function VoterAvatars({ voters, size = 22, popUp = true }) { const [open, setOpen] = useState(null); if (!voters.length) return null; return (
{voters.slice(0, 5).map((t, i) => ( { e.stopPropagation(); setOpen(open === i ? null : i); }} title={`${t.name} — ${t.up} up · ${t.down} down`} style={{ marginLeft: i === 0 ? 0 : -size * 0.34, zIndex: open === i ? 30 : voters.length - i, cursor: 'pointer', position: 'relative' }}> {open === i && (
e.stopPropagation()} style={{ position: 'absolute', [popUp ? 'bottom' : 'top']: 'calc(100% + 8px)', left: '50%', transform: 'translateX(-50%)', zIndex: 60, width: 150, padding: '10px 12px', borderRadius: 'var(--radius-md)', background: 'var(--surface-glass-strong)', border: '1px solid var(--line-2)', backdropFilter: 'blur(18px)', WebkitBackdropFilter: 'blur(18px)', boxShadow: 'var(--shadow-lg)', }}>
{t.name}
{t.up} {t.down}
)}
))}
); } function SectionLabel({ children, icon }) { return (
{icon}{children}
); } function Header({ wave, project = 'Pickle Paws', onGenerate, generating, goto, onInvite, onMoreNames, onAddCustom, sprint, onToggleSprint }) { return (
{project}
Name Nebula · Wave {wave}
); } function ZoomControl({ zoom, setZoom, onReset }) { const btn = { width: 30, height: 30, borderRadius: 8, border: '1px solid var(--line-2)', background: 'var(--surface-2)', color: 'var(--text-1)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }; return (
); } function Welcome() { const [show, setShow] = useState(true); useEffect(() => { const t = setTimeout(() => setShow(false), 30000); return () => clearTimeout(t); }, []); if (!show) { return ( ); } return (
Welcome
This is your Name Nebula. Liked names grow and drift to the centre; rejected ones cool and drift out. Boost a keyword to pull its names closer. Hover a name to see its family. Drag a name into a portal to evolve it, or onto another to fuse them.
); } function ActivityFeed({ events }) { return (
Activity
{events.slice(0, 5).map((e, i) => (
{e.who.split(' ')[0]} {e.what}
))}
); } // Banner shown after a wave: AI explainability function WaveBanner({ text }) { if (!text) return null; return (
{text}
); } function SprintBar({ seconds }) { return (
🔥 Fire round — vote fast! 0:{String(seconds).padStart(2, '0')}
); } // Evolve portals — fade in while dragging a name const PORTALS = [ { id: 'shorter', label: 'Shorter', ic: 'Minimize' }, { id: 'premium', label: 'More premium', ic: 'Crown' }, { id: 'playful', label: 'More playful', ic: 'Sparkles' }, { id: 'unusual', label: 'More unusual', ic: 'Wand' }, { id: 'british', label: 'More British', ic: 'Flag' }, { id: 'like', label: 'More like this', ic: 'Copy' }, ]; function Portals({ active, hoverId }) { return (
{PORTALS.map((p) => { const on = hoverId === p.id; const Ic = I[p.ic] || I.Sparkles; return (
{p.label}
); })}
); } function InviteModal({ onClose }) { const [emails, setEmails] = useState(['alex@studio.co.uk', 'priya@studio.co.uk']); const [val, setVal] = useState(''); const [copied, setCopied] = useState(false); const [sent, setSent] = useState(false); const share = 'brainstorm-x.app/s/pickle-paws-9f3'; const add = (e) => { e.preventDefault(); const v = val.trim(); if (v && !emails.includes(v)) setEmails([...emails, v]); setVal(''); }; return (
e.stopPropagation()} style={{ width: 'min(520px,100%)', padding: '26px 28px', position: 'relative' }}>
Invite your team

Bring people into this session

They’ll get a secure link to vote, comment and shape the shortlist — no password needed. The more people vote, the sooner names catch fire.

setVal(e.target.value)} iconLeft={} />
{emails.map((em) => setEmails(emails.filter((x) => x !== em))} accent="var(--neon-violet)">{em})}
{share}
); } function AddCustomModal({ onAdd, onClose }) { const [val, setVal] = useState(''); const [styl, setStyl] = useState('brandable'); const [added, setAdded] = useState([]); const STYLES = ['brandable', 'evocative', 'compound', 'real_word', 'short_phrase']; const submit = (e) => { e.preventDefault(); const v = val.trim(); if (!v) return; onAdd(v, styl); setAdded((a) => [v, ...a]); setVal(''); }; return (
e.stopPropagation()} style={{ width: 'min(460px,100%)', padding: '26px 28px', position: 'relative' }}>
Add your own

Add a custom name

Got a name in mind? Drop it into the nebula to vote on it alongside the AI ideas.

setVal(e.target.value)} iconLeft={} />
{STYLES.map((s) => setStyl(s)}>{s.replace('_', ' ')})}
{added.length > 0 && (
Added this session
{added.map((a, i) => {a})}
)}
); } function DetailPanel({ name, names, onClose, onEvolve, onOpen, onVote }) { const variants = [['like', 'More like this'], ['shorter', 'Make it shorter'], ['premium', 'More premium'], ['playful', 'More playful']]; const voters = deriveVoters(name); const domains = domainRows(name.name); const legal = legalRows(name.name); const related = [...relatedIds(name, names)].map((id) => names.find((m) => m.id === id)).filter(Boolean).slice(0, 6); return (
{name.style}

{name.name}

{name.pronunciation}

{name.reason}

{/* AI explainability */}
{whyText(name, names)}{name.wave ? ` · Wave ${name.wave}.` : ''}
{name.keywords.map((k) => {k})}
SCORE
{name.score > 0 ? '+' + name.score : name.score}
CONFIDENCE
{name.confidence != null ? name.confidence + '%' : '—'}
}>Evolve this name
{variants.map(([dir, lbl]) => )}
{related.length > 0 && (
}>Similar names
{related.map((m) => ( ))}
)} {voters.length > 0 && (
}>Who voted
tap for votes
)}
}>Domains
{domains.map((d) => (
{d.domain} {d.available ? 'Available' : 'Taken'}
))}
Indicative — live availability lookups coming soon.
}>Legal
{legal.map((l) => (
{l.name} {l.label}
))}
Indicative similar-mark scan — not legal advice. Run a formal trademark search before deciding.
}>Comments
I like how short this is.
Works for a .co.uk domain too 👍
); } window.BXNebula = { clamp, hash, deriveVoters, relatedIds, whyText, Stars, FireFX, FrostFX, VoterAvatars, SectionLabel, Header, ZoomControl, Welcome, ActivityFeed, WaveBanner, SprintBar, Portals, InviteModal, AddCustomModal, DetailPanel, }; })();