feat: rebrand to R&D-lab + WBD palette + lab-slide cards
Identity refresh aligned to Waterschap Brabantse Delta.
Brand
- HELIX → R&D-lab everywhere user-facing (SITE.name; literal "HELIX"s
swept across routes, app.html, login, error messages, seed).
- New tagline: "Projects, innovations, and every strand between."
- Site description updated.
Palette (sourced from the official WSBD-logo.svg)
- Primary #0d4f9e, secondary #1fa0db, accent #bed137 — added as
--wbd-blue / --wbd-cyan / --wbd-lime CSS vars and as wbd.* in
tailwind.config.js. helix.* aliases now point to the WBD palette.
- Strand A (Projects) → #1fa0db cyan. Strand B (Innovations) → #bed137 lime.
- Body vignette + scroll-bar + legend dots repainted accordingly.
Composite logo
- New 24px nav glyph + favicon.svg: WBD-style tilted-square mark in WBD
blues at the centre, helix strands (lime + cyan) wrapping it, lime
"active site" dot at the crossing. Says "R&D-lab × Brabantse Delta"
in one mark.
Lab-slide cards (VerticalHelix)
- Frosted-glass surface (backdrop-filter blur+saturate).
- Thick 5px strand-coloured stripe on the helix-facing edge (gradient,
glowing shadow). Slide rounds the stripe corners; the rest is square.
- Slide header has the strand badge and a monospace serial number
(01/03 etc) — lab-specimen feel.
- Dashed footer rule + "Open detail →" CTA.
- Inline link chips (Gitea / Dashboard / Demo / Docs / Paper / Video)
with inline SVG icons + short labels. Hover lights up in the strand
colour. Capped at 5 visible, "+N" overflow indicator.
- Real <a> chips inside the card without nested <a>: overlay-link
pattern (transparent slide-link absolute fills the card, chips sit on
z-index: 2 above it).
Server load
- + Page now fetches each project's links in one Drizzle relational
query (db.query.projects.findMany with: { links }), capped at 12.
- + Form: strand picker (Project / Innovation radios) reads + persists
the new column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,13 +42,13 @@ const projects = [
|
|||||||
{
|
{
|
||||||
id: 'prj_helix',
|
id: 'prj_helix',
|
||||||
slug: 'helix',
|
slug: 'helix',
|
||||||
title: 'HELIX',
|
title: 'R&D-lab',
|
||||||
summary:
|
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: [
|
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',
|
'## Stack',
|
||||||
'',
|
'',
|
||||||
@@ -60,7 +60,7 @@ const projects = [
|
|||||||
'',
|
'',
|
||||||
'## Why?',
|
'## 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'),
|
].join('\n'),
|
||||||
cover_url: null,
|
cover_url: null,
|
||||||
strand: 'A',
|
strand: 'A',
|
||||||
@@ -117,13 +117,13 @@ const links = [
|
|||||||
const posts = [
|
const posts = [
|
||||||
{
|
{
|
||||||
id: 'pst_welcome',
|
id: 'pst_welcome',
|
||||||
slug: 'welcome-to-helix',
|
slug: 'welcome-to-rd-lab',
|
||||||
title: 'Welcome to HELIX',
|
title: 'Welcome to R&D-lab',
|
||||||
summary: 'Why this site exists and how to contribute.',
|
summary: 'Why this site exists and how to contribute.',
|
||||||
body_md: [
|
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',
|
'## How to post',
|
||||||
'',
|
'',
|
||||||
|
|||||||
43
src/app.css
43
src/app.css
@@ -5,14 +5,21 @@
|
|||||||
/* Design tokens exposed as CSS vars so Svelte component <style> blocks
|
/* Design tokens exposed as CSS vars so Svelte component <style> blocks
|
||||||
can use them without going through Tailwind. */
|
can use them without going through Tailwind. */
|
||||||
:root {
|
:root {
|
||||||
/* S88-inspired hierarchy palette (mirrors EVOLV) */
|
/* Waterschap Brabantse Delta brand palette.
|
||||||
--color-helix-area: #0f52a5;
|
Sourced from the official WSBD logo SVG (fd-cdn.nl/.../WSBD-logo.svg). */
|
||||||
--color-helix-process: #0c99d9;
|
--wbd-deep: #0a3d80; /* deeper shade derived from primary, for hover/depth */
|
||||||
--color-helix-unit: #50a8d9;
|
--wbd-blue: #0d4f9e; /* WBD primary blue */
|
||||||
--color-helix-equipment: #86bbdd;
|
--wbd-cyan: #1fa0db; /* WBD secondary blue */
|
||||||
--color-helix-control: #a9daee;
|
--wbd-lime: #bed137; /* WBD accent lime/green */
|
||||||
|
|
||||||
/* Surfaces */
|
/* Hierarchy palette (semantic) — points back to the WBD palette */
|
||||||
|
--color-helix-area: var(--wbd-deep);
|
||||||
|
--color-helix-process: var(--wbd-blue);
|
||||||
|
--color-helix-unit: var(--wbd-cyan);
|
||||||
|
--color-helix-equipment: #6fc3ec;
|
||||||
|
--color-helix-control: #b8dff5;
|
||||||
|
|
||||||
|
/* Surfaces (dark UI, on-brand) */
|
||||||
--color-helix-bg: #07111d;
|
--color-helix-bg: #07111d;
|
||||||
--color-helix-bg-2: #0c1c30;
|
--color-helix-bg-2: #0c1c30;
|
||||||
--color-helix-bg-3: #122842;
|
--color-helix-bg-3: #122842;
|
||||||
@@ -23,9 +30,17 @@
|
|||||||
--color-helix-ink-dim: #8fa6b8;
|
--color-helix-ink-dim: #8fa6b8;
|
||||||
--color-helix-ink-faint: #5b7388;
|
--color-helix-ink-faint: #5b7388;
|
||||||
|
|
||||||
/* Accent (helix glow / R&D signal) */
|
/* Accents
|
||||||
--color-helix-accent: #4dd0c2;
|
accent = WBD lime — used for primary CTAs, focus, and Strand B (Innovations)
|
||||||
--color-helix-accent-2: #c084fc;
|
accent2 = a lighter lime, used for highlights */
|
||||||
|
--color-helix-accent: var(--wbd-lime);
|
||||||
|
--color-helix-accent-2: #d8e36a;
|
||||||
|
|
||||||
|
/* Strand colors (semantic) — pick from the WBD palette */
|
||||||
|
--strand-a-primary: var(--wbd-cyan); /* Projects */
|
||||||
|
--strand-a-secondary: var(--wbd-blue);
|
||||||
|
--strand-b-primary: var(--wbd-lime); /* Innovations */
|
||||||
|
--strand-b-secondary: #8fa024;
|
||||||
|
|
||||||
--font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
--font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
||||||
@@ -40,15 +55,15 @@ body {
|
|||||||
font-feature-settings: 'cv11', 'ss01', 'ss03';
|
font-feature-settings: 'cv11', 'ss01', 'ss03';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtle radial vignette anchoring the landing page */
|
/* Subtle radial vignette anchoring the landing page (WBD palette) */
|
||||||
body::before {
|
body::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background:
|
background:
|
||||||
radial-gradient(1200px 800px at 20% 0%, rgba(12, 153, 217, 0.18), transparent 60%),
|
radial-gradient(1200px 800px at 20% 0%, rgba(31, 160, 219, 0.18), transparent 60%),
|
||||||
radial-gradient(900px 700px at 90% 20%, rgba(77, 208, 194, 0.12), transparent 60%),
|
radial-gradient(900px 700px at 90% 20%, rgba(13, 79, 158, 0.16), transparent 60%),
|
||||||
radial-gradient(700px 500px at 50% 100%, rgba(192, 132, 252, 0.08), transparent 60%);
|
radial-gradient(700px 500px at 50% 100%, rgba(190, 209, 55, 0.08), transparent 60%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#0c99d9" />
|
<meta name="theme-color" content="#0c99d9" />
|
||||||
<meta name="description" content="HELIX — the R&D showcase platform of Waterschap Brabantse Delta. EVOLV and every R&D strand, one helix." />
|
<meta name="description" content="R&D-lab — the R&D platform of Waterschap Brabantse Delta. Projects, innovations, and every strand between." />
|
||||||
<link rel="preconnect" href="https://rsms.me/" />
|
<link rel="preconnect" href="https://rsms.me/" />
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
|
|||||||
123
src/lib/components/LinkChip.svelte
Normal file
123
src/lib/components/LinkChip.svelte
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { LINK_KIND_SHORT, type LinkKind } from '$lib/config';
|
||||||
|
|
||||||
|
let {
|
||||||
|
kind,
|
||||||
|
label,
|
||||||
|
url
|
||||||
|
}: {
|
||||||
|
kind: string;
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
// Display short kind name; fall back to label for unknown kinds
|
||||||
|
const shortKind = $derived(
|
||||||
|
(LINK_KIND_SHORT as Record<string, string>)[kind] ?? label.slice(0, 6)
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="chip"
|
||||||
|
data-kind={kind}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
aria-label={`${label} (${kind})`}
|
||||||
|
title={label}
|
||||||
|
>
|
||||||
|
<span class="ico" aria-hidden="true">
|
||||||
|
{#if kind === 'gitea'}
|
||||||
|
<!-- code-branch -->
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="4" cy="3" r="1.5" />
|
||||||
|
<circle cx="4" cy="13" r="1.5" />
|
||||||
|
<circle cx="12" cy="6" r="1.5" />
|
||||||
|
<path d="M4 4.5v7" />
|
||||||
|
<path d="M4 8c0-1.5 1-3 4-3v0" />
|
||||||
|
</svg>
|
||||||
|
{:else if kind === 'dashboard'}
|
||||||
|
<!-- grid -->
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="2" width="5" height="5" rx="0.6" />
|
||||||
|
<rect x="9" y="2" width="5" height="5" rx="0.6" />
|
||||||
|
<rect x="2" y="9" width="5" height="5" rx="0.6" />
|
||||||
|
<rect x="9" y="9" width="5" height="5" rx="0.6" />
|
||||||
|
</svg>
|
||||||
|
{:else if kind === 'demo'}
|
||||||
|
<!-- play -->
|
||||||
|
<svg viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M5 3.5v9l8-4.5z" />
|
||||||
|
</svg>
|
||||||
|
{:else if kind === 'docs'}
|
||||||
|
<!-- doc lines -->
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3.5 2.5h7l2.5 2.5v8.5h-9.5z" />
|
||||||
|
<path d="M10 2.5v3h3" />
|
||||||
|
<path d="M5 8h6" />
|
||||||
|
<path d="M5 10.5h6" />
|
||||||
|
<path d="M5 13h4" />
|
||||||
|
</svg>
|
||||||
|
{:else if kind === 'paper'}
|
||||||
|
<!-- paper / page -->
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 2.5h10v11h-10z" />
|
||||||
|
<path d="M5.5 5.5h5" />
|
||||||
|
<path d="M5.5 8h5" />
|
||||||
|
<path d="M5.5 10.5h3" />
|
||||||
|
</svg>
|
||||||
|
{:else if kind === 'video'}
|
||||||
|
<!-- video rect with play -->
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="3.5" width="12" height="9" rx="1" />
|
||||||
|
<path d="M7 6v4l3.5-2z" fill="currentColor" stroke="none" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<!-- generic external link -->
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9.5 3h3.5v3.5" />
|
||||||
|
<path d="M13 3l-5.5 5.5" />
|
||||||
|
<path d="M11 9v3.5h-7v-7h3.5" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="lbl">{shortKind}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.25rem 0.6rem 0.25rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid var(--color-helix-border);
|
||||||
|
color: var(--color-helix-ink-dim);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
transition: color 160ms ease, border-color 160ms ease, background 160ms ease;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.chip:hover {
|
||||||
|
color: var(--color-helix-ink);
|
||||||
|
border-color: var(--card-accent, var(--color-helix-accent));
|
||||||
|
background: color-mix(in oklab, var(--card-accent, var(--color-helix-accent)) 14%, transparent);
|
||||||
|
}
|
||||||
|
.ico {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: var(--card-accent, var(--color-helix-accent));
|
||||||
|
}
|
||||||
|
.ico svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.lbl {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -20,21 +20,20 @@
|
|||||||
<div class="inner">
|
<div class="inner">
|
||||||
<a href="/" class="brand">
|
<a href="/" class="brand">
|
||||||
<span class="mark" aria-hidden="true">
|
<span class="mark" aria-hidden="true">
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22">
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
<path
|
<!-- WBD tilted-square core -->
|
||||||
d="M4 4 C 4 10, 20 14, 20 20"
|
<g transform="rotate(45 12 12)">
|
||||||
fill="none"
|
<rect x="5.5" y="5.5" width="13" height="13" rx="0.5" fill="#0d4f9e" fill-opacity="0.9"/>
|
||||||
stroke="#4dd0c2"
|
<rect x="8.2" y="8.2" width="7.6" height="7.6" rx="0.4" fill="#1fa0db"/>
|
||||||
stroke-width="2"
|
</g>
|
||||||
stroke-linecap="round"
|
<!-- Helix strands wrapping the core -->
|
||||||
/>
|
<path d="M4 4 C 4 10, 20 14, 20 20"
|
||||||
<path
|
fill="none" stroke="#bed137" stroke-width="2.1" stroke-linecap="round"/>
|
||||||
d="M4 20 C 4 14, 20 10, 20 4"
|
<path d="M4 20 C 4 14, 20 10, 20 4"
|
||||||
fill="none"
|
fill="none" stroke="#1fa0db" stroke-width="2.1" stroke-linecap="round" stroke-opacity="0.9"/>
|
||||||
stroke="#0c99d9"
|
<!-- Active-site lime dot -->
|
||||||
stroke-width="2"
|
<circle cx="12" cy="12" r="1.9" fill="#bed137"/>
|
||||||
stroke-linecap="round"
|
<circle cx="12" cy="12" r="0.9" fill="#07111d"/>
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="wordmark">{SITE.name}</span>
|
<span class="wordmark">{SITE.name}</span>
|
||||||
@@ -105,8 +104,8 @@
|
|||||||
}
|
}
|
||||||
.wordmark {
|
.wordmark {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.02em;
|
||||||
font-size: 0.95rem;
|
font-size: 0.98rem;
|
||||||
}
|
}
|
||||||
.links {
|
.links {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -7,23 +7,36 @@
|
|||||||
*
|
*
|
||||||
* Each project sits at slot Y = TOP_PAD + i * SLOT_HEIGHT, at the
|
* 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.
|
* 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.
|
* Cards are lab-slides: frosted glass surface with a thick strand-coloured
|
||||||
* The strand geometry itself is static — keeps project anchors stable.
|
* 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 <a>, 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 = {
|
type StrandProject = {
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
strand: 'A' | 'B';
|
strand: 'A' | 'B';
|
||||||
coverUrl: string | null;
|
coverUrl: string | null;
|
||||||
|
links: ProjectLink[];
|
||||||
};
|
};
|
||||||
|
|
||||||
let { projects }: { projects: StrandProject[] } = $props();
|
let { projects }: { projects: StrandProject[] } = $props();
|
||||||
|
|
||||||
// Geometry constants (user-space units)
|
// Geometry (user-space units inside the SVG)
|
||||||
const W = 1000;
|
const W = 1000;
|
||||||
const CX = W / 2;
|
const CX = W / 2;
|
||||||
const AMP = 140;
|
const AMP = 140;
|
||||||
@@ -102,8 +115,7 @@
|
|||||||
<section
|
<section
|
||||||
class="vhelix"
|
class="vhelix"
|
||||||
style:--vh-h="{H}px"
|
style:--vh-h="{H}px"
|
||||||
style:--vh-w="{W}px"
|
aria-label="Projects bound to the R&D-lab strands"
|
||||||
aria-label="Projects bound to the HELIX strands"
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="vhelix-svg"
|
class="vhelix-svg"
|
||||||
@@ -113,24 +125,24 @@
|
|||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="vstrand-a" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="vstrand-a" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0%" stop-color="#0f52a5">
|
<stop offset="0%" stop-color="#0d4f9e">
|
||||||
<animate attributeName="offset" values="-0.3;1.3" dur="18s" repeatCount="indefinite" />
|
<animate attributeName="offset" values="-0.3;1.3" dur="18s" repeatCount="indefinite" />
|
||||||
</stop>
|
</stop>
|
||||||
<stop offset="50%" stop-color="#0c99d9">
|
<stop offset="50%" stop-color="#1fa0db">
|
||||||
<animate attributeName="offset" values="0.0;1.6" dur="18s" repeatCount="indefinite" />
|
<animate attributeName="offset" values="0.0;1.6" dur="18s" repeatCount="indefinite" />
|
||||||
</stop>
|
</stop>
|
||||||
<stop offset="100%" stop-color="#4dd0c2">
|
<stop offset="100%" stop-color="#6fc3ec">
|
||||||
<animate attributeName="offset" values="0.3;1.9" dur="18s" repeatCount="indefinite" />
|
<animate attributeName="offset" values="0.3;1.9" dur="18s" repeatCount="indefinite" />
|
||||||
</stop>
|
</stop>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="vstrand-b" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="vstrand-b" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0%" stop-color="#c084fc">
|
<stop offset="0%" stop-color="#bed137">
|
||||||
<animate attributeName="offset" values="-0.3;1.3" dur="22s" repeatCount="indefinite" />
|
<animate attributeName="offset" values="-0.3;1.3" dur="22s" repeatCount="indefinite" />
|
||||||
</stop>
|
</stop>
|
||||||
<stop offset="50%" stop-color="#7e6ce8">
|
<stop offset="50%" stop-color="#d8e36a">
|
||||||
<animate attributeName="offset" values="0.0;1.6" dur="22s" repeatCount="indefinite" />
|
<animate attributeName="offset" values="0.0;1.6" dur="22s" repeatCount="indefinite" />
|
||||||
</stop>
|
</stop>
|
||||||
<stop offset="100%" stop-color="#50a8d9">
|
<stop offset="100%" stop-color="#8fa024">
|
||||||
<animate attributeName="offset" values="0.3;1.9" dur="22s" repeatCount="indefinite" />
|
<animate attributeName="offset" values="0.3;1.9" dur="22s" repeatCount="indefinite" />
|
||||||
</stop>
|
</stop>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
@@ -154,8 +166,8 @@
|
|||||||
y1={r.y}
|
y1={r.y}
|
||||||
x2={r.x2}
|
x2={r.x2}
|
||||||
y2={r.y}
|
y2={r.y}
|
||||||
stroke="#86bbdd"
|
stroke="#6fc3ec"
|
||||||
stroke-opacity={0.08 + 0.32 * r.depth}
|
stroke-opacity={0.08 + 0.30 * r.depth}
|
||||||
stroke-width={0.6 + 1.0 * r.depth}
|
stroke-width={0.6 + 1.0 * r.depth}
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
@@ -181,8 +193,8 @@
|
|||||||
y1={r.y}
|
y1={r.y}
|
||||||
x2={r.x2}
|
x2={r.x2}
|
||||||
y2={r.y}
|
y2={r.y}
|
||||||
stroke="#a9daee"
|
stroke="#b8dff5"
|
||||||
stroke-opacity={0.15 + 0.50 * r.depth}
|
stroke-opacity={0.18 + 0.50 * r.depth}
|
||||||
stroke-width={1.0 + 1.6 * r.depth}
|
stroke-width={1.0 + 1.6 * r.depth}
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
@@ -206,7 +218,7 @@
|
|||||||
cx={s.nodeX}
|
cx={s.nodeX}
|
||||||
cy={s.y}
|
cy={s.y}
|
||||||
r="22"
|
r="22"
|
||||||
fill={s.project.strand === 'A' ? '#0c99d9' : '#c084fc'}
|
fill={s.project.strand === 'A' ? '#1fa0db' : '#bed137'}
|
||||||
fill-opacity="0.25"
|
fill-opacity="0.25"
|
||||||
filter="url(#vnode-glow)"
|
filter="url(#vnode-glow)"
|
||||||
class="vnode-halo"
|
class="vnode-halo"
|
||||||
@@ -215,49 +227,78 @@
|
|||||||
cx={s.nodeX}
|
cx={s.nodeX}
|
||||||
cy={s.y}
|
cy={s.y}
|
||||||
r="12"
|
r="12"
|
||||||
fill={s.project.strand === 'A' ? '#0c99d9' : '#c084fc'}
|
fill={s.project.strand === 'A' ? '#1fa0db' : '#bed137'}
|
||||||
/>
|
/>
|
||||||
<circle cx={s.nodeX} cy={s.y} r="6" fill="#07111d" />
|
<circle cx={s.nodeX} cy={s.y} r="6" fill="#07111d" />
|
||||||
<circle
|
<circle
|
||||||
cx={s.nodeX}
|
cx={s.nodeX}
|
||||||
cy={s.y}
|
cy={s.y}
|
||||||
r="3.5"
|
r="3.5"
|
||||||
fill={s.project.strand === 'A' ? '#4dd0c2' : '#c084fc'}
|
fill={s.project.strand === 'A' ? '#bed137' : '#1fa0db'}
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- Connector lines from each node out to its card edge -->
|
<!-- Connector dash lines from node to card -->
|
||||||
{#each slots as s}
|
{#each slots as s}
|
||||||
<line
|
<line
|
||||||
x1={s.nodeX}
|
x1={s.nodeX}
|
||||||
y1={s.y}
|
y1={s.y}
|
||||||
x2={s.side === 'right' ? W - 80 : 80}
|
x2={s.side === 'right' ? W - 80 : 80}
|
||||||
y2={s.y}
|
y2={s.y}
|
||||||
stroke={s.project.strand === 'A' ? '#0c99d9' : '#c084fc'}
|
stroke={s.project.strand === 'A' ? '#1fa0db' : '#bed137'}
|
||||||
stroke-opacity="0.35"
|
stroke-opacity="0.32"
|
||||||
stroke-width="1"
|
stroke-width="1"
|
||||||
stroke-dasharray="2 4"
|
stroke-dasharray="2 4"
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- HTML card layer absolutely positioned over the SVG -->
|
<!-- HTML lab-slide cards layered over the SVG -->
|
||||||
<div class="vhelix-cards">
|
<div class="vhelix-cards">
|
||||||
{#each slots as s}
|
{#each slots as s}
|
||||||
<a
|
<article
|
||||||
href="/projects/{s.project.slug}"
|
class="slide side-{s.side} strand-{s.project.strand}"
|
||||||
class="vcard side-{s.side} strand-{s.project.strand}"
|
style:top="{((s.y / H) * 100).toFixed(2)}%"
|
||||||
style:top="{(s.y / H * 100).toFixed(2)}%"
|
|
||||||
>
|
>
|
||||||
<span class="badge">
|
<!-- Strand-coloured edge stripe (lab-slide signature) -->
|
||||||
<span class="dot" aria-hidden="true"></span>
|
<span class="stripe" aria-hidden="true"></span>
|
||||||
{s.project.strand === 'A' ? 'Project' : 'Innovation'}
|
|
||||||
</span>
|
<!-- Overlay link covers the body but not the chips -->
|
||||||
<h3>{s.project.title}</h3>
|
<a
|
||||||
<p>{s.project.summary}</p>
|
href="/projects/{s.project.slug}"
|
||||||
<span class="cta">Open <span class="arrow">→</span></span>
|
class="slide-link"
|
||||||
</a>
|
aria-label={`Open ${s.project.title}`}
|
||||||
|
></a>
|
||||||
|
|
||||||
|
<header class="slide-head">
|
||||||
|
<span class="badge">
|
||||||
|
<span class="dot" aria-hidden="true"></span>
|
||||||
|
{s.project.strand === 'A' ? 'Project' : 'Innovation'}
|
||||||
|
</span>
|
||||||
|
<span class="serial" aria-hidden="true">
|
||||||
|
{String(s.index + 1).padStart(2, '0')}/{String(slots.length).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h3 class="title">{s.project.title}</h3>
|
||||||
|
<p class="summary">{s.project.summary}</p>
|
||||||
|
|
||||||
|
{#if s.project.links.length > 0}
|
||||||
|
<div class="chips">
|
||||||
|
{#each s.project.links.slice(0, 5) as l}
|
||||||
|
<LinkChip kind={l.kind} label={l.label} url={l.url} />
|
||||||
|
{/each}
|
||||||
|
{#if s.project.links.length > 5}
|
||||||
|
<span class="more-chips">+{s.project.links.length - 5}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<footer class="slide-foot">
|
||||||
|
<span class="open">Open detail <span class="arrow">→</span></span>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -286,41 +327,90 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vcard {
|
/* ========== LAB SLIDE CARD ========== */
|
||||||
|
.slide {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 280px;
|
width: 300px;
|
||||||
max-width: calc(50% - 110px);
|
max-width: calc(50% - 90px);
|
||||||
padding: 1rem 1.15rem 1.1rem;
|
padding: 1.1rem 1.25rem 0.95rem;
|
||||||
border-radius: 12px;
|
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);
|
border: 1px solid var(--color-helix-border);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(10px) saturate(140%);
|
||||||
text-decoration: none;
|
-webkit-backdrop-filter: blur(10px) saturate(140%);
|
||||||
color: inherit;
|
|
||||||
transform: translateY(-50%);
|
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);
|
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));
|
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;
|
right: 1.5rem;
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
.vcard.side-left {
|
.slide.side-left {
|
||||||
left: 1.5rem;
|
left: 1.5rem;
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vcard.strand-A {
|
.slide.strand-A {
|
||||||
--card-accent: var(--color-helix-process);
|
--card-accent: #1fa0db;
|
||||||
--card-accent-soft: rgba(12, 153, 217, 0.18);
|
--card-accent-soft: rgba(31, 160, 219, 0.18);
|
||||||
}
|
}
|
||||||
.vcard.strand-B {
|
.slide.strand-B {
|
||||||
--card-accent: var(--color-helix-accent-2);
|
--card-accent: #bed137;
|
||||||
--card-accent-soft: rgba(192, 132, 252, 0.18);
|
--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 {
|
.badge {
|
||||||
@@ -328,14 +418,13 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 0.68rem;
|
font-size: 0.66rem;
|
||||||
letter-spacing: 0.16em;
|
letter-spacing: 0.18em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--card-accent);
|
color: var(--card-accent);
|
||||||
padding: 0.18rem 0.55rem;
|
padding: 0.18rem 0.55rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--card-accent-soft);
|
background: var(--card-accent-soft);
|
||||||
margin-bottom: 0.6rem;
|
|
||||||
}
|
}
|
||||||
.badge .dot {
|
.badge .dot {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@@ -345,37 +434,73 @@
|
|||||||
box-shadow: 0 0 8px var(--card-accent);
|
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-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
margin: 0 0 0.35rem;
|
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);
|
color: var(--color-helix-ink-dim);
|
||||||
font-size: 0.92rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin: 0 0 0.55rem;
|
margin: 0 0 0.7rem;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
line-clamp: 3;
|
line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
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);
|
color: var(--card-accent);
|
||||||
font-size: 0.85rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.arrow {
|
.arrow {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition: transform 200ms ease;
|
transition: transform 200ms ease;
|
||||||
}
|
}
|
||||||
.vcard:hover .arrow {
|
.slide:hover .arrow {
|
||||||
transform: translateX(3px);
|
transform: translateX(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Node pulse (subtle "alive" feeling, no rotation) */
|
/* Node pulse */
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
:global(.vnode-halo) {
|
:global(.vnode-halo) {
|
||||||
animation: vnode-pulse 3.5s ease-in-out infinite;
|
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) {
|
@media (max-width: 760px) {
|
||||||
.vhelix {
|
.vhelix {
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
.vhelix-svg {
|
.vhelix-svg {
|
||||||
display: none; /* hide the wide-helix SVG on narrow screens */
|
display: none;
|
||||||
}
|
}
|
||||||
.vhelix-cards {
|
.vhelix-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -411,7 +535,7 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
}
|
}
|
||||||
.vcard {
|
.slide {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: auto !important;
|
top: auto !important;
|
||||||
right: auto !important;
|
right: auto !important;
|
||||||
@@ -420,7 +544,7 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
.vcard:hover {
|
.slide:hover {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
* Site-wide configuration. Edit here to rebrand without touching components.
|
* Site-wide configuration. Edit here to rebrand without touching components.
|
||||||
*/
|
*/
|
||||||
export const SITE = {
|
export const SITE = {
|
||||||
name: 'HELIX',
|
name: 'R&D-lab',
|
||||||
tagline: 'EVOLV and every R&D strand, one helix.',
|
shortName: 'R&D-lab',
|
||||||
|
tagline: 'Projects, innovations, and every strand between.',
|
||||||
description:
|
description:
|
||||||
'The R&D showcase platform of Waterschap Brabantse Delta. EVOLV at its core, every innovation along the strands.',
|
'The R&D lab of Waterschap Brabantse Delta. EVOLV at its core, every project and innovation along the strands.',
|
||||||
organization: 'Waterschap Brabantse Delta R&D',
|
organization: 'Waterschap Brabantse Delta',
|
||||||
giteaOrg: 'RnD',
|
giteaOrg: 'RnD',
|
||||||
giteaBaseUrl: 'https://gitea.wbd-rd.nl'
|
giteaBaseUrl: 'https://gitea.wbd-rd.nl'
|
||||||
} as const;
|
} as const;
|
||||||
@@ -32,3 +33,13 @@ export const LINK_KIND_LABEL: Record<LinkKind, string> = {
|
|||||||
paper: 'Paper',
|
paper: 'Paper',
|
||||||
video: 'Video'
|
video: 'Video'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Short labels used inline on cards (≤ 6 chars). */
|
||||||
|
export const LINK_KIND_SHORT: Record<LinkKind, string> = {
|
||||||
|
gitea: 'Repo',
|
||||||
|
dashboard: 'Dash',
|
||||||
|
demo: 'Demo',
|
||||||
|
docs: 'Docs',
|
||||||
|
paper: 'Paper',
|
||||||
|
video: 'Video'
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ marked.setOptions({
|
|||||||
*
|
*
|
||||||
* Trust model: authoring is gated to members of the configured Gitea org,
|
* 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).
|
* 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 {
|
export function renderMarkdown(md: string): string {
|
||||||
return marked.parse(md) as string;
|
return marked.parse(md) as string;
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { projects, posts } from '$lib/server/db/schema';
|
import { projects, posts, projectLinks } from '$lib/server/db/schema';
|
||||||
import { desc, eq, isNotNull } from 'drizzle-orm';
|
import { asc, desc, eq, isNotNull } from 'drizzle-orm';
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
const helixProjects = db
|
// Top 12 projects + their links (one round-trip via Drizzle relational query)
|
||||||
.select({
|
const helixProjects = await db.query.projects.findMany({
|
||||||
slug: projects.slug,
|
where: eq(projects.status, 'published'),
|
||||||
title: projects.title,
|
orderBy: [desc(projects.updatedAt)],
|
||||||
summary: projects.summary,
|
limit: 12,
|
||||||
strand: projects.strand,
|
columns: {
|
||||||
coverUrl: projects.coverUrl
|
slug: true,
|
||||||
})
|
title: true,
|
||||||
.from(projects)
|
summary: true,
|
||||||
.where(eq(projects.status, 'published'))
|
strand: true,
|
||||||
.orderBy(desc(projects.updatedAt))
|
coverUrl: true
|
||||||
.limit(12)
|
},
|
||||||
.all();
|
with: {
|
||||||
|
links: {
|
||||||
|
columns: { kind: true, label: true, url: true, position: true },
|
||||||
|
orderBy: [asc(projectLinks.position)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const totalPublished = db
|
const totalPublished = db
|
||||||
.select({ slug: projects.slug })
|
.select({ slug: projects.slug })
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<p class="eyebrow">R&D · {SITE.organization}</p>
|
<p class="eyebrow">R&D · {SITE.organization}</p>
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
<span class="word">HELIX</span>
|
<span class="word">{SITE.name}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="tagline">{SITE.tagline}</p>
|
<p class="tagline">{SITE.tagline}</p>
|
||||||
<p class="lede">{SITE.description}</p>
|
<p class="lede">{SITE.description}</p>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
{#if data.helixProjects.length === 0}
|
{#if data.helixProjects.length === 0}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<h2>The strands are empty</h2>
|
<h2>The strands are empty</h2>
|
||||||
<p>HELIX needs its first project. <a href="/projects/new">Add one →</a></p>
|
<p>R&D-lab needs its first project. <a href="/projects/new">Add one →</a></p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<header class="section-head">
|
<header class="section-head">
|
||||||
@@ -167,12 +167,12 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.leg-a {
|
.leg-a {
|
||||||
background: var(--color-helix-process);
|
background: #1fa0db;
|
||||||
box-shadow: 0 0 10px var(--color-helix-process);
|
box-shadow: 0 0 10px #1fa0db;
|
||||||
}
|
}
|
||||||
.leg-b {
|
.leg-b {
|
||||||
background: var(--color-helix-accent-2);
|
background: #bed137;
|
||||||
box-shadow: 0 0 10px var(--color-helix-accent-2);
|
box-shadow: 0 0 10px #bed137;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-hint {
|
.scroll-hint {
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- HELIX SECTION ---------- */
|
/* ---------- STRAND SECTION ---------- */
|
||||||
.helix-anchor {
|
.helix-anchor {
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const GET = async ({ url, cookies }) => {
|
|||||||
if (GITEA_ALLOWED_ORG) {
|
if (GITEA_ALLOWED_ORG) {
|
||||||
const allowed = await isUserInOrg(accessToken, giteaUser.login, GITEA_ALLOWED_ORG);
|
const allowed = await isUserInOrg(accessToken, giteaUser.login, GITEA_ALLOWED_ORG);
|
||||||
if (!allowed) {
|
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.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="rounded-2xl border border-helix-border bg-helix-bg-2/60 p-10 backdrop-blur">
|
<div class="rounded-2xl border border-helix-border bg-helix-bg-2/60 p-10 backdrop-blur">
|
||||||
<h1 class="text-3xl font-semibold tracking-tight">Sign in to {SITE.name}</h1>
|
<h1 class="text-3xl font-semibold tracking-tight">Sign in to {SITE.name}</h1>
|
||||||
<p class="mt-3 text-helix-ink-dim">
|
<p class="mt-3 text-helix-ink-dim">
|
||||||
HELIX uses your <strong class="text-helix-ink">Gitea</strong> account at
|
{SITE.name} uses your <strong class="text-helix-ink">Gitea</strong> account at
|
||||||
<code class="font-mono text-sm">{SITE.giteaBaseUrl.replace('https://', '')}</code>.
|
<code class="font-mono text-sm">{SITE.giteaBaseUrl.replace('https://', '')}</code>.
|
||||||
Anyone can read; authoring is restricted to the
|
Anyone can read; authoring is restricted to the
|
||||||
<code class="font-mono text-sm">{SITE.giteaOrg}</code> organisation.
|
<code class="font-mono text-sm">{SITE.giteaOrg}</code> organisation.
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p class="mt-6 text-xs text-helix-ink-faint">
|
<p class="mt-6 text-xs text-helix-ink-faint">
|
||||||
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}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PostCard from '$lib/components/PostCard.svelte';
|
import PostCard from '$lib/components/PostCard.svelte';
|
||||||
|
import { SITE } from '$lib/config';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Posts · HELIX</title>
|
<title>Posts · {SITE.name}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="page">
|
<section class="page">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { SITE } from '$lib/config';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
const published = $derived(
|
const published = $derived(
|
||||||
data.post.publishedAt
|
data.post.publishedAt
|
||||||
@@ -12,7 +14,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{data.post.title} · HELIX</title>
|
<title>{data.post.title} · {SITE.name}</title>
|
||||||
<meta name="description" content={data.post.summary || data.post.title} />
|
<meta name="description" content={data.post.summary || data.post.title} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import { SITE } from '$lib/config';
|
||||||
|
|
||||||
let { form } = $props();
|
let { form } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>New post · HELIX</title>
|
<title>New post · {SITE.name}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="page">
|
<section class="page">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ProjectCard from '$lib/components/ProjectCard.svelte';
|
import ProjectCard from '$lib/components/ProjectCard.svelte';
|
||||||
|
import { SITE } from '$lib/config';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Projects · HELIX</title>
|
<title>Projects · {SITE.name}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="page">
|
<section class="page">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LinkChips from '$lib/components/LinkChips.svelte';
|
import LinkChips from '$lib/components/LinkChips.svelte';
|
||||||
import DashboardEmbed from '$lib/components/DashboardEmbed.svelte';
|
import DashboardEmbed from '$lib/components/DashboardEmbed.svelte';
|
||||||
|
import { SITE } from '$lib/config';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
const updated = $derived(
|
const updated = $derived(
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{data.project.title} · HELIX</title>
|
<title>{data.project.title} · {SITE.name}</title>
|
||||||
<meta name="description" content={data.project.summary} />
|
<meta name="description" content={data.project.summary} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { LINK_KINDS, LINK_KIND_LABEL } from '$lib/config';
|
import { LINK_KINDS, LINK_KIND_LABEL, SITE } from '$lib/config';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
let { form } = $props();
|
let { form } = $props();
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>New project · HELIX</title>
|
<title>New project · {SITE.name}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="page">
|
<section class="page">
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<!-- Dark backplate -->
|
||||||
<rect width="24" height="24" rx="5" fill="#07111d"/>
|
<rect width="24" height="24" rx="5" fill="#07111d"/>
|
||||||
<path d="M5 4 C 5 10, 19 14, 19 20" fill="none" stroke="#4dd0c2" stroke-width="2.2" stroke-linecap="round"/>
|
|
||||||
<path d="M5 20 C 5 14, 19 10, 19 4" fill="none" stroke="#0c99d9" stroke-width="2.2" stroke-linecap="round"/>
|
<!-- WBD-style tilted square mark (echo of the Waterschap diamond) -->
|
||||||
<line x1="6" y1="6" x2="6" y2="6.5" stroke="#a9daee" stroke-width="1.2"/>
|
<g transform="rotate(45 12 12)">
|
||||||
<line x1="12" y1="12" x2="12" y2="12.5" stroke="#a9daee" stroke-width="1.2"/>
|
<rect x="5.5" y="5.5" width="13" height="13" rx="0.5" fill="#0d4f9e" fill-opacity="0.9"/>
|
||||||
<line x1="18" y1="18" x2="18" y2="18.5" stroke="#a9daee" stroke-width="1.2"/>
|
<rect x="8.2" y="8.2" width="7.6" height="7.6" rx="0.4" fill="#1fa0db"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Helix strand A (front) — WBD lime, wraps around -->
|
||||||
|
<path d="M4 4 C 4 10, 20 14, 20 20"
|
||||||
|
fill="none" stroke="#bed137" stroke-width="2.1" stroke-linecap="round"/>
|
||||||
|
<!-- Helix strand B (back) — softer cyan -->
|
||||||
|
<path d="M4 20 C 4 14, 20 10, 20 4"
|
||||||
|
fill="none" stroke="#1fa0db" stroke-width="2.1" stroke-linecap="round" stroke-opacity="0.9"/>
|
||||||
|
|
||||||
|
<!-- Center "active site" lime dot -->
|
||||||
|
<circle cx="12" cy="12" r="1.9" fill="#bed137"/>
|
||||||
|
<circle cx="12" cy="12" r="0.9" fill="#07111d"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 581 B After Width: | Height: | Size: 950 B |
@@ -4,12 +4,19 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
// Waterschap Brabantse Delta brand palette (from WSBD-logo.svg)
|
||||||
|
wbd: {
|
||||||
|
deep: '#0a3d80',
|
||||||
|
blue: '#0d4f9e',
|
||||||
|
cyan: '#1fa0db',
|
||||||
|
lime: '#bed137'
|
||||||
|
},
|
||||||
helix: {
|
helix: {
|
||||||
area: '#0f52a5',
|
area: '#0a3d80',
|
||||||
process: '#0c99d9',
|
process: '#0d4f9e',
|
||||||
unit: '#50a8d9',
|
unit: '#1fa0db',
|
||||||
equipment: '#86bbdd',
|
equipment: '#6fc3ec',
|
||||||
control: '#a9daee',
|
control: '#b8dff5',
|
||||||
bg: '#07111d',
|
bg: '#07111d',
|
||||||
'bg-2': '#0c1c30',
|
'bg-2': '#0c1c30',
|
||||||
'bg-3': '#122842',
|
'bg-3': '#122842',
|
||||||
@@ -17,8 +24,8 @@ export default {
|
|||||||
ink: '#e6f1fb',
|
ink: '#e6f1fb',
|
||||||
'ink-dim': '#8fa6b8',
|
'ink-dim': '#8fa6b8',
|
||||||
'ink-faint': '#5b7388',
|
'ink-faint': '#5b7388',
|
||||||
accent: '#4dd0c2',
|
accent: '#bed137',
|
||||||
'accent-2': '#c084fc'
|
'accent-2': '#d8e36a'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
|
|||||||
Reference in New Issue
Block a user