feat: vertical helix with projects bound to strands

Reshape the landing so each project anchors to a slot on one of the two
helix strands. Strand A = Projects (durable, in-production), strand B =
Innovations (experiments, prototypes). Cards alternate L/R based on the
strand's instantaneous position at the slot — strand identity is shown
by colour + badge, not by side.

Schema
- ALTER projects ADD strand TEXT NOT NULL DEFAULT 'A' CHECK ('A','B').
  Generated as drizzle/0001_*.sql.

Component
- New VerticalHelix.svelte replaces Helix.svelte on the landing.
- Slot Y = 120 + i * 240; period = 480, so each strand is at an extreme
  at every slot. Node sits at the strand's actual x; card hugs that side.
- Pulsing node halos and animated gradient stops give "alive" feeling
  without moving the strand geometry — projects stay anchored.
- prefers-reduced-motion disables the pulse.
- <760px: SVG hides, cards stack full-width.

Authoring
- /projects/new form gets a Project / Innovation radio picker.
- Seed adds an "DNA Scout" example on strand B so the helix renders with
  both strands populated on first boot.

Landing
- Hero shrinks (60vh, no helix backdrop) and gains a strand legend.
- Top 12 projects bind to the helix; "See all N →" appears if there's
  more.
- Posts band becomes compact (3 most recent), below the helix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rene De Ren
2026-05-20 11:24:41 +02:00
parent 103ba3cb5d
commit 408cf4460a
10 changed files with 1312 additions and 89 deletions

View File

@@ -36,6 +36,7 @@ const projects = [
'- Used at Waterschap Brabantse Delta for plant simulation and live control'
].join('\n'),
cover_url: null,
strand: 'A',
status: 'published'
},
{
@@ -52,7 +53,7 @@ const projects = [
'## Stack',
'',
'- **SvelteKit 2** + **Svelte 5** + TypeScript',
'- **Tailwind v4** (CSS-first design tokens)',
'- **Tailwind v3** + CSS-first design tokens',
'- **SQLite** + **Drizzle ORM** — single-file, easy to back up',
'- **Gitea OAuth** for authoring',
'- Pure SVG + CSS for the helix animation — no WebGL',
@@ -62,6 +63,34 @@ const projects = [
'Innovations were scattered: Gitea repos here, Grafana dashboards there, slide decks elsewhere. HELIX is the strand they share.'
].join('\n'),
cover_url: null,
strand: 'A',
status: 'published'
},
{
id: 'prj_dna_scout',
slug: 'dna-scout',
title: 'DNA Scout',
summary:
'Experimental anomaly-detection prototype that fingerprints reactor telemetry. R&D scout, not yet plant-ready.',
body_md: [
'# DNA Scout',
'',
"An experimental innovation: build short \"fingerprints\" of reactor telemetry to spot anomalies before the operator does. Think of it as a sequence read on the plant's metabolism.",
'',
'## Status',
'',
"Prototype. Runs on bench data from one reactor at the WBD test site. Not production-ready — accuracy on overflow events is still 60-something percent.",
'',
'## Idea',
'',
'- Slide a 5-minute window across all telemetry channels',
'- Hash each window into a compact descriptor',
'- Cluster descriptors → anomalies fall outside the dense clusters',
'',
'Belongs on strand B (Innovation) — it might never ship, and that\'s fine. If it does, it graduates to a Project.'
].join('\n'),
cover_url: null,
strand: 'B',
status: 'published'
}
];
@@ -113,9 +142,9 @@ const posts = [
const insertProject = db.prepare(`
INSERT OR IGNORE INTO projects
(id, slug, title, summary, body_md, cover_url, author_id, status, created_at, updated_at)
(id, slug, title, summary, body_md, cover_url, strand, author_id, status, created_at, updated_at)
VALUES
(@id, @slug, @title, @summary, @body_md, @cover_url, NULL, @status, ${now}, ${now})
(@id, @slug, @title, @summary, @body_md, @cover_url, @strand, NULL, @status, ${now}, ${now})
`);
const insertLink = db.prepare(`
INSERT OR IGNORE INTO project_links