From 419e9a2dd19dd02674c298d67aa2a8d8a791caf9 Mon Sep 17 00:00:00 2001 From: ctao Date: Sat, 25 Jul 2026 07:32:11 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20content-UX=20=E2=80=94=20header=20searc?= =?UTF-8?q?h,=20metadata=20de-noise,=20icon/microcopy=20coherence,=20artic?= =?UTF-8?q?le=20composition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DESIGN.md | 73 +++++++++++ public/admin/config.yml | 20 +-- src/layouts/Base.astro | 81 ++++++++++--- src/pages/dashboard.astro | 2 +- src/pages/index.astro | 31 +++-- src/pages/login.astro | 7 +- src/pages/news/[...page].astro | 8 +- src/pages/news/[slug].astro | 33 ++++- src/pages/proposals.astro | 6 +- src/pages/search.astro | 2 +- src/pages/support.astro | 2 +- src/styles/global.css | 215 ++++++++++++++++++++++++--------- 12 files changed, 361 insertions(+), 119 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 24cf2c3..e55eeb8 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -123,6 +123,79 @@ is untouched; the TOC works fully without JS. Same precedent covers the search `` term highlight (Cherenkov tint) and the `.pagination` pill nav (Galaxy pill = current page, 44px targets). +## Header search & one-row width math + +Search lives in the header. Desktop (≥1025px): compact GET-form pill (flex +130–180px) in the utility cluster; suggestions reuse `.suggest`. ≤1024px: a +44px magnifier `
` reveals a full-width search row under the header +(visible search beats search buried in a menu — NN/g mobile search). Both +forms are plain `GET /search` (full no-JS path); `
` +makes search + menu a native exclusive pair (no JS). The `/search` page stays +the full-results and deep-link target. + +Nav content rule: the header nav reflects end-user tasks only (NN/g — +navigation mirrors user goals, not org tooling). The content editor is +internal-team tooling (SPEC §3.2), so it lives as a discreet "Content editor" +utility link in the footer, pointed to by the demo note; /admin keeps working +as the direct URL. + +One-row invariant: `.nav > *` is `flex: none`; only the spacer and the search +field flex, so the row can never wrap or overflow. Tier math (Inter widths +estimated ±8%; logo 153:32 → 124px at 26px height; inner width = viewport − +2×gutter, container caps at 1120): + +| Tier | Budget | Available | +|---|---|---| +| ≥1100 | brand+sub 247 + nav 415 + search 186 + Sign in 91 + gaps 16 ≈ 955 | 1044 @1100 | +| 1025–1099 (sub-brand hidden) | ≈ 842 | 969 @1025 | +| 481–1024 (menu + magnifier) | brand 134 + magnifier 44 + Sign in 91 + Menu 92 + gaps 16 ≈ 377 | 445 @481 | +| ≤480 (logo 22px, icon-only menu) | ≈ 312 | 339 @375 | +| ≤360 (compact paddings, gap 2) | ≈ 277 | 292 @320 | + +The search field's 50px shrink range (180 → 130) absorbs estimate error in +every tier. No focus-grow on the field: a width transition would be a layout +animation (motion policy allows transform/opacity only) and an unanimated +snap is worse than a calm fixed width. + +## Content & microcopy + +- Card metadata must differentiate (NN/g: omit non-differentiating metadata): + category and author are identical on all articles, so cards show + date · reading time (body words ÷ 220 wpm, computed at build). Category and + author remain on the article page; meta order there is + category · author · date · read time. +- Badges: "Mock — " (bare "Mock" only on tiles); "External — + "; "Planned — not yet available". Mock badges are never removed. +- Terminology is fixed: "Sign in" (never Log in), "Editor", "Search", "News". + Buttons start with a verb, sentence case. Real ellipsis (…), never "...". +- Search links (`.suggest a`, `.search-results a`) lay out as flowing text — + never `display: flex` on the `` itself: `` term-highlight splits the + title into fragments, and flex would turn each into its own item. +- Overflow containment happens at the source: wide `.prose` tables scroll in + their own box, long words/URLs wrap via `overflow-wrap`. The + `html, body { overflow-x: clip }` rule is a regression guard, not a fix — + never rely on it for new components. +- Articles end with `.post-nav` (Newer ← → Older from the date-sorted + collection). Image captions: italic line under the image + (`.prose img + em` / `p > em:only-child`), muted and smaller. + +### Element rules (condensed) + +| Element | Rule | Source | +|---|---|---| +| Phase banner | THE one loud demo disclosure: Moon bar above header, "Demo" tag + one sentence | GOV.UK phase banner | +| Header nav | End-user tasks only; order News · Data · Proposals · Dashboard · Support (entry first, help last, personal area next to Sign in) | NN/g serial-position | +| Header search | Compact field ≥1025px, magnifier reveal ≤1024px; both plain GET /search | NN/g site search | +| Hero (home) | Headline + brand subtitle + two task CTAs (Cherenkov primary, ghost secondary) + one-line Latest teaser. Never a second search box; no fake live widgets | hero-CTA convention, one-search-location | +| Badges | Quiet hairline pill, muted sentence-case text, Galaxy dot on mocks; "Mock — reason"; never removed | REQUIREMENTS §5, NN/g hierarchy | +| Cards | Cover · title (clamp 3) · desc (clamp 2) · date · read time; no non-differentiating metadata | NN/g metadata | +| Pagination | Newer/Older + windowed numbers, 44px targets, aria-current | NN/g pagination | +| Article | Back "← All news"; meta category · author · date · read time; italic muted captions; TOC at ≥3 headings; post-nav | editorial convention | +| Forms | Visible labels above fields; placeholders are examples only; buttons start with a verb | GOV.UK forms, NN/g | +| Login | One line + button + one small-print line; no reassurance prose | auth brevity | +| Footer | Identity line → Contact · Search · Disclaimer · Privacy · Site settings · Content editor → © line; no implementation talk in UI copy | NN/g footers | +| Search results | Links lay out as flowing text (mark-safe); date chip; "Press Enter for all results" hint | — | + ## Do not - No new hues, tints, or grays — derive via `color-mix` from brand tokens only. diff --git a/public/admin/config.yml b/public/admin/config.yml index fbc8a04..0aa3617 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -18,8 +18,8 @@ public_folder: "/uploads" collections: - name: news - label: "Newsy" - label_singular: "Artykuł" + label: "News" + label_singular: "Article" folder: "src/content/news" create: true slug: "{{slug}}" @@ -27,14 +27,14 @@ collections: format: frontmatter summary: "{{title}} · {{date}}" fields: - - { name: title, label: "Tytuł", widget: string } - - { name: description, label: "Krótki opis", widget: text } - - { name: date, label: "Data", widget: datetime, date_format: "YYYY-MM-DD", time_format: false } - - { name: category, label: "Kategoria", widget: string, default: "news" } - - { name: author, label: "Autor", widget: string, default: "CTAO" } - - { name: cover, label: "Okładka", widget: image, required: false } - - { name: draft, label: "Szkic (nieopublikowany)", widget: boolean, default: false } - - { name: body, label: "Treść", widget: markdown } + - { name: title, label: "Title", widget: string } + - { name: description, label: "Short description", widget: text } + - { name: date, label: "Date", widget: datetime, date_format: "YYYY-MM-DD", time_format: false } + - { name: category, label: "Category", widget: string, default: "news" } + - { name: author, label: "Author", widget: string, default: "CTAO" } + - { name: cover, label: "Cover image", widget: image, required: false } + - { name: draft, label: "Draft (unpublished)", widget: boolean, default: false } + - { name: body, label: "Body", widget: markdown } - name: pages label: "Pages" diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 8fdeaf9..04a1d4f 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -11,12 +11,15 @@ const ogImage = new URL(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). -// External systems built by other teams are links out, clearly marked. +// 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: 'https://padc-ctao-data-explorer.obspm.fr/', label: 'Data', ext: true }, { href: '/support', label: 'Support' }, ]; --- @@ -45,6 +48,15 @@ const links = [ + {/* Demo disclosure — GOV.UK phase-banner pattern: one slim, honest banner + instead of implementation talk scattered through the UI. Not sticky; + it scrolls away and the header sticks below it. */} +
+
+ Demo + This is a demonstration of the CTAO Science Portal — content is sample data. +
+
+ +
@@ -92,23 +133,25 @@ const links = [
CTAO -

Science Portal — news, observation proposals, data and user services of the - Cherenkov Telescope Array Observatory.

+

CTAO Science Portal — demo by Cyfronet (SUSS-PORT team).

+ {/* 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. */}
{/* The real brand flash — path taken from the official logo SVG (BRAND D.4). Sole decorative accent in this view; static by design. */} - Science Portal Core demo (Cyfronet / SUSS-PORT team). Sample content - imported from ctao.org; the Site settings link is a placeholder. Statically - generated from Markdown (git-based CMS). + © 2026 CTAO — demonstration, not an official service.
diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index f038b15..31a94dc 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -58,7 +58,7 @@ const tickets = [

Search and download in the Data Explorer (external, LUX team).

-

Help-desk tickets

+

Support tickets

    {tickets.map((t) => (
  • diff --git a/src/pages/index.astro b/src/pages/index.astro index 299335b..2f64d22 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -7,6 +7,9 @@ const posts = (await getCollection('news', ({ data }) => !data.draft)) .slice(0, 3); const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d); const iso = (d) => d.toISOString().slice(0, 10); +// Reading time from the Markdown body (~220 wpm) — differentiating card metadata; +// category/author are identical across all articles, so cards omit them (NN/g). +const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220)); // Services per the Science Portal spec; unbuilt ones are labelled, never faked. const services = [ { title: 'Proposals', href: '/proposals', desc: 'Submit observation proposals, including ToO / MWL requests.', badge: 'mock' }, @@ -24,23 +27,18 @@ const services = [
    Cherenkov Telescope Array Observatory

    CTAO Science Portal

    Exploring the Universe at the Highest Energies

    - + {/* Search lives in the header (one search location); the hero carries the + two core scientist tasks per SPEC §3.1 instead — classic hero-CTA pair. */} + + {posts[0] && ( +

    Latest: {posts[0].data.title}

    + )}
- - - {/* Surface rhythm (DESIGN.md): services on Moon Gray, editorial news on white */}
@@ -56,7 +54,7 @@ const services = [

{s.desc}

{s.badge === 'mock' && Mock} {s.badge === 'ext' && External — LUX team} - {s.badge === 'tbd' && Planned (TBD)} + {s.badge === 'tbd' && Planned — not yet available} ))}
@@ -73,10 +71,9 @@ const services = [
{post.data.cover && }
- {post.data.category}

{post.data.title}

{post.data.description}

-
{post.data.author} ·
+
· {readMin(post)} min read
))} diff --git a/src/pages/login.astro b/src/pages/login.astro index d5edf68..bb511a8 100644 --- a/src/pages/login.astro +++ b/src/pages/login.astro @@ -8,11 +8,10 @@ import Base from '../layouts/Base.astro'; Mock — AAI integration pending

Sign in

-

One account for all CTAO services. Authentication is handled by the - central CTAO AAI (single sign-on) — the portal never sees your password.

+ {/* Auth pages are scanned, not read — one line, no reassurance prose */} +

Use your CTAO account — single sign-on via CTAO AAI.

-

No account yet? Register via CTAO AAI · Access level follows - your CTAO role and permissions.

+

No account? Register via CTAO AAI

diff --git a/src/pages/news/[...page].astro b/src/pages/news/[...page].astro index 2fe483a..960d381 100644 --- a/src/pages/news/[...page].astro +++ b/src/pages/news/[...page].astro @@ -15,6 +15,9 @@ const { page } = Astro.props; const first = page.currentPage === 1; const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d); const iso = (d) => d.toISOString().slice(0, 10); +// Reading time from the Markdown body (~220 wpm) — differentiating card metadata; +// category/author are identical across all articles, so cards omit them (NN/g). +const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220)); // Windowed page list — 1 … n-1 n n+1 … last (0 marks an ellipsis) const nums = []; for (let n = 1; n <= page.lastPage; n++) { @@ -32,7 +35,7 @@ const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);
Cherenkov Telescope Array Observatory

News & Announcements

-

Exploring the Universe at the Highest Energies

+

Science highlights and updates from the observatory and its partners

@@ -45,10 +48,9 @@ const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`); )}
- {post.data.category}

{post.data.title}

{post.data.description}

-
{post.data.author} ·
+
· {readMin(post)} min read
))} diff --git a/src/pages/news/[slug].astro b/src/pages/news/[slug].astro index 8e89138..058ea6e 100644 --- a/src/pages/news/[slug].astro +++ b/src/pages/news/[slug].astro @@ -3,14 +3,22 @@ import { getCollection, render } from 'astro:content'; import Base from '../../layouts/Base.astro'; export async function getStaticPaths() { - const posts = await getCollection('news', ({ data }) => !data.draft); - return posts.map((post) => ({ params: { slug: post.id }, props: { post } })); + // Date-sorted so each article knows its sequential neighbours (prev/next + // pattern of every editorial site — keeps readers in the content flow). + const posts = (await getCollection('news', ({ data }) => !data.draft)).sort( + (a, b) => b.data.date.valueOf() - a.data.date.valueOf(), + ); + return posts.map((post, i) => ({ + params: { slug: post.id }, + props: { post, newer: posts[i - 1] ?? null, older: posts[i + 1] ?? null }, + })); } -const { post } = Astro.props; +const { post, newer, older } = Astro.props; const { Content, headings } = await render(post); const toc = headings.filter((h) => h.depth === 2 || h.depth === 3); const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d); +const readMin = Math.max(1, Math.round((post.body ?? '').split(/\s+/).length / 220)); --- {/* TOC pattern (MDN/Stripe/NN-g): sticky right rail ≥1200px, collapsed
@@ -19,7 +27,8 @@ const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).forma
← All news

{post.data.title}

-
{post.data.category}{post.data.author}·
+ {/* Meta order: category · author · date · reading time */} +
{post.data.category}{post.data.author}··{readMin} min read
{toc.length >= 3 && (
On this page @@ -34,6 +43,22 @@ const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).forma
+ {(newer || older) && ( + + )}
{toc.length >= 3 && (