feat: website
BIN
public/assets/cluster.png
Normal file
|
After Width: | Height: | Size: 458 KiB |
BIN
public/assets/editor.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
public/assets/firmware.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
public/assets/ledlab.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
public/assets/monitoring.png
Normal file
|
After Width: | Height: | Size: 455 KiB |
BIN
public/assets/spore-2.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
public/assets/spore-3.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
public/assets/spore.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
public/assets/topology.png
Normal file
|
After Width: | Height: | Size: 390 KiB |
358
public/index.html
Normal file
@@ -0,0 +1,358 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SPORE - Sprocket Orchestration Engine</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<!-- Prism.js for syntax highlighting -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<div class="header-content">
|
||||
<div class="brand">
|
||||
<h1>SPORE</h1>
|
||||
<p class="tagline">Sprocket Orchestration Engine</p>
|
||||
</div>
|
||||
<button class="burger" id="menu-toggle" aria-label="Menu" aria-controls="site-nav" aria-expanded="false">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav id="site-nav" class="site-nav">
|
||||
<div class="container">
|
||||
<a href="#overview">Overview</a>
|
||||
<a href="#spore">Core</a>
|
||||
<a href="#ui">UI</a>
|
||||
<a href="#apps">Apps</a>
|
||||
<a href="#docs">Docs</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section id="overview" class="hero">
|
||||
<div class="container">
|
||||
<img src="assets/spore.png" alt="SPORE" class="hero-image">
|
||||
<p>A totally based cluster engine for ESP8266 microcontrollers with automatic node discovery, health monitoring, and over-the-air updates. Built for real-time control of distributed embedded systems.</p>
|
||||
</div>
|
||||
<p class="subtitle">
|
||||
</section>
|
||||
|
||||
<section id="spore" class="content-section">
|
||||
<div class="container">
|
||||
<h2>Core</h2>
|
||||
<p class="subtitle">Cluster engine firmware for ESP8266 microcontrollers</p>
|
||||
<p class="repo-link"><a href="https://git.dcentral.systems/iot/spore" target="_blank" rel="noopener">Repository: spore</a></p>
|
||||
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<h3>Auto Discovery</h3>
|
||||
<p>UDP-based node discovery with automatic cluster membership on port 4210</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Health Monitoring</h3>
|
||||
<p>Real-time node status tracking with resource monitoring and automatic failover</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Event System</h3>
|
||||
<p>Local and cluster-wide event publishing/subscription with WebSocket and UDP streaming API</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>OTA Updates</h3>
|
||||
<p>Seamless firmware updates across the cluster via HTTP API</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Service Registry</h3>
|
||||
<p>Dynamic API endpoint discovery and registration for custom services</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Task Scheduler</h3>
|
||||
<p>Cooperative multitasking system for background operations</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-block">
|
||||
<h3>Quick Start</h3>
|
||||
<pre><code class="language-cpp">#include <Arduino.h>
|
||||
#include "spore/Spore.h"
|
||||
#include "spore/Service.h"
|
||||
#include "spore/core/ApiServer.h"
|
||||
#include "spore/core/TaskManager.h"
|
||||
|
||||
class HelloService : public Service {
|
||||
public:
|
||||
HelloService() {}
|
||||
|
||||
const char* getName() const override { return "hello"; }
|
||||
|
||||
void registerEndpoints(ApiServer& api) override {
|
||||
api.addEndpoint("/api/hello", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
req->send(200, "application/json", "{\"message\":\"hello\"}");
|
||||
});
|
||||
}
|
||||
|
||||
void registerTasks(TaskManager& taskManager) override {
|
||||
taskManager.registerTask("heartbeat", 1000, [this]() { this->heartbeat(); });
|
||||
}
|
||||
|
||||
private:
|
||||
void heartbeat() {
|
||||
// e.g., blink LED, publish telemetry, etc.
|
||||
}
|
||||
};
|
||||
|
||||
Spore spore({
|
||||
{"app", "my_app"},
|
||||
{"role", "controller"}
|
||||
});
|
||||
|
||||
void setup() {
|
||||
spore.setup();
|
||||
spore.addService(new HelloService());
|
||||
spore.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
spore.loop();
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="content-block">
|
||||
<h3>Technical Specifications</h3>
|
||||
<ul>
|
||||
<li><strong>Supported Hardware:</strong> ESP-01/ESP-01S (1MB Flash), Wemos D1 (4MB Flash)</li>
|
||||
<li><strong>Discovery Protocol:</strong> UDP broadcast on port 4210</li>
|
||||
<li><strong>API Interface:</strong> RESTful HTTP + WebSocket streaming</li>
|
||||
<li><strong>Dependencies:</strong> ESPAsyncWebServer, ArduinoJson</li>
|
||||
<li><strong>Framework:</strong> Arduino with PlatformIO build system</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="ui" class="content-section alt">
|
||||
<div class="container">
|
||||
<h2>UI</h2>
|
||||
<p class="subtitle">Zero-configuration web interface for cluster monitoring and management</p>
|
||||
<p class="repo-link"><a href="https://git.dcentral.systems/iot/spore-ui" target="_blank" rel="noopener">Repository: spore-ui</a></p>
|
||||
|
||||
<div class="screenshot-grid">
|
||||
<div class="screenshot">
|
||||
<img src="assets/cluster.png" alt="Cluster monitoring view">
|
||||
<p>Real-time cluster member overview with auto-discovery</p>
|
||||
</div>
|
||||
<div class="screenshot">
|
||||
<img src="assets/topology.png" alt="Network topology visualization">
|
||||
<p>Network topology visualization with node relationships</p>
|
||||
</div>
|
||||
<div class="screenshot">
|
||||
<img src="assets/monitoring.png" alt="Node monitoring dashboard">
|
||||
<p>Detailed system metrics and task monitoring</p>
|
||||
</div>
|
||||
<div class="screenshot">
|
||||
<img src="assets/firmware.png" alt="Firmware management interface">
|
||||
<p>Clusterwide over-the-air firmware updates</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-block">
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li>Cluster monitoring with real-time status updates</li>
|
||||
<li>Node details including running tasks and available endpoints</li>
|
||||
<li>Direct HTTP API access to all nodes in the cluster</li>
|
||||
<li>Over-the-air firmware updates for entire cluster</li>
|
||||
<li>WebSocket terminal for direct node interaction</li>
|
||||
<li>UDP auto-discovery eliminates hardcoded IP addresses</li>
|
||||
<li>Responsive design for all devices</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="content-block">
|
||||
<h3>Technology Stack</h3>
|
||||
<ul>
|
||||
<li><strong>Backend:</strong> Express.js, Node.js</li>
|
||||
<li><strong>Frontend:</strong> Vanilla JavaScript, CSS3, HTML5</li>
|
||||
<li><strong>Architecture:</strong> Custom component-based framework</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="apps" class="content-section">
|
||||
<div class="container">
|
||||
<h2>Apps</h2>
|
||||
<p class="subtitle">Application suite built on SPORE</p>
|
||||
<div class="content-block">
|
||||
<p>Explore applications that extend SPORE.</p>
|
||||
<ul>
|
||||
<li><a href="#ledlab">LEDLab</a> — Real-time LED matrix animation streaming and visual preset editor</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="ledlab" class="content-section">
|
||||
<div class="container">
|
||||
<h2>LEDLab</h2>
|
||||
<p class="subtitle">Real-time LED matrix animation streaming and visual preset editor</p>
|
||||
<p class="repo-link"><a href="https://git.dcentral.systems/iot/spore-ledlab" target="_blank" rel="noopener">Repository: spore-ledlab</a></p>
|
||||
|
||||
<div class="screenshot-grid">
|
||||
<div class="screenshot">
|
||||
<img src="assets/ledlab.png" alt="LEDLab interface">
|
||||
<p>Multi-node management with live canvas preview</p>
|
||||
</div>
|
||||
<div class="screenshot">
|
||||
<img src="assets/editor.png" alt="Preset editor">
|
||||
<p>Visual preset editor with building blocks</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-block">
|
||||
<h3>Firmware Requirements</h3>
|
||||
<p>LEDLab requires SPORE nodes running the <strong>PixelStreamController</strong> firmware, which handles UDP-based RGB data streaming to NeoPixel strips and matrices. The firmware subscribes to <code>udp/raw</code> cluster events and converts incoming hex-encoded pixel data into real-time LED animations.</p>
|
||||
<p>Key firmware features include support for both strip and matrix configurations, serpentine (zig-zag) wiring patterns, and configurable pixel counts, brightness, and matrix dimensions. The controller automatically remaps pixel coordinates for proper matrix display.</p>
|
||||
<p>See <a href="https://git.dcentral.systems/iot/spore/src/branch/main/examples/pixelstream/README.md" target="_blank" rel="noopener">PixelStream documentation</a> for more information.</p>
|
||||
</div>
|
||||
|
||||
<div class="content-block">
|
||||
<h3>Capabilities</h3>
|
||||
<ul>
|
||||
<li>Multi-node management with individual control</li>
|
||||
<li>Real-time canvas preview for each node</li>
|
||||
<li>10+ built-in animation presets (rainbow, lava lamp, aurora, etc.)</li>
|
||||
<li>Visual preset editor with reusable building blocks</li>
|
||||
<li>Live parameter control with instant feedback</li>
|
||||
<li>Import/export custom presets as JSON</li>
|
||||
<li>Auto-discovery of SPORE nodes on network</li>
|
||||
<li>Configurable FPS (1-60) and matrix dimensions</li>
|
||||
<li>Requires SPORE nodes running PixelStreamController</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="content-block">
|
||||
<h3>Building Blocks</h3>
|
||||
<div class="building-blocks">
|
||||
<div>
|
||||
<strong>Shapes:</strong> Circle, Rectangle, Triangle, Blob, Point, Line
|
||||
</div>
|
||||
<div>
|
||||
<strong>Patterns:</strong> Trail, Radial, Spiral
|
||||
</div>
|
||||
<div>
|
||||
<strong>Colors:</strong> Solid, Gradient, Palette, Rainbow
|
||||
</div>
|
||||
<div>
|
||||
<strong>Animations:</strong> Move, Rotate, Pulse, Oscillate, Fade
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="docs" class="content-section">
|
||||
<div class="container">
|
||||
<h2>Documentation</h2>
|
||||
</div>
|
||||
|
||||
<div class="doc-grid">
|
||||
<div class="doc-card">
|
||||
<h3>SPORE Core</h3>
|
||||
<ul>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/Architecture.md" target="_blank" rel="noopener">Architecture Guide</a> - System design and implementation</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/API.md" target="_blank" rel="noopener">API Reference</a> - Complete REST API documentation</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/Development.md" target="_blank" rel="noopener">Development Guide</a> - Build, deployment, configuration</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/TaskManagement.md" target="_blank" rel="noopener">Task Management</a> - Background task system</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/ClusterBroadcast.md" target="_blank" rel="noopener">Cluster Broadcast</a> - Event distribution protocol</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/StreamingAPI.md" target="_blank" rel="noopener">Streaming API</a> - WebSocket integration</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="doc-card">
|
||||
<h3>SPORE UI</h3>
|
||||
<ul>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore-ui/src/branch/main/README.md" target="_blank" rel="noopener">Getting Started</a> - Installation and setup</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore-ui/src/branch/main/docs/DISCOVERY.md" target="_blank" rel="noopener">UDP Auto Discovery</a> - Network discovery protocol</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore-ui/src/branch/main/api/openapi.yaml" target="_blank" rel="noopener">API Spec</a> - Backend API reference (OpenAPI)</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore-ui/src/branch/main/docs/FRAMEWORK_README.md" target="_blank" rel="noopener">Component Framework</a> - Custom UI architecture</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="doc-card">
|
||||
<h3>SPORE LEDLab</h3>
|
||||
<ul>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/README.md" target="_blank" rel="noopener">Quick Start</a> - Installation and usage</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/PRESET_EDITOR.md" target="_blank" rel="noopener">Preset Editor</a> - Visual animation builder</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/presets/examples" target="_blank" rel="noopener">Custom Presets</a> - JSON format and examples</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/presets/building-blocks.js" target="_blank" rel="noopener">Building Blocks</a> - Reusable components</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/MULTI_NODE_UPDATE.md" target="_blank" rel="noopener">Multi-Node Management</a> - Cluster control</li>
|
||||
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/examples/pixelstream/README.md" target="_blank" rel="noopener">PixelStream Controller</a> - Required SPORE node firmware</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="getting-started">
|
||||
<h3>Getting Started</h3>
|
||||
<div class="start-grid">
|
||||
<div class="start-card">
|
||||
<h4>SPORE Core</h4>
|
||||
<pre><code># Build and flash firmware
|
||||
./ctl.sh build target esp01
|
||||
./ctl.sh flash target esp01</code></pre>
|
||||
</div>
|
||||
<div class="start-card">
|
||||
<h4>SPORE UI</h4>
|
||||
<pre><code># Install and start
|
||||
npm install
|
||||
npm start
|
||||
|
||||
# Open browser
|
||||
http://localhost:3001</code></pre>
|
||||
</div>
|
||||
<div class="start-card">
|
||||
<h4>SPORE LEDLab</h4>
|
||||
<pre><code># Install and start
|
||||
npm install
|
||||
npm start
|
||||
|
||||
# Open browser
|
||||
http://localhost:3000</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>SPORE - Sprocket Orchestration Engine</p>
|
||||
<p>
|
||||
Source:
|
||||
<a href="https://git.dcentral.systems/iot/spore" target="_blank" rel="noopener">spore</a> ·
|
||||
<a href="https://git.dcentral.systems/iot/spore-ui" target="_blank" rel="noopener">spore-ui</a> ·
|
||||
<a href="https://git.dcentral.systems/iot/spore-ledlab" target="_blank" rel="noopener">spore-ledlab</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Lightbox Overlay for screenshots -->
|
||||
<div id="lightbox" class="lightbox" aria-hidden="true" role="dialog">
|
||||
<div class="lightbox-backdrop" data-close></div>
|
||||
<figure class="lightbox-content">
|
||||
<img id="lightbox-image" alt="Expanded screenshot" />
|
||||
<figcaption id="lightbox-caption"></figcaption>
|
||||
<button class="lightbox-close" type="button" title="Close" aria-label="Close" data-close>×</button>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
<!-- Prism.js for syntax highlighting -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-core.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
169
public/script.js
Normal file
@@ -0,0 +1,169 @@
|
||||
// Smooth scroll for navigation links
|
||||
function getHeaderHeight() {
|
||||
const header = document.querySelector('header');
|
||||
return header ? header.offsetHeight : 80;
|
||||
}
|
||||
|
||||
function smoothScrollToSection(hash) {
|
||||
const target = document.querySelector(hash);
|
||||
if (!target) return;
|
||||
|
||||
// Recalculate header height at the moment of scrolling
|
||||
const headerHeight = getHeaderHeight();
|
||||
|
||||
// Use getBoundingClientRect for robust position calc across layouts
|
||||
const targetTopRelative = target.getBoundingClientRect().top;
|
||||
const absoluteTargetTop = window.scrollY + targetTopRelative;
|
||||
|
||||
// Exact offset: align section start just below the header bottom
|
||||
const top = Math.max(absoluteTargetTop - headerHeight, 0);
|
||||
|
||||
window.scrollTo({ top, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function updateURLHash(hash) {
|
||||
// Update URL hash without triggering scroll
|
||||
if (hash && hash !== '#') {
|
||||
history.pushState(null, null, hash);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
// Remove hash from URL and scroll to top
|
||||
history.pushState(null, null, window.location.pathname);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
document.querySelectorAll('nav a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// If mobile menu is open, close it first so header height is correct
|
||||
const siteNavEl = document.getElementById('site-nav');
|
||||
const menuBtn = document.getElementById('menu-toggle');
|
||||
const wasOpen = siteNavEl && siteNavEl.classList.contains('open');
|
||||
if (wasOpen) {
|
||||
siteNavEl.classList.remove('open');
|
||||
if (menuBtn) menuBtn.setAttribute('aria-expanded', 'false');
|
||||
// Wait for next frame so layout updates and header height is correct
|
||||
requestAnimationFrame(() => {
|
||||
smoothScrollToSection(this.getAttribute('href'));
|
||||
updateURLHash(this.getAttribute('href'));
|
||||
});
|
||||
} else {
|
||||
smoothScrollToSection(this.getAttribute('href'));
|
||||
updateURLHash(this.getAttribute('href'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add active class to navigation based on scroll position
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
const navLinks = document.querySelectorAll('nav a');
|
||||
|
||||
function setActiveNav() {
|
||||
let current = '';
|
||||
const headerHeight = getHeaderHeight();
|
||||
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
// Account for fixed header in active link detection
|
||||
if (window.scrollY + headerHeight + 30 >= sectionTop && window.scrollY < sectionTop + sectionHeight) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === `#${current}`) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', setActiveNav);
|
||||
window.addEventListener('load', setActiveNav);
|
||||
|
||||
// Adjust on resize (header height can change)
|
||||
window.addEventListener('resize', () => {
|
||||
// Re-run active link calculation to keep alignment correct
|
||||
setActiveNav();
|
||||
});
|
||||
|
||||
// Handle direct hash navigation (e.g., page load with #section)
|
||||
window.addEventListener('load', () => {
|
||||
if (location.hash) {
|
||||
// Use the same precise scroll logic on initial load
|
||||
smoothScrollToSection(location.hash);
|
||||
}
|
||||
});
|
||||
|
||||
// Lightbox for screenshots (desktop only behavior)
|
||||
const lightbox = document.getElementById('lightbox');
|
||||
const lightboxImg = document.getElementById('lightbox-image');
|
||||
const lightboxCaption = document.getElementById('lightbox-caption');
|
||||
|
||||
function openLightbox(src, caption) {
|
||||
if (!lightbox) return;
|
||||
lightboxImg.src = src;
|
||||
lightboxCaption.textContent = caption || '';
|
||||
lightbox.setAttribute('aria-hidden', 'false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
if (!lightbox) return;
|
||||
lightbox.setAttribute('aria-hidden', 'true');
|
||||
lightboxImg.src = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Delegate clicks on screenshots
|
||||
document.addEventListener('click', (e) => {
|
||||
const img = e.target.closest('.screenshot img');
|
||||
if (img) {
|
||||
// Only open overlay on wider screens
|
||||
if (window.innerWidth >= 768) {
|
||||
openLightbox(img.src, img.alt || img.nextElementSibling?.textContent || '');
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.target.matches('[data-close]')) {
|
||||
closeLightbox();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on ESC
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeLightbox();
|
||||
});
|
||||
|
||||
// Mobile burger menu
|
||||
const menuToggle = document.getElementById('menu-toggle');
|
||||
const siteNav = document.getElementById('site-nav');
|
||||
if (menuToggle && siteNav) {
|
||||
menuToggle.addEventListener('click', () => {
|
||||
const isOpen = siteNav.classList.toggle('open');
|
||||
menuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
});
|
||||
|
||||
// Close menu after navigating
|
||||
siteNav.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
if (siteNav.classList.contains('open')) {
|
||||
siteNav.classList.remove('open');
|
||||
menuToggle.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle SPORE title click to scroll to top and remove hash
|
||||
const sporeTitle = document.querySelector('.brand h1');
|
||||
if (sporeTitle) {
|
||||
sporeTitle.addEventListener('click', scrollToTop);
|
||||
sporeTitle.style.cursor = 'pointer';
|
||||
}
|
||||
|
||||
633
public/styles.css
Normal file
@@ -0,0 +1,633 @@
|
||||
/* CSS Variables */
|
||||
:root {
|
||||
--color-bg: #0a0e14;
|
||||
--color-surface: #1a1f28;
|
||||
--color-surface-alt: #0f1318;
|
||||
--color-border: #2d3541;
|
||||
--color-text: #c5cdd9;
|
||||
--color-text-dim: #7d8799;
|
||||
--color-primary: #6db5ff;
|
||||
--color-accent: #00d4aa;
|
||||
--color-code-bg: #0d1117;
|
||||
/* Glassmorphism */
|
||||
--glass-bg: rgba(26, 31, 40, 0.55);
|
||||
--glass-elevated-bg: rgba(26, 31, 40, 0.65);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--glass-highlight: rgba(255, 255, 255, 0.18);
|
||||
--glass-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
--glass-blur: 12px;
|
||||
--spacing-xs: 0.5rem;
|
||||
--spacing-sm: 1rem;
|
||||
--spacing-md: 2rem;
|
||||
--spacing-lg: 3rem;
|
||||
--spacing-xl: 4rem;
|
||||
--border-radius: 8px;
|
||||
--max-width: 1200px;
|
||||
}
|
||||
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
/* Subtle gradient backdrop for depth */
|
||||
background:
|
||||
radial-gradient(1200px 800px at 10% -10%, rgba(109,181,255,0.08), transparent 60%),
|
||||
radial-gradient(1000px 700px at 110% 10%, rgba(0,212,170,0.07), transparent 55%),
|
||||
linear-gradient(180deg, #0a0e14 0%, #0a0e14 100%);
|
||||
color: var(--color-text);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
padding-top: 90px; /* Reduced for more compact header */
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
background: var(--glass-bg);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: var(--spacing-xs) 0; /* More compact padding */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Hide burger on desktop */
|
||||
.burger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
font-size: 1.5rem; /* More compact font size */
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 0.1rem; /* Reduced margin */
|
||||
}
|
||||
|
||||
.brand .tagline {
|
||||
color: var(--color-text-dim);
|
||||
font-size: 0.85rem; /* More compact tagline */
|
||||
}
|
||||
|
||||
.site-nav {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
padding: 0.25rem 0; /* More compact padding */
|
||||
border-top: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
}
|
||||
|
||||
.site-nav .container {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.site-nav a {
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
padding: 0.4rem var(--spacing-sm); /* More compact padding */
|
||||
border-radius: var(--border-radius);
|
||||
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
||||
font-size: 0.9rem; /* Slightly smaller font */
|
||||
border: 1px solid transparent; /* Prevent hover border pop-in */
|
||||
}
|
||||
|
||||
.site-nav a:hover,
|
||||
.site-nav a.active {
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
|
||||
border-color: var(--glass-highlight);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Link styles */
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.hero {
|
||||
padding: var(--spacing-xl) 0;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, var(--color-surface-alt) 0%, var(--color-bg) 100%);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
max-height: 60vh;
|
||||
object-fit: contain;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text);
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
padding: var(--spacing-xl) 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.content-section.alt {
|
||||
background-color: var(--color-surface-alt);
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 2rem;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--color-text-dim);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.repo-link {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.repo-link a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.repo-link a:hover {
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Feature Grid */
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: var(--glass-elevated-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-md);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
color: var(--color-accent);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.content-block h3 {
|
||||
color: var(--color-accent);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--color-code-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-md);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Prism.js syntax highlighting overrides for dark theme */
|
||||
pre[class*="language-"] {
|
||||
background-color: var(--color-code-bg) !important;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
code[class*="language-"] {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Override Prism theme colors to match our design */
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #6a9955;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #b5cea8;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #d16969;
|
||||
}
|
||||
|
||||
/* Tech Specs */
|
||||
.content-block h3,
|
||||
.architecture h3 {
|
||||
color: var(--color-accent);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.content-block ul {
|
||||
list-style: none;
|
||||
margin-left: var(--spacing-md);
|
||||
}
|
||||
|
||||
.content-block li {
|
||||
margin: var(--spacing-xs) 0;
|
||||
padding-left: var(--spacing-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-block li:before {
|
||||
content: "▹";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Screenshot Grid */
|
||||
.screenshot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.screenshot {
|
||||
background: var(--glass-elevated-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.screenshot img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.screenshot p {
|
||||
padding: var(--spacing-md);
|
||||
color: var(--color-text-dim);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Building Blocks */
|
||||
.building-blocks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.building-blocks > div {
|
||||
background: var(--glass-elevated-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-md);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.building-blocks strong {
|
||||
color: var(--color-primary);
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* Documentation Grid */
|
||||
.doc-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin: var(--spacing-lg) 0;
|
||||
max-width: var(--max-width);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.doc-card {
|
||||
background: var(--glass-elevated-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-md);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.doc-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.doc-card ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.doc-card li {
|
||||
padding: var(--spacing-xs) 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.doc-card li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Getting Started */
|
||||
.getting-started {
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.getting-started h3 {
|
||||
color: var(--color-accent);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.start-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.start-card h4 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
background: var(--glass-bg);
|
||||
border-top: 1px solid var(--glass-border);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
padding: var(--spacing-md) 0;
|
||||
text-align: center;
|
||||
color: var(--color-text-dim);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.hero h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Burger button */
|
||||
.burger {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
width: 44px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.burger span {
|
||||
display: block;
|
||||
height: 2px;
|
||||
background: var(--color-text);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Collapsible nav */
|
||||
.site-nav {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
}
|
||||
|
||||
.site-nav .container {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
.site-nav.open {
|
||||
display: flex;
|
||||
}
|
||||
.site-nav a {
|
||||
padding: 12px var(--spacing-sm);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.feature-grid,
|
||||
.screenshot-grid,
|
||||
.doc-grid,
|
||||
.start-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.screenshot-grid {
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth Scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Offset anchored scroll positions to account for fixed header */
|
||||
section[id] {
|
||||
scroll-margin-top: 58px; /* Position sections so separators align with header bottom */
|
||||
}
|
||||
|
||||
/* Extra spacing for mobile to prevent top section cutoff */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
section[id] {
|
||||
scroll-margin-top: 120px; /* Position sections so separators align with mobile header bottom */
|
||||
}
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
/* Lightbox */
|
||||
.lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.lightbox[aria-hidden="false"] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.lightbox-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.lightbox-content {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
margin: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.lightbox-content img {
|
||||
max-width: 100%;
|
||||
max-height: 85vh;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.lightbox-content figcaption {
|
||||
text-align: center;
|
||||
color: var(--color-text-dim);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.lightbox-close:hover {
|
||||
background: var(--color-surface-alt);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||