/* 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 (
);
}
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 (
setZoom((z) => clamp(z - 0.15, 0.3, 1.7))}>−
{Math.round(zoom * 100)}%
setZoom((z) => clamp(z + 0.15, 0.3, 1.7))}>
);
}
function Welcome() {
const [show, setShow] = useState(true);
useEffect(() => { const t = setTimeout(() => setShow(false), 30000); return () => clearTimeout(t); }, []);
if (!show) {
return (
setShow(true)} title="Show welcome" aria-label="Show welcome" style={{
position: 'absolute', right: 18, bottom: 300, zIndex: 27, width: 40, height: 40, borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
background: 'var(--surface-glass)', border: '1px solid var(--line-2)', color: 'var(--neon-cyan)',
backdropFilter: 'blur(18px)', WebkitBackdropFilter: 'blur(18px)', boxShadow: 'var(--shadow-lg)',
}}>
);
}
return (
Welcome
setShow(false)} aria-label="Dismiss welcome" style={{ display: 'flex', background: 'none', border: 'none', color: 'var(--text-3)', cursor: 'pointer', padding: 0 }}>
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.
{emails.map((em) => setEmails(emails.filter((x) => x !== em))} accent="var(--neon-violet)">{em} )}
{share}
setCopied(true)} iconLeft={ }>{copied ? 'Copied' : 'Copy'}
setSent(true)} iconLeft={ }>{sent ? `Sent to ${emails.length} ✓` : `Send ${emails.length} invites`}
);
}
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.
{STYLES.map((s) => setStyl(s)}>{s.replace('_', ' ')} )}
}>Add to nebula
{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]) => } onClick={() => onEvolve && onEvolve(name.id, dir)}>{lbl})}
{related.length > 0 && (
}>Similar names
{related.map((m) => (
onOpen && onOpen(m.id)} style={{ cursor: 'pointer', padding: '6px 11px', borderRadius: 'var(--radius-pill)', border: '1px solid var(--line-2)', background: 'var(--surface-2)', color: 'var(--text-1)', fontFamily: 'var(--font-sans)', fontWeight: 600, fontSize: 13 }}>{m.name}
))}
)}
{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,
};
})();