UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

389 lines (362 loc) 13 kB
#!/usr/bin/env node 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); }