revenge-next-plugs/worker/src/theme.ts

211 lines
9.9 KiB
TypeScript
Raw Normal View History

2026-08-15 12:14:45 -04:00
export const PAGE_CSS = `
* { box-sizing: border-box; }
body {
margin: 0;
background: linear-gradient(180deg, #0c0d12, #08090c);
color: #fff;
2026-08-15 12:14:45 -04:00
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif;
overflow-x: hidden;
2026-08-15 12:14:45 -04:00
}
a { color: inherit; text-decoration: none; }
.wrap { max-width: 1040px; margin: 0 auto; padding: 0 32px; position: relative; z-index: 1; }
2026-08-15 12:14:45 -04:00
header {
display: flex; align-items: center; justify-content: space-between;
padding: 20px 32px; border-bottom: 1px solid rgba(255,255,255,0.07);
position: relative; z-index: 1;
2026-08-15 12:14:45 -04:00
}
.bg-blobs { position: fixed; inset: 0; overflow: hidden; z-index: 0; pointer-events: none; }
.bg-blob { position: absolute; border-radius: 50%; filter: blur(90px); opacity: 0.32; animation: blobDrift ease-in-out infinite alternate; }
.bg-blob.b1 { width: 440px; height: 440px; top: -100px; left: -80px; background: #9a8cff; animation-duration: 24s; }
.bg-blob.b2 { width: 380px; height: 380px; top: 38%; right: -120px; background: #ff8fc7; animation-duration: 30s; animation-delay: -6s; }
.bg-blob.b3 { width: 360px; height: 360px; bottom: -120px; left: 28%; background: #6fe3e8; animation-duration: 34s; animation-delay: -12s; }
@keyframes blobDrift {
0% { transform: translate(0,0) scale(1); }
50% { transform: translate(40px,-30px) scale(1.08); }
100% { transform: translate(-30px,20px) scale(0.95); }
}
.floaters { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.float-icon { position: absolute; opacity: 0.16; animation: floatDrift linear infinite; will-change: transform; }
@keyframes floatDrift {
0% { transform: translate(0,0) rotate(0deg); }
25% { transform: translate(18px,-26px) rotate(6deg); }
50% { transform: translate(-14px,-46px) rotate(-4deg); }
75% { transform: translate(-22px,-16px) rotate(5deg); }
100% { transform: translate(0,0) rotate(0deg); }
}
html.custom-cursor, html.custom-cursor a, html.custom-cursor button, html.custom-cursor input,
html.custom-cursor textarea, html.custom-cursor .card, html.custom-cursor .chip { cursor: none; }
.cursor-ring {
position: fixed; top: 0; left: 0; width: 20px; height: 20px; border-radius: 50%;
border: 1.5px solid rgba(154,140,255,0.85); pointer-events: none; z-index: 9999;
transform: translate(-50%,-50%);
transition: width 0.2s ease, height 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
}
.cursor-ring.hover { width: 42px; height: 42px; background: rgba(154,140,255,0.14); border-color: rgba(154,140,255,1); }
@media (hover: none), (pointer: coarse) { .cursor-ring { display: none; } }
2026-08-15 12:14:45 -04:00
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
width: 24px; height: 24px; border-radius: 7px; background: #15161c;
2026-08-15 12:14:45 -04:00
border: 1px solid rgba(255,255,255,0.12); display: flex; align-items: center;
justify-content: center; font-size: 12px; font-weight: 700; color: #9a8cff;
2026-08-15 12:14:45 -04:00
}
.brand-name { font-weight: 650; font-size: 14.5px; letter-spacing: -0.01em; }
nav { display: flex; align-items: center; gap: 22px; font-size: 13px; color: rgba(255,255,255,0.5); }
nav a:hover { color: rgba(255,255,255,0.85); }
h1 { font-size: 58px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; margin: 0 0 16px; }
h1 .accent { color: #9a8cff; }
.page-title { font-size: 58px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; margin: 44px 0 16px; }
.page-sub { font-size: 16.5px; color: rgba(255,255,255,0.55); max-width: 520px; line-height: 1.55; margin-bottom: 30px; }
.stats { display: flex; gap: 36px; margin-bottom: 44px; padding-bottom: 30px; border-bottom: 1px solid rgba(255,255,255,0.08); }
.stat b { display: block; font-size: 30px; font-weight: 750; }
.stat span { font-size: 12px; color: rgba(255,255,255,0.45); text-transform: uppercase; letter-spacing: 0.04em; }
2026-08-15 12:14:45 -04:00
.chip {
background: transparent; border: 1px solid rgba(255,255,255,0.07); border-radius: 7px;
padding: 6px 13px; font-size: 12.5px; color: rgba(255,255,255,0.4); display: inline-block;
transition: border-color 0.2s ease, color 0.2s ease;
2026-08-15 12:14:45 -04:00
}
.chip.active { background: #17181e; border-color: rgba(255,255,255,0.09); color: rgba(255,255,255,0.85); }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 4px 0 40px; }
2026-08-15 12:14:45 -04:00
.card {
background: #131318; border: 1px solid rgba(255,255,255,0.07); border-radius: 16px; padding: 24px;
position: relative; overflow: hidden;
transition: transform 0.3s cubic-bezier(.2,.8,.2,1), border-color 0.3s ease;
2026-08-15 12:14:45 -04:00
}
.card::before {
content: ''; position: absolute; inset: 0; opacity: 0; transition: opacity 0.3s ease; pointer-events: none;
background: radial-gradient(160px 120px at 20% 0%, rgba(154,140,255,0.22), transparent 70%);
}
.card:hover { transform: translateY(-6px) scale(1.01); border-color: rgba(154,140,255,0.45); }
.card:hover::before { opacity: 1; }
.card-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 14px; }
2026-08-15 12:14:45 -04:00
.card-icon {
width: 34px; height: 34px; border-radius: 9px; background: #1a1b22;
2026-08-15 12:14:45 -04:00
border: 1px solid rgba(255,255,255,0.1); display: flex; align-items: center;
justify-content: center; font-size: 15px;
2026-08-15 12:14:45 -04:00
}
.badge-row { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
2026-08-15 12:14:45 -04:00
.badge {
background: rgba(154,140,255,0.12); color: #b6acff; border: 1px solid rgba(154,140,255,0.25);
font-size: 10px; font-weight: 650; padding: 3px 8px; border-radius: 5px;
2026-08-15 12:14:45 -04:00
}
.card-name { position: relative; font-size: 18px; font-weight: 700; letter-spacing: -0.01em; }
.card-desc { position: relative; font-size: 13.5px; color: rgba(255,255,255,0.55); margin-top: 6px; line-height: 1.5; }
.card-foot { position: relative; display: flex; align-items: center; justify-content: space-between; margin-top: 18px; }
.card-meta { font-size: 11.5px; color: rgba(255,255,255,0.4); }
button { font-family: inherit; }
2026-08-15 12:14:45 -04:00
.btn {
background: #e8e9ed; color: #0b0c10; font-size: 13px; font-weight: 650;
padding: 8px 16px; border-radius: 8px; display: inline-block; border: none; cursor: pointer;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.btn:hover { transform: translateY(-2px); opacity: 0.92; }
.btn-ghost {
background: transparent; border: 1px solid rgba(255,255,255,0.16); color: #fff;
font-size: 13px; font-weight: 600; padding: 8px 16px; border-radius: 8px;
display: inline-block; cursor: pointer; transition: border-color 0.2s ease, background 0.2s ease;
2026-08-15 12:14:45 -04:00
}
.btn-ghost:hover { border-color: rgba(255,255,255,0.32); background: rgba(255,255,255,0.04); }
2026-08-15 12:14:45 -04:00
`
export const SOURCE_URL =
'https://github.com/everestmcarthur/revenge-next-plugs'
export const ISSUES_URL =
'https://github.com/everestmcarthur/revenge-next-plugs/issues'
export function renderHeader(opts: {
brandName: string
brandHref?: string
}): string {
const brandHref = opts.brandHref ?? '/'
return `
<header>
<div class="brand">
<a href="${brandHref}" class="brand-mark" style="text-decoration:none;">N</a>
<div class="brand-name">${escapeHtml(opts.brandName)}</div>
</div>
<nav>
<a href="/">Plugins</a>
<a href="${SOURCE_URL}" target="_blank" rel="noopener">Source</a>
<a href="${ISSUES_URL}" target="_blank" rel="noopener">Issues</a>
</nav>
</header>`
}
const FLOATERS = `
<div class="bg-blobs"><div class="bg-blob b1"></div><div class="bg-blob b2"></div><div class="bg-blob b3"></div></div>
<div class="floaters" aria-hidden="true">
<span class="float-icon" style="top:12%;left:8%;font-size:34px;animation-duration:22s;">🏳️‍🌈</span>
<span class="float-icon" style="top:65%;left:87%;font-size:26px;animation-duration:18s;animation-delay:-4s;">✨</span>
<span class="float-icon" style="top:80%;left:15%;font-size:30px;animation-duration:26s;animation-delay:-9s;">🚀</span>
<span class="float-icon" style="top:20%;left:92%;font-size:24px;animation-duration:20s;animation-delay:-3s;">⚡</span>
<span class="float-icon" style="top:46%;left:50%;font-size:22px;animation-duration:28s;animation-delay:-14s;">🎮</span>
</div>`
const CURSOR_SCRIPT = `
<script>
(function() {
if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return
var ring = document.createElement('div')
ring.className = 'cursor-ring'
document.body.appendChild(ring)
document.documentElement.classList.add('custom-cursor')
var x = innerWidth / 2, y = innerHeight / 2, rx = x, ry = y
addEventListener('mousemove', function (e) { x = e.clientX; y = e.clientY })
function loop() {
rx += (x - rx) * 0.22
ry += (y - ry) * 0.22
ring.style.transform = 'translate(' + rx + 'px,' + ry + 'px) translate(-50%,-50%)'
requestAnimationFrame(loop)
}
loop()
document.addEventListener('mouseover', function (e) {
if (e.target.closest && e.target.closest('a, button, .card, .chip, #install-card')) ring.classList.add('hover')
})
document.addEventListener('mouseout', function (e) {
if (e.target.closest && e.target.closest('a, button, .card, .chip, #install-card')) ring.classList.remove('hover')
})
})()
</script>`
2026-08-15 12:14:45 -04:00
export function htmlShell(opts: {
title: string
description: string
ogTitle?: string
ogDescription?: string
body: string
}): string {
const ogTitle = opts.ogTitle ?? opts.title
const ogDescription = opts.ogDescription ?? opts.description
return `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(opts.title)}</title>
<meta name="description" content="${escapeHtml(opts.description)}">
<meta property="og:title" content="${escapeHtml(ogTitle)}">
<meta property="og:description" content="${escapeHtml(ogDescription)}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="${escapeHtml(ogTitle)}">
<meta name="twitter:description" content="${escapeHtml(ogDescription)}">
<style>${PAGE_CSS}</style>
</head>
<body>${FLOATERS}${opts.body}${CURSOR_SCRIPT}</body>
2026-08-15 12:14:45 -04:00
</html>`
}
export function escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}