Skip to content

Commit a203cc9

Browse files
committed
feat(template): have common site-docs components
1 parent bad3fab commit a203cc9

10 files changed

Lines changed: 950 additions & 0 deletions

File tree

copier.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,4 @@ publish_to_pypi:
7979
############# Tasks ##################
8080
_tasks:
8181
- "ln -sf AGENTS.md CLAUDE.md"
82+
- "bash -c '[[ -f site-docs/zensical.toml ]] && [[ ! -e zensical.toml ]] && ln -sf site-docs/zensical.toml . || echo No need to link zensical.toml'"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
icon: lucide/rocket
3+
description: {{ project_description }}
4+
tags:
5+
- installation
6+
- quickstart
7+
- presets
8+
---
9+
10+
# Getting Started
11+
12+
## Installation
13+
14+
```bash
15+
uv tool install {{ repository_name }}
16+
{{ project_name }} --version
17+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
icon: lucide/rocket
3+
template: landing.html
4+
---
5+
6+
# Home
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Fetches total commit count from the GitHub API and displays it
3+
* in the #commit-count-badge element on the landing page.
4+
*
5+
* Uses the Link header pagination trick: request 1 commit per page,
6+
* read the last page number from the Link header — that IS the count.
7+
*/
8+
9+
; (function () {
10+
const GITHUB_REPO = '{{ repository_namespace }}/{{ repository_name }}';
11+
12+
function formatCount(n) {
13+
if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
14+
return String(n);
15+
}
16+
17+
async function fetchCommitCount() {
18+
const label = document.getElementById('github-commit-count');
19+
if (!label) return;
20+
21+
try {
22+
const res = await fetch(
23+
`https://api.github.com/repos/${GITHUB_REPO}/commits?per_page=1`,
24+
{ headers: { Accept: 'application/vnd.github+json' } }
25+
);
26+
if (!res.ok) return;
27+
28+
const link = res.headers.get('Link') || '';
29+
const match = link.match(/[?&]page=(\d+)>;\s*rel="last"/);
30+
if (!match) return;
31+
32+
const total = parseInt(match[1], 10);
33+
label.textContent = `${formatCount(total)} commits`;
34+
} catch (_) {
35+
// Silently fail — label stays with placeholder
36+
}
37+
}
38+
39+
if (typeof document$ !== 'undefined') {
40+
document$.subscribe(fetchCommitCount);
41+
} else if (document.readyState === 'loading') {
42+
document.addEventListener('DOMContentLoaded', fetchCommitCount);
43+
} else {
44+
fetchCommitCount();
45+
}
46+
})();
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/**
2+
* Flowing Wave Field — abstract background animation using Three.js.
3+
*
4+
* A grid of particles undulates using layered sine waves, creating an
5+
* organic, ocean-like surface. Particles are orange (#F97316), connections
6+
* are white with low opacity. Mouse movement warps the wave locally.
7+
*
8+
*/
9+
10+
;(function () {
11+
// ── Bookkeeping for teardown ──
12+
let animationId = null;
13+
let renderer = null;
14+
let mouseMoveHandler = null;
15+
let resizeHandler = null;
16+
17+
function teardown() {
18+
if (animationId) {
19+
cancelAnimationFrame(animationId);
20+
animationId = null;
21+
}
22+
if (renderer) {
23+
renderer.dispose();
24+
renderer = null;
25+
}
26+
if (mouseMoveHandler) {
27+
document.removeEventListener('mousemove', mouseMoveHandler);
28+
mouseMoveHandler = null;
29+
}
30+
if (resizeHandler) {
31+
window.removeEventListener('resize', resizeHandler);
32+
resizeHandler = null;
33+
}
34+
}
35+
36+
function init() {
37+
teardown();
38+
39+
const canvas = document.getElementById('hero-canvas');
40+
if (!canvas) return;
41+
42+
// ── Scene & Renderer ──
43+
const scene = new THREE.Scene();
44+
renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
45+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
46+
47+
// ── Camera — angled top-down for wave perspective ──
48+
const camera = new THREE.PerspectiveCamera(
49+
60,
50+
window.innerWidth / window.innerHeight,
51+
1,
52+
1000
53+
);
54+
camera.position.set(0, 120, 200);
55+
camera.lookAt(0, 0, 0);
56+
57+
// ── Wave grid configuration ──
58+
const COLS = 50;
59+
const ROWS = 50;
60+
const SPACING = 8;
61+
const PARTICLE_COUNT = COLS * ROWS;
62+
63+
// Center the grid so it looks symmetric
64+
const offsetX = ((COLS - 1) * SPACING) / 2;
65+
const offsetZ = ((ROWS - 1) * SPACING) / 2;
66+
67+
// ── Create particles (Points) ──
68+
const geometry = new THREE.BufferGeometry();
69+
const positions = new Float32Array(PARTICLE_COUNT * 3);
70+
71+
// Initialize flat grid positions
72+
for (let row = 0; row < ROWS; row++) {
73+
for (let col = 0; col < COLS; col++) {
74+
const i = (row * COLS + col) * 3;
75+
positions[i] = col * SPACING - offsetX; // x
76+
positions[i + 1] = 0; // y (will be animated)
77+
positions[i + 2] = row * SPACING - offsetZ; // z
78+
}
79+
}
80+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
81+
82+
// Circle texture for soft round particles
83+
function createCircleTexture() {
84+
const c = document.createElement('canvas');
85+
c.width = 32;
86+
c.height = 32;
87+
const ctx = c.getContext('2d');
88+
// Soft radial gradient for glow effect
89+
const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
90+
grad.addColorStop(0, 'rgba(255, 255, 255, 1)');
91+
grad.addColorStop(0.4, 'rgba(255, 255, 255, 0.8)');
92+
grad.addColorStop(1, 'rgba(255, 255, 255, 0)');
93+
ctx.fillStyle = grad;
94+
ctx.fillRect(0, 0, 32, 32);
95+
const tex = new THREE.Texture(c);
96+
tex.needsUpdate = true;
97+
return tex;
98+
}
99+
100+
const pointsMaterial = new THREE.PointsMaterial({
101+
color: 0xF97316, // Orange accent
102+
size: 2.5,
103+
map: createCircleTexture(),
104+
transparent: true,
105+
alphaTest: 0.01,
106+
opacity: 0.9,
107+
depthWrite: false,
108+
blending: THREE.AdditiveBlending
109+
});
110+
111+
const points = new THREE.Points(geometry, pointsMaterial);
112+
scene.add(points);
113+
114+
// ── Connection lines between nearby particles ──
115+
const lineGeometry = new THREE.BufferGeometry();
116+
// Max possible lines: each particle connects to right + down neighbor
117+
const maxLines = (COLS - 1) * ROWS + COLS * (ROWS - 1);
118+
const linePositions = new Float32Array(maxLines * 6);
119+
lineGeometry.setAttribute(
120+
'position',
121+
new THREE.BufferAttribute(linePositions, 3).setUsage(THREE.DynamicDrawUsage)
122+
);
123+
124+
const lineMaterial = new THREE.LineBasicMaterial({
125+
color: 0xffffff, // White connections
126+
transparent: true,
127+
opacity: 0.08,
128+
depthWrite: false
129+
});
130+
131+
const lines = new THREE.LineSegments(lineGeometry, lineMaterial);
132+
scene.add(lines);
133+
134+
// ── Mouse tracking ──
135+
const mouse = { x: 9999, y: 9999 };
136+
mouseMoveHandler = (e) => {
137+
// Normalize mouse to [-1, 1] range relative to canvas
138+
const rect = canvas.parentNode.getBoundingClientRect();
139+
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
140+
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
141+
};
142+
document.addEventListener('mousemove', mouseMoveHandler);
143+
144+
// ── Resize handler ──
145+
resizeHandler = () => {
146+
const container = canvas.parentNode;
147+
if (!container) return;
148+
const w = container.clientWidth;
149+
const h = container.clientHeight;
150+
renderer.setSize(w, h);
151+
camera.aspect = w / h;
152+
camera.updateProjectionMatrix();
153+
};
154+
window.addEventListener('resize', resizeHandler);
155+
resizeHandler();
156+
157+
// ── Animation loop ──
158+
const clock = new THREE.Clock();
159+
160+
function animate() {
161+
animationId = requestAnimationFrame(animate);
162+
const t = clock.getElapsedTime();
163+
const pos = points.geometry.attributes.position.array;
164+
165+
// Project mouse into world space for local wave distortion
166+
const mouseVec = new THREE.Vector3(mouse.x, mouse.y, 0.5);
167+
mouseVec.unproject(camera);
168+
const dir = mouseVec.sub(camera.position).normalize();
169+
// Intersect with y=0 plane
170+
const dist = -camera.position.y / dir.y;
171+
const mouseWorld = camera.position.clone().add(dir.multiplyScalar(dist));
172+
173+
// Update each particle's Y using layered sine waves
174+
for (let row = 0; row < ROWS; row++) {
175+
for (let col = 0; col < COLS; col++) {
176+
const i = (row * COLS + col) * 3;
177+
const x = pos[i];
178+
const z = pos[i + 2];
179+
180+
// Layer 1: primary wave (large, slow)
181+
let y = Math.sin(x * 0.04 + t * 0.8) * 12;
182+
// Layer 2: cross wave (medium, faster)
183+
y += Math.sin(z * 0.06 + t * 1.2) * 8;
184+
// Layer 3: diagonal ripple (detail)
185+
y += Math.sin((x + z) * 0.05 + t * 0.6) * 5;
186+
187+
// Mouse influence — push wave up near cursor
188+
const dx = x - mouseWorld.x;
189+
const dz = z - mouseWorld.z;
190+
const mouseDist = Math.sqrt(dx * dx + dz * dz);
191+
if (mouseDist < 60) {
192+
const influence = 1 - mouseDist / 60;
193+
y += influence * 20;
194+
}
195+
196+
pos[i + 1] = y;
197+
}
198+
}
199+
points.geometry.attributes.position.needsUpdate = true;
200+
201+
// Update connection lines between grid neighbors
202+
const lnPos = lines.geometry.attributes.position.array;
203+
let li = 0;
204+
205+
for (let row = 0; row < ROWS; row++) {
206+
for (let col = 0; col < COLS; col++) {
207+
const i = (row * COLS + col) * 3;
208+
209+
// Connect to right neighbor
210+
if (col < COLS - 1) {
211+
const j = (row * COLS + col + 1) * 3;
212+
lnPos[li++] = pos[i]; lnPos[li++] = pos[i + 1]; lnPos[li++] = pos[i + 2];
213+
lnPos[li++] = pos[j]; lnPos[li++] = pos[j + 1]; lnPos[li++] = pos[j + 2];
214+
}
215+
// Connect to bottom neighbor
216+
if (row < ROWS - 1) {
217+
const j = ((row + 1) * COLS + col) * 3;
218+
lnPos[li++] = pos[i]; lnPos[li++] = pos[i + 1]; lnPos[li++] = pos[i + 2];
219+
lnPos[li++] = pos[j]; lnPos[li++] = pos[j + 1]; lnPos[li++] = pos[j + 2];
220+
}
221+
}
222+
}
223+
224+
lines.geometry.attributes.position.needsUpdate = true;
225+
lines.geometry.setDrawRange(0, li / 3);
226+
227+
// Slow rotation for dynamism
228+
scene.rotation.y += 0.0008;
229+
230+
renderer.render(scene, camera);
231+
}
232+
233+
animate();
234+
}
235+
236+
// ── Hook into Zensical SPA navigation ──
237+
function safeInit() {
238+
if (typeof THREE === 'undefined') return;
239+
init();
240+
}
241+
242+
if (typeof document$ !== 'undefined') {
243+
document$.subscribe(safeInit);
244+
}
245+
246+
if (document.readyState === 'loading') {
247+
document.addEventListener('DOMContentLoaded', safeInit);
248+
} else {
249+
safeInit();
250+
}
251+
})();

0 commit comments

Comments
 (0)