initial web pages
This commit is contained in:
17 files changed
+1341
No files matched your search
@@ -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">·</span>
|
||||
<a href="${REPO}" target="_blank" rel="noreferrer">
|
||||
<svg class="icon" aria-hidden="true"><use href="/icons.svg#gitea" /></svg>
|
||||
Source
|
||||
</a>
|
||||
</footer>`;
|
||||
}
|
||||
@@ -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>`;
|
||||
}
|
||||
Whitespace-only changes.
@@ -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();
|
||||
@@ -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 →</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 →</span>
|
||||
</a>
|
||||
</div>
|
||||
</main>`;
|
||||
}
|
||||
@@ -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>`;
|
||||
}
|
||||
@@ -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>`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user