191 lines
11 KiB
Plaintext
191 lines
11 KiB
Plaintext
---
|
|
import '../styles/global.css';
|
|
// `ambient` opts a page into the whole-page drifting brand wash (home only — see DESIGN.md)
|
|
// `type`/`image` feed the social meta: articles pass type="article" + their cover.
|
|
const { title = 'CTAO Science Portal', description = 'CTAO Science Portal — demo', ambient = false, type = 'website', image, lang = 'en' } = Astro.props;
|
|
// Absolute URLs for canonical/OG/RSS (head pattern from the official Astro blog
|
|
// template). `site` is a placeholder domain until the portal has a real one.
|
|
const site = Astro.site ?? new URL('https://portal.ctao.org');
|
|
const canonical = new URL(Astro.url.pathname, site);
|
|
// SVG covers never reach og:image — link-preview crawlers (Slack/Teams/
|
|
// LinkedIn) don't render SVG, so those articles fall back to the brand card.
|
|
const ogImage = new URL(image && !image.endsWith('.svg') ? image : '/brand/og-card.jpg', site);
|
|
const path = Astro.url.pathname;
|
|
const isActive = (href) => (href === '/' ? path === '/' : path.startsWith(href));
|
|
// Navigation = services from the Science Portal spec (REQUIREMENTS.md §2/§5).
|
|
// Order: serial-position effect (NN/g) — News (universal entry) first, Support
|
|
// (help convention) last; Data/Proposals by task frequency; Dashboard is the
|
|
// personal area, placed late next to the Sign-in cluster. External systems
|
|
// built by other teams are links out, clearly marked.
|
|
const links = [
|
|
{ href: '/news', label: 'News' },
|
|
{ href: 'https://padc-ctao-data-explorer.obspm.fr/', label: 'Data', ext: true },
|
|
{ href: '/proposals', label: 'Proposals' },
|
|
{ href: '/dashboard', label: 'Dashboard' },
|
|
{ href: '/support', label: 'Support' },
|
|
];
|
|
---
|
|
<!doctype html>
|
|
<html lang={lang}>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>{title}</title>
|
|
<meta name="description" content={description} />
|
|
<link rel="canonical" href={canonical} />
|
|
{/* Social cards (Open Graph + Twitter) — article cover when present, brand card otherwise */}
|
|
<meta property="og:site_name" content="CTAO Science Portal" />
|
|
<meta property="og:title" content={title} />
|
|
<meta property="og:description" content={description} />
|
|
<meta property="og:type" content={type} />
|
|
<meta property="og:url" content={canonical} />
|
|
<meta property="og:image" content={ogImage} />
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<link rel="alternate" type="application/rss+xml" title="CTAO Science Portal — News" href={new URL('/rss.xml', site)} />
|
|
<link rel="sitemap" href="/sitemap.xml" />
|
|
<link rel="icon" href="/brand/CTAO_Logo_positive.svg" type="image/svg+xml" />
|
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
<meta name="theme-color" content="#00004a" />
|
|
{/* The home hero photo is a CSS background, which browsers discover late —
|
|
preloading it moves the page's LCP element to the front of the queue. */}
|
|
{path === '/' && <link rel="preload" as="image" href="/brand/hero.jpg" fetchpriority="high" />}
|
|
{/* Brand typography (BRAND D.3): Inter (body) + Space Grotesk (headlines),
|
|
self-hosted latin woff2 (variable) — @font-face lives in global.css. */}
|
|
{/* Both brand fonts ship inlined in the render-blocking stylesheet —
|
|
no font swap exists anywhere (see global.css @font-face). */}
|
|
</head>
|
|
<body class={ambient ? 'has-ambient' : undefined}>
|
|
<a class="skip" href="#main">Skip to content</a>
|
|
<header class="site-header">
|
|
<div class="container nav">
|
|
<a class="brand" href="/" aria-label="CTAO Science Portal — home">
|
|
<img src="/brand/AAFF_CTAO_Logo_RGB_Monochrome-negative.svg" alt="CTAO" />
|
|
<span class="brand-sub">Science Portal</span>
|
|
</a>
|
|
<nav aria-label="Main navigation">
|
|
{links.map((l) => l.ext ? (
|
|
<a class="navlink external" href={l.href} target="_blank" rel="noopener">{l.label}</a>
|
|
) : (
|
|
<a class="navlink" href={l.href} aria-current={isActive(l.href) ? 'page' : undefined}>{l.label}</a>
|
|
))}
|
|
</nav>
|
|
<span class="spacer"></span>
|
|
{/* Site search. Desktop ≥1025px: a VISIBLE quiet field — NN/g's
|
|
magnifying-glass-icon research: hiding search behind an icon
|
|
measurably reduces discoverability and usage; a visible input is
|
|
recommended when search matters (200+ articles, spec-required).
|
|
Plain GET form (works without JS); suggestions via search-client.js. */}
|
|
<form class="nav-search" role="search" aria-label="Site search" action="/search" method="get">
|
|
<label class="sr-only" for="hdr-q">Search news</label>
|
|
<svg class="ico search-ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg>
|
|
<input id="hdr-q" name="q" type="search" placeholder="Search…" autocomplete="off" />
|
|
<ul class="suggest" id="hdr-suggest"></ul>
|
|
<p class="sr-only" id="hdr-suggest-status" role="status" aria-live="polite"></p>
|
|
</form>
|
|
{/* ≤1024px: the magnifier reveals a search row under the header (NN/g
|
|
mobile-search pattern). name="header-panel" pairs it with the menu as
|
|
a native exclusive group — opening one closes the other, no JS. */}
|
|
<details class="search-pop" name="header-panel">
|
|
<summary aria-label="Search">
|
|
<svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg>
|
|
</summary>
|
|
<form role="search" aria-label="Site search" action="/search" method="get">
|
|
<label class="sr-only" for="pop-q">Search news</label>
|
|
<input id="pop-q" name="q" type="search" placeholder="Search news…" autocomplete="off" />
|
|
<button class="btn" type="submit">Search</button>
|
|
<ul class="suggest" id="pop-suggest"></ul>
|
|
<p class="sr-only" id="pop-suggest-status" role="status" aria-live="polite"></p>
|
|
</form>
|
|
</details>
|
|
<a class="login-mock" href="/login">Sign in</a>
|
|
{/* Mobile disclosure menu — native <details>/<summary>, no JS; desktop hides it via CSS */}
|
|
<details class="menu" name="header-panel">
|
|
<summary aria-label="Menu">
|
|
<svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><path class="i-menu" d="M3 6h18M3 12h18M3 18h18" /><path class="i-close" d="M5 5l14 14M19 5 5 19" /></svg>
|
|
<span class="menu-label">Menu</span>
|
|
</summary>
|
|
<nav aria-label="Main navigation">
|
|
{links.map((l) => l.ext ? (
|
|
<a class="external" href={l.href} target="_blank" rel="noopener">{l.label}</a>
|
|
) : (
|
|
<a href={l.href} aria-current={isActive(l.href) ? 'page' : undefined}>{l.label}</a>
|
|
))}
|
|
</nav>
|
|
</details>
|
|
</div>
|
|
</header>
|
|
{/* Search enhancement is deferred — a synchronous script here would stall
|
|
the parser mid-body on every navigation (blank below the header until
|
|
it arrives). `defer` + the inline `type="module"` init join the same
|
|
after-parse in-order queue (HTML spec), so newsSearch is defined first.
|
|
The forms are plain GET /search, so nothing user-facing waits on JS. */}
|
|
<script is:inline src="/search-client.js" defer></script>
|
|
<script is:inline type="module">
|
|
// Header search suggestions (desktop field + mobile reveal); the forms
|
|
// are plain GET /search, so everything below is progressive enhancement.
|
|
newsSearch({ input: 'hdr-q', list: 'hdr-suggest', status: 'hdr-suggest-status', limit: 6, unit: 'suggestion', dismiss: true });
|
|
newsSearch({ input: 'pop-q', list: 'pop-suggest', status: 'pop-suggest-status', limit: 6, unit: 'suggestion', dismiss: true });
|
|
var pop = document.querySelector('.search-pop');
|
|
if (pop) pop.addEventListener('toggle', function () {
|
|
var q = document.getElementById('pop-q');
|
|
if (pop.open && q) q.focus();
|
|
});
|
|
// Light dismiss for the header panels (menu + search reveal) — scrim
|
|
// convention (Material/iOS, NN/g error prevention): a tap outside an
|
|
// open panel ONLY closes it and must never activate what's underneath.
|
|
// <details> has no native light dismiss; the Popover API was evaluated
|
|
// and rejected here — its light dismiss deliberately lets the outside
|
|
// click THROUGH (::backdrop is spec-forced pointer-events: none), the
|
|
// exact behavior research ruled out. Canceling pointerdown doesn't
|
|
// cancel the later click (Pointer Events spec), hence the click guard.
|
|
var swallow = false;
|
|
document.addEventListener('pointerdown', function (e) {
|
|
var open = document.querySelector('details[name="header-panel"][open]');
|
|
swallow = !!(open && !open.contains(e.target));
|
|
if (swallow) { open.open = false; e.preventDefault(); }
|
|
}, true);
|
|
document.addEventListener('click', function (e) {
|
|
if (swallow) { swallow = false; e.preventDefault(); e.stopPropagation(); }
|
|
}, true);
|
|
// Escape closes the open panel and returns focus to its trigger button
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key !== 'Escape') return;
|
|
var open = document.querySelector('details[name="header-panel"][open]');
|
|
if (open) { open.open = false; open.querySelector('summary').focus(); }
|
|
});
|
|
</script>
|
|
<main id="main">
|
|
<slot />
|
|
</main>
|
|
<footer class="footer">
|
|
{/* Bottom menu required by SPEC §3.3.1: disclaimers, privacy, contact, settings, search.
|
|
Site settings has no target yet — placeholder link = <a> without href. */}
|
|
<div class="container">
|
|
<div class="foot-top">
|
|
<img src="/brand/AAFF_CTAO_Logo_RGB_Monochrome-negative.svg" alt="CTAO" />
|
|
<p>CTAO Science Portal — demo by Cyfronet (SUSS-PORT team).</p>
|
|
</div>
|
|
{/* Order: action/utility first, legal middle, meta last (GOV.UK/NN-g
|
|
footer anatomy). Spec §3.3.1 requires the five items, not an order. */}
|
|
<nav class="foot-links" aria-label="Footer">
|
|
<a href="/pages/contact">Contact</a>
|
|
<a href="/search">Search</a>
|
|
<a href="/rss.xml">RSS</a>
|
|
<a href="/pages/disclaimer">Disclaimer</a>
|
|
<a href="/pages/privacy">Privacy</a>
|
|
<a>Site settings</a>
|
|
{/* Internal tooling stays out of the main nav (NN/g: navigation reflects
|
|
user tasks); the editor is a discreet utility link for the demo. */}
|
|
<a href="/admin/">Content editor</a>
|
|
</nav>
|
|
<div class="foot-note">
|
|
{/* The real brand flash — path taken from the official logo SVG (BRAND D.4).
|
|
Sole decorative accent in this view; static by design. */}
|
|
<svg class="flash" viewBox="85.3 15 15 15" aria-hidden="true"><path fill="currentColor" d="M94.7,23.8c1-1.4,3.3-2.1,5.2-2.7c-2,0.1-4.4,0.3-5.8-0.6c-1.4-1-2.1-3.3-2.7-5.2c0.1,2,0.3,4.4-0.6,5.8c-1,1.4-3.3,2.1-5.2,2.7c2-0.1,4.4-0.3,5.8,0.6c1.4,1,2.1,3.3,2.7,5.2C93.9,27.6,93.7,25.2,94.7,23.8z"/></svg>
|
|
<span>© 2026 CTAO — demonstration, not an official service.</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</body>
|
|
</html>
|