diff --git a/scripts/seed.js b/scripts/seed.js index ddd69f4..feb11f3 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -42,13 +42,13 @@ const projects = [ { id: 'prj_helix', slug: 'helix', - title: 'HELIX', + title: 'R&D-lab', summary: - 'This very site — the R&D showcase platform. EVOLV and every R&D strand, one helix.', + 'This very site — the R&D lab of Waterschap Brabantse Delta. Projects, innovations, and every strand between.', body_md: [ - '# HELIX', + '# R&D-lab', '', - 'HELIX is the R&D showcase platform of Waterschap Brabantse Delta. It collects projects, innovations, and updates from across the team in one place — with deep links to the actual repos, dashboards, and demos.', + 'The R&D-lab is the showcase platform of Waterschap Brabantse Delta. It collects projects, innovations, and updates from across the team in one place — with deep links to the actual repos, dashboards, and demos.', '', '## Stack', '', @@ -60,7 +60,7 @@ const projects = [ '', '## Why?', '', - 'Innovations were scattered: Gitea repos here, Grafana dashboards there, slide decks elsewhere. HELIX is the strand they share.' + 'Innovations were scattered: Gitea repos here, Grafana dashboards there, slide decks elsewhere. R&D-lab is the strand they share.' ].join('\n'), cover_url: null, strand: 'A', @@ -117,13 +117,13 @@ const links = [ const posts = [ { id: 'pst_welcome', - slug: 'welcome-to-helix', - title: 'Welcome to HELIX', + slug: 'welcome-to-rd-lab', + title: 'Welcome to R&D-lab', summary: 'Why this site exists and how to contribute.', body_md: [ - '# Welcome to HELIX', + '# Welcome to R&D-lab', '', - 'HELIX is the home of R&D output at Waterschap Brabantse Delta. If you have a project, a dashboard, or a one-off experiment worth showing — it belongs here.', + 'R&D-lab is the home of R&D output at Waterschap Brabantse Delta. If you have a project, a dashboard, or a one-off experiment worth showing — it belongs here.', '', '## How to post', '', diff --git a/src/app.css b/src/app.css index 9444520..dbf4574 100644 --- a/src/app.css +++ b/src/app.css @@ -5,14 +5,21 @@ /* Design tokens exposed as CSS vars so Svelte component diff --git a/src/lib/components/Nav.svelte b/src/lib/components/Nav.svelte index 4813cd3..e4211c7 100644 --- a/src/lib/components/Nav.svelte +++ b/src/lib/components/Nav.svelte @@ -20,21 +20,20 @@
{SITE.name} @@ -105,8 +104,8 @@ } .wordmark { font-weight: 700; - letter-spacing: 0.08em; - font-size: 0.95rem; + letter-spacing: 0.02em; + font-size: 0.98rem; } .links { display: flex; diff --git a/src/lib/components/VerticalHelix.svelte b/src/lib/components/VerticalHelix.svelte index 80f5385..0636175 100644 --- a/src/lib/components/VerticalHelix.svelte +++ b/src/lib/components/VerticalHelix.svelte @@ -7,23 +7,36 @@ * * Each project sits at slot Y = TOP_PAD + i * SLOT_HEIGHT, at the * instantaneous x of its strand. Card sits on whichever side the node is. - * Strand colour + badge encode strand identity regardless of L/R position. * - * Animation: gradient stops drift along the strands + node pulse. - * The strand geometry itself is static — keeps project anchors stable. + * Cards are lab-slides: frosted glass surface with a thick strand-coloured + * stripe on the helix-facing edge. Inline link chips per project are real + * anchor elements stacked above an overlay link to the detail page + * (HTML doesn't allow nested , so we use the overlay pattern). + * + * Palette: Waterschap Brabantse Delta — #0d4f9e, #1fa0db, #bed137. */ + import LinkChip from './LinkChip.svelte'; + + type ProjectLink = { + kind: string; + label: string; + url: string; + position: number; + }; + type StrandProject = { slug: string; title: string; summary: string; strand: 'A' | 'B'; coverUrl: string | null; + links: ProjectLink[]; }; let { projects }: { projects: StrandProject[] } = $props(); - // Geometry constants (user-space units) + // Geometry (user-space units inside the SVG) const W = 1000; const CX = W / 2; const AMP = 140; @@ -102,8 +115,7 @@
- + - + - + - + - + - + @@ -154,8 +166,8 @@ y1={r.y} x2={r.x2} y2={r.y} - stroke="#86bbdd" - stroke-opacity={0.08 + 0.32 * r.depth} + stroke="#6fc3ec" + stroke-opacity={0.08 + 0.30 * r.depth} stroke-width={0.6 + 1.0 * r.depth} stroke-linecap="round" /> @@ -181,8 +193,8 @@ y1={r.y} x2={r.x2} y2={r.y} - stroke="#a9daee" - stroke-opacity={0.15 + 0.50 * r.depth} + stroke="#b8dff5" + stroke-opacity={0.18 + 0.50 * r.depth} stroke-width={1.0 + 1.6 * r.depth} stroke-linecap="round" /> @@ -206,7 +218,7 @@ cx={s.nodeX} cy={s.y} r="22" - fill={s.project.strand === 'A' ? '#0c99d9' : '#c084fc'} + fill={s.project.strand === 'A' ? '#1fa0db' : '#bed137'} fill-opacity="0.25" filter="url(#vnode-glow)" class="vnode-halo" @@ -215,49 +227,78 @@ cx={s.nodeX} cy={s.y} r="12" - fill={s.project.strand === 'A' ? '#0c99d9' : '#c084fc'} + fill={s.project.strand === 'A' ? '#1fa0db' : '#bed137'} /> {/each} - + {#each slots as s} {/each} - +
{#each slots as s} - - - - {s.project.strand === 'A' ? 'Project' : 'Innovation'} - -

{s.project.title}

-

{s.project.summary}

- Open -
+ + + + + + +
+ + + {s.project.strand === 'A' ? 'Project' : 'Innovation'} + + +
+ +

{s.project.title}

+

{s.project.summary}

+ + {#if s.project.links.length > 0} +
+ {#each s.project.links.slice(0, 5) as l} + + {/each} + {#if s.project.links.length > 5} + +{s.project.links.length - 5} + {/if} +
+ {/if} + +
+ Open detail +
+ {/each}
@@ -286,41 +327,90 @@ height: 100%; } - .vcard { + /* ========== LAB SLIDE CARD ========== */ + .slide { position: absolute; - width: 280px; - max-width: calc(50% - 110px); - padding: 1rem 1.15rem 1.1rem; + width: 300px; + max-width: calc(50% - 90px); + padding: 1.1rem 1.25rem 0.95rem; border-radius: 12px; - background: color-mix(in oklab, var(--color-helix-bg-2) 90%, transparent); + background: color-mix(in oklab, var(--color-helix-bg-2) 88%, transparent); border: 1px solid var(--color-helix-border); - backdrop-filter: blur(6px); - text-decoration: none; - color: inherit; + backdrop-filter: blur(10px) saturate(140%); + -webkit-backdrop-filter: blur(10px) saturate(140%); transform: translateY(-50%); - transition: border-color 200ms ease, background 200ms ease, transform 200ms ease; + transition: border-color 220ms ease, background 220ms ease, transform 220ms ease, + box-shadow 220ms ease; + overflow: hidden; } - .vcard:hover { + .slide:hover { border-color: var(--card-accent); - background: var(--color-helix-bg-3); + background: color-mix(in oklab, var(--color-helix-bg-3) 90%, transparent); transform: translateY(calc(-50% - 2px)); + box-shadow: 0 16px 40px -20px color-mix(in oklab, var(--card-accent) 60%, transparent); } - .vcard.side-right { + .slide.side-right { right: 1.5rem; - text-align: left; } - .vcard.side-left { + .slide.side-left { left: 1.5rem; - text-align: left; } - .vcard.strand-A { - --card-accent: var(--color-helix-process); - --card-accent-soft: rgba(12, 153, 217, 0.18); + .slide.strand-A { + --card-accent: #1fa0db; + --card-accent-soft: rgba(31, 160, 219, 0.18); } - .vcard.strand-B { - --card-accent: var(--color-helix-accent-2); - --card-accent-soft: rgba(192, 132, 252, 0.18); + .slide.strand-B { + --card-accent: #bed137; + --card-accent-soft: rgba(190, 209, 55, 0.22); + } + + /* Thick coloured stripe on the helix-facing edge */ + .stripe { + position: absolute; + top: 0; + bottom: 0; + width: 5px; + background: linear-gradient( + 180deg, + var(--card-accent) 0%, + color-mix(in oklab, var(--card-accent) 60%, transparent) 100% + ); + box-shadow: 0 0 12px var(--card-accent-soft); + } + .slide.side-right .stripe { + left: 0; + border-radius: 12px 0 0 12px; + } + .slide.side-left .stripe { + right: 0; + border-radius: 0 12px 12px 0; + } + + /* Invisible overlay link: covers the card except the chips row */ + .slide-link { + position: absolute; + inset: 0; + z-index: 1; + text-indent: -9999px; + overflow: hidden; + } + + .slide-head, + .title, + .summary, + .slide-foot { + position: relative; + z-index: 0; /* below the overlay link */ + pointer-events: none; + } + + .slide-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.6rem; } .badge { @@ -328,14 +418,13 @@ align-items: center; gap: 0.4rem; font-family: var(--font-mono); - font-size: 0.68rem; - letter-spacing: 0.16em; + font-size: 0.66rem; + letter-spacing: 0.18em; text-transform: uppercase; color: var(--card-accent); padding: 0.18rem 0.55rem; border-radius: 999px; background: var(--card-accent-soft); - margin-bottom: 0.6rem; } .badge .dot { width: 6px; @@ -345,37 +434,73 @@ box-shadow: 0 0 8px var(--card-accent); } - .vcard h3 { + .serial { + font-family: var(--font-mono); + font-size: 0.66rem; + letter-spacing: 0.12em; + color: var(--color-helix-ink-faint); + } + + .title { font-size: 1.1rem; font-weight: 600; letter-spacing: -0.01em; margin: 0 0 0.35rem; + padding-left: 0.45rem; /* clear the stripe a hair */ } - .vcard p { + .slide.side-left .title { + padding-left: 0; + padding-right: 0.45rem; + } + + .summary { color: var(--color-helix-ink-dim); - font-size: 0.92rem; + font-size: 0.9rem; line-height: 1.5; - margin: 0 0 0.55rem; + margin: 0 0 0.7rem; display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } - .cta { + + /* Chips ROW — above the overlay link */ + .chips { + position: relative; + z-index: 2; + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin: 0 0 0.6rem; + } + .more-chips { + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--color-helix-ink-faint); + align-self: center; + } + + .slide-foot { + border-top: 1px dashed color-mix(in oklab, var(--card-accent) 30%, transparent); + padding-top: 0.55rem; + display: flex; + justify-content: flex-end; + } + .open { color: var(--card-accent); - font-size: 0.85rem; + font-size: 0.82rem; font-weight: 500; } .arrow { display: inline-block; transition: transform 200ms ease; } - .vcard:hover .arrow { + .slide:hover .arrow { transform: translateX(3px); } - /* Node pulse (subtle "alive" feeling, no rotation) */ + /* Node pulse */ @media (prefers-reduced-motion: no-preference) { :global(.vnode-halo) { animation: vnode-pulse 3.5s ease-in-out infinite; @@ -395,14 +520,13 @@ } } - /* Mobile: helix becomes a thin centerline; cards stack full-width below each node */ + /* Mobile: helix hides, cards stack full-width */ @media (max-width: 760px) { .vhelix { height: auto; - min-height: 0; } .vhelix-svg { - display: none; /* hide the wide-helix SVG on narrow screens */ + display: none; } .vhelix-cards { display: flex; @@ -411,7 +535,7 @@ height: auto; padding: 1rem 0; } - .vcard { + .slide { position: relative; top: auto !important; right: auto !important; @@ -420,7 +544,7 @@ max-width: 100%; transform: none; } - .vcard:hover { + .slide:hover { transform: none; } } diff --git a/src/lib/config.ts b/src/lib/config.ts index 67e8933..9016f28 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -2,11 +2,12 @@ * Site-wide configuration. Edit here to rebrand without touching components. */ export const SITE = { - name: 'HELIX', - tagline: 'EVOLV and every R&D strand, one helix.', + name: 'R&D-lab', + shortName: 'R&D-lab', + tagline: 'Projects, innovations, and every strand between.', description: - 'The R&D showcase platform of Waterschap Brabantse Delta. EVOLV at its core, every innovation along the strands.', - organization: 'Waterschap Brabantse Delta R&D', + 'The R&D lab of Waterschap Brabantse Delta. EVOLV at its core, every project and innovation along the strands.', + organization: 'Waterschap Brabantse Delta', giteaOrg: 'RnD', giteaBaseUrl: 'https://gitea.wbd-rd.nl' } as const; @@ -32,3 +33,13 @@ export const LINK_KIND_LABEL: Record = { paper: 'Paper', video: 'Video' }; + +/** Short labels used inline on cards (≤ 6 chars). */ +export const LINK_KIND_SHORT: Record = { + gitea: 'Repo', + dashboard: 'Dash', + demo: 'Demo', + docs: 'Docs', + paper: 'Paper', + video: 'Video' +}; diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 65fad20..aab9faa 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -10,7 +10,7 @@ marked.setOptions({ * * Trust model: authoring is gated to members of the configured Gitea org, * so we render markdown as-is (raw HTML in markdown is passed through). - * If HELIX is opened to untrusted authors, swap this for a DOMPurify pass. + * If authoring is opened to untrusted authors, swap this for a DOMPurify pass. */ export function renderMarkdown(md: string): string { return marked.parse(md) as string; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 923513f..4864be8 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,22 +1,28 @@ import type { PageServerLoad } from './$types'; import { db } from '$lib/server/db'; -import { projects, posts } from '$lib/server/db/schema'; -import { desc, eq, isNotNull } from 'drizzle-orm'; +import { projects, posts, projectLinks } from '$lib/server/db/schema'; +import { asc, desc, eq, isNotNull } from 'drizzle-orm'; export const load: PageServerLoad = async () => { - const helixProjects = db - .select({ - slug: projects.slug, - title: projects.title, - summary: projects.summary, - strand: projects.strand, - coverUrl: projects.coverUrl - }) - .from(projects) - .where(eq(projects.status, 'published')) - .orderBy(desc(projects.updatedAt)) - .limit(12) - .all(); + // Top 12 projects + their links (one round-trip via Drizzle relational query) + const helixProjects = await db.query.projects.findMany({ + where: eq(projects.status, 'published'), + orderBy: [desc(projects.updatedAt)], + limit: 12, + columns: { + slug: true, + title: true, + summary: true, + strand: true, + coverUrl: true + }, + with: { + links: { + columns: { kind: true, label: true, url: true, position: true }, + orderBy: [asc(projectLinks.position)] + } + } + }); const totalPublished = db .select({ slug: projects.slug }) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a9ab24f..aebcc34 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -10,7 +10,7 @@

R&D · {SITE.organization}

- HELIX + {SITE.name}

{SITE.tagline}

{SITE.description}

@@ -37,7 +37,7 @@ {#if data.helixProjects.length === 0}

The strands are empty

-

HELIX needs its first project. Add one →

+

R&D-lab needs its first project. Add one →

{:else}
@@ -167,12 +167,12 @@ border-radius: 50%; } .leg-a { - background: var(--color-helix-process); - box-shadow: 0 0 10px var(--color-helix-process); + background: #1fa0db; + box-shadow: 0 0 10px #1fa0db; } .leg-b { - background: var(--color-helix-accent-2); - box-shadow: 0 0 10px var(--color-helix-accent-2); + background: #bed137; + box-shadow: 0 0 10px #bed137; } .scroll-hint { @@ -208,7 +208,7 @@ } } - /* ---------- HELIX SECTION ---------- */ + /* ---------- STRAND SECTION ---------- */ .helix-anchor { margin-top: 4rem; padding: 0 1rem; diff --git a/src/routes/auth/gitea/callback/+server.ts b/src/routes/auth/gitea/callback/+server.ts index 17ce975..24ecf8e 100644 --- a/src/routes/auth/gitea/callback/+server.ts +++ b/src/routes/auth/gitea/callback/+server.ts @@ -45,7 +45,7 @@ export const GET = async ({ url, cookies }) => { if (GITEA_ALLOWED_ORG) { const allowed = await isUserInOrg(accessToken, giteaUser.login, GITEA_ALLOWED_ORG); if (!allowed) { - error(403, `HELIX is restricted to members of the "${GITEA_ALLOWED_ORG}" Gitea organisation.`); + error(403, `R&D-lab is restricted to members of the "${GITEA_ALLOWED_ORG}" Gitea organisation.`); } } diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index e0e141b..168eaa8 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -10,7 +10,7 @@

Sign in to {SITE.name}

- HELIX uses your Gitea account at + {SITE.name} uses your Gitea account at {SITE.giteaBaseUrl.replace('https://', '')}. Anyone can read; authoring is restricted to the {SITE.giteaOrg} organisation. @@ -30,7 +30,7 @@

- You'll be redirected to Gitea to approve, then back here. No password is stored by HELIX. + You'll be redirected to Gitea to approve, then back here. No password is stored by {SITE.name}.

diff --git a/src/routes/posts/+page.svelte b/src/routes/posts/+page.svelte index 07fc132..58b3f97 100644 --- a/src/routes/posts/+page.svelte +++ b/src/routes/posts/+page.svelte @@ -1,11 +1,12 @@ - Posts · HELIX + Posts · {SITE.name}
diff --git a/src/routes/posts/[slug]/+page.svelte b/src/routes/posts/[slug]/+page.svelte index 162b7ed..443689d 100644 --- a/src/routes/posts/[slug]/+page.svelte +++ b/src/routes/posts/[slug]/+page.svelte @@ -1,4 +1,6 @@ - {data.post.title} · HELIX + {data.post.title} · {SITE.name} diff --git a/src/routes/posts/new/+page.svelte b/src/routes/posts/new/+page.svelte index d539a05..1ff44bc 100644 --- a/src/routes/posts/new/+page.svelte +++ b/src/routes/posts/new/+page.svelte @@ -1,11 +1,12 @@ - New post · HELIX + New post · {SITE.name}
diff --git a/src/routes/projects/+page.svelte b/src/routes/projects/+page.svelte index c8e9e20..34cfca0 100644 --- a/src/routes/projects/+page.svelte +++ b/src/routes/projects/+page.svelte @@ -1,11 +1,12 @@ - Projects · HELIX + Projects · {SITE.name}
diff --git a/src/routes/projects/[slug]/+page.svelte b/src/routes/projects/[slug]/+page.svelte index 2cd14bb..24e0318 100644 --- a/src/routes/projects/[slug]/+page.svelte +++ b/src/routes/projects/[slug]/+page.svelte @@ -1,6 +1,7 @@ - {data.project.title} · HELIX + {data.project.title} · {SITE.name} diff --git a/src/routes/projects/new/+page.svelte b/src/routes/projects/new/+page.svelte index 466e6a3..a05b8b2 100644 --- a/src/routes/projects/new/+page.svelte +++ b/src/routes/projects/new/+page.svelte @@ -1,5 +1,5 @@ - New project · HELIX + New project · {SITE.name}
diff --git a/static/favicon.svg b/static/favicon.svg index fe38f77..1388e22 100644 --- a/static/favicon.svg +++ b/static/favicon.svg @@ -1,8 +1,21 @@ + - - - - - + + + + + + + + + + + + + + + diff --git a/tailwind.config.js b/tailwind.config.js index f016a3d..a663e27 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,12 +4,19 @@ export default { theme: { extend: { colors: { + // Waterschap Brabantse Delta brand palette (from WSBD-logo.svg) + wbd: { + deep: '#0a3d80', + blue: '#0d4f9e', + cyan: '#1fa0db', + lime: '#bed137' + }, helix: { - area: '#0f52a5', - process: '#0c99d9', - unit: '#50a8d9', - equipment: '#86bbdd', - control: '#a9daee', + area: '#0a3d80', + process: '#0d4f9e', + unit: '#1fa0db', + equipment: '#6fc3ec', + control: '#b8dff5', bg: '#07111d', 'bg-2': '#0c1c30', 'bg-3': '#122842', @@ -17,8 +24,8 @@ export default { ink: '#e6f1fb', 'ink-dim': '#8fa6b8', 'ink-faint': '#5b7388', - accent: '#4dd0c2', - 'accent-2': '#c084fc' + accent: '#bed137', + 'accent-2': '#d8e36a' } }, fontFamily: {