bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
389 lines (362 loc) • 13 kB
JavaScript
import * as fs from "node:fs";
import * as path from "node:path";
import { chromium } from "playwright";
// Tweakcn theme registry (subset for demo)
const tweakcnThemes = {
"modern-minimal": {
light: {
background: "oklch(1.00 0 0)",
foreground: "oklch(0.27 0 0)",
primary: "oklch(0.62 0.19 259.81)",
"primary-foreground": "oklch(1.00 0 0)",
border: "oklch(0.90 0 0)",
ring: "oklch(0.62 0.19 259.81)",
radius: "0.375rem",
},
dark: {
background: "oklch(0.20 0 0)",
foreground: "oklch(0.97 0 0)",
primary: "oklch(0.62 0.19 259.81)",
"primary-foreground": "oklch(1.00 0 0)",
border: "oklch(0.26 0 0)",
ring: "oklch(0.62 0.19 259.81)",
radius: "0.375rem",
},
},
claude: {
light: {
background: "oklch(0.99 0.00 106.87)",
foreground: "oklch(0.21 0.01 106.87)",
primary: "oklch(0.52 0.11 34.71)",
"primary-foreground": "oklch(0.99 0.00 106.87)",
border: "oklch(0.91 0.00 106.87)",
ring: "oklch(0.52 0.11 34.71)",
radius: "0.75rem",
},
dark: {
background: "oklch(0.14 0.01 106.87)",
foreground: "oklch(0.98 0.00 106.87)",
primary: "oklch(0.67 0.10 32.27)",
"primary-foreground": "oklch(0.14 0.01 106.87)",
border: "oklch(0.23 0.01 106.87)",
ring: "oklch(0.67 0.10 32.27)",
radius: "0.75rem",
},
},
cyberpunk: {
light: {
background: "oklch(0.17 0.00 0.00)",
foreground: "oklch(0.87 0.14 336.03)",
primary: "oklch(0.75 0.29 349.65)",
"primary-foreground": "oklch(0.17 0.00 0.00)",
border: "oklch(0.27 0.00 0.00)",
ring: "oklch(0.75 0.29 349.65)",
radius: "0rem",
},
dark: {
background: "oklch(0.10 0.00 0.00)",
foreground: "oklch(0.87 0.14 336.03)",
primary: "oklch(0.75 0.29 349.65)",
"primary-foreground": "oklch(0.10 0.00 0.00)",
border: "oklch(0.20 0.00 0.00)",
ring: "oklch(0.75 0.29 349.65)",
radius: "0rem",
},
},
};
// Component demos - for now just LoadingButton
const componentDemos = {
LoadingButton: {
name: "LoadingButton",
description: "Button with loading state indicator",
demo: (theme) => `
<div class="min-h-screen bg-background p-8">
<div class="max-w-4xl mx-auto space-y-8">
<div>
<h1 class="text-3xl font-bold mb-2">LoadingButton Component</h1>
<p class="text-muted-foreground">BigBlocks loading button with theme: ${theme.name || "default"}</p>
</div>
<div class="space-y-6">
<!-- Default states -->
<div class="space-y-3">
<h3 class="text-lg font-semibold">States</h3>
<div class="flex gap-3 items-center">
<button class="btn">Default</button>
<button class="btn" disabled>
<svg class="mr-2 h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading...
</button>
<button class="btn" disabled>Disabled</button>
</div>
</div>
<!-- Interactive demo -->
<div class="space-y-3">
<h3 class="text-lg font-semibold">Interactive Demo</h3>
<div class="p-6 border rounded-lg">
<button id="demo-btn" class="btn">
<span id="btn-text">Click to Submit</span>
</button>
<p class="text-sm text-muted-foreground mt-4">Click the button to see the loading state</p>
</div>
</div>
<!-- Variants -->
<div class="space-y-3">
<h3 class="text-lg font-semibold">Variants</h3>
<div class="flex flex-wrap gap-3">
<button class="btn btn-destructive">Destructive</button>
<button class="btn btn-outline">Outline</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-ghost">Ghost</button>
</div>
</div>
<!-- Sizes -->
<div class="space-y-3">
<h3 class="text-lg font-semibold">Sizes</h3>
<div class="flex gap-3 items-center">
<button class="btn btn-sm">Small</button>
<button class="btn">Default</button>
<button class="btn btn-lg">Large</button>
</div>
</div>
</div>
</div>
</div>
<script>
// Interactive demo script
const btn = document.getElementById('demo-btn');
const btnText = document.getElementById('btn-text');
const spinner = '<svg class="mr-2 h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
btn.addEventListener('click', () => {
btn.disabled = true;
btn.innerHTML = spinner + 'Processing...';
setTimeout(() => {
btn.innerHTML = '✓ Success!';
btn.classList.add('btn-success');
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = '<span id="btn-text">Click to Submit</span>';
btn.classList.remove('btn-success');
}, 2000);
}, 2000);
});
</script>
`,
},
};
function generateHTML(component, theme, themeName, mode) {
const demo = componentDemos[component];
if (!demo)
throw new Error(`Component ${component} not found`);
const vars = theme[mode];
const cssVars = Object.entries(vars)
.map(([key, value]) => `--${key}: ${value};`)
.join("\n ");
return `
<!DOCTYPE html>
<html class="${mode}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${demo.name} - BigBlocks Component</title>
<style>
@import 'tailwindcss';
:root {
${cssVars}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: ${vars.background};
color: ${vars.foreground};
}
/* Button base styles */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
border-radius: ${vars.radius};
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s;
padding: 0 1rem;
height: 2.5rem;
background: ${vars.primary};
color: ${vars["primary-foreground"]};
border: 1px solid transparent;
cursor: pointer;
outline: none;
}
.btn:hover:not(:disabled) {
opacity: 0.9;
}
.btn:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px ${vars.ring};
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Variants */
.btn-destructive {
background: oklch(0.58 0.25 27.32);
color: oklch(1.00 0 0);
}
.btn-outline {
background: transparent;
border-color: ${vars.border};
color: ${vars.foreground};
}
.btn-secondary {
background: oklch(0.97 0 0);
color: oklch(0.27 0 0);
}
.btn-ghost {
background: transparent;
color: ${vars.foreground};
}
.btn-ghost:hover {
background: oklch(0.97 0 0);
}
.btn-success {
background: oklch(0.62 0.19 153.74);
color: white;
}
/* Sizes */
.btn-sm {
height: 2rem;
padding: 0 0.75rem;
font-size: 0.75rem;
}
.btn-lg {
height: 3rem;
padding: 0 1.5rem;
font-size: 1rem;
}
/* Utilities */
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.space-y-3 > * + * { margin-top: 0.75rem; }
.space-y-6 > * + * { margin-top: 1.5rem; }
.space-y-8 > * + * { margin-top: 2rem; }
.gap-3 { gap: 0.75rem; }
.p-6 { padding: 1.5rem; }
.p-8 { padding: 2rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mr-2 { margin-right: 0.5rem; }
.text-3xl { font-size: 1.875rem; }
.text-lg { font-size: 1.125rem; }
.text-sm { font-size: 0.875rem; }
.font-bold { font-weight: 700; }
.font-semibold { font-weight: 600; }
.border { border: 1px solid ${vars.border}; }
.rounded-lg { border-radius: 0.5rem; }
.min-h-screen { min-height: 100vh; }
.max-w-4xl { max-width: 56rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.flex-wrap { flex-wrap: wrap; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.bg-background { background: ${vars.background}; }
.text-muted-foreground { color: oklch(0.56 0 0); opacity: 0.8; }
</style>
</head>
<body>
${demo.demo({ name: themeName, ...vars })}
</body>
</html>
`;
}
export async function generateComponentScreenshot(options) {
const { component = "LoadingButton", theme = "modern-minimal", mode = "light", output, video = false, } = options;
console.log(`🎨 Generating ${video ? "video" : "screenshot"} for ${component}`);
console.log(` Theme: ${theme} (${mode} mode)`);
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
...(video && {
recordVideo: {
dir: "./videos",
size: { width: 1280, height: 720 },
},
}),
});
const page = await context.newPage();
try {
// Generate HTML with theme
const themeData = tweakcnThemes[theme] || tweakcnThemes["modern-minimal"];
const html = generateHTML(component, themeData, theme, mode);
// Set content
await page.setContent(html);
await page.waitForLoadState("networkidle");
if (video) {
console.log("📹 Recording interactions...");
// Wait a moment to start clean
await page.waitForTimeout(1000);
// Click the demo button to show loading state
await page.click("#demo-btn");
await page.waitForTimeout(2500); // Wait for loading animation
// Wait for success state
await page.waitForTimeout(2500);
// Click again to reset
await page.click("#demo-btn");
await page.waitForTimeout(1000);
// Close to save video
await context.close();
const videoPath = output || `./videos/${component}-${theme}-${mode}.webm`;
console.log(`✅ Video saved to: ${videoPath}`);
}
else {
// Take screenshot
const screenshotPath = output || `./screenshots/${component}-${theme}-${mode}.png`;
// Ensure directory exists
const dir = path.dirname(screenshotPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
await page.screenshot({
path: screenshotPath,
fullPage: false,
});
console.log(`✅ Screenshot saved to: ${screenshotPath}`);
}
}
finally {
if (!video) {
await browser.close();
}
}
}
// CLI interface
if (typeof require !== "undefined" && require.main === module) {
const args = process.argv.slice(2);
const component = args[0] || "LoadingButton";
const theme = args[1] || "modern-minimal";
const mode = (args[2] || "light");
const isVideo = args.includes("--video");
generateComponentScreenshot({
component,
theme,
mode,
video: isVideo,
}).catch(console.error);
}