* Introduced BevyScene React component in landing-component for rendering a 3D cockpit visualization.

* Included WebAssembly asset `yachtpit.js` for cockpit functionality.
* Added Bevy MIT license file.
* Implemented a service worker to cache assets locally instead of fetching them remotely.
* Added collapsible functionality to **Tweakbox** and included the `@chakra-ui/icons` dependency.
* Applied the `hidden` prop to the Tweakbox Heading for better accessibility.
* Refactored **Particles** component for improved performance, clarity, and maintainability.

  * Introduced helper functions for particle creation and count management.
  * Added responsive resizing with particle repositioning.
  * Optimized animation updates, including velocity adjustments for speed changes.
  * Ensured canvas size and particle state are cleanly managed on component unmount.
This commit is contained in:
geoffsee
2025-06-28 16:55:14 -04:00
parent 57ad9df087
commit c3ea9ba599
21 changed files with 798 additions and 38 deletions

11
.gitignore vendored
View File

@@ -7,11 +7,16 @@
**/.idea/ **/.idea/
**/html/ **/html/
**/.env **/.env
packages/client/public/static/fonts/*
**/secrets.json **/secrets.json
**/.dev.vars **/.dev.vars
packages/client/public/sitemap.xml
packages/client/public/robots.txt
wrangler.dev.jsonc wrangler.dev.jsonc
/packages/client/public/static/fonts/ /packages/client/public/static/fonts/
/packages/client/public/robots.txt
/packages/client/public/sitemap.xml
/packages/client/public/assets/textures/bevy.png
/packages/client/public/assets/audio/flying.ogg
/packages/client/public/assets/textures/github.png
/packages/client/public/yachtpit.html
/packages/client/public/yachtpit.js
/packages/client/public/yachtpit_bg.wasm

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "crates/yachtpit"]
path = crates/yachtpit
url = https://github.com/seemueller-io/yachtpit.git

View File

@@ -2,6 +2,9 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"workspaces": { "workspaces": {
"": { "": {
"dependencies": {
"@chakra-ui/icons": "^2.2.4",
},
"devDependencies": { "devDependencies": {
"@types/bun": "^1.2.17", "@types/bun": "^1.2.17",
"@typescript-eslint/eslint-plugin": "^8.35.0", "@typescript-eslint/eslint-plugin": "^8.35.0",
@@ -32,6 +35,7 @@
"packages/client": { "packages/client": {
"name": "@open-gsio/client", "name": "@open-gsio/client",
"devDependencies": { "devDependencies": {
"@chakra-ui/icons": "^2.2.4",
"@chakra-ui/react": "^2.10.6", "@chakra-ui/react": "^2.10.6",
"@cloudflare/workers-types": "^4.20241205.0", "@cloudflare/workers-types": "^4.20241205.0",
"@emotion/react": "^11.13.5", "@emotion/react": "^11.13.5",
@@ -61,7 +65,6 @@
"mobx": "^6.13.5", "mobx": "^6.13.5",
"mobx-react-lite": "^4.0.7", "mobx-react-lite": "^4.0.7",
"mobx-state-tree": "^6.0.1", "mobx-state-tree": "^6.0.1",
"moo": "^0.5.2",
"qrcode.react": "^4.1.0", "qrcode.react": "^4.1.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@@ -69,10 +72,11 @@
"react-streaming": "^0.3.44", "react-streaming": "^0.3.44",
"react-textarea-autosize": "^8.5.5", "react-textarea-autosize": "^8.5.5",
"shiki": "^1.24.0", "shiki": "^1.24.0",
"tslog": "^4.9.3",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vike": "^0.4.235", "vike": "^0.4.235",
"vite": "^7.0.0", "vite": "^7.0.0",
"vite-plugin-pwa": "^1.0.0", "vite-plugin-pwa": "^1.0.1",
"vitest": "^3.1.4", "vitest": "^3.1.4",
}, },
}, },
@@ -400,6 +404,8 @@
"@chakra-ui/hooks": ["@chakra-ui/hooks@2.4.4", "", { "dependencies": { "@chakra-ui/utils": "2.2.4", "@zag-js/element-size": "0.31.1", "copy-to-clipboard": "3.3.3", "framesync": "6.1.2" }, "peerDependencies": { "react": ">=18" } }, "sha512-+gMwLIkabtddIL/GICU7JmnYtvfONP+fNiTfdYLV9/I1eyCz8igKgLmFJOGM6F+BpUev6hh+/+DX5ezGQ9VTbQ=="], "@chakra-ui/hooks": ["@chakra-ui/hooks@2.4.4", "", { "dependencies": { "@chakra-ui/utils": "2.2.4", "@zag-js/element-size": "0.31.1", "copy-to-clipboard": "3.3.3", "framesync": "6.1.2" }, "peerDependencies": { "react": ">=18" } }, "sha512-+gMwLIkabtddIL/GICU7JmnYtvfONP+fNiTfdYLV9/I1eyCz8igKgLmFJOGM6F+BpUev6hh+/+DX5ezGQ9VTbQ=="],
"@chakra-ui/icons": ["@chakra-ui/icons@2.2.4", "", { "peerDependencies": { "@chakra-ui/react": ">=2.0.0", "react": ">=18" } }, "sha512-l5QdBgwrAg3Sc2BRqtNkJpfuLw/pWRDwwT58J6c4PqQT6wzXxyNa8Q0PForu1ltB5qEiFb1kxr/F/HO1EwNa6g=="],
"@chakra-ui/react": ["@chakra-ui/react@2.10.7", "", { "dependencies": { "@chakra-ui/hooks": "2.4.4", "@chakra-ui/styled-system": "2.12.2", "@chakra-ui/theme": "3.4.8", "@chakra-ui/utils": "2.2.4", "@popperjs/core": "^2.11.8", "@zag-js/focus-visible": "^0.31.1", "aria-hidden": "^1.2.3", "react-fast-compare": "3.2.2", "react-focus-lock": "^2.9.6", "react-remove-scroll": "^2.5.7" }, "peerDependencies": { "@emotion/react": ">=11", "@emotion/styled": ">=11", "framer-motion": ">=4.0.0", "react": ">=18", "react-dom": ">=18" } }, "sha512-GX1dCmnvrxxyZEofDX9GMAtRakZJKnUqFM9k8qhaycPaeyfkiTNNTjhPNX917hgVx1yhC3kcJOs5IeC7yW56/g=="], "@chakra-ui/react": ["@chakra-ui/react@2.10.7", "", { "dependencies": { "@chakra-ui/hooks": "2.4.4", "@chakra-ui/styled-system": "2.12.2", "@chakra-ui/theme": "3.4.8", "@chakra-ui/utils": "2.2.4", "@popperjs/core": "^2.11.8", "@zag-js/focus-visible": "^0.31.1", "aria-hidden": "^1.2.3", "react-fast-compare": "3.2.2", "react-focus-lock": "^2.9.6", "react-remove-scroll": "^2.5.7" }, "peerDependencies": { "@emotion/react": ">=11", "@emotion/styled": ">=11", "framer-motion": ">=4.0.0", "react": ">=18", "react-dom": ">=18" } }, "sha512-GX1dCmnvrxxyZEofDX9GMAtRakZJKnUqFM9k8qhaycPaeyfkiTNNTjhPNX917hgVx1yhC3kcJOs5IeC7yW56/g=="],
"@chakra-ui/styled-system": ["@chakra-ui/styled-system@2.12.2", "", { "dependencies": { "@chakra-ui/utils": "2.2.4", "csstype": "^3.1.2" } }, "sha512-BlQ7i3+GYC0S0c72B+paa0sYo+QeNSMfz6fwQRFsc8A5Aax9i9lSdRL+vwJVC+k6r/0HWfRwk016R2RD2ihEwQ=="], "@chakra-ui/styled-system": ["@chakra-ui/styled-system@2.12.2", "", { "dependencies": { "@chakra-ui/utils": "2.2.4", "csstype": "^3.1.2" } }, "sha512-BlQ7i3+GYC0S0c72B+paa0sYo+QeNSMfz6fwQRFsc8A5Aax9i9lSdRL+vwJVC+k6r/0HWfRwk016R2RD2ihEwQ=="],
@@ -1768,6 +1774,8 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tslog": ["tslog@4.9.3", "", {}, "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
@@ -1844,7 +1852,7 @@
"vite-node": ["vite-node@3.1.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA=="], "vite-node": ["vite-node@3.1.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA=="],
"vite-plugin-pwa": ["vite-plugin-pwa@1.0.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA=="], "vite-plugin-pwa": ["vite-plugin-pwa@1.0.1", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-STyUomQbydj7vGamtgQYIJI0YsUZ3T4pJLGBQDQPhzMse6aGSncmEN21OV35PrFsmCvmtiH+Nu1JS1ke4RqBjQ=="],
"vitest": ["vitest@3.1.4", "", { "dependencies": { "@vitest/expect": "3.1.4", "@vitest/mocker": "3.1.4", "@vitest/pretty-format": "^3.1.4", "@vitest/runner": "3.1.4", "@vitest/snapshot": "3.1.4", "@vitest/spy": "3.1.4", "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.13", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.1.4", "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ=="], "vitest": ["vitest@3.1.4", "", { "dependencies": { "@vitest/expect": "3.1.4", "@vitest/mocker": "3.1.4", "@vitest/pretty-format": "^3.1.4", "@vitest/runner": "3.1.4", "@vitest/snapshot": "3.1.4", "@vitest/spy": "3.1.4", "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.13", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.1.4", "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ=="],

1
crates/yachtpit Submodule

Submodule crates/yachtpit added at 66b8a855b5

View File

@@ -10,6 +10,7 @@
], ],
"scripts": { "scripts": {
"clean": "packages/scripts/cleanup.sh", "clean": "packages/scripts/cleanup.sh",
"restore:submodules": "rm -rf crates/yachtpit && (git rm --cached crates/yachtpit) && git submodule add --force https://github.com/seemueller-io/yachtpit.git crates/yachtpit ",
"test:all": "bun run --filter='*' tests", "test:all": "bun run --filter='*' tests",
"client:dev": "(cd packages/client && bun run dev)", "client:dev": "(cd packages/client && bun run dev)",
"server:dev": "bun build:client && (cd packages/server && bun run dev)", "server:dev": "bun build:client && (cd packages/server && bun run dev)",
@@ -41,5 +42,8 @@
"peerDependencies": { "peerDependencies": {
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"dependencies": {
"@chakra-ui/icons": "^2.2.4"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }

View File

@@ -8,7 +8,8 @@
"tests:coverage": "vitest run --coverage.enabled=true", "tests:coverage": "vitest run --coverage.enabled=true",
"generate:sitemap": "bun ./scripts/generate_sitemap.js open-gsio.seemueller.workers.dev", "generate:sitemap": "bun ./scripts/generate_sitemap.js open-gsio.seemueller.workers.dev",
"generate:robotstxt": "bun ./scripts/generate_robots_txt.js open-gsio.seemueller.workers.dev", "generate:robotstxt": "bun ./scripts/generate_robots_txt.js open-gsio.seemueller.workers.dev",
"generate:fonts": "cp -r ../../node_modules/katex/dist/fonts public/static" "generate:fonts": "cp -r ../../node_modules/katex/dist/fonts public/static",
"generate:bevy:bundle": "bun scripts/generate-bevy-bundle.js"
}, },
"exports": { "exports": {
"./server/index.ts": { "./server/index.ts": {
@@ -17,19 +18,22 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@open-gsio/env": "workspace:*", "@chakra-ui/icons": "^2.2.4",
"@open-gsio/scripts": "workspace:*",
"@chakra-ui/react": "^2.10.6", "@chakra-ui/react": "^2.10.6",
"@cloudflare/workers-types": "^4.20241205.0", "@cloudflare/workers-types": "^4.20241205.0",
"@emotion/react": "^11.13.5", "@emotion/react": "^11.13.5",
"@emotion/styled": "^11.13.5", "@emotion/styled": "^11.13.5",
"@open-gsio/env": "workspace:*",
"@open-gsio/scripts": "workspace:*",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"@types/bun": "^1.2.17",
"@types/marked": "^6.0.0", "@types/marked": "^6.0.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.1.4", "@vitest/coverage-v8": "^3.1.4",
"@vitest/ui": "^3.1.4", "@vitest/ui": "^3.1.4",
"bun": "^1.2.17",
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"framer-motion": "^11.13.1", "framer-motion": "^11.13.1",
"isomorphic-dompurify": "^2.19.0", "isomorphic-dompurify": "^2.19.0",
@@ -44,7 +48,6 @@
"mobx": "^6.13.5", "mobx": "^6.13.5",
"mobx-react-lite": "^4.0.7", "mobx-react-lite": "^4.0.7",
"mobx-state-tree": "^6.0.1", "mobx-state-tree": "^6.0.1",
"moo": "^0.5.2",
"qrcode.react": "^4.1.0", "qrcode.react": "^4.1.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@@ -52,12 +55,11 @@
"react-streaming": "^0.3.44", "react-streaming": "^0.3.44",
"react-textarea-autosize": "^8.5.5", "react-textarea-autosize": "^8.5.5",
"shiki": "^1.24.0", "shiki": "^1.24.0",
"tslog": "^4.9.3",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vike": "^0.4.235", "vike": "^0.4.235",
"vite": "^7.0.0", "vite": "^7.0.0",
"vite-plugin-pwa": "^1.0.0", "vite-plugin-pwa": "^1.0.1",
"vitest": "^3.1.4", "vitest": "^3.1.4"
"bun": "^1.2.17",
"@types/bun": "^1.2.17"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

@@ -0,0 +1,164 @@
import { execSync } from 'node:child_process';
import {
existsSync,
readdirSync,
readFileSync,
writeFileSync,
renameSync,
rmSync,
cpSync,
} from 'node:fs';
import { resolve, dirname, join, basename } from 'node:path';
import { Logger } from 'tslog';
const logger = new Logger({
stdio: 'inherit',
prettyLogTimeZone: 'local',
type: 'pretty',
stylePrettyLogs: true,
prefix: ['\n'],
overwrite: true,
});
function main() {
bundleCrate();
cleanup();
logger.info('🎉 yachtpit built successfully');
}
const getRepoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
const repoRoot = resolve(getRepoRoot);
const publicDir = resolve(repoRoot, 'packages/client/public');
const indexHtml = resolve(publicDir, 'index.html');
function bundleCrate() {
// ───────────── Build yachtpit project ───────────────────────────────────
logger.info('🔨 Building yachtpit...');
logger.info(`📁 Repository root: ${repoRoot}`);
// Check if submodules need to be initialized
const yachtpitPath = resolve(repoRoot, 'crates/yachtpit');
logger.info(`📁 Yachtpit path: ${yachtpitPath}`);
if (!existsSync(yachtpitPath)) {
logger.info('📦 Initializing submodules...');
execSync('git submodule update --init --remote', { stdio: 'inherit' });
} else {
logger.info(`✅ Submodules already initialized at: ${yachtpitPath}`);
}
// Build the yachtpit project
const buildCwd = resolve(repoRoot, 'crates/yachtpit');
logger.info(`🔨 Building in directory: ${buildCwd}`);
try {
execSync('trunk build --release', {
cwd: buildCwd,
});
logger.info('✅ Yachtpit built');
} catch (error) {
console.error('❌ Failed to build yachtpit:', error.message);
process.exit(1);
}
// ───────────── Copy assets to public directory ──────────────────────────
const yachtpitDistDir = join(yachtpitPath, 'dist');
logger.info(`📋 Copying assets to public directory...`);
// Remove existing yachtpit assets from public directory
const skipRemoveOldAssets = false;
if (!skipRemoveOldAssets) {
const existingAssets = readdirSync(publicDir).filter(
file => file.startsWith('yachtpit') && (file.endsWith('.js') || file.endsWith('.wasm')),
);
existingAssets.forEach(asset => {
const assetPath = join(publicDir, asset);
rmSync(assetPath, { force: true });
logger.info(`🗑️ Removed old asset: ${assetPath}`);
});
} else {
logger.warn('SKIPPING REMOVING OLD ASSETS');
}
// Copy new assets from yachtpit/dist to public directory
if (existsSync(yachtpitDistDir)) {
logger.info(`📍Located yachtpit build: ${yachtpitDistDir}`);
try {
cpSync(yachtpitDistDir, publicDir, {
recursive: true,
force: true,
});
logger.info(`✅ Assets copied from ${yachtpitDistDir} to ${publicDir}`);
} catch (error) {
console.error('❌ Failed to copy assets:', error.message);
process.exit(1);
}
} else {
console.error(`❌ Yachtpit dist directory not found at: ${yachtpitDistDir}`);
process.exit(1);
}
// ───────────── locate targets ───────────────────────────────────────────
const dstPath = join(publicDir, 'yachtpit.html');
// Regexes for the hashed filenames produced by most bundlers
const JS_RE = /^yachtpit-[\da-f]{16}\.js$/i;
const WASM_RE = /^yachtpit-[\da-f]{16}_bg\.wasm$/i;
// Always perform renaming of bundle files
const files = readdirSync(publicDir);
// helper that doesn't explode if the target file is already present
const safeRename = (from, to) => {
if (!existsSync(from)) return;
if (existsSync(to)) {
logger.info(` ${to} already exists removing and replacing.`);
rmSync(to, { force: true });
}
renameSync(from, to);
logger.info(`📝 Renamed: ${basename(from)}${basename(to)}`);
};
files.forEach(f => {
const fullPath = join(publicDir, f);
if (JS_RE.test(f)) safeRename(fullPath, join(publicDir, 'yachtpit.js'));
if (WASM_RE.test(f)) safeRename(fullPath, join(publicDir, 'yachtpit_bg.wasm'));
});
// ───────────── patch markup inside HTML ─────────────────────────────────
if (existsSync(indexHtml)) {
logger.info(`📝 Patching HTML file: ${indexHtml}`);
let html = readFileSync(indexHtml, 'utf8');
html = html
.replace(/yachtpit-[\da-f]{16}\.js/gi, 'yachtpit.js')
.replace(/yachtpit-[\da-f]{16}_bg\.wasm/gi, 'yachtpit_bg.wasm');
writeFileSync(indexHtml, html, 'utf8');
// ───────────── rename HTML entrypoint ─────────────────────────────────
if (basename(indexHtml) !== 'yachtpit.html') {
logger.info(`📝 Renaming HTML file: ${indexHtml}${dstPath}`);
// Remove existing yachtpit.html if it exists
if (existsSync(dstPath)) {
rmSync(dstPath, { force: true });
}
renameSync(indexHtml, dstPath);
}
} else {
logger.info(`⚠️ ${indexHtml} not found skipping HTML processing.`);
}
}
function cleanup() {
logger.info('Running cleanup...');
rmSync(indexHtml, { force: true });
const creditsDir = resolve(`${repoRoot}/packages/client/public`, 'credits');
rmSync(creditsDir, { force: true, recursive: true });
}
main();

View File

@@ -0,0 +1,45 @@
import { Box } from '@chakra-ui/react';
import { useEffect } from 'react';
export interface BevySceneProps {
speed?: number; // transition seconds
intensity?: number;
glow?: boolean;
}
export const BevyScene: React.FC<BevySceneProps> = ({ speed = 1, intensity = 1, glow = false }) => {
useEffect(() => {
const script = document.createElement('script');
script.src = '/yachtpit.js';
script.type = 'module';
document.body.appendChild(script);
script.onload = loaded => {
console.log('loaded', loaded);
};
}, []);
return (
<Box
pos="absolute"
inset={0}
zIndex={0}
pointerEvents="none"
opacity={Math.min(Math.max(intensity, 0), 1)}
filter={glow ? 'blur(1px)' : 'none'}
transition={`opacity ${speed}s ease-in-out`}
>
<script type="module"></script>
<canvas id="yachtpit-canvas" width="1280" height="720"></canvas>
{/*<iframe*/}
{/* src="/yachtpit.html"*/}
{/* style={{*/}
{/* width: '100%',*/}
{/* height: '100%',*/}
{/* border: 'none',*/}
{/* backgroundColor: 'transparent',*/}
{/* }}*/}
{/* title="Bevy Scene"*/}
{/*/>*/}
</Box>
);
};

View File

@@ -0,0 +1,102 @@
import { Box } from '@chakra-ui/react';
import React, { useState } from 'react';
import { BevyScene } from './BevyScene.tsx';
import { MatrixRain } from './MatrixRain.tsx';
import Particles from './Particles.tsx';
import Tweakbox from './Tweakbox.tsx';
export const LandingComponent: React.FC = () => {
const [speed, setSpeed] = useState(0.2);
const [intensity, setIntensity] = useState(0.5);
const [particles, setParticles] = useState(false);
const [glow, setGlow] = useState(false);
const [matrixRain, setMatrixRain] = useState(false);
const [bevyScene, setBevyScene] = useState(true);
return (
<Box
as="section"
bg="background.primary"
w="100%"
h="100vh"
overflow="hidden"
position="relative"
>
<Box
position="fixed"
bottom="24px"
right="24px"
maxWidth="300px"
minWidth="200px"
zIndex={1000}
>
<Tweakbox
sliders={{
speed: {
value: speed,
onChange: setSpeed,
label: 'Animation Speed',
min: 0.01,
max: 0.99,
step: 0.01,
ariaLabel: 'animation-speed',
},
intensity: {
value: intensity,
onChange: setIntensity,
label: 'Effect Intensity',
min: 0.01,
max: 0.99,
step: 0.01,
ariaLabel: 'effect-intensity',
},
}}
switches={{
particles: {
value: particles,
onChange(enabled) {
if (enabled) {
setMatrixRain(!enabled);
setBevyScene(!enabled);
}
setParticles(enabled);
},
label: 'Particles',
},
matrixRain: {
value: matrixRain,
onChange(enabled) {
if (enabled) {
setParticles(!enabled);
setBevyScene(!enabled);
}
setMatrixRain(enabled);
},
label: 'Matrix Rain',
},
bevyScene: {
value: bevyScene,
onChange(enabled) {
if (enabled) {
setParticles(!enabled);
setMatrixRain(!enabled);
}
setBevyScene(enabled);
},
label: 'Bevy Scene',
},
glow: {
value: glow,
onChange: setGlow,
label: 'Glow Effect',
},
}}
/>
</Box>
{!particles && !matrixRain && <BevyScene speed={speed} intensity={intensity} glow={glow} />}
{!particles && matrixRain && <MatrixRain speed={speed} intensity={intensity} glow={glow} />}
{particles && <Particles particles glow speed={speed} intensity={intensity} />}
</Box>
);
};

View File

@@ -0,0 +1,117 @@
import { useBreakpointValue, useTheme } from '@chakra-ui/react';
import React, { useEffect, useRef, useMemo } from 'react';
const MATRIX_CHARS =
'アイウエオカキクケコサシスセソタチツテトナニヌネハヒフヘホ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
interface MatrixRainProps {
speed?: number;
glow?: boolean;
intensity?: number;
}
export const MatrixRain: React.FC<MatrixRainProps> = ({
speed = 1,
glow = false,
intensity = 1,
}) => {
const fontSize = useBreakpointValue({ base: 14, md: 18, lg: 22 }) ?? 14;
const theme = useTheme();
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animationRef = useRef<number | null>(null);
const dropsRef = useRef<number[]>([]);
const columnsRef = useRef<number>(0);
const colors = useMemo(
() => ({
background: theme.colors.background.primary,
textAccent: theme.colors.text.accent,
}),
[theme.colors.background.primary, theme.colors.text.accent],
);
const colorsRef = useRef(colors);
colorsRef.current = colors;
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const newColumns = Math.floor(canvas.width / fontSize);
if (newColumns !== columnsRef.current) {
columnsRef.current = newColumns;
const newDrops: number[] = [];
for (let i = 0; i < newColumns; i++) {
if (i < dropsRef.current.length) {
newDrops[i] = dropsRef.current[i];
} else {
newDrops[i] = Math.random() * (canvas.height / fontSize);
}
}
dropsRef.current = newDrops;
}
};
resize();
window.addEventListener('resize', resize);
if (dropsRef.current.length === 0) {
const columns = Math.floor(canvas.width / fontSize);
columnsRef.current = columns;
for (let i = 0; i < columns; i++) {
dropsRef.current[i] = Math.random() * (canvas.height / fontSize);
}
}
const draw = () => {
if (!ctx || !canvas) return;
const currentColors = colorsRef.current;
ctx.fillStyle = currentColors.background;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = `${fontSize}px monospace`;
for (let i = 0; i < dropsRef.current.length; i++) {
const text = MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)];
const x = i * fontSize;
const y = dropsRef.current[i] * fontSize;
ctx.fillStyle = currentColors.textAccent;
if (glow) {
ctx.shadowBlur = 10;
ctx.shadowColor = currentColors.textAccent;
}
ctx.fillText(text, x, y);
if (y > canvas.height) {
dropsRef.current[i] = -Math.random() * 5;
} else {
dropsRef.current[i] += (0.1 + Math.random() * 0.5) * speed * intensity;
}
}
animationRef.current = requestAnimationFrame(draw);
};
animationRef.current = requestAnimationFrame(draw);
return () => {
window.removeEventListener('resize', resize);
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [fontSize, speed, glow, intensity]);
return <canvas ref={canvasRef} style={{ display: 'block' }} />;
};

View File

@@ -0,0 +1,161 @@
import { Box, useTheme } from '@chakra-ui/react';
import React, { useEffect, useRef } from 'react';
interface ParticlesProps {
speed: number;
intensity: number;
particles: boolean;
glow: boolean;
}
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
size: number;
}
const Particles: React.FC<ParticlesProps> = ({ speed, intensity, particles, glow }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const particlesRef = useRef<Particle[]>([]);
const animationFrameRef = useRef<number | undefined>(undefined);
const theme = useTheme();
// Helper function to create a single particle with proper canvas dimensions
const createParticle = (canvas: HTMLCanvasElement): Particle => ({
x: Math.random() * canvas.parentElement!.getBoundingClientRect().width,
y: Math.random() * canvas.parentElement!.getBoundingClientRect().height,
vx: (Math.random() - 0.5) * speed,
vy: (Math.random() - 0.5) * speed,
size: Math.random() * 3 + 1,
});
// Main animation effect
useEffect(() => {
if (!particles) {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
}
particlesRef.current = []; // Clear particles when disabled
return;
}
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Reposition existing particles that are outside new bounds
particlesRef.current.forEach(particle => {
if (particle.x > canvas.width) particle.x = Math.random() * canvas.width;
if (particle.y > canvas.height) particle.y = Math.random() * canvas.height;
});
};
const ensureParticleCount = () => {
const targetCount = Math.floor(intensity * 100);
const currentCount = particlesRef.current.length;
if (currentCount < targetCount) {
// Add new particles
const newParticles = Array.from({ length: targetCount - currentCount }, () =>
createParticle(canvas),
);
particlesRef.current = [...particlesRef.current, ...newParticles];
} else if (currentCount > targetCount) {
// Remove excess particles
particlesRef.current = particlesRef.current.slice(0, targetCount);
}
};
const updateParticles = () => {
particlesRef.current.forEach(particle => {
particle.x += particle.vx;
particle.y += particle.vy;
if (particle.x < 0) particle.x = canvas.width;
if (particle.x > canvas.width) particle.x = 0;
if (particle.y < 0) particle.y = canvas.height;
if (particle.y > canvas.height) particle.y = 0;
});
};
const drawParticles = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = theme.colors.text.accent;
ctx.globalCompositeOperation = 'lighter';
if (glow) {
ctx.shadowBlur = 10;
ctx.shadowColor = 'white';
} else {
ctx.shadowBlur = 0;
}
particlesRef.current.forEach(particle => {
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fill();
});
};
const animate = () => {
updateParticles();
drawParticles();
animationFrameRef.current = requestAnimationFrame(animate);
};
const handleResize = () => {
resizeCanvas();
};
window.addEventListener('resize', handleResize);
resizeCanvas(); // Set canvas size first
ensureParticleCount(); // Then create particles with proper dimensions
animate();
return () => {
window.removeEventListener('resize', handleResize);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
}
};
}, [particles, intensity, speed, glow, theme.colors.text.accent]);
// Separate effect for speed changes - update existing particle velocities
useEffect(() => {
if (!particles) return;
particlesRef.current.forEach(particle => {
const currentSpeed = Math.sqrt(particle.vx * particle.vx + particle.vy * particle.vy);
if (currentSpeed > 0) {
const normalizedVx = particle.vx / currentSpeed;
const normalizedVy = particle.vy / currentSpeed;
particle.vx = normalizedVx * speed;
particle.vy = normalizedVy * speed;
} else {
particle.vx = (Math.random() - 0.5) * speed;
particle.vy = (Math.random() - 0.5) * speed;
}
});
}, [speed, particles]);
return (
<Box zIndex={0} pointerEvents={'none'}>
<canvas
ref={canvasRef}
style={{ display: particles ? 'block' : 'none', pointerEvents: 'none' }}
/>
</Box>
);
};
export default Particles;

View File

@@ -0,0 +1,111 @@
import {
Box,
Grid,
GridItem,
Heading,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
Text,
Switch,
Collapse,
IconButton,
} from '@chakra-ui/react';
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import React, { useState } from 'react';
interface SliderControl {
value: number;
onChange: (value: number) => void;
label: string;
min: number;
max: number;
step: number;
ariaLabel: string;
}
interface SwitchControl {
value: boolean;
onChange: (enabled: boolean) => void;
label: string;
exclusive?: boolean;
}
interface TweakboxProps {
sliders: {
speed: SliderControl;
intensity: SliderControl;
};
switches: {
particles: SwitchControl;
glow: SwitchControl;
} & Record<string, SwitchControl>;
}
const Tweakbox = observer(({ sliders, switches }: TweakboxProps) => {
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<Box display="flex" alignItems="flex-start">
<IconButton
aria-label="Toggle controls"
borderRadius="lg"
bg="whiteAlpha.300"
backdropFilter="blur(10px)"
boxShadow="xl"
icon={isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
onClick={() => setIsCollapsed(!isCollapsed)}
size="sm"
marginRight={2}
/>
<Collapse in={!isCollapsed} style={{ width: '100%' }}>
<Box p={4} borderRadius="lg" bg="whiteAlpha.100" backdropFilter="blur(10px)" boxShadow="xl">
<Grid templateColumns="1fr" gap={4}>
<GridItem>
<Heading hidden={true} size="sm" mb={4} color="text.accent">
Controls
</Heading>
</GridItem>
{Object.keys(switches).map(key => {
return (
<GridItem key={key}>
<Text mb={2} color="text.accent">
{switches[key].label}
</Text>
<Switch
isChecked={switches[key].value}
onChange={e => switches[key].onChange(e.target.checked)}
/>
</GridItem>
);
})}
{Object.entries(sliders).map(([key, slider]) => (
<GridItem key={key}>
<Text mb={2} color="text.accent">
{slider.label}
</Text>
<Slider
aria-label={slider.ariaLabel}
value={slider.value}
min={slider.min}
step={slider.step}
max={slider.max}
onChange={slider.onChange}
>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb />
</Slider>
</GridItem>
))}
</Grid>
</Box>
</Collapse>
</Box>
);
});
export default Tweakbox;

View File

@@ -17,9 +17,9 @@ export default function Hero() {
minWidth="90px" minWidth="90px"
maxWidth={'220px'} maxWidth={'220px'}
color="text.accent" color="text.accent"
as="h3" // as="h3"
letterSpacing={'tight'} letterSpacing={'tight'}
size="lg" size="xl"
> >
{Routes[normalizePath(pageContext.urlPathname)]?.heroLabel} {Routes[normalizePath(pageContext.urlPathname)]?.heroLabel}
</Heading> </Heading>

View File

@@ -1,4 +1,4 @@
import { Box, Collapse, Grid, GridItem, useBreakpointValue } from '@chakra-ui/react'; import { Box, Collapse, Grid, GridItem, useBreakpointValue, useTheme } from '@chakra-ui/react';
import { MenuIcon } from 'lucide-react'; import { MenuIcon } from 'lucide-react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
@@ -18,6 +18,8 @@ const Navigation = observer(({ children, routeRegistry }) => {
const currentPath = pageContext.urlPathname || '/'; const currentPath = pageContext.urlPathname || '/';
const theme = useTheme();
const getTopValue = () => { const getTopValue = () => {
if (!isMobile) return undefined; if (!isMobile) return undefined;
if (currentPath === '/') return 12; if (currentPath === '/') return 12;
@@ -53,9 +55,10 @@ const Navigation = observer(({ children, routeRegistry }) => {
<GridItem> <GridItem>
<MenuIcon <MenuIcon
cursor="pointer" cursor="pointer"
color="text.accent"
w={6} w={6}
h={6} h={6}
stroke={getTheme(userOptionsStore.theme).colors.text.accent} stroke={theme.colors.text.accent}
onClick={() => { onClick={() => {
switch (menuState.isOpen) { switch (menuState.isOpen) {
case true: case true:

View File

@@ -15,8 +15,8 @@ export default {
}, },
background: { background: {
primary: 'linear-gradient(360deg, #15171C 100%, #353A47 100%)', // primary: 'linear-gradient(360deg, #15171C 100%, #353A47 100%)',
primary: '#15171C',
secondary: '#1B1F26', secondary: '#1B1F26',
tertiary: '#1E1E2E', tertiary: '#1E1E2E',
}, },

View File

@@ -2,3 +2,20 @@
import UserOptionsStore from '../stores/UserOptionsStore'; import UserOptionsStore from '../stores/UserOptionsStore';
UserOptionsStore.initialize(); UserOptionsStore.initialize();
try {
const isLocal = window.location.hostname.includes('localhost');
if (!isLocal) {
navigator.serviceWorker.register('/service-worker.js');
} else {
(async () => {
await navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.map(r => {
r.unregister();
});
});
})();
}
} catch (e) {
// fail silent
}

View File

@@ -2,6 +2,7 @@ import { Stack } from '@chakra-ui/react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import Chat from '../../components/chat/Chat'; import Chat from '../../components/chat/Chat';
import { LandingComponent } from '../../components/landing-component/LandingComponent.tsx';
import clientChatStore from '../../stores/ClientChatStore'; import clientChatStore from '../../stores/ClientChatStore';
// renders "/" // renders "/"
@@ -18,7 +19,8 @@ export default function IndexPage() {
return ( return (
<Stack direction="column" height="100%" width="100%" spacing={0}> <Stack direction="column" height="100%" width="100%" spacing={0}>
<Chat height="100%" width="100%" /> <LandingComponent />
{/*<Chat height="100%" width="100%" />*/}
</Stack> </Stack>
); );
} }

View File

@@ -17,6 +17,10 @@ const prebuildPlugin = () => ({
console.log('Generated robots.txt -> public/robots.txt'); console.log('Generated robots.txt -> public/robots.txt');
child_process.execSync('bun run generate:fonts'); child_process.execSync('bun run generate:fonts');
console.log('Copied fonts -> public/static/fonts'); console.log('Copied fonts -> public/static/fonts');
child_process.execSync('bun run generate:bevy:bundle', {
stdio: 'inherit',
});
console.log('Bundled bevy app -> public/yachtpit.html');
} }
}, },
}); });
@@ -31,6 +35,26 @@ export default defineConfig(({ command }) => {
prerender: true, prerender: true,
disableAutoFullBuild: false, disableAutoFullBuild: false,
}), }),
VitePWA({
registerType: 'autoUpdate',
injectRegister: null,
minify: true,
disable: false,
filename: 'service-worker.js',
devOptions: {
enabled: false,
},
manifest: {
name: 'open-gsio',
short_name: 'open-gsio',
description: 'Assistant',
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,wasm}'],
navigateFallbackDenylist: [/^\/api\//],
maximumFileSizeToCacheInBytes: 25000000,
},
}),
// PWA plugin saves money on data transfer by caching assets on the client // PWA plugin saves money on data transfer by caching assets on the client
/* /*
For safari, use this script in the console to unregister the service worker. For safari, use this script in the console to unregister the service worker.
@@ -41,21 +65,6 @@ export default defineConfig(({ command }) => {
}) })
}) })
*/ */
// VitePWA({
// registerType: 'autoUpdate',
// devOptions: {
// enabled: false,
// },
// manifest: {
// name: "open-gsio",
// short_name: "open-gsio",
// description: "Assistant"
// },
// workbox: {
// globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
// navigateFallbackDenylist: [/^\/api\//],
// }
// })
], ],
server: { server: {
port: 3000, port: 3000,

View File

@@ -20,9 +20,10 @@
{ {
"binding": "KV_STORAGE", "binding": "KV_STORAGE",
// $ npx wrangler kv namespace create open-gsio // $ npx wrangler kv namespace create open-gsio
// $ npx wrangler kv namespace create open-gsio
"id": "placeholderId", "id": "placeholderId",
// $ npx wrangler kv namespace create open-gsio --preview // $ npx wrangler kv namespace create open-gsio --preview
"preview_id": "placeholderIdPreview" "preview_id": "placeholderId"
} }
], ],
"migrations": [ "migrations": [

View File

@@ -15,7 +15,12 @@ find . -name ".wrangler" -type d -prune -exec rm -rf {} \;
# Remove build directories # Remove build directories
find . -name "dist" -type d -prune -exec rm -rf {} \; find . -name "dist" -type d -prune -exec rm -rf {} \;
find . -name "build" -type d -prune -exec rm -rf {} \;
#-----
# crates/yachtpit uses a directory called build for staging assets so it can't be removed
#find . -name "build" -type d -prune -exec rm -rf {} \;
#-----
find . -name "fonts" -type d -prune -exec rm -rf {} \; find . -name "fonts" -type d -prune -exec rm -rf {} \;