// Shared cross-page chrome + section primitives for the TensorHost
// auxiliary-service pages. Reuses the marketing primitives already loaded
// from site/shared.jsx (Button, Eyebrow, Section, StatusDot) and the theme
// tokens defined on each host page.
// Canonical page map — keep hrefs encoded so spaces survive on file:// too.
const AUX_PAGES = [
{ key: 'web', label: 'Web', href: 'Web%20Hosting.html' },
{ key: 'storage', label: 'Storage', href: 'Cloud%20Storage.html' },
{ key: 'email', label: 'Email', href: 'TensorMail.html' },
{ key: 'gpu', label: 'GPU', href: 'GPU%20Rentals.html' },
];
const HOME_HREF = 'TensorHost%20Home.html';
const CONTACT_HREF = 'TensorHost%20Home.html#contact';
const SIGNIN_HREF = 'Sign%20In.html';
const REGISTER_HREF = 'Register.html';
// Header links mirror the original home-page nav (anchors live on the home
// Unified top-nav links — identical on every page. Anchors point back to the
// home page; Privacy and FAQ are their own dedicated pages.
const NAV_LINKS = [
{ label: 'Home', href: HOME_HREF, active: 'home' },
{ label: 'Privacy', href: 'Privacy.html', active: 'privacy' },
{ label: 'FAQ', href: 'FAQ.html', active: 'faq' },
{ label: 'Contact', href: 'Contact.html', active: 'contact' },
];
// The four dedicated service pages, surfaced under the Services menu.
const SERVICE_LINKS = [
{ key: 'web', label: 'Web hosting', href: 'Web%20Hosting.html' },
{ key: 'storage', label: 'Cloud storage', href: 'Cloud%20Storage.html' },
{ key: 'email', label: 'Business email', href: 'TensorMail.html' },
{ key: 'gpu', label: 'GPU rentals', href: 'GPU%20Rentals.html' },
];
// ── Cross-page top navigation ──────────────────────────────────────────────
function AuxNav({ theme, current }) {
const [scrolled, setScrolled] = React.useState(false);
const logo = theme === 'dark' ? 'site/assets/logo-mark-white.svg' : 'site/assets/logo-mark.svg';
React.useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
);
}
// ── Services dropdown ──────────────────────────────────────────────────────
// "Services" header item; on hover/focus reveals the four dedicated pages.
function ServicesMenu({ current }) {
const [open, setOpen] = React.useState(false);
const ref = React.useRef(null);
const closeT = React.useRef(null);
const enter = () => { clearTimeout(closeT.current); setOpen(true); };
const leave = () => { closeT.current = setTimeout(() => setOpen(false), 120); };
React.useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
window.addEventListener('keydown', onKey);
return () => { window.removeEventListener('keydown', onKey); clearTimeout(closeT.current); };
}, []);
const onAux = current === 'services' || SERVICE_LINKS.some((s) => s.key === current);
return (
);
}
// ── Page hero scaffold ─────────────────────────────────────────────────────
// Left: eyebrow + headline + tagline + lead + CTAs. Right: a visual slot.
// Below: a 4-up stat strip. Visual is any node (the signature interactive).
function PageHero({ eyebrow, title, tagline, lead, ctas, visual, stats }) {
return (
{eyebrow}
{title}
{tagline && (
{tagline}
)}
{lead}
{ctas}
{visual}
{stats && (
{stats.map(([big, label, sub], i) => (
))}
)}
);
}
// ── Feature grid ───────────────────────────────────────────────────────────
// features: [{ title, body, tag? }]
function FeatureSection({ id = 'features', index, eyebrow, title, intro, features, cols = 3, bgAlt }) {
return (
{eyebrow}
{title}
{intro && {intro}
}
{features.map((f, i) => )}
);
}
function FeatureCell({ f, n, cols }) {
const [hover, setHover] = React.useState(false);
// Top border on every cell after the first row; left border except first col.
const firstCol = (n - 1) % cols === 0;
const firstRow = n <= cols;
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{
padding: 30, position: 'relative',
borderLeft: firstCol ? 'none' : '1px solid var(--rule-soft)',
borderTop: firstRow ? 'none' : '1px solid var(--rule-soft)',
background: hover ? 'var(--bg-alt)' : 'var(--surface)', transition: 'background 140ms',
}}>
{String(n).padStart(2, '0')}
{f.tag && {f.tag} }
{f.title}
{f.body}
);
}
// ── Comparison table ───────────────────────────────────────────────────────
// cols: [{ label, highlight? }] rows: [{ label, cells: [str|node] }]
// The highlighted column gets the accent treatment (TensorHost).
function CompareSection({ id = 'compare', index, eyebrow, title, intro, cornerLabel = '', cols, rows, note, bgAlt }) {
return (
{eyebrow}
{title}
{intro && {intro}
}
{cornerLabel}
{cols.map((c) => (
{c.highlight
? ● {c.label}
: c.label}
))}
{rows.map((r) => (
{r.label}
{r.cells.map((v, i) => {
const hl = cols[i] && cols[i].highlight;
return (
{v}
);
})}
))}
{note && {note}
}
);
}
// ── "What we are NOT" — dark differentiator list ────────────────────────────
// items: [{ not, body }]
function NotSection({ index, title, intro, items }) {
return (
What we're not
{title}
{intro &&
{intro}
}
);
}
// ── Closing CTA band (dark) ─────────────────────────────────────────────────
const CONTACT_PAGE = 'Contact.html';
// Resolve a CTA label to its destination: "Talk to us" / "Contact" → contact
// page; "Email …" → mailto; everything else → the standard contact route.
function ctaHref(label) {
const l = (label || '').toLowerCase();
if (l.startsWith('email')) return 'mailto:info@tensorhost.com';
if (l.includes('talk') || l.includes('contact')) return CONTACT_PAGE;
return CONTACT_HREF;
}
function CtaBand({ index, title, lead, primaryLabel = 'Get started', secondaryLabel = 'Talk to us' }) {
return (
Get started
{title}
{lead}
);
}
// ── shared style atoms ───────────────────────────────────────────────────────
const auxH2 = { fontSize: 'clamp(28px,3.2vw,40px)', fontWeight: 400, letterSpacing: '-0.02em', lineHeight: 1.15, margin: '16px 0 12px', color: 'var(--fg)' };
const auxMiniTag = { fontSize: 10, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'var(--fg-4)', border: '1px solid var(--rule-soft)', borderRadius: 2, padding: '3px 8px' };
const auxThp = (first) => ({
padding: '14px 16px', textAlign: first ? 'left' : 'center', fontWeight: 400,
fontSize: 11, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--fg-3)',
borderBottom: '2px solid var(--fg)', whiteSpace: 'nowrap',
});
const auxTdp = { padding: '15px 16px' };
const auxInvPrimary = { fontFamily: 'inherit', fontSize: 15, padding: '14px 24px', borderRadius: 2, textDecoration: 'none', background: 'var(--inv-fg)', color: 'var(--inv-bg)', border: '1px solid var(--inv-fg)', display: 'inline-flex', gap: 8, alignItems: 'center' };
const auxInvSecondary = { fontFamily: 'inherit', fontSize: 15, padding: '14px 24px', borderRadius: 2, textDecoration: 'none', background: 'transparent', color: 'var(--inv-fg)', border: '1px solid var(--inv-rule)', display: 'inline-flex', gap: 8, alignItems: 'center' };
// ── Footer (cross-page) ──────────────────────────────────────────────────────
function AuxFooter() {
const cols = [
['Services', [['Web hosting', 'Web%20Hosting.html'], ['Cloud storage', 'Cloud%20Storage.html'], ['Business email', 'TensorMail.html'], ['GPU rentals', 'GPU%20Rentals.html']]],
['Company', [['Home', HOME_HREF], ['Privacy', 'Privacy.html'], ['Pricing', HOME_HREF + '#pricing'], ['FAQ', 'FAQ.html'], ['Contact', CONTACT_HREF]]],
['Legal', [['Terms', 'Terms.html'], ['Privacy policy', 'Privacy%20Policy.html'], ['Data processing (DPA)', 'Data%20Processing.html']]],
];
return (
);
}
Object.assign(window, {
AUX_PAGES, HOME_HREF, CONTACT_HREF, SIGNIN_HREF, REGISTER_HREF, NAV_LINKS, SERVICE_LINKS,
AuxNav, SiteNav: AuxNav, ServicesMenu,
PageHero, FeatureSection, CompareSection, NotSection, CtaBand, AuxFooter,
});