13
client/index.html
Normal file
13
client/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Stellar</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
34
client/package.json
Normal file
34
client/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"matter-js": "^0.20.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.2",
|
||||
"vitest": "^3.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
7
client/postcss.config.cjs
Normal file
7
client/postcss.config.cjs
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
|
||||
28
client/src/App.tsx
Normal file
28
client/src/App.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Hud } from "./components/Hud";
|
||||
import { GameShell } from "./game/GameShell";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<main className="min-h-screen bg-ink text-slate-100">
|
||||
<div className="mx-auto flex min-h-screen max-w-7xl flex-col px-4 py-6 sm:px-6 lg:px-8">
|
||||
<header className="mb-6 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[0.3em] text-glow/80">Stellar</p>
|
||||
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Become the gravity well.
|
||||
</h1>
|
||||
</div>
|
||||
<div className="max-w-sm text-right text-sm text-slate-300">
|
||||
M0 scaffold: UI shell, Matter.js mount point, and API-ready project structure.
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid flex-1 gap-6 lg:grid-cols-[1fr_320px]">
|
||||
<GameShell />
|
||||
<Hud />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
11
client/src/components/Hud.test.tsx
Normal file
11
client/src/components/Hud.test.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Hud } from "./Hud";
|
||||
|
||||
describe("Hud", () => {
|
||||
it("renders the boot sequence panel", () => {
|
||||
render(<Hud />);
|
||||
|
||||
expect(screen.getByText("Boot Sequence")).toBeTruthy();
|
||||
expect(screen.getByText("Bootstrap Matter.js world")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
47
client/src/components/Hud.tsx
Normal file
47
client/src/components/Hud.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
const objectives = [
|
||||
"Bootstrap Matter.js world",
|
||||
"Wire player state to the API",
|
||||
"Start M1 absorber loop"
|
||||
];
|
||||
|
||||
export function Hud() {
|
||||
return (
|
||||
<aside className="rounded-3xl border border-white/10 bg-white/5 p-5 shadow-2xl shadow-cyan-950/30 backdrop-blur">
|
||||
<div className="mb-6">
|
||||
<p className="text-xs uppercase tracking-[0.3em] text-glow/70">Mission Board</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-white">Boot Sequence</h2>
|
||||
</div>
|
||||
|
||||
<dl className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="rounded-2xl bg-black/20 p-3">
|
||||
<dt className="text-slate-400">Mass</dt>
|
||||
<dd className="mt-1 text-xl font-semibold">12</dd>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-black/20 p-3">
|
||||
<dt className="text-slate-400">Tier</dt>
|
||||
<dd className="mt-1 text-xl font-semibold">Dustling</dd>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-black/20 p-3">
|
||||
<dt className="text-slate-400">Pull Radius</dt>
|
||||
<dd className="mt-1 text-xl font-semibold">48m</dd>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-black/20 p-3">
|
||||
<dt className="text-slate-400">Mode</dt>
|
||||
<dd className="mt-1 text-xl font-semibold">Sandbox</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="mt-6">
|
||||
<p className="text-xs uppercase tracking-[0.3em] text-amber-300/70">Next Steps</p>
|
||||
<ul className="mt-3 space-y-3 text-sm text-slate-200">
|
||||
{objectives.map((objective) => (
|
||||
<li key={objective} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
|
||||
{objective}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
27
client/src/game/GameShell.tsx
Normal file
27
client/src/game/GameShell.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { initPlaceholderScene } from "./placeholderScene";
|
||||
|
||||
export function GameShell() {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
return initPlaceholderScene(canvas);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden rounded-[2rem] border border-white/10 bg-nebula shadow-2xl shadow-slate-950/40">
|
||||
<canvas ref={canvasRef} className="h-[560px] w-full" />
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 flex items-center justify-between p-4 text-xs uppercase tracking-[0.25em] text-slate-300/70">
|
||||
<span>Simulation viewport</span>
|
||||
<span>Placeholder render</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
69
client/src/game/placeholderScene.ts
Normal file
69
client/src/game/placeholderScene.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export function initPlaceholderScene(canvas: HTMLCanvasElement) {
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
let animationFrame = 0;
|
||||
|
||||
const resize = () => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.floor(rect.width * window.devicePixelRatio);
|
||||
canvas.height = Math.floor(rect.height * window.devicePixelRatio);
|
||||
context.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
|
||||
};
|
||||
|
||||
const render = (time: number) => {
|
||||
const width = canvas.width / window.devicePixelRatio;
|
||||
const height = canvas.height / window.devicePixelRatio;
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
|
||||
context.clearRect(0, 0, width, height);
|
||||
|
||||
const gradient = context.createRadialGradient(cx, cy, 24, cx, cy, Math.max(width, height) * 0.45);
|
||||
gradient.addColorStop(0, "rgba(14, 165, 233, 0.35)");
|
||||
gradient.addColorStop(0.35, "rgba(15, 23, 42, 0.4)");
|
||||
gradient.addColorStop(1, "rgba(2, 6, 23, 1)");
|
||||
|
||||
context.fillStyle = gradient;
|
||||
context.fillRect(0, 0, width, height);
|
||||
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
const orbit = 40 + i * 6;
|
||||
const angle = time * 0.0002 * (1 + i * 0.01) + i;
|
||||
const x = cx + Math.cos(angle) * orbit;
|
||||
const y = cy + Math.sin(angle) * orbit * 0.55;
|
||||
const size = 1 + (i % 3);
|
||||
|
||||
context.fillStyle = i % 8 === 0 ? "#f97316" : "#e2e8f0";
|
||||
context.beginPath();
|
||||
context.arc(x, y, size, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
}
|
||||
|
||||
context.fillStyle = "#020617";
|
||||
context.beginPath();
|
||||
context.arc(cx, cy, 38, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
|
||||
context.strokeStyle = "rgba(125, 211, 252, 0.45)";
|
||||
context.lineWidth = 8;
|
||||
context.beginPath();
|
||||
context.arc(cx, cy, 54 + Math.sin(time * 0.002) * 4, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
|
||||
animationFrame = window.requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
resize();
|
||||
animationFrame = window.requestAnimationFrame(render);
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
window.removeEventListener("resize", resize);
|
||||
};
|
||||
}
|
||||
|
||||
11
client/src/main.tsx
Normal file
11
client/src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles/global.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
26
client/src/styles/global.css
Normal file
26
client/src/styles/global.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(14, 165, 233, 0.2), transparent 35%),
|
||||
linear-gradient(180deg, #020617 0%, #0f172a 100%);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
2
client/src/utils/constants.ts
Normal file
2
client/src/utils/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const GAME_TITLE = "Stellar";
|
||||
|
||||
1
client/src/vitest.setup.ts
Normal file
1
client/src/vitest.setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
15
client/tailwind.config.d.ts
vendored
Normal file
15
client/tailwind.config.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
declare const _default: {
|
||||
content: string[];
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
ink: string;
|
||||
glow: string;
|
||||
ember: string;
|
||||
nebula: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
plugins: any[];
|
||||
};
|
||||
export default _default;
|
||||
14
client/tailwind.config.js
Normal file
14
client/tailwind.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
ink: "#020617",
|
||||
glow: "#7dd3fc",
|
||||
ember: "#f97316",
|
||||
nebula: "#0f172a"
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
17
client/tailwind.config.ts
Normal file
17
client/tailwind.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
ink: "#020617",
|
||||
glow: "#7dd3fc",
|
||||
ember: "#f97316",
|
||||
nebula: "#0f172a"
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
} satisfies Config;
|
||||
|
||||
26
client/tsconfig.json
Normal file
26
client/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
client/tsconfig.node.json
Normal file
10
client/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "tailwind.config.ts", "postcss.config.cjs"]
|
||||
}
|
||||
1
client/tsconfig.node.tsbuildinfo
Normal file
1
client/tsconfig.node.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
1
client/tsconfig.tsbuildinfo
Normal file
1
client/tsconfig.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vitest.setup.ts","./src/components/hud.test.tsx","./src/components/hud.tsx","./src/game/gameshell.tsx","./src/game/placeholderscene.ts","./src/utils/constants.ts"],"version":"5.9.3"}
|
||||
2
client/vite.config.d.ts
vendored
Normal file
2
client/vite.config.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare const _default: import("vite").UserConfig;
|
||||
export default _default;
|
||||
13
client/vite.config.js
Normal file
13
client/vite.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: "./src/vitest.setup.ts"
|
||||
}
|
||||
});
|
||||
14
client/vite.config.ts
Normal file
14
client/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: "./src/vitest.setup.ts"
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user