David Montero 2026-03-06 21:25:08 +01:00
commit a0e223004a
18 changed files with 1095 additions and 310 deletions

View File

@ -243,6 +243,14 @@ Wire positions auto-update when components move via `updateWirePositions()`.
- [frontend/src/pages/UserProfilePage.tsx](frontend/src/pages/UserProfilePage.tsx) - Profile with project grid
- [frontend/src/pages/ProjectPage.tsx](frontend/src/pages/ProjectPage.tsx) - Loads project into editor
### Frontend - SEO & Public Files
- `frontend/index.html` — Full SEO meta tags, OG, Twitter Card, JSON-LD. **Domain is `https://velxio.dev`** — update if domain changes.
- `frontend/public/favicon.svg` — SVG chip favicon (scales to all sizes)
- `frontend/public/og-image.svg` — 1200×630 social preview image (OG/Twitter). Export as PNG for max compatibility.
- `frontend/public/robots.txt` — Allow all crawlers, points to sitemap
- `frontend/public/sitemap.xml` — All public routes with priorities
- `frontend/public/manifest.webmanifest` — PWA manifest, theme color `#007acc`
### Docker & CI
- [Dockerfile.standalone](Dockerfile.standalone) - Multi-stage Docker build
- [.github/workflows/docker-publish.yml](.github/workflows/docker-publish.yml) - Publishes to GHCR + Docker Hub on push to master

View File

@ -2,9 +2,206 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<!-- ═══════════════════════════════════════════════
PRIMARY SEO
Replace https://velxio.dev with your real domain
═══════════════════════════════════════════════ -->
<title>Velxio — Free Local Arduino Emulator | AVR8 · RP2040 · 48+ Components</title>
<meta name="description" content="Velxio is a free, open-source Arduino emulator that runs entirely in your browser. Real AVR8 emulation at 16 MHz, 48+ electronic components, Monaco Editor, Serial Monitor, and Library Manager — no cloud, no latency." />
<meta name="keywords" content="arduino emulator, arduino simulator, avr8 emulator, avr simulator, rp2040 emulator, arduino online, wokwi alternative, arduino ide browser, arduino simulator free, attmega328p simulator, raspberry pi pico emulator, arduino uno simulator, electronics simulator, velxio" />
<meta name="author" content="David Montero Crespo" />
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<link rel="canonical" href="https://velxio.dev/" />
<!-- ═══════════════════════════════════════════════
OPEN GRAPH (Facebook, LinkedIn, WhatsApp, Telegram, Discord, Slack…)
═══════════════════════════════════════════════ -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Velxio" />
<meta property="og:title" content="Velxio — Free Local Arduino Emulator" />
<meta property="og:description" content="Write, compile, and simulate Arduino sketches entirely in your browser. Real AVR8 CPU at 16 MHz, 48+ electronic components, Serial Monitor, and Library Manager. Free &amp; open source." />
<meta property="og:url" content="https://velxio.dev/" />
<meta property="og:image" content="https://velxio.dev/og-image.svg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Velxio Arduino Emulator — Real AVR8, 48+ Components, Monaco Editor" />
<meta property="og:locale" content="en_US" />
<!-- ═══════════════════════════════════════════════
TWITTER / X CARD
═══════════════════════════════════════════════ -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@velxio_app" />
<meta name="twitter:creator" content="@velxio_app" />
<meta name="twitter:title" content="Velxio — Free Local Arduino Emulator" />
<meta name="twitter:description" content="Write, compile, and simulate Arduino sketches entirely in your browser. Real AVR8 CPU at 16 MHz, 48+ components, Serial Monitor, Library Manager. Free &amp; open source." />
<meta name="twitter:image" content="https://velxio.dev/og-image.svg" />
<meta name="twitter:image:alt" content="Velxio Arduino Emulator — Real AVR8, 48+ Components, Monaco Editor" />
<!-- ═══════════════════════════════════════════════
THEME & APP
═══════════════════════════════════════════════ -->
<meta name="theme-color" content="#007acc" />
<meta name="color-scheme" content="dark" />
<meta name="application-name" content="Velxio" />
<meta name="generator" content="Vite + React + FastAPI" />
<!-- ═══════════════════════════════════════════════
FAVICON — full set generated by scripts/generate-favicons.mjs
═══════════════════════════════════════════════ -->
<!-- SVG: modern browsers (scales perfectly) -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- ICO: legacy browsers / Windows taskbar -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<!-- PNG fallbacks -->
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<!-- Apple devices (Safari, iOS home screen) -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<!-- ═══════════════════════════════════════════════
PWA / WEB APP MANIFEST
═══════════════════════════════════════════════ -->
<link rel="manifest" href="/manifest.webmanifest" />
<!-- ═══════════════════════════════════════════════
PRECONNECT (speed up external resources)
═══════════════════════════════════════════════ -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="dns-prefetch" href="https://github.com" />
<!-- ═══════════════════════════════════════════════
JSON-LD STRUCTURED DATA
Helps Google understand the page type and show
rich results in search (ratings, price, etc.)
═══════════════════════════════════════════════ -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": "https://velxio.dev/#website",
"url": "https://velxio.dev/",
"name": "Velxio",
"description": "Free local Arduino emulator with real AVR8 CPU emulation.",
"inLanguage": "en-US",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://velxio.dev/examples?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
},
{
"@type": "SoftwareApplication",
"@id": "https://velxio.dev/#app",
"name": "Velxio",
"alternateName": ["Velxio Arduino Emulator", "Velxio AVR Simulator"],
"description": "Free, open-source Arduino emulator running entirely in the browser. Features real ATmega328p (AVR8) emulation at 16 MHz, RP2040 emulation, 48+ interactive electronic components, Monaco Editor, Serial Monitor with baud-rate detection, arduino-cli compilation backend, and Library Manager.",
"url": "https://velxio.dev/",
"applicationCategory": "DeveloperApplication",
"applicationSubCategory": "Electronics Simulator",
"operatingSystem": "Web Browser",
"browserRequirements": "Requires JavaScript. Works in Chrome, Firefox, Safari, Edge.",
"softwareVersion": "1.0.0",
"releaseNotes": "https://github.com/davidmonterocrespo24/velxio/releases",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
},
"featureList": [
"Real ATmega328p AVR8 emulation at 16 MHz",
"RP2040 (Raspberry Pi Pico) emulation",
"48+ electronic web components",
"Monaco Code Editor with C++ syntax highlighting",
"arduino-cli compilation backend",
"Serial Monitor with auto baud-rate detection",
"Library Manager for Arduino libraries",
"Multi-file workspace",
"Wire system with orthogonal routing",
"ILI9341 TFT display simulation",
"I2C, SPI, USART, ADC, PWM support"
],
"screenshot": "https://velxio.dev/og-image.svg",
"downloadUrl": "https://hub.docker.com/r/davidmonterocrespo24/velxio",
"installUrl": "https://velxio.dev/editor",
"codeRepository": "https://github.com/davidmonterocrespo24/velxio",
"license": "https://opensource.org/licenses/MIT",
"isAccessibleForFree": true,
"author": {
"@type": "Person",
"name": "David Montero Crespo",
"url": "https://github.com/davidmonterocrespo24"
},
"maintainer": {
"@type": "Person",
"name": "David Montero Crespo",
"url": "https://github.com/davidmonterocrespo24"
},
"keywords": "arduino emulator, avr8 simulator, rp2040 emulator, arduino online ide, electronics simulator, wokwi alternative",
"inLanguage": "en-US"
},
{
"@type": "WebPage",
"@id": "https://velxio.dev/#webpage",
"url": "https://velxio.dev/",
"name": "Velxio — Free Local Arduino Emulator | AVR8 · RP2040 · 48+ Components",
"description": "Velxio is a free, open-source Arduino emulator. Real AVR8 CPU emulation, 48+ components, Monaco Editor, Serial Monitor — no cloud, no latency.",
"isPartOf": { "@id": "https://velxio.dev/#website" },
"about": { "@id": "https://velxio.dev/#app" },
"inLanguage": "en-US",
"potentialAction": {
"@type": "ReadAction",
"target": "https://velxio.dev/"
}
},
{
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Is Velxio free?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, Velxio is completely free and open source under the MIT License."
}
},
{
"@type": "Question",
"name": "Does Velxio work offline?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Velxio can be self-hosted via Docker. Once running, the simulation engine works fully offline in the browser. Compilation requires the local arduino-cli backend."
}
},
{
"@type": "Question",
"name": "What boards does Velxio support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Velxio supports Arduino Uno (ATmega328p) with full AVR8 emulation and Raspberry Pi Pico (RP2040) emulation. More boards are planned."
}
},
{
"@type": "Question",
"name": "Is Velxio a Wokwi alternative?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Velxio is a free, self-hosted alternative to Wokwi. It uses the same avr8js and wokwi-elements open-source libraries but runs entirely on your machine with no cloud dependency."
}
}
]
}
]
}
</script>
</head>
<body>
<div id="root"></div>

View File

@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"generate:metadata": "cd .. && npx tsx scripts/generate-component-metadata.ts",
"generate:favicons": "node ../scripts/generate-favicons.mjs",
"dev": "npm run generate:metadata && vite",
"build": "npm run generate:metadata && tsc -b && vite build",
"build:docker": "vite build",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="7" fill="#0d0d0f"/>
<rect x="7" y="7" width="18" height="18" rx="2.5" stroke="#007acc" stroke-width="2"/>
<rect x="11" y="11" width="10" height="10" rx="1.5" stroke="#007acc" stroke-width="1.5"/>
<!-- top pins -->
<line x1="12" y1="3" x2="12" y2="7" stroke="#007acc" stroke-width="1.5" stroke-linecap="round"/>
<line x1="20" y1="3" x2="20" y2="7" stroke="#007acc" stroke-width="1.5" stroke-linecap="round"/>
<!-- bottom pins -->
<line x1="12" y1="25" x2="12" y2="29" stroke="#007acc" stroke-width="1.5" stroke-linecap="round"/>
<line x1="20" y1="25" x2="20" y2="29" stroke="#007acc" stroke-width="1.5" stroke-linecap="round"/>
<!-- left pins -->
<line x1="3" y1="12" x2="7" y2="12" stroke="#007acc" stroke-width="1.5" stroke-linecap="round"/>
<line x1="3" y1="20" x2="7" y2="20" stroke="#007acc" stroke-width="1.5" stroke-linecap="round"/>
<!-- right pins -->
<line x1="25" y1="12" x2="29" y2="12" stroke="#007acc" stroke-width="1.5" stroke-linecap="round"/>
<line x1="25" y1="20" x2="29" y2="20" stroke="#007acc" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,51 @@
{
"name": "Velxio — Arduino Emulator",
"short_name": "Velxio",
"description": "Free local Arduino emulator with real AVR8 CPU emulation, 48+ electronic components, Monaco Editor, and Serial Monitor. No cloud, no latency.",
"start_url": "/editor",
"scope": "/",
"display": "standalone",
"orientation": "landscape",
"background_color": "#0d0d0f",
"theme_color": "#007acc",
"categories": ["developer", "education", "productivity"],
"lang": "en",
"icons": [
{
"src": "/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/android-chrome-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/android-chrome-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/og-image.svg",
"sizes": "1200x630",
"type": "image/svg+xml",
"label": "Velxio Arduino Emulator"
}
]
}

View File

@ -0,0 +1,142 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0d0d0f"/>
<stop offset="100%" stop-color="#0a1520"/>
</linearGradient>
<linearGradient id="title-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#007acc"/>
<stop offset="100%" stop-color="#00c896"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="20" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="url(#bg)"/>
<!-- Glow blob -->
<ellipse cx="600" cy="280" rx="380" ry="240" fill="#007acc" opacity="0.06" filter="url(#glow)"/>
<!-- Grid lines decoration -->
<g stroke="#1e1e2a" stroke-width="1" opacity="0.6">
<line x1="0" y1="105" x2="1200" y2="105"/>
<line x1="0" y1="210" x2="1200" y2="210"/>
<line x1="0" y1="315" x2="1200" y2="315"/>
<line x1="0" y1="420" x2="1200" y2="420"/>
<line x1="0" y1="525" x2="1200" y2="525"/>
<line x1="200" y1="0" x2="200" y2="630"/>
<line x1="400" y1="0" x2="400" y2="630"/>
<line x1="600" y1="0" x2="600" y2="630"/>
<line x1="800" y1="0" x2="800" y2="630"/>
<line x1="1000" y1="0" x2="1000" y2="630"/>
</g>
<!-- Left decorative chip -->
<g transform="translate(80, 200)" opacity="0.15" stroke="#007acc" stroke-width="2" fill="none">
<rect x="0" y="0" width="80" height="80" rx="8"/>
<rect x="15" y="15" width="50" height="50" rx="4"/>
<line x1="20" y1="-15" x2="20" y2="0"/>
<line x1="60" y1="-15" x2="60" y2="0"/>
<line x1="20" y1="80" x2="20" y2="95"/>
<line x1="60" y1="80" x2="60" y2="95"/>
<line x1="-15" y1="20" x2="0" y2="20"/>
<line x1="-15" y1="60" x2="0" y2="60"/>
<line x1="80" y1="20" x2="95" y2="20"/>
<line x1="80" y1="60" x2="95" y2="60"/>
</g>
<!-- Right decorative chip -->
<g transform="translate(1020, 320)" opacity="0.12" stroke="#00c896" stroke-width="2" fill="none">
<rect x="0" y="0" width="60" height="60" rx="6"/>
<rect x="12" y="12" width="36" height="36" rx="3"/>
<line x1="15" y1="-12" x2="15" y2="0"/>
<line x1="45" y1="-12" x2="45" y2="0"/>
<line x1="15" y1="60" x2="15" y2="72"/>
<line x1="45" y1="60" x2="45" y2="72"/>
<line x1="-12" y1="15" x2="0" y2="15"/>
<line x1="-12" y1="45" x2="0" y2="45"/>
<line x1="60" y1="15" x2="72" y2="15"/>
<line x1="60" y1="45" x2="72" y2="45"/>
</g>
<!-- Top-right small chip -->
<g transform="translate(1050, 80)" opacity="0.10" stroke="#007acc" stroke-width="1.5" fill="none">
<rect x="0" y="0" width="44" height="44" rx="5"/>
<rect x="9" y="9" width="26" height="26" rx="2.5"/>
<line x1="11" y1="-10" x2="11" y2="0"/>
<line x1="33" y1="-10" x2="33" y2="0"/>
<line x1="11" y1="44" x2="11" y2="54"/>
<line x1="33" y1="44" x2="33" y2="54"/>
<line x1="-10" y1="11" x2="0" y2="11"/>
<line x1="-10" y1="33" x2="0" y2="33"/>
<line x1="44" y1="11" x2="54" y2="11"/>
<line x1="44" y1="33" x2="54" y2="33"/>
</g>
<!-- Badge pill -->
<rect x="480" y="140" width="240" height="34" rx="17" fill="none" stroke="#007acc" stroke-width="1.5" opacity="0.5"/>
<rect x="481" y="141" width="238" height="32" rx="16" fill="#007acc" opacity="0.1"/>
<text x="600" y="163" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="13" font-weight="600" fill="#007acc" letter-spacing="2">OPEN SOURCE · FREE · LOCAL</text>
<!-- Main logo chip icon -->
<g transform="translate(548, 192)" stroke="#007acc" stroke-width="3" fill="none" stroke-linecap="round">
<rect x="0" y="0" width="44" height="44" rx="6"/>
<rect x="10" y="10" width="24" height="24" rx="3"/>
<line x1="14" y1="-10" x2="14" y2="0"/>
<line x1="30" y1="-10" x2="30" y2="0"/>
<line x1="14" y1="44" x2="14" y2="54"/>
<line x1="30" y1="44" x2="30" y2="54"/>
<line x1="-10" y1="14" x2="0" y2="14"/>
<line x1="-10" y1="30" x2="0" y2="30"/>
<line x1="44" y1="14" x2="54" y2="14"/>
<line x1="44" y1="30" x2="54" y2="30"/>
</g>
<!-- Title: VELXIO -->
<text x="600" y="340" text-anchor="middle"
font-family="system-ui, -apple-system, 'Segoe UI', sans-serif"
font-size="96" font-weight="900" letter-spacing="-3"
fill="url(#title-grad)">VELXIO</text>
<!-- Subtitle -->
<text x="600" y="400" text-anchor="middle"
font-family="system-ui, -apple-system, sans-serif"
font-size="26" font-weight="400" fill="#7a7a8c">
Arduino Emulator — Real AVR8 · 48+ Components · Monaco Editor
</text>
<!-- Tags row -->
<g font-family="'Consolas', 'Courier New', monospace" font-size="15" fill="#007acc">
<!-- Tag 1 -->
<rect x="282" y="444" width="100" height="30" rx="6" fill="#007acc" opacity="0.12" stroke="#007acc" stroke-width="1" opacity2="0.3"/>
<text x="332" y="464" text-anchor="middle" fill="#007acc">arduino-cli</text>
<!-- Tag 2 -->
<rect x="398" y="444" width="80" height="30" rx="6" fill="#007acc" opacity="0.12" stroke="#007acc" stroke-width="1"/>
<text x="438" y="464" text-anchor="middle" fill="#007acc">avr8js</text>
<!-- Tag 3 -->
<rect x="494" y="444" width="90" height="30" rx="6" fill="#007acc" opacity="0.12" stroke="#007acc" stroke-width="1"/>
<text x="539" y="464" text-anchor="middle" fill="#007acc">rp2040js</text>
<!-- Tag 4 -->
<rect x="600" y="444" width="110" height="30" rx="6" fill="#007acc" opacity="0.12" stroke="#007acc" stroke-width="1"/>
<text x="655" y="464" text-anchor="middle" fill="#007acc">TypeScript</text>
<!-- Tag 5 -->
<rect x="726" y="444" width="70" height="30" rx="6" fill="#00c896" opacity="0.12" stroke="#00c896" stroke-width="1"/>
<text x="761" y="464" text-anchor="middle" fill="#00c896">FastAPI</text>
<!-- Tag 6 -->
<rect x="812" y="444" width="60" height="30" rx="6" fill="#00c896" opacity="0.12" stroke="#00c896" stroke-width="1"/>
<text x="842" y="464" text-anchor="middle" fill="#00c896">React</text>
</g>
<!-- Bottom border accent -->
<rect x="0" y="622" width="1200" height="8" fill="url(#title-grad)" opacity="0.8"/>
<!-- URL bottom -->
<text x="600" y="608" text-anchor="middle"
font-family="system-ui, -apple-system, sans-serif"
font-size="16" fill="#444" letter-spacing="0.5">
github.com/davidmonterocrespo24/velxio
</text>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,5 @@
User-agent: *
Allow: /
# Sitemap
Sitemap: https://velxio.dev/sitemap.xml

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://velxio.dev/</loc>
<lastmod>2026-03-06</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://velxio.dev/editor</loc>
<lastmod>2026-03-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://velxio.dev/examples</loc>
<lastmod>2026-03-06</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://velxio.dev/login</loc>
<lastmod>2026-03-06</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://velxio.dev/register</loc>
<lastmod>2026-03-06</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
</urlset>

View File

@ -1,16 +1,16 @@
/* ── Base ──────────────────────────────────────────────── */
:root {
--accent: #007acc;
--accent-glow: rgba(0, 122, 204, 0.35);
--accent2: #00c896;
--bg: #0d0d0f;
--bg-card: #131318;
--bg-alt: #0a0a0d;
--border: #1e1e2a;
--text: #e2e2e8;
--text-muted: #7a7a8c;
--bg: #09090b;
--bg-card: #0f0f12;
--bg-alt: #07070a;
--border: #18181f;
--border-hi: #252530;
--text: #d8d8e0;
--text-muted: #72727e;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--mono: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
--radius: 3px;
}
.landing {
@ -30,9 +30,9 @@
align-items: center;
justify-content: space-between;
padding: 0 40px;
height: 56px;
background: rgba(13, 13, 15, 0.88);
backdrop-filter: blur(12px);
height: 52px;
background: rgba(9, 9, 11, 0.92);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
}
@ -40,22 +40,23 @@
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-size: 17px;
font-weight: 700;
color: var(--text);
letter-spacing: -0.3px;
letter-spacing: -0.2px;
font-family: var(--mono);
}
.landing-nav-brand svg {
width: 22px;
height: 22px;
width: 20px;
height: 20px;
color: var(--accent);
}
.landing-nav-links {
display: flex;
align-items: center;
gap: 8px;
gap: 4px;
}
.nav-link {
@ -65,135 +66,131 @@
padding: 5px 12px;
color: var(--text-muted);
text-decoration: none;
font-size: 13.5px;
border-radius: 6px;
font-size: 13px;
border-radius: var(--radius);
transition: color 0.15s, background 0.15s;
}
.nav-link svg {
width: 16px;
height: 16px;
width: 15px;
height: 15px;
}
.nav-link:hover {
color: var(--text);
background: rgba(255,255,255,0.06);
background: rgba(255, 255, 255, 0.05);
}
.nav-btn-primary {
padding: 6px 16px;
padding: 5px 14px;
background: var(--accent);
color: #fff;
text-decoration: none;
font-size: 13.5px;
font-size: 13px;
font-weight: 600;
border-radius: 6px;
transition: background 0.15s, box-shadow 0.15s;
border-radius: var(--radius);
transition: background 0.15s;
border: 1px solid rgba(255,255,255,0.1);
}
.nav-btn-primary:hover {
background: #0990e0;
box-shadow: 0 0 12px var(--accent-glow);
background: #1a8fd1;
}
/* ── Hero ─────────────────────────────────────────────── */
.landing-hero {
position: relative;
min-height: 88vh;
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: 48px;
padding: 80px 80px 80px 80px;
max-width: 1200px;
margin: 0 auto;
min-height: 80vh;
}
.hero-left {
display: flex;
flex-direction: column;
gap: 0;
}
.hero-eyebrow {
display: flex;
align-items: center;
justify-content: center;
padding: 80px 40px 60px;
overflow: hidden;
gap: 8px;
margin-bottom: 20px;
}
.hero-glow {
position: absolute;
top: -120px;
left: 50%;
transform: translateX(-50%);
width: 700px;
height: 700px;
background: radial-gradient(ellipse, rgba(0,122,204,0.14) 0%, transparent 70%);
pointer-events: none;
}
.hero-content {
position: relative;
z-index: 2;
max-width: 720px;
text-align: center;
}
.hero-badge {
display: inline-block;
margin-bottom: 24px;
padding: 4px 14px;
background: rgba(0, 122, 204, 0.12);
border: 1px solid rgba(0, 122, 204, 0.3);
border-radius: 20px;
font-size: 12px;
.eyebrow-tag {
font-family: var(--mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 1.2px;
color: var(--accent);
letter-spacing: 0.5px;
text-transform: uppercase;
border: 1px solid rgba(0, 122, 204, 0.3);
padding: 2px 7px;
border-radius: 2px;
}
.eyebrow-dot {
width: 3px;
height: 3px;
background: var(--border-hi);
border-radius: 50%;
}
.hero-title {
margin: 0 0 20px;
font-size: clamp(36px, 6vw, 64px);
margin: 0 0 16px;
font-size: clamp(32px, 4.5vw, 56px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -1.5px;
color: #fff;
color: #e8e8f0;
}
.hero-accent {
background: linear-gradient(90deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
color: var(--accent);
font-weight: 800;
}
.hero-subtitle {
margin: 0 0 40px;
font-size: 17px;
margin: 0 0 32px;
font-size: 15.5px;
line-height: 1.7;
color: var(--text-muted);
max-width: 560px;
margin-left: auto;
margin-right: auto;
max-width: 480px;
}
.hero-ctas {
display: flex;
gap: 14px;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 28px;
}
.cta-primary {
display: flex;
align-items: center;
gap: 8px;
padding: 13px 28px;
padding: 11px 24px;
background: var(--accent);
color: #fff;
text-decoration: none;
font-size: 15px;
font-size: 14px;
font-weight: 700;
border-radius: 8px;
transition: background 0.15s, box-shadow 0.2s, transform 0.15s;
border-radius: var(--radius);
transition: background 0.15s, transform 0.1s;
border: 1px solid rgba(255,255,255,0.1);
}
.cta-primary svg {
width: 18px;
height: 18px;
width: 16px;
height: 16px;
}
.cta-primary:hover {
background: #0990e0;
box-shadow: 0 0 24px var(--accent-glow);
background: #1a8fd1;
transform: translateY(-1px);
}
@ -201,65 +198,73 @@
display: flex;
align-items: center;
gap: 8px;
padding: 13px 28px;
background: rgba(255,255,255,0.05);
border: 1px solid var(--border);
padding: 11px 24px;
background: transparent;
border: 1px solid var(--border-hi);
color: var(--text);
text-decoration: none;
font-size: 15px;
font-size: 14px;
font-weight: 600;
border-radius: 8px;
transition: background 0.15s, border-color 0.15s, transform 0.15s;
border-radius: var(--radius);
transition: border-color 0.15s, background 0.15s, transform 0.1s;
}
.cta-secondary svg {
width: 18px;
height: 18px;
width: 16px;
height: 16px;
}
.cta-secondary:hover {
background: rgba(255,255,255,0.09);
border-color: #444;
background: rgba(255, 255, 255, 0.04);
border-color: #333;
transform: translateY(-1px);
}
/* ── Hero decoration (floating chips) ─────────────────── */
.hero-decoration {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 1;
/* Specs strip */
.hero-specs {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.deco-chip {
position: absolute;
opacity: 0.04;
animation: floatChip 8s ease-in-out infinite;
.spec-pill {
font-family: var(--mono);
font-size: 11.5px;
color: var(--text-muted);
background: var(--bg-card);
border: 1px solid var(--border);
padding: 3px 8px;
border-radius: 2px;
}
.deco-chip:nth-child(1) { top: 8%; left: 5%; width: 64px; }
.deco-chip:nth-child(2) { top: 15%; left: 85%; width: 40px; }
.deco-chip:nth-child(3) { top: 55%; left: 4%; width: 48px; }
.deco-chip:nth-child(4) { top: 70%; left: 88%; width: 56px; }
.deco-chip:nth-child(5) { top: 35%; left: 92%; width: 36px; }
.deco-chip:nth-child(6) { top: 80%; left: 10%; width: 44px; }
.deco-chip:nth-child(7) { top: 25%; left: 2%; width: 32px; }
.deco-chip:nth-child(8) { top: 90%; left: 75%; width: 38px; }
.deco-chip:nth-child(9) { top: 5%; left: 50%; width: 30px; }
.deco-chip:nth-child(10) { top: 60%; left: 95%; width: 42px; }
.deco-chip:nth-child(11) { top: 88%; left: 40%; width: 36px; }
.deco-chip:nth-child(12) { top: 45%; left: 0%; width: 50px; }
.spec-sep {
color: var(--border-hi);
font-size: 11px;
}
@keyframes floatChip {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-14px) rotate(8deg); }
/* Hero right — schematic */
.hero-right {
display: flex;
align-items: center;
justify-content: center;
}
.schematic-svg {
width: 100%;
max-width: 440px;
height: auto;
border: 1px solid var(--border);
border-radius: 4px;
filter: drop-shadow(0 8px 32px rgba(0, 0, 0, 0.6));
}
/* ── Sections ─────────────────────────────────────────── */
.landing-section {
padding: 80px 40px;
max-width: 1100px;
padding: 80px 80px;
max-width: 1200px;
margin: 0 auto;
border-top: 1px solid var(--border);
}
.landing-section-alt {
@ -270,236 +275,276 @@
padding: 80px 0;
}
.landing-section-alt > * {
max-width: 1100px;
.landing-section-alt .section-header,
.landing-section-alt .features-grid {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
padding-left: 40px;
padding-right: 40px;
padding-left: 80px;
padding-right: 80px;
}
.section-header {
margin-bottom: 40px;
}
.section-label {
display: block;
font-family: var(--mono);
font-size: 11px;
color: var(--accent);
letter-spacing: 0.5px;
margin-bottom: 10px;
opacity: 0.7;
}
.section-title {
margin: 0 0 10px;
font-size: clamp(24px, 4vw, 36px);
margin: 0 0 8px;
font-size: clamp(22px, 3vw, 30px);
font-weight: 800;
letter-spacing: -0.8px;
text-align: center;
color: #fff;
letter-spacing: -0.6px;
color: #e8e8f0;
}
.section-sub {
margin: 0 0 48px;
font-size: 16px;
margin: 0;
font-size: 14.5px;
color: var(--text-muted);
text-align: center;
max-width: 480px;
line-height: 1.65;
}
/* ── Boards grid ──────────────────────────────────────── */
/* ── Boards ───────────────────────────────────────────── */
.boards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 16px;
}
.board-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px 20px 20px;
border-radius: var(--radius);
padding: 24px 16px 18px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
transition: border-color 0.2s;
position: relative;
}
.board-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--accent);
opacity: 0;
transition: opacity 0.2s;
border-radius: var(--radius) var(--radius) 0 0;
}
.board-card:hover {
border-color: rgba(0, 122, 204, 0.4);
box-shadow: 0 4px 24px rgba(0, 122, 204, 0.1);
transform: translateY(-3px);
border-color: var(--border-hi);
}
.board-card:hover::before {
opacity: 1;
}
.board-svg {
width: 100%;
max-width: 220px;
max-width: 210px;
height: auto;
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.5));
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.5));
}
.board-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
gap: 5px;
width: 100%;
border-top: 1px solid var(--border);
padding-top: 14px;
}
.board-name {
font-size: 15px;
font-size: 13.5px;
font-weight: 700;
color: var(--text);
}
.board-chip {
font-size: 12px;
font-size: 11px;
font-family: var(--mono);
color: var(--text-muted);
background: rgba(255,255,255,0.05);
padding: 2px 8px;
border-radius: 4px;
}
/* ── Features grid ────────────────────────────────────── */
/* ── Features ─────────────────────────────────────────── */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.feature-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
background: var(--bg-alt);
padding: 28px 24px;
transition: border-color 0.2s, transform 0.2s;
transition: background 0.15s;
border-radius: 0;
}
.feature-card:hover {
border-color: rgba(0, 122, 204, 0.35);
transform: translateY(-2px);
background: var(--bg-card);
}
.feature-icon {
width: 44px;
height: 44px;
background: rgba(0, 122, 204, 0.1);
border-radius: 10px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
margin-bottom: 14px;
color: var(--accent);
border: 1px solid rgba(0, 122, 204, 0.25);
border-radius: 2px;
background: rgba(0, 122, 204, 0.06);
}
.feature-icon svg {
width: 22px;
height: 22px;
width: 18px;
height: 18px;
}
.feature-title {
margin: 0 0 8px;
font-size: 15px;
margin: 0 0 7px;
font-size: 14px;
font-weight: 700;
color: #fff;
color: #e8e8f0;
}
.feature-desc {
margin: 0;
font-size: 13.5px;
font-size: 13px;
line-height: 1.65;
color: var(--text-muted);
}
/* ── Support ──────────────────────────────────────────── */
.landing-support {
background: linear-gradient(135deg, #0d0d0f 0%, #0a1520 100%);
background: var(--bg-alt);
border-top: 1px solid var(--border);
text-align: center;
padding: 80px 40px;
max-width: 100%;
padding: 72px 40px;
}
.support-content {
max-width: 560px;
max-width: 520px;
margin: 0 auto;
text-align: center;
}
.support-icon {
display: flex;
justify-content: center;
margin-bottom: 20px;
color: #e85d75;
font-size: 0;
margin-bottom: 18px;
color: var(--accent);
}
.support-icon svg {
width: 44px;
height: 44px;
filter: drop-shadow(0 0 12px rgba(232, 93, 117, 0.5));
width: 36px;
height: 36px;
}
.support-title {
margin: 0 0 10px;
font-size: clamp(20px, 2.5vw, 26px);
font-weight: 800;
letter-spacing: -0.5px;
color: #e8e8f0;
}
.support-sub {
margin: 0 0 28px;
font-size: 14px;
line-height: 1.7;
color: var(--text-muted);
}
.support-btns {
display: flex;
gap: 14px;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
margin-top: 8px;
}
.support-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
font-size: 14px;
padding: 10px 20px;
font-size: 13.5px;
font-weight: 600;
border-radius: 8px;
border-radius: var(--radius);
text-decoration: none;
transition: opacity 0.15s, transform 0.15s, box-shadow 0.15s;
transition: opacity 0.15s, transform 0.1s;
}
.support-btn:hover {
opacity: 0.88;
opacity: 0.85;
transform: translateY(-1px);
}
.support-btn-gh {
background: #24292e;
color: #fff;
border: 1px solid #444;
}
.support-btn-gh:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
background: #1c2128;
color: #e6edf3;
border: 1px solid #30363d;
}
.support-btn-pp {
background: #0070ba;
background: #003087;
color: #fff;
}
.support-btn-pp:hover {
box-shadow: 0 4px 16px rgba(0, 112, 186, 0.4);
border: 1px solid rgba(255,255,255,0.1);
}
.support-btn svg {
width: 18px;
height: 18px;
width: 16px;
height: 16px;
flex-shrink: 0;
}
/* ── Footer ───────────────────────────────────────────── */
.landing-footer {
padding: 32px 40px;
padding: 28px 40px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
background: var(--bg);
}
.footer-brand {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-family: var(--mono);
font-size: 14px;
font-weight: 700;
color: var(--text-muted);
}
.footer-brand svg {
width: 18px;
height: 18px;
width: 16px;
height: 16px;
color: var(--accent);
}
@ -520,15 +565,16 @@
}
.footer-copy {
font-size: 12px;
color: #444;
font-size: 11.5px;
color: #333;
margin: 0;
width: 100%;
text-align: center;
font-family: var(--mono);
}
.footer-copy a {
color: #555;
color: #3a3a3a;
text-decoration: none;
transition: color 0.15s;
}
@ -538,7 +584,39 @@
}
/* ── Responsive ───────────────────────────────────────── */
@media (max-width: 768px) {
@media (max-width: 900px) {
.landing-hero {
grid-template-columns: 1fr;
padding: 60px 32px;
min-height: auto;
gap: 40px;
}
.hero-right {
order: -1;
}
.schematic-svg {
max-width: 360px;
}
.hero-title {
font-size: 36px;
}
.landing-section,
.landing-support {
padding: 60px 32px;
}
.landing-section-alt .section-header,
.landing-section-alt .features-grid {
padding-left: 32px;
padding-right: 32px;
}
}
@media (max-width: 600px) {
.landing-nav {
padding: 0 20px;
}
@ -548,21 +626,26 @@
}
.landing-hero {
padding: 60px 20px 40px;
min-height: 70vh;
}
.hero-title {
font-size: 32px;
}
.hero-subtitle {
font-size: 15px;
padding: 48px 20px;
}
.landing-section,
.landing-support {
padding: 60px 20px;
padding: 48px 20px;
}
.landing-section-alt .section-header,
.landing-section-alt .features-grid {
padding-left: 20px;
padding-right: 20px;
}
.hero-title {
font-size: 30px;
}
.features-grid {
grid-template-columns: 1fr;
}
.landing-footer {
@ -570,8 +653,4 @@
flex-direction: column;
align-items: flex-start;
}
.footer-links {
gap: 16px;
}
}

View File

@ -6,7 +6,7 @@ const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio';
const PAYPAL_URL = 'https://paypal.me/odoonext';
const GITHUB_SPONSORS_URL = 'https://github.com/sponsors/davidmonterocrespo24';
/* ── Inline SVG icons ─────────────────────────────────── */
/* ── Icons ───────────────────────────────────────────── */
const IcoChip = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="5" width="14" height="14" rx="2" />
@ -65,118 +65,271 @@ const IcoGitHub = () => (
</svg>
);
const IcoHeart = () => (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
/* ── Circuit Schematic SVG (hero illustration) ───────── */
const CircuitSchematic = () => (
<svg viewBox="0 0 400 270" className="schematic-svg" aria-hidden="true">
<defs>
<pattern id="schgrid" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="10" cy="10" r="0.65" fill="rgba(0,180,70,0.18)" />
</pattern>
<clipPath id="scope-clip">
<rect x="290" y="200" width="100" height="60" />
</clipPath>
</defs>
{/* PCB background */}
<rect width="400" height="270" rx="4" fill="#040c06" />
<rect width="400" height="270" rx="4" fill="url(#schgrid)" />
{/* PCB edge cuts */}
<rect x="1.5" y="1.5" width="397" height="267" rx="3.5" fill="none" stroke="#081808" strokeWidth="2" />
{/* PCB corner marks */}
<path d="M10,1.5 L1.5,1.5 L1.5,10" fill="none" stroke="#0d2a0d" strokeWidth="1" />
<path d="M390,1.5 L398.5,1.5 L398.5,10" fill="none" stroke="#0d2a0d" strokeWidth="1" />
<path d="M1.5,260 L1.5,268.5 L10,268.5" fill="none" stroke="#0d2a0d" strokeWidth="1" />
<path d="M398.5,260 L398.5,268.5 L390,268.5" fill="none" stroke="#0d2a0d" strokeWidth="1" />
{/* Silkscreen header */}
<text x="12" y="14" fill="#092010" fontFamily="monospace" fontSize="6.5" letterSpacing="0.8">VELXIO BLINK DEMO</text>
<text x="388" y="14" textAnchor="end" fill="#092010" fontFamily="monospace" fontSize="6.5" letterSpacing="0.5">REV 1.0</text>
{/* ── ARDUINO UNO BLOCK ── */}
<rect x="20" y="45" width="88" height="165" rx="3" fill="#001400" stroke="#003810" strokeWidth="1.5" />
{/* MCU (ATmega328P) */}
<rect x="32" y="80" width="64" height="70" rx="2" fill="#0a0a0a" stroke="#1a1a1a" strokeWidth="1" />
{/* MCU pins (left side of chip) */}
{[0,1,2,3,4].map((i) => (
<rect key={`cl${i}`} x="28" y={85 + i * 12} width="4" height="3" rx="0.5" fill="#111" stroke="#222" strokeWidth="0.5" />
))}
{/* MCU pins (right side of chip) */}
{[0,1,2,3,4].map((i) => (
<rect key={`cr${i}`} x="64" y={85 + i * 12} width="4" height="3" rx="0.5" fill="#111" stroke="#222" strokeWidth="0.5" />
))}
{/* MCU label */}
<text x="64" y="113" textAnchor="middle" fill="#252525" fontFamily="monospace" fontSize="6">ATmega</text>
<text x="64" y="123" textAnchor="middle" fill="#252525" fontFamily="monospace" fontSize="6">328P</text>
{/* Board ref */}
<text x="64" y="62" textAnchor="middle" fill="#003a14" fontFamily="monospace" fontSize="7" fontWeight="bold">U1 ARDUINO UNO</text>
{/* USB-B port (left edge) */}
<rect x="9" y="90" width="13" height="22" rx="1.5" fill="#0f0f0f" stroke="#1a1a1a" strokeWidth="1" />
<rect x="11" y="93" width="9" height="16" rx="1" fill="#090909" />
{/* Status LED */}
<circle cx="86" cy="57" r="3" fill="#00bb44" />
<circle cx="86" cy="57" r="6" fill="rgba(0,180,60,0.08)" />
{/* Reset button */}
<rect x="38" y="48" width="10" height="10" rx="5" fill="#111" stroke="#1c1c1c" strokeWidth="1" />
{/* Power connector */}
<rect x="53" y="47" width="24" height="9" rx="1" fill="#111" stroke="#1a1a1a" strokeWidth="1" />
{[0,1,2].map((i) => (
<circle key={`pw${i}`} cx={57 + i * 8} cy="51.5" r="2" fill="#0a0a0a" stroke="#333" strokeWidth="0.5" />
))}
{/* Header pins (bottom of board) */}
{[0,1,2,3,4,5].map((i) => (
<rect key={`ph${i}`} x={26 + i * 10} y="207" width="5" height="4" rx="0.5" fill="#a07a00" />
))}
{/* ── ARDUINO RIGHT-SIDE PIN STUBS ── */}
{/* 5V */}
<line x1="108" y1="68" x2="118" y2="68" stroke="#003810" strokeWidth="1" />
<text x="107" y="66" textAnchor="end" fill="#004018" fontFamily="monospace" fontSize="5.5">5V</text>
{/* D13 */}
<line x1="108" y1="108" x2="118" y2="108" stroke="#003810" strokeWidth="1" />
<text x="107" y="106" textAnchor="end" fill="#004018" fontFamily="monospace" fontSize="5.5">D13</text>
{/* GND */}
<line x1="108" y1="178" x2="118" y2="178" stroke="#003810" strokeWidth="1" />
<text x="107" y="176" textAnchor="end" fill="#004018" fontFamily="monospace" fontSize="5.5">GND</text>
{/* TX */}
<line x1="108" y1="143" x2="118" y2="143" stroke="#003810" strokeWidth="1" />
<text x="107" y="141" textAnchor="end" fill="#004018" fontFamily="monospace" fontSize="5.5">TX</text>
{/* ── POWER RAILS ── */}
{/* VCC rail */}
<line x1="118" y1="68" x2="365" y2="68" stroke="#880000" strokeWidth="1" strokeDasharray="6 3" opacity="0.55" />
<text x="367" y="71" fill="#440000" fontFamily="monospace" fontSize="6">+5V</text>
{/* GND rail */}
<line x1="118" y1="178" x2="348" y2="178" stroke="#003388" strokeWidth="1" strokeDasharray="6 3" opacity="0.55" />
{/* GND symbol */}
<line x1="348" y1="178" x2="363" y2="178" stroke="#001a44" strokeWidth="1" />
<line x1="356" y1="173" x2="356" y2="183" stroke="#001a44" strokeWidth="1.5" />
<line x1="351" y1="185" x2="361" y2="185" stroke="#001a44" strokeWidth="1" />
<line x1="353" y1="188" x2="359" y2="188" stroke="#001a44" strokeWidth="0.8" />
{/* ── DECOUPLING CAPACITOR C1 ── */}
{/* Wire from VCC rail */}
<line x1="315" y1="68" x2="315" y2="100" stroke="#007acc" strokeWidth="1.2" />
{/* Top plate */}
<line x1="309" y1="100" x2="321" y2="100" stroke="#007acc" strokeWidth="1.5" />
{/* Bottom plate */}
<line x1="309" y1="106" x2="321" y2="106" stroke="#007acc" strokeWidth="1.5" />
{/* Wire to GND rail */}
<line x1="315" y1="106" x2="315" y2="178" stroke="#007acc" strokeWidth="1.2" />
{/* Junction dots */}
<circle cx="315" cy="68" r="3" fill="#007acc" />
<circle cx="315" cy="178" r="3" fill="#007acc" />
{/* C1 label */}
<text x="325" y="101" fill="#003a55" fontFamily="monospace" fontSize="5.5">C1</text>
<text x="325" y="109" fill="#003a55" fontFamily="monospace" fontSize="5.5">100nF</text>
{/* ── D13 SIGNAL TRACE ── */}
<line x1="118" y1="108" x2="152" y2="108" stroke="#00aa44" strokeWidth="1.5" />
{/* ── RESISTOR R1 (IEC rectangle) ── */}
{/* Left stub */}
<line x1="152" y1="108" x2="162" y2="108" stroke="#00aa44" strokeWidth="1.5" />
{/* Body */}
<rect x="162" y="100" width="44" height="16" rx="1" fill="none" stroke="#00aa44" strokeWidth="1.5" />
{/* Right stub */}
<line x1="206" y1="108" x2="220" y2="108" stroke="#00aa44" strokeWidth="1.5" />
{/* Labels */}
<text x="184" y="97" textAnchor="middle" fill="#00661a" fontFamily="monospace" fontSize="5.5">R1</text>
<text x="184" y="124" textAnchor="middle" fill="#00661a" fontFamily="monospace" fontSize="5.5">330 Ω</text>
{/* ── LED D1 (IEC triangle + bar) ── */}
{/* Trace R1 to anode */}
<line x1="220" y1="108" x2="230" y2="108" stroke="#00aa44" strokeWidth="1.5" />
{/* Triangle (pointing right) */}
<polygon points="230,99 230,117 258,108" fill="rgba(0,255,100,0.08)" stroke="#00aa44" strokeWidth="1.5" />
{/* Cathode bar */}
<line x1="258" y1="99" x2="258" y2="117" stroke="#00aa44" strokeWidth="2" />
{/* Trace cathode → right */}
<line x1="258" y1="108" x2="280" y2="108" stroke="#00aa44" strokeWidth="1.5" />
{/* LED glow */}
<circle cx="244" cy="108" r="20" fill="rgba(0,255,90,0.04)" />
{/* Light emission rays */}
<line x1="264" y1="96" x2="272" y2="90" stroke="rgba(0,220,80,0.22)" strokeWidth="1" />
<line x1="267" y1="108" x2="276" y2="108" stroke="rgba(0,220,80,0.22)" strokeWidth="1" />
<line x1="264" y1="120" x2="272" y2="126" stroke="rgba(0,220,80,0.22)" strokeWidth="1" />
{/* D1 label */}
<text x="244" y="95" textAnchor="middle" fill="#00661a" fontFamily="monospace" fontSize="5.5">D1</text>
<text x="244" y="126" textAnchor="middle" fill="#00661a" fontFamily="monospace" fontSize="5.5">GREEN</text>
{/* ── TRACE: cathode → GND rail ── */}
<line x1="280" y1="108" x2="280" y2="178" stroke="#00aa44" strokeWidth="1.5" />
{/* Junction dot on GND rail */}
<circle cx="280" cy="178" r="3.5" fill="#00aa44" />
{/* ── OSCILLOSCOPE WINDOW ── */}
<rect x="288" y="192" width="100" height="66" rx="2" fill="#000c03" stroke="#0a2010" strokeWidth="1" />
{/* Scope header bg */}
<rect x="288" y="192" width="100" height="14" rx="2" fill="#000" />
<text x="291" y="202" fill="#005522" fontFamily="monospace" fontSize="5.5">CH1 D13</text>
<text x="385" y="202" textAnchor="end" fill="#003314" fontFamily="monospace" fontSize="5.5">5V/div</text>
{/* Scope grid lines */}
<line x1="288" y1="218" x2="388" y2="218" stroke="#051405" strokeWidth="0.5" />
<line x1="288" y1="234" x2="388" y2="234" stroke="#051405" strokeWidth="0.5" />
<line x1="288" y1="250" x2="388" y2="250" stroke="#051405" strokeWidth="0.5" />
<line x1="313" y1="206" x2="313" y2="258" stroke="#051405" strokeWidth="0.5" />
<line x1="338" y1="206" x2="338" y2="258" stroke="#051405" strokeWidth="0.5" />
<line x1="363" y1="206" x2="363" y2="258" stroke="#051405" strokeWidth="0.5" />
{/* Square wave trace (clipped) */}
<polyline
clipPath="url(#scope-clip)"
points="290,250 290,214 303,214 303,250 316,250 316,214 329,214 329,250 342,250 342,214 355,214 355,250 368,250 368,214 381,214 381,250 388,250"
fill="none"
stroke="#00dd55"
strokeWidth="1.5"
strokeLinejoin="miter"
/>
<text x="291" y="258" fill="#003314" fontFamily="monospace" fontSize="5">1s/div</text>
{/* ── TX trace decorative ── */}
<line x1="118" y1="143" x2="148" y2="143" stroke="#007acc" strokeWidth="1" strokeDasharray="4 2" opacity="0.4" />
<text x="152" y="146" fill="#003a55" fontFamily="monospace" fontSize="5.5">Serial TX </text>
{/* Bottom silkscreen */}
<text x="12" y="263" fill="#092010" fontFamily="monospace" fontSize="6" letterSpacing="0.5">MIT LICENSE</text>
<text x="388" y="263" textAnchor="end" fill="#092010" fontFamily="monospace" fontSize="6">velxio.dev</text>
</svg>
);
/* ── Arduino board SVGs ───────────────────────────────── */
/* ── Board SVGs ───────────────────────────────────────── */
const BoardUno = () => (
<svg viewBox="0 0 120 80" className="board-svg">
{/* PCB */}
<rect x="2" y="2" width="116" height="76" rx="4" fill="#006633" stroke="#004d26" strokeWidth="1.5"/>
{/* MCU */}
<rect x="42" y="22" width="36" height="36" rx="2" fill="#1a1a1a" stroke="#333" strokeWidth="1"/>
{/* USB */}
<rect x="0" y="28" width="14" height="24" rx="2" fill="#555" stroke="#444" strokeWidth="1"/>
{/* Power jack */}
<circle cx="108" cy="20" r="7" fill="#333" stroke="#222" strokeWidth="1"/>
{/* Header pins top */}
<rect x="2" y="2" width="116" height="76" rx="4" fill="#006633" stroke="#004d26" strokeWidth="1.5" />
<rect x="42" y="22" width="36" height="36" rx="2" fill="#1a1a1a" stroke="#333" strokeWidth="1" />
<rect x="0" y="28" width="14" height="24" rx="2" fill="#555" stroke="#444" strokeWidth="1" />
<circle cx="108" cy="20" r="7" fill="#333" stroke="#222" strokeWidth="1" />
{[0,1,2,3,4,5,6,7,8,9,11,12,13].map((i) => (
<rect key={i} x={20 + i * 6.5} y="4" width="3" height="6" rx="0.5" fill="#d4a017" />
))}
{/* Header pins bottom */}
{[0,1,2,3,4,5].map((i) => (
<rect key={i} x={40 + i * 8} y="70" width="3" height="6" rx="0.5" fill="#d4a017" />
))}
{/* LED */}
<circle cx="90" cy="12" r="2.5" fill="#00ff88" opacity="0.9"/>
{/* Label */}
<circle cx="90" cy="12" r="2.5" fill="#00ff88" opacity="0.9" />
<text x="60" y="77" textAnchor="middle" fill="#00aa55" fontSize="5" fontFamily="monospace">Arduino Uno</text>
</svg>
);
const BoardNano = () => (
<svg viewBox="0 0 120 50" className="board-svg">
{/* PCB */}
<rect x="2" y="2" width="116" height="46" rx="3" fill="#003399" stroke="#002277" strokeWidth="1.5"/>
{/* MCU */}
<rect x="44" y="12" width="24" height="24" rx="1.5" fill="#1a1a1a" stroke="#333" strokeWidth="1"/>
{/* USB mini */}
<rect x="50" y="0" width="20" height="8" rx="2" fill="#555" stroke="#444" strokeWidth="1"/>
{/* Header left */}
<rect x="2" y="2" width="116" height="46" rx="3" fill="#003399" stroke="#002277" strokeWidth="1.5" />
<rect x="44" y="12" width="24" height="24" rx="1.5" fill="#1a1a1a" stroke="#333" strokeWidth="1" />
<rect x="50" y="0" width="20" height="8" rx="2" fill="#555" stroke="#444" strokeWidth="1" />
{[0,1,2,3,4,5,6,7].map((i) => (
<rect key={i} x="4" y={8 + i * 4.5} width="6" height="3" rx="0.5" fill="#d4a017" />
))}
{/* Header right */}
{[0,1,2,3,4,5,6,7].map((i) => (
<rect key={i} x="110" y={8 + i * 4.5} width="6" height="3" rx="0.5" fill="#d4a017" />
))}
{/* LED */}
<circle cx="28" cy="10" r="2" fill="#00ff88" opacity="0.9"/>
<circle cx="28" cy="10" r="2" fill="#00ff88" opacity="0.9" />
<text x="60" y="44" textAnchor="middle" fill="#6699ff" fontSize="5" fontFamily="monospace">Arduino Nano</text>
</svg>
);
const BoardPico = () => (
<svg viewBox="0 0 120 60" className="board-svg">
{/* PCB */}
<rect x="2" y="2" width="116" height="56" rx="3" fill="#f0f0f0" stroke="#ccc" strokeWidth="1.5"/>
{/* RP2040 chip */}
<rect x="40" y="14" width="32" height="32" rx="2" fill="#1a1a1a" stroke="#333" strokeWidth="1"/>
<rect x="44" y="18" width="24" height="24" rx="1" fill="#222" stroke="#444" strokeWidth="0.5"/>
{/* USB micro */}
<rect x="50" y="0" width="20" height="8" rx="2" fill="#888" stroke="#666" strokeWidth="1"/>
{/* Header left */}
<rect x="2" y="2" width="116" height="56" rx="3" fill="#f0f0f0" stroke="#ccc" strokeWidth="1.5" />
<rect x="40" y="14" width="32" height="32" rx="2" fill="#1a1a1a" stroke="#333" strokeWidth="1" />
<rect x="44" y="18" width="24" height="24" rx="1" fill="#222" stroke="#444" strokeWidth="0.5" />
<rect x="50" y="0" width="20" height="8" rx="2" fill="#888" stroke="#666" strokeWidth="1" />
{[0,1,2,3,4,5,6].map((i) => (
<rect key={i} x="4" y={10 + i * 6} width="6" height="4" rx="0.5" fill="#888" />
))}
{/* Header right */}
{[0,1,2,3,4,5,6].map((i) => (
<rect key={i} x="110" y={10 + i * 6} width="6" height="4" rx="0.5" fill="#888" />
))}
{/* LED */}
<circle cx="88" cy="10" r="2.5" fill="#00ccff" opacity="0.9"/>
<circle cx="88" cy="10" r="2.5" fill="#00ccff" opacity="0.9" />
<text x="60" y="57" textAnchor="middle" fill="#555" fontSize="5" fontFamily="monospace">Raspberry Pi Pico</text>
</svg>
);
const BoardMega = () => (
<svg viewBox="0 0 160 80" className="board-svg">
{/* PCB */}
<rect x="2" y="2" width="156" height="76" rx="4" fill="#006633" stroke="#004d26" strokeWidth="1.5"/>
{/* MCU - ATmega2560 */}
<rect x="55" y="20" width="50" height="40" rx="2" fill="#1a1a1a" stroke="#333" strokeWidth="1"/>
{/* USB */}
<rect x="0" y="28" width="14" height="24" rx="2" fill="#555" stroke="#444" strokeWidth="1"/>
{/* Power jack */}
<circle cx="148" cy="20" r="7" fill="#333" stroke="#222" strokeWidth="1"/>
{/* Top headers */}
<rect x="2" y="2" width="156" height="76" rx="4" fill="#006633" stroke="#004d26" strokeWidth="1.5" />
<rect x="55" y="20" width="50" height="40" rx="2" fill="#1a1a1a" stroke="#333" strokeWidth="1" />
<rect x="0" y="28" width="14" height="24" rx="2" fill="#555" stroke="#444" strokeWidth="1" />
<circle cx="148" cy="20" r="7" fill="#333" stroke="#222" strokeWidth="1" />
{Array.from({length: 18}).map((_, i) => (
<rect key={i} x={18 + i * 7} y="4" width="3" height="6" rx="0.5" fill="#d4a017" />
))}
{/* Bottom headers */}
{Array.from({length: 18}).map((_, i) => (
<rect key={i} x={18 + i * 7} y="70" width="3" height="6" rx="0.5" fill="#d4a017" />
))}
{/* LEDs */}
<circle cx="130" cy="12" r="2.5" fill="#00ff88" opacity="0.9"/>
<circle cx="138" cy="12" r="2.5" fill="#ff6600" opacity="0.9"/>
<circle cx="130" cy="12" r="2.5" fill="#00ff88" opacity="0.9" />
<circle cx="138" cy="12" r="2.5" fill="#ff6600" opacity="0.9" />
<text x="80" y="77" textAnchor="middle" fill="#00aa55" fontSize="5" fontFamily="monospace">Arduino Mega 2560</text>
</svg>
);
/* ── Features data ────────────────────────────────────── */
/* ── Features ─────────────────────────────────────────── */
const features = [
{ icon: <IcoCpu />, title: 'Real AVR8 Emulation', desc: 'Full ATmega328p at 16 MHz — timers, USART, ADC, SPI, I2C, PWM all wired.' },
{ icon: <IcoLayers />, title: '48+ Components', desc: 'LEDs, LCDs, TFT displays, servos, buzzers, sensors and more from wokwi-elements.' },
{ icon: <IcoCode />, title: 'Monaco Editor', desc: 'VS Code-grade C++ editor with syntax highlighting, autocomplete, and minimap.' },
{ icon: <IcoZap />, title: 'arduino-cli Backend', desc: 'Compile sketches locally in seconds. No cloud. No latency. No limits.' },
{ icon: <IcoMonitor />, title: 'Serial Monitor', desc: 'Live TX/RX with auto baud-rate detection, send data, and autoscroll.' },
{ icon: <IcoBook />, title: 'Library Manager', desc: 'Browse and install the full Arduino library index directly from the UI.' },
{ icon: <IcoCpu />, title: 'Real AVR8 Emulation', desc: 'Full ATmega328p at 16 MHz — timers, USART, ADC, SPI, I2C and PWM all wired.' },
{ icon: <IcoLayers />, title: '48+ Components', desc: 'LEDs, LCDs, TFT displays, servos, buzzers, sensors and more from wokwi-elements.' },
{ icon: <IcoCode />, title: 'Monaco Editor', desc: 'VS Code-grade C++ editor with syntax highlighting, autocomplete and minimap.' },
{ icon: <IcoZap />, title: 'arduino-cli Backend', desc: 'Compile sketches locally in seconds. No cloud. No latency. No limits.' },
{ icon: <IcoMonitor />, title: 'Serial Monitor', desc: 'Live TX/RX with auto baud-rate detection, send data and autoscroll.' },
{ icon: <IcoBook />, title: 'Library Manager', desc: 'Browse and install the full Arduino library index directly from the UI.' },
];
/* ── Sponsor SVG icon ─────────────────────────────────── */
const IcoSponsor = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M12 6v6l4 2" />
<path d="M8 14h.01M12 18h.01M16 14h.01" />
</svg>
);
/* ── Component ────────────────────────────────────────── */
export const LandingPage: React.FC = () => {
const user = useAuthStore((s) => s.user);
@ -207,16 +360,21 @@ export const LandingPage: React.FC = () => {
{/* Hero */}
<section className="landing-hero">
<div className="hero-glow" />
<div className="hero-content">
<div className="hero-badge">Open Source · Free · Local</div>
<div className="hero-left">
<div className="hero-eyebrow">
<span className="eyebrow-tag">OPEN SOURCE</span>
<span className="eyebrow-dot" />
<span className="eyebrow-tag">FREE</span>
<span className="eyebrow-dot" />
<span className="eyebrow-tag">LOCAL</span>
</div>
<h1 className="hero-title">
Arduino Emulator<br />
Arduino Simulator<br />
<span className="hero-accent">in your browser</span>
</h1>
<p className="hero-subtitle">
Write, compile, and simulate Arduino projects no hardware required.<br />
Real AVR8 emulation. 48+ electronic components. Runs entirely on your machine.
Write, compile, and simulate Arduino projects with no hardware required.
Real AVR8 emulation running entirely on your machine.
</p>
<div className="hero-ctas">
<Link to="/editor" className="cta-primary">
@ -228,48 +386,55 @@ export const LandingPage: React.FC = () => {
View on GitHub
</a>
</div>
<div className="hero-specs">
<span className="spec-pill">ATmega328p</span>
<span className="spec-sep">/</span>
<span className="spec-pill">RP2040</span>
<span className="spec-sep">/</span>
<span className="spec-pill">16 MHz</span>
<span className="spec-sep">/</span>
<span className="spec-pill">48+ components</span>
</div>
</div>
{/* Floating chip grid decoration */}
<div className="hero-decoration" aria-hidden>
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="deco-chip" style={{ animationDelay: `${i * 0.4}s` }}>
<IcoChip />
</div>
))}
<div className="hero-right">
<CircuitSchematic />
</div>
</section>
{/* Boards */}
<section className="landing-section">
<h2 className="section-title">Supported Boards</h2>
<p className="section-sub">Pick your hardware. The emulator adapts.</p>
<div className="section-header">
<span className="section-label">// boards</span>
<h2 className="section-title">Supported Hardware</h2>
<p className="section-sub">Pick your target board. The emulator adapts its register map and timing.</p>
</div>
<div className="boards-grid">
<div className="board-card">
<BoardUno />
<div className="board-info">
<span className="board-name">Arduino Uno</span>
<span className="board-chip">ATmega328p · AVR8</span>
<span className="board-chip">ATmega328p · AVR8 · 16 MHz</span>
</div>
</div>
<div className="board-card">
<BoardNano />
<div className="board-info">
<span className="board-name">Arduino Nano</span>
<span className="board-chip">ATmega328p · AVR8</span>
<span className="board-chip">ATmega328p · AVR8 · 16 MHz</span>
</div>
</div>
<div className="board-card">
<BoardMega />
<div className="board-info">
<span className="board-name">Arduino Mega</span>
<span className="board-chip">ATmega2560 · AVR8</span>
<span className="board-chip">ATmega2560 · AVR8 · 16 MHz</span>
</div>
</div>
<div className="board-card">
<BoardPico />
<div className="board-info">
<span className="board-name">Raspberry Pi Pico</span>
<span className="board-chip">RP2040 · Dual-core ARM</span>
<span className="board-chip">RP2040 · Dual-core ARM · 133 MHz</span>
</div>
</div>
</div>
@ -277,8 +442,11 @@ export const LandingPage: React.FC = () => {
{/* Features */}
<section className="landing-section landing-section-alt">
<h2 className="section-title">Everything you need</h2>
<p className="section-sub">A complete IDE and simulator, running locally.</p>
<div className="section-header">
<span className="section-label">// features</span>
<h2 className="section-title">Everything you need</h2>
<p className="section-sub">A complete IDE and simulator, running locally with no external dependencies.</p>
</div>
<div className="features-grid">
{features.map((f) => (
<div key={f.title} className="feature-card">
@ -291,11 +459,11 @@ export const LandingPage: React.FC = () => {
</section>
{/* Support */}
<section className="landing-section landing-support">
<section className="landing-support">
<div className="support-content">
<div className="support-icon"><IcoHeart /></div>
<h2 className="section-title">Support the project</h2>
<p className="section-sub">
<div className="support-icon"><IcoSponsor /></div>
<h2 className="support-title">Support the project</h2>
<p className="support-sub">
Velxio is free and open source. If it saves you time, consider supporting its development.
</p>
<div className="support-btns">
@ -303,8 +471,8 @@ export const LandingPage: React.FC = () => {
<IcoGitHub /> GitHub Sponsors
</a>
<a href={PAYPAL_URL} target="_blank" rel="noopener noreferrer" className="support-btn support-btn-pp">
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
<path d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.106zm14.146-14.42a3.35 3.35 0 0 0-.607-.541c-.013.076-.026.175-.041.254-.93 4.778-4.005 7.201-9.138 7.201h-2.19a.563.563 0 0 0-.556.479l-1.187 7.527h-.506l-.24 1.516a.56.56 0 0 0 .554.647h3.882c.46 0 .85-.334.922-.788.06-.26.76-4.852.816-5.09a.932.932 0 0 1 .923-.788h.58c3.76 0 6.705-1.528 7.565-5.946.36-1.847.174-3.388-.777-4.471z"/>
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.106zm14.146-14.42a3.35 3.35 0 0 0-.607-.541c-.013.076-.026.175-.041.254-.93 4.778-4.005 7.201-9.138 7.201h-2.19a.563.563 0 0 0-.556.479l-1.187 7.527h-.506l-.24 1.516a.56.56 0 0 0 .554.647h3.882c.46 0 .85-.334.922-.788.06-.26.76-4.852.816-5.09a.932.932 0 0 1 .923-.788h.58c3.76 0 6.705-1.528 7.565-5.946.36-1.847.174-3.388-.777-4.471z" />
</svg>
Donate via PayPal
</a>

View File

@ -0,0 +1,77 @@
/**
* Favicon generator converts favicon.svg into all required sizes.
* Run from project root: node scripts/generate-favicons.mjs
*
* Generates:
* frontend/public/favicon-16x16.png
* frontend/public/favicon-32x32.png
* frontend/public/favicon-48x48.png
* frontend/public/apple-touch-icon.png (180x180)
* frontend/public/android-chrome-192.png
* frontend/public/android-chrome-512.png
* frontend/public/favicon.ico (16+32+48 multi-size)
*/
import { readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const PUBLIC = join(ROOT, 'frontend', 'public');
// ── install deps on the fly if missing ─────────────────────────────
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
async function ensureDep(pkg) {
try { return await import(pkg); } catch {}
console.log(`Installing ${pkg}`);
const { execSync } = await import('child_process');
execSync(`npm install --no-save ${pkg}`, { stdio: 'inherit', cwd: ROOT });
return await import(pkg);
}
const { Resvg } = await ensureDep('@resvg/resvg-js');
const pngToIcoMod = await ensureDep('png-to-ico');
const pngToIco = pngToIcoMod.default ?? pngToIcoMod;
// ── render helper ───────────────────────────────────────────────────
const svgSrc = readFileSync(join(PUBLIC, 'favicon.svg'));
function renderPng(size) {
const resvg = new Resvg(svgSrc, {
fitTo: { mode: 'width', value: size },
font: { loadSystemFonts: false },
});
return resvg.render().asPng();
}
// ── generate PNGs ───────────────────────────────────────────────────
const sizes = [
{ name: 'favicon-16x16.png', size: 16 },
{ name: 'favicon-32x32.png', size: 32 },
{ name: 'favicon-48x48.png', size: 48 },
{ name: 'apple-touch-icon.png', size: 180 },
{ name: 'android-chrome-192.png', size: 192 },
{ name: 'android-chrome-512.png', size: 512 },
];
const pngBuffers = {};
for (const { name, size } of sizes) {
const buf = renderPng(size);
writeFileSync(join(PUBLIC, name), buf);
pngBuffers[size] = buf;
console.log(`${name} (${size}x${size})`);
}
// ── generate favicon.ico (16 + 32 + 48) ────────────────────────────
const icoBuffer = await pngToIco([
pngBuffers[16],
pngBuffers[32],
pngBuffers[48],
]);
writeFileSync(join(PUBLIC, 'favicon.ico'), icoBuffer);
console.log('✓ favicon.ico (16+32+48)');
console.log('\nAll favicon assets generated in frontend/public/');