first commit

This commit is contained in:
Aaron Po
2026-03-03 01:25:51 -05:00
commit 72467d8140
16 changed files with 1593 additions and 0 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules
dist
.git
.idea
.vite
*.md

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
.vite/

15
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,15 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/.idea.ayerble.com.iml
/contentModel.xml
/modules.xml
/projectSettingsUpdater.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

8
.idea/indexLayout.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
# Build stage
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:22-alpine
RUN npm install -g serve
WORKDIR /app
COPY --from=build /app/dist ./dist
EXPOSE 3627
CMD ["serve", "dist", "-l", "3627"]

8
docker-compose.yml Normal file
View File

@@ -0,0 +1,8 @@
services:
ayerble:
build: .
container_name: ayerble
ports:
- "127.0.0.1:3627:3627"
restart: unless-stopped

49
index.html Normal file
View File

@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>ayerble</title>
<meta name="description" content="ayerble — personal pages and projects." />
<script type="module" src="/src/main.ts"></script>
</head>
<body>
<canvas id="bg-canvas" aria-hidden="true"></canvas>
<a class="skip-link" href="#main">Skip to main content</a>
<main id="main">
<div class="container">
<h1 class="title">ayerble</h1>
<p class="subtitle">// personal pages &amp; projects</p>
<nav aria-label="Site links">
<ul class="site-list" id="site-list">
<li>
<a href="https://aaronwilliampo.com" target="_blank" rel="noopener noreferrer"
aria-label="aaronwilliampo.com (opens in new tab)">
aaronwilliampo.com
</a>
</li>
<li>
<a href="https://git.aaronwilliampo.com" target="_blank" rel="noopener noreferrer"
aria-label="git.aaronwilliampo.com (opens in new tab)">
git.aaronwilliampo.com
</a>
</li>
<li>
<a href="https://yerb-engine.aaronwilliampo.com" target="_blank" rel="noopener noreferrer"
aria-label="yerb-engine.aaronwilliampo.com (opens in new tab)">
yerb-engine.aaronwilliampo.com
</a>
</li>
</ul>
</nav>
</div>
</main>
<footer><small>&copy; 2026 ayerble</small></footer>
</body>
</html>

1186
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "ayerble.com",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"three": "^0.172.0"
},
"devDependencies": {
"@types/three": "^0.172.0",
"typescript": "^5.7.0",
"vite": "^6.2.0"
}
}

58
src/background.ts Normal file
View File

@@ -0,0 +1,58 @@
import * as THREE from 'three';
/**
* Initialise the Three.js wireframe background scene.
* @param canvas the `<canvas>` element to render into.
*/
export function initBackground(canvas: HTMLCanvasElement): void {
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
const scene = new THREE.Scene();
const aspect = window.innerWidth / window.innerHeight;
const camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 200);
camera.position.set(0, 0, 10);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// Wireframe sphere at exact origin
const geo = new THREE.SphereGeometry(3.5, 32, 32);
const mat = new THREE.MeshBasicMaterial({
color: 0x00ffe7,
wireframe: true,
opacity: 0.28,
transparent: true,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(0, 0, 0);
scene.add(mesh);
// Grid floor
const grid = new THREE.GridHelper(30, 30, 0xff2d78, 0xff2d78);
grid.material.opacity = 0.1;
grid.material.transparent = true;
grid.position.y = -3.2;
scene.add(grid);
// Handle resize
window.addEventListener('resize', () => {
const w = window.innerWidth;
const h = window.innerHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
});
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
(function animate() {
requestAnimationFrame(animate);
if (!reduced) {
mesh.rotation.y += 0.003;
mesh.rotation.z += 0.004;
grid.rotation.y += 0.0008;
}
renderer.render(scene, camera);
})();
}

11
src/main.ts Normal file
View File

@@ -0,0 +1,11 @@
import './style.css';
import { initBackground } from './background';
import { initNavigation } from './navigation';
const canvas = document.getElementById('bg-canvas') as HTMLCanvasElement | null;
if (canvas) {
initBackground(canvas);
}
initNavigation();

40
src/navigation.ts Normal file
View File

@@ -0,0 +1,40 @@
/**
* Initialise arrow-key (↑ / ↓ / Enter) navigation for the site link list.
*/
export function initNavigation(): void {
const links = Array.from(
document.querySelectorAll<HTMLAnchorElement>('#site-list a'),
);
let idx = -1;
function setActive(i: number): void {
links.forEach((l) => l.classList.remove('active'));
if (i >= 0 && i < links.length) {
links[i].classList.add('active');
links[i].focus();
idx = i;
}
}
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setActive(idx < links.length - 1 ? idx + 1 : 0);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActive(idx > 0 ? idx - 1 : links.length - 1);
} else if (e.key === 'Enter' && idx >= 0) {
links[idx].click();
}
});
// Sync idx when focus moves via mouse / tab
links.forEach((l, i) => {
l.addEventListener('focus', () => {
idx = i;
l.classList.add('active');
});
l.addEventListener('blur', () => l.classList.remove('active'));
});
}

139
src/style.css Normal file
View File

@@ -0,0 +1,139 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; }
}
:root {
--cyan: #00ffe7;
--magenta: #ff2d78;
--yellow: #f5e642;
--text: #c8d0e0;
--text-dim: #6b7280;
--font: "Courier New", Courier, monospace;
}
html { background: #0d0d0f; }
body {
background: transparent;
color: var(--text);
font-family: var(--font);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* Scanlines */
body::after {
content: "";
position: fixed;
inset: 0;
background: repeating-linear-gradient(
to bottom,
transparent 0px, transparent 3px,
rgba(0,0,0,.15) 3px, rgba(0,0,0,.15) 4px
);
pointer-events: none;
z-index: 9999;
}
/* Background canvas */
#bg-canvas {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
}
/* Skip link */
.skip-link {
position: absolute;
top: -100%;
left: 1rem;
background: var(--cyan);
color: #000;
font-weight: bold;
padding: .4rem .8rem;
border-radius: 4px;
z-index: 10000;
text-decoration: none;
}
.skip-link:focus { top: 1rem; }
/* Main content */
.container {
position: relative;
z-index: 1;
text-align: center;
padding: 2.5rem 3rem;
background: rgba(13, 13, 15, 0.65);
border: 1px solid rgba(0, 255, 231, 0.08);
border-radius: 6px;
backdrop-filter: blur(2px);
}
h1 {
font-size: clamp(2rem, 8vw, 3.5rem);
letter-spacing: .12em;
text-transform: uppercase;
color: var(--cyan);
margin-bottom: .4rem;
}
h1 span { color: var(--magenta); }
.subtitle {
color: var(--text-dim);
font-size: .9rem;
margin-bottom: 3rem;
}
/* Link list */
.site-list {
list-style: none;
display: flex;
flex-direction: column;
gap: .75rem;
align-items: center;
}
.site-list li a {
display: block;
font-size: clamp(.95rem, 2.5vw, 1.15rem);
color: #8a95a8;
text-decoration: none;
letter-spacing: .06em;
padding: .55rem 1.5rem;
border: 1px solid rgba(138, 149, 168, 0.2);
border-radius: 4px;
transition: color .12s, border-color .12s, background .12s;
outline: none;
}
/* Arrow-key highlight + hover + focus all share the same style */
.site-list li a.active,
.site-list li a:hover,
.site-list li a:focus-visible {
color: var(--cyan);
border-color: var(--cyan);
background: rgba(0,255,231,.07);
}
/* Visible keyboard focus ring for AODA */
.site-list li a:focus-visible {
outline: 2px solid var(--yellow);
outline-offset: 2px;
}
footer {
position: relative;
z-index: 1;
padding: 1.5rem;
font-size: .75rem;
color: var(--text-dim);
}

2
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}