initial web pages

This commit is contained in:
SowinskiBraeden committed 2026-10-20 22:56:11 -07:00
1 parent 3f43811dbb
commit b209099d7b
17 files changed
+1341

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
const REPO = 'https://git.sowinski.dev/SowinskiBraeden/visual-strategy';
export function footer(): string {
return `
<footer>
<svg class="icon" aria-hidden="true"><use href="/icons.svg#chip" /></svg>
<span>Rust/WebAssembly demo</span>
<span class="dot">&middot;</span>
<a href="${REPO}" target="_blank" rel="noreferrer">
<svg class="icon" aria-hidden="true"><use href="/icons.svg#gitea" /></svg>
Source
</a>
</footer>`;
}
+26
View File
@@ -0,0 +1,26 @@
const links = [
{ path: '/learn', label: 'Learn' },
{ path: '/playground', label: 'Playground' },
];
export function header(path: string): string {
const nav = links
.map(
(l) =>
`<a href="#${l.path}"${path === l.path ? ' aria-current="page"' : ''}>${l.label}</a>`,
)
.join('\n ');
return `
<header>
<a class="brand" href="#/">
<span class="mark">
<svg class="icon" aria-hidden="true"><use href="/icons.svg#flag" /></svg>
</span>
Stint.
</a>
<nav>
${nav}
</nav>
</header>`;
}
View File
Whitespace-only changes.
+26
View File
@@ -0,0 +1,26 @@
import './style.css';
import { header } from './components/header.ts';
import { footer } from './components/footer.ts';
import { home } from './pages/home.ts';
import { learn } from './pages/learn.ts';
import { playground } from './pages/playground.ts';
import { loadSimulation } from './simulation/wasm.ts';
const routes: Record<string, () => string> = {
'/': home,
'/learn': learn,
'/playground': playground,
};
const app = document.querySelector<HTMLDivElement>('#app')!;
function render(): void {
const path = location.hash.slice(1) || '/';
const page = routes[path] ?? home;
app.innerHTML = [header(path), page(), footer()].join('\n');
}
addEventListener('hashchange', render);
render();
await loadSimulation();
+32
View File
@@ -0,0 +1,32 @@
export function home(): string {
return `
<main>
<p class="badge">Interactive race strategy</p>
<h1>Every second counts.</h1>
<p class="lede">
Understand the strategy behind motorsport. Explore tyre performance,
pit stops, and race decisions through interactive simulations.
</p>
<div class="cards">
<a class="card" href="#/learn">
<div class="card-head">
<svg class="icon" aria-hidden="true"><use href="/icons.svg#cap" /></svg>
<span class="tag">Start here</span>
</div>
<h2>Learn</h2>
<p>Discover race strategy step by step with interactive examples.</p>
<span class="btn primary">Start learning &rarr;</span>
</a>
<a class="card" href="#/playground">
<div class="card-head">
<svg class="icon" aria-hidden="true"><use href="/icons.svg#sliders" /></svg>
</div>
<h2>Playground</h2>
<p>Build your own strategies and experiment with the simulation.</p>
<span class="btn">Open playground &rarr;</span>
</a>
</div>
</main>`;
}
+7
View File
@@ -0,0 +1,7 @@
export function learn(): string {
return `
<main class="page">
<h1>Learn</h1>
<p class="lede">Race strategy, step by step.</p>
</main>`;
}
+7
View File
@@ -0,0 +1,7 @@
export function playground(): string {
return `
<main class="page">
<h1>Playground</h1>
<p class="lede">Build a strategy and run the simulation.</p>
</main>`;
}
+9
View File
@@ -0,0 +1,9 @@
import init from 'visual-strategy';
let loading: ReturnType<typeof init> | null = null;
// Instantiates the Rust/WebAssembly module once, however many callers ask.
export function loadSimulation(): ReturnType<typeof init> {
loading ??= init();
return loading;
}
+250
View File
@@ -0,0 +1,250 @@
:root {
--bg: #0d1117;
--surface: #151b23;
--border: #222b36;
--text: #e6edf3;
--muted: #8b949e;
--accent: #d4f24a;
--accent-dim: rgba(212, 242, 74, 0.12);
font: 16px/1.5 system-ui, 'Segoe UI', Roboto, sans-serif;
color-scheme: dark;
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
}
a {
color: inherit;
text-decoration: none;
}
.icon {
width: 20px;
height: 20px;
fill: currentColor;
}
#app {
max-width: 1000px;
min-height: 100svh;
margin: 0 auto;
padding: 20px 16px 24px;
display: flex;
flex-direction: column;
}
/* header */
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-size: 20px;
font-weight: 700;
letter-spacing: -0.4px;
}
.mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 9px;
color: #0d1117;
background: var(--accent);
}
nav {
display: flex;
gap: 24px;
font-size: 13px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
nav a:hover,
nav a[aria-current='page'] {
color: var(--text);
}
/* hero */
main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 48px 0;
}
.badge {
margin: 0;
padding: 5px 14px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
h1 {
margin: 24px 0 0;
font-size: 44px;
line-height: 1.1;
letter-spacing: -1.2px;
}
.lede {
max-width: 34rem;
margin: 16px 0 0;
color: var(--muted);
}
/* inner pages */
main.page {
justify-content: flex-start;
align-items: flex-start;
text-align: left;
padding-top: 72px;
}
main.page h1 {
margin-top: 0;
}
/* cards */
.cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
width: 100%;
margin-top: 44px;
text-align: left;
}
.card {
display: flex;
flex-direction: column;
padding: 24px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
transition: border-color 0.2s;
}
.card:hover {
border-color: #38414d;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 24px;
color: var(--accent);
}
.card-head .icon {
width: 24px;
height: 24px;
}
.tag {
padding: 3px 10px;
border-radius: 999px;
background: var(--accent-dim);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h2 {
margin: 28px 0 8px;
font-size: 22px;
font-weight: 600;
letter-spacing: -0.3px;
}
.card p {
margin: 0 0 24px;
font-size: 14px;
color: var(--muted);
}
.btn {
margin-top: auto;
padding: 11px;
border: 1px solid var(--border);
border-radius: 9px;
background: #0f1620;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
text-align: center;
}
.btn.primary {
border-color: var(--accent);
color: #0d1117;
background: var(--accent);
}
/* footer */
footer {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 12px;
color: var(--muted);
}
footer .icon {
width: 14px;
height: 14px;
}
footer a {
display: flex;
align-items: center;
gap: 6px;
}
footer a:hover {
color: var(--text);
}
@media (max-width: 720px) {
h1 {
font-size: 34px;
}
.cards {
grid-template-columns: 1fr;
}
}