feat: website

This commit is contained in:
2025-10-13 15:10:47 +02:00
commit 2f37486295
19 changed files with 2136 additions and 0 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
**/.DS_Store
**/Thumbs.db

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
*.log
.DS_Store

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
# Build a small production image for the SPORE website
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Install only production dependencies first (leverage build cache)
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev || npm install --omit=dev
# Copy application files
COPY public ./public
COPY server.js ./server.js
# App runs on 3002 by default; allow override via PORT
ENV NODE_ENV=production \
PORT=3002
EXPOSE 3002
# Start the server
CMD ["node", "server.js"]

66
README.md Normal file
View File

@@ -0,0 +1,66 @@
# SPORE Website
Technical documentation and showcase for the SPORE ecosystem - a distributed embedded system framework for ESP8266 microcontrollers.
## Features
- Clean, technical presentation of the SPORE ecosystem
- Showcases SPORE Core, SPORE UI, and SPORE LEDLab
- Screenshots and visual demonstrations
- Code examples and technical specifications
- Built with vanilla JavaScript and CSS
## Getting Started
### Prerequisites
- Node.js (v14 or higher)
- npm (v6 or higher)
### Installation
```bash
npm install
```
### Run
```bash
npm start
```
The website will be available at `http://localhost:3002`
### Docker
Build and run with Docker:
```bash
# Build image
docker build -t spore-website .
# Run (maps container port 3002 to host 3002)
docker run --rm -p 3002:3002 spore-website
# Override port (optional)
docker run --rm -e PORT=8080 -p 8080:8080 spore-website
```
## Technology Stack
- **Backend:** Express.js
- **Frontend:** Vanilla JavaScript, CSS3, HTML5
- **Design:** Clean, dark theme with technical focus
## Structure
```
spore-website/
├── server.js # Express.js server
├── public/ # Static files
│ ├── index.html # Main HTML page
│ ├── styles.css # Stylesheet
│ ├── script.js # Minimal JavaScript
│ └── assets/ # Screenshots and images
└── package.json # Dependencies
```

842
package-lock.json generated Normal file
View File

@@ -0,0 +1,842 @@
{
"name": "spore-website",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "spore-website",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"express": "^5.1.0"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.0",
"http-errors": "^2.0.0",
"iconv-lite": "^0.6.3",
"on-finished": "^2.4.1",
"qs": "^6.14.0",
"raw-body": "^3.0.0",
"type-is": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.0",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"vary": "^1.1.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/http-errors/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.7.0",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
"is-promise": "^4.0.0",
"parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.5",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"mime-types": "^3.0.1",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}
}

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "spore-website",
"version": "1.0.0",
"description": "This is the public website for the spore ecosystem.",
"main": "index.js",
"scripts": {
"start": "node server.js",
"dev": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^5.1.0"
}
}

BIN
public/assets/cluster.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

BIN
public/assets/editor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
public/assets/firmware.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

BIN
public/assets/ledlab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

BIN
public/assets/spore-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

BIN
public/assets/spore-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
public/assets/spore.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
public/assets/topology.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

358
public/index.html Normal file
View 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 &lt;Arduino.h&gt;
#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
View 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
View 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);
}

14
server.js Normal file
View File

@@ -0,0 +1,14 @@
const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3002;
// Serve static files from the public directory
app.use(express.static(path.join(__dirname, 'public')));
// Start server
app.listen(PORT, () => {
console.log(`SPORE website running at http://localhost:${PORT}`);
});