Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8b47ac888 | ||
|
|
6ed5d5859a | ||
|
|
751fd0ec79 | ||
|
|
13f945fbf3 | ||
|
|
2e6d374b4b | ||
|
|
c4d21b59be | ||
|
|
cec8018c18 | ||
|
|
c64528a1d3 | ||
|
|
746b015e85 | ||
|
|
a74eef0e47 | ||
|
|
c6de06f017 | ||
|
|
bbbb090cce | ||
|
|
54de9d3ee9 | ||
|
|
f6340a89b3 | ||
|
|
93712a1481 | ||
|
|
71b9808a11 | ||
|
|
c0edb2033d | ||
|
|
145490fe60 | ||
|
|
48fa17c351 | ||
|
|
ba629e2a96 | ||
|
|
d06c2cf0c0 | ||
|
|
c0d06f4a17 | ||
|
|
99f5601456 | ||
|
|
29385a5ca7 | ||
|
|
5744c16a1c | ||
|
|
bc0201252d | ||
|
|
46ec5e2e3c | ||
|
|
80d886c0cb | ||
|
|
ed7a9c01e7 | ||
|
|
4102974927 | ||
|
|
beb585e5dc | ||
|
|
47902f7278 | ||
|
|
0bb378e0cc | ||
|
|
694b7d2003 | ||
|
|
61f5d8444f | ||
|
|
affa3262ef | ||
|
|
1ef2cab89e | ||
|
|
c9161af655 | ||
|
|
d0eeedb62b | ||
|
|
185380ffa5 | ||
|
|
e7c804c9da | ||
|
|
075d44dce3 | ||
|
|
6e3f8e23b6 | ||
|
|
95f8f7b1a6 | ||
|
|
4082be0c37 | ||
|
|
97d1a0aa43 | ||
|
|
3362b214c3 | ||
|
|
ffe54bc766 | ||
|
|
4fa35e5317 | ||
|
|
eaf511a976 | ||
|
|
3202b853a7 | ||
|
|
d564f840e8 | ||
|
|
0516cae5b2 | ||
|
|
d014445248 | ||
|
|
419e9a2dd1 | ||
|
|
a8833ce736 | ||
|
|
4ace039233 | ||
|
|
2de0cf148e | ||
|
|
3b736af25e | ||
|
|
ea4ebd2d70 | ||
|
|
a8c0e802e2 | ||
|
|
cf35739a64 | ||
|
|
11e7eb484f | ||
|
|
009a6ae52a | ||
|
|
7c949bbb3c | ||
|
|
02f30af010 | ||
|
|
778ab25063 | ||
|
|
7286121f71 | ||
|
|
366c95aa86 | ||
|
|
f8181396b1 | ||
|
|
757752d91f | ||
|
|
45c5e3c407 | ||
|
|
228ad5b60b | ||
|
|
ed45ff6d54 | ||
|
|
8a2bf69576 | ||
|
|
4180882065 | ||
|
|
c2016e723f | ||
|
|
a1c85d73c7 |
@@ -1,4 +0,0 @@
|
||||
*.sh text eol=lf
|
||||
*.service text eol=lf
|
||||
*.timer text eol=lf
|
||||
*.container text eol=lf
|
||||
@@ -3,15 +3,10 @@ dist/
|
||||
.astro/
|
||||
.DS_Store
|
||||
|
||||
# Content lives in the ctao/content repo (deploy copies it in; npm run content:link links ../content locally)
|
||||
# Content lives in the ctao/content repo (build overlays it; scripts/link-content.sh locally)
|
||||
src/content/news/
|
||||
src/content/pages/
|
||||
public/uploads/
|
||||
|
||||
# Session scratch
|
||||
.tmp/
|
||||
|
||||
# Local secrets and editor state
|
||||
.env*
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Design handoff
|
||||
|
||||
All styling lives in **`src/styles/global.css`** — design tokens in `:root`
|
||||
(colors, type scale, spacing, radius, shadows, `--header-h`) + plain-CSS
|
||||
components below them. No frameworks, no preprocessors, no runtime JS for
|
||||
presentation. Pages are `.astro` templates (plain HTML).
|
||||
|
||||
## Brand (official: CTAO Brand Guidelines, April 2024 — PDF in the project docs)
|
||||
|
||||
- Colors: Galaxy Blue `#00004A`, Cherenkov Blue `#00E4D8`, Moon Gray
|
||||
`#F5F5F5`, Cosmic Azure `#007AFF` (links/focus), Interstellar Indigo
|
||||
`#00009C`. Cherenkov is an ACCENT (brand book rations it — currently:
|
||||
primary CTA, hero subtitle, logo flash); it fails AA as text on white.
|
||||
- Type: **Inter** for everything, **Space Grotesk** (weight 500) only for
|
||||
communicative headlines (h1/h2). Sizes come from the `--fs-*` scale.
|
||||
- Logo files in `public/brand/`; always "CTAO", min 100px wide, no effects.
|
||||
|
||||
## Practical notes (the non-obvious ones)
|
||||
|
||||
- **`public/admin/preview.css` mirrors the `.prose` styles** so the CMS
|
||||
editor preview matches articles 1:1 — when you touch `.prose` or its
|
||||
tokens, update the mirror (both files carry a KEEP IN SYNC comment).
|
||||
- Fonts ship **inlined as data: URIs** in global.css (no font requests, no
|
||||
swap flash). The woff2 files in `public/fonts/` look unused but feed the
|
||||
CMS preview — keep them.
|
||||
- All `animation`/`transition` rules sit in the single
|
||||
`prefers-reduced-motion: no-preference` block at the end of global.css.
|
||||
- Scroll-driven effects need `vite.build.cssMinify: 'esbuild'`
|
||||
(astro.config.mjs) — the default minifier breaks `animation-timeline`.
|
||||
- `--header-h` drives the sticky-header offsets (anchors, TOC rail, reading
|
||||
progress); change the header height only through the token.
|
||||
- Deterministic gate before pushing: `npm run check` (build + link check).
|
||||
@@ -1,54 +1,78 @@
|
||||
# CTAO Science Portal
|
||||
# CTAO Science Portal — static site + git-based CMS
|
||||
|
||||
Static Astro site with a git-based CMS (Sveltia). Code lives here; editorial
|
||||
content lives in the separate [`ctao/content`](https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content)
|
||||
repository. The Home page and four Example pages are editable; the rest are placeholders.
|
||||
Public portal for CTAO news, built as a fully static site. Editors get a
|
||||
browser WYSIWYG editor (Sveltia CMS) that commits Markdown to a Gitea repo;
|
||||
a build job turns commits into static HTML. No application or database runs
|
||||
on the public path.
|
||||
|
||||
- Live demo: https://astro.isl-dev.grid.cyfronet.pl
|
||||
- Editor: https://astro.isl-dev.grid.cyfronet.pl/admin/ (Gitea sign-in)
|
||||
Live demo: <https://astro.isl-dev.grid.cyfronet.pl> ·
|
||||
Gitea on the demo machine: <https://astro-git.isl-dev.grid.cyfronet.pl>
|
||||
|
||||
## Run locally
|
||||
## Repository layout & deployment
|
||||
|
||||
Node 22.19 or newer. Clone the content repository next to this one:
|
||||
Code and content are SEPARATE repositories, so editorial commits never mix
|
||||
with development:
|
||||
|
||||
```sh
|
||||
git clone https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git ../content
|
||||
| Repo | Holds | Source of truth | Who commits |
|
||||
|---|---|---|---|
|
||||
| `ctao-portal` (this one, Bitbucket) | Templates, CSS, deploy | development | the team |
|
||||
| `ctao/content` (Gitea on the demo machine) | `news/`, `pages/`, `uploads/` | editorial | Sveltia CMS / editors |
|
||||
|
||||
The demo machine polls ITS Gitea (`ctao/portal` mirror + `ctao/content`) and
|
||||
republishes on a push to either — so a push to Bitbucket does NOT deploy;
|
||||
deploying code = pushing it to the machine's Gitea (a deliberate step). The
|
||||
build overlays content onto code (`deploy/build.sh`); the content paths are
|
||||
gitignored here.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Astro** (static output). Pages are `.astro` templates (plain HTML with a
|
||||
JS frontmatter block). No React/Vue/Svelte components, no client-side
|
||||
framework.
|
||||
- **Plain CSS** — everything lives in `src/styles/global.css` as design
|
||||
tokens (custom properties) + rules. No Tailwind, no preprocessor.
|
||||
The design system (tokens, type scale, spacing, motion policy) is
|
||||
documented in `DESIGN.md` — read it before touching styles.
|
||||
- **Sveltia CMS** — a single prebuilt JS bundle, vendored in
|
||||
`public/vendor/sveltia-cms.js` and loaded on `/admin/`
|
||||
(`src/pages/admin/index.astro`), configured by `public/admin/config.yml`.
|
||||
It runs entirely in the editor's browser and talks to the Gitea API
|
||||
(OAuth PKCE). There is no CMS server. To update it:
|
||||
`curl -sL https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js -o public/vendor/sveltia-cms.js`
|
||||
- Runtime dependencies: none beyond Astro. This is deliberate — keep it
|
||||
that way.
|
||||
|
||||
## Develop
|
||||
|
||||
```
|
||||
npm ci
|
||||
git clone https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git ../ctao-content
|
||||
./scripts/link-content.sh # copies news/pages/uploads into the dev tree
|
||||
npm run dev # http://localhost:4321
|
||||
npm run build # static output in dist/
|
||||
npm run check # deterministic gate: build + internal-link check
|
||||
```
|
||||
|
||||
The site reads `../content` directly through links, so pull that repository
|
||||
for the latest content. To use a checkout elsewhere:
|
||||
`npm run content:link -- <path>`.
|
||||
## Layout
|
||||
|
||||
Before pushing code, run `npm run check` (types, build, internal links).
|
||||
|
||||
## Edit content locally
|
||||
|
||||
With `npm run dev` running, open `http://localhost:4321/admin/` in Chrome or
|
||||
Edge, choose **Work with Local Repository** and select the `../content` folder.
|
||||
Saves write the Markdown files and show up on the local site on reload. They
|
||||
are not committed: commit and push in `../content` to publish.
|
||||
|
||||
## Publishing
|
||||
|
||||
Pushing to `main` of `ctao/portal` (code) or `ctao/content` (content) on
|
||||
Gitea rebuilds and publishes the live demo automatically; the online editor
|
||||
commits to `ctao/content` directly. Pushing to Bitbucket does not deploy.
|
||||
Server setup is in [deploy/README.md](deploy/README.md).
|
||||
|
||||
## Where things live
|
||||
|
||||
| Path | Purpose |
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `src/layouts/Base.astro` | Header, navigation, footer, page shell |
|
||||
| `src/layouts/ContentPage.astro` | Layout of the editable pages (Home and Examples) |
|
||||
| `src/data/examples.json` | Example pages: menu labels, order, slugs |
|
||||
| `src/data/placeholders.json` | Placeholder pages |
|
||||
| `src/styles/global.css` | All site styles |
|
||||
| `public/admin/config.yml` | CMS fields and Gitea connection |
|
||||
| `scripts/preview-styles.mjs` | Builds the CMS preview styles from `global.css` |
|
||||
| `scripts/content.mjs` | Links the content checkout into the site |
|
||||
| `src/pages/` | Routes (`.astro` templates), incl. `news/`, `search`, RSS/sitemap |
|
||||
| `src/layouts/Base.astro` | HTML shell: head, header/nav, footer |
|
||||
| `src/content/news/*.md` | Articles (from the content repo — gitignored here; schema in `src/content.config.ts`) |
|
||||
| `src/styles/global.css` | All CSS: tokens + components + prose |
|
||||
| `DESIGN.md` | Design handoff: where the tokens live, brand basics, practical notes |
|
||||
| `public/admin/` | CMS config (`config.yml`) + editor preview styles (`preview.css`) |
|
||||
| `public/uploads/` | Editor-uploaded media (from the content repo — gitignored here) |
|
||||
| `deploy/` | Runbook + container/systemd units for the demo machine — see `deploy/README.md` |
|
||||
|
||||
To add an Example page, add it to `src/data/examples.json` and
|
||||
`public/admin/config.yml`, and commit its Markdown file to `ctao/content`.
|
||||
## Editing content
|
||||
|
||||
Editors use `/admin/` (link in the footer) and sign in with a Gitea account.
|
||||
Saving commits to `main`; the machine polls and republishes automatically
|
||||
(seconds). Full history/rollback = git history in Gitea. Articles with
|
||||
`draft: true` are excluded from the build.
|
||||
|
||||
Note for styling work: `public/admin/preview.css` mirrors the `.prose` rules
|
||||
from `global.css` so the editor preview matches the site 1:1 — keep them in
|
||||
sync (both files carry a KEEP IN SYNC comment).
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// Static output (zero runtime) — the whole point of the git-based approach.
|
||||
export default defineConfig({
|
||||
output: 'static',
|
||||
// Public origin — used to build absolute URLs (canonical, og:*, RSS,
|
||||
// sitemap). Change when the portal moves to its production domain.
|
||||
site: 'https://astro.isl-dev.grid.cyfronet.pl',
|
||||
trailingSlash: 'always',
|
||||
build: { format: 'directory' },
|
||||
compressHTML: true,
|
||||
// Built-in prefetch on every internal link (no per-link attributes needed).
|
||||
// Default 'hover' strategy: near-instant navigation without the bandwidth
|
||||
// cost of 'viewport' on a 24-card grid; auto-falls back to 'tap' on
|
||||
// data-saver / slow connections (Astro handles that).
|
||||
prefetch: { prefetchAll: true },
|
||||
server: { port: 4321, host: true },
|
||||
vite: {
|
||||
build: {
|
||||
// Lightning CSS (the default minifier) illegally folds animation-timeline
|
||||
// INTO the `animation` shorthand (`animation: ... view()`), which browsers
|
||||
// reject — silently killing every scroll-driven effect in production.
|
||||
// esbuild minifies without property merging.
|
||||
cssMinify: 'esbuild',
|
||||
},
|
||||
server: {
|
||||
// Vite blocks unknown Host headers by default; the demo is viewed through an
|
||||
// ephemeral cloudflared quick tunnel, so allow any *.trycloudflare.com host.
|
||||
allowedHosts: ['.trycloudflare.com'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# Deployment log — CTAO portal demo on strapi-experimental.cyfronet
|
||||
|
||||
Living document: what was done, what broke, what is left. Update it as work
|
||||
proceeds so any agent (or human) can resume without the chat history.
|
||||
Started 2026-07-28.
|
||||
|
||||
## Goal
|
||||
|
||||
Run the git-based CMS demo (Gitea + Sveltia + Astro static build) on the
|
||||
Cyfronet test machine so the SUSS-PORT team can click through it. Shape must
|
||||
be production-viable, not throwaway. Machine footprint: one directory
|
||||
(`~/ctao-portal-demo/`) + 4 unit files, fully removable (README "Uninstall").
|
||||
|
||||
## Ground rules (from the user, non-negotiable)
|
||||
|
||||
- **Write every command out in chat, marked read-only / write, BEFORE running
|
||||
it** — the user reviews on a phone and cannot see truncated tool calls.
|
||||
- Non-invasive verification first, debugging second: confirm each precondition
|
||||
instead of fixing after the fact.
|
||||
- Never read secrets (`~/.ssh/*`, tokens). Mask token URLs in any output
|
||||
(`sed 's|://[^@]*@|://***@|g'`). Secrets stay in 0600 files on the machine.
|
||||
- No sudo, nothing outside `$HOME` on the machine, no traces for the admin.
|
||||
- Temp/working files under `.tmp/<date>-<topic>/` in the repo, never `/tmp`.
|
||||
- Commit messages: **no "Co-Authored-By: Claude"**, no session links.
|
||||
|
||||
## Architecture (decided, see also README.md in this dir)
|
||||
|
||||
```
|
||||
editor → Sveltia (/admin, static) → commit → Gitea :3000 (container, SQLite)
|
||||
│ polled every 10 s
|
||||
build.sh (systemd user timer)
|
||||
└─ podman run node+git, --network=container:ctao-demo-gitea
|
||||
npm ci (only if lockfile changed) → astro build
|
||||
→ releases/<sha>/ → atomic symlink flip
|
||||
│
|
||||
nginx :8080 serves releases/current
|
||||
│ (TODO) ingress vhost → team access
|
||||
```
|
||||
|
||||
## Machine facts (verified 2026-07-28, read-only)
|
||||
|
||||
- Rocky Linux 9.7, host `outline.openstacklocal`, IP 192.168.10.15, 2 vCPU,
|
||||
3.6 GB RAM, no swap. User `strapi`, no sudo, `Linger=yes`, user systemd running.
|
||||
- Podman 5.6.0 rootless. Host has node 24.17, npm, curl, jq, rsync — **no git**,
|
||||
**no nginx** (hence the build container / nginx container).
|
||||
- Neighbours: pod-strapi (`:1337`), Outline (`:9091`), code-server (`127.0.0.1:41787`).
|
||||
- **VPN does NOT route to machine ports** (`curl 192.168.10.15:1337` times out);
|
||||
only the admin-managed HTTPS ingress reaches the machine
|
||||
(`https://strapi.isl-dev.grid.cyfronet.pl` → 204). Hence ports 3000/8080 are
|
||||
reachable for us only via `ssh -L`, and the team needs vhosts from Hubert.
|
||||
|
||||
## DONE
|
||||
|
||||
### 1. Local: files authored + reviewed + tested (before touching the machine)
|
||||
- Wrote quadlets, `build.sh`, systemd service/timer, `nginx.conf`,
|
||||
`Containerfile.build`, `README.md` (runbook incl. uninstall).
|
||||
- **Opus security review** — 8 findings, every one verified personally:
|
||||
BLOCKER `--network=host` (build code could reach host loopback: code-server
|
||||
= RCE, Strapi) → replaced; `OnUnitActiveSec` is start-relative, not
|
||||
end-relative (agent corrected me) → `OnUnitInactiveSec`; missing `mkdir` for
|
||||
`~/.config/*`; nginx-before-first-build race; prune `pipefail` landmine;
|
||||
noisy unit failure when Gitea is down; CORS `SCHEME` note; RAM headroom.
|
||||
All fixed. No hallucinated findings.
|
||||
- **Local end-to-end test** against the local Gitea (`:3010`), sandbox
|
||||
`.tmp/2026-07-28-deploy-test/`: cold build 23 s · no-op poll 45 ms ·
|
||||
incremental rebuild 4 s · prune ✓ · Gitea-down grace ✓ · anonymous clone
|
||||
(no token in `.git/config`) ✓.
|
||||
- Commits (local Gitea repo `ctao/portal`): `ed7a9c0`, `80d886c`.
|
||||
|
||||
### 2. Machine: install steps 1–7 of README
|
||||
- Created `~/ctao-portal-demo/{gitea-data,gitea-config,releases,state,bin,config}`
|
||||
and `~/.config/{containers/systemd,systemd/user}`.
|
||||
- Copied unit files, `build.sh`, `nginx.conf`, `Containerfile.build`.
|
||||
- Pulled + **pinned by digest**: gitea 1.27-rootless, nginx stable-alpine,
|
||||
node:24-alpine; built `localhost/ctao-portal-build:1`.
|
||||
- Gitea running (`:3000`, API version 1.27.1), admin `ctao` created with
|
||||
`--random-password` → `~/ctao-portal-demo/state/initial-admin.txt` (0600),
|
||||
push token → `state/push-token.txt` (0600). **Never printed in chat.**
|
||||
- Content pushed via git bundle (78 MB, one-off bootstrap): repo `ctao/portal`
|
||||
at `80d886c`, public, anonymously clonable. Bundle deleted afterwards.
|
||||
- **First build on the machine: 44 s** (`npm ci` ~35 s + astro build 6.5 s),
|
||||
225 pages. Incremental rebuilds should be ~10 s (measure to confirm).
|
||||
- nginx container running, `http://localhost:8080/` → **200, `<title>CTAO
|
||||
Science Portal</title>`**.
|
||||
- Poll timer enabled (`ctao-portal-build.timer`), ticking every ~10 s, journal
|
||||
clean.
|
||||
|
||||
### 3. Problems hit on the machine (and the fixes)
|
||||
1. **Rootless bridge network impossible**: `netavark: modprobe ip_tables:
|
||||
Operation not permitted` → quadlet restart loop. Root cause: kernel module
|
||||
not loaded, rootless cannot modprobe, no sudo. **Fix:** dropped
|
||||
`ctao-demo.network`; build joins Gitea's netns
|
||||
(`--network=container:ctao-demo-gitea`), so it reaches Gitea on
|
||||
`localhost:3000` while the host loopback stays unreachable (pasta).
|
||||
Same security property as the bridge, zero privileges. Lesson written to
|
||||
`.skills/podman-quadlet/SKILL.md`.
|
||||
2. **Push rejected**: admin was created with `--must-change-password` →
|
||||
`remote: Update your password`. **Fix:** `gitea admin user
|
||||
must-change-password --unset ctao`.
|
||||
3. **My rc-chain bug**: `... | sed` made `$?` the sed's status, so a failed
|
||||
push printed "push-ok" and the bundle was deleted prematurely → had to
|
||||
re-upload 78 MB. **Fix:** capture output in a var, check rc of the real
|
||||
command. (Also why the upload "took so long" — it ran twice.)
|
||||
4. **Push-created repo was private** despite `DEFAULT_PRIVATE=public` —
|
||||
push-to-create has its own key. **Fix:** PATCH via API to public +
|
||||
`GITEA__repository__DEFAULT_PUSH_CREATE_PRIVATE=false` in the quadlet.
|
||||
5. **`npm ci` failed: `ETXTBSY` spawning `esbuild`** (postinstall race in a
|
||||
rootless container, ~5 min wasted). **Fix:** `npm ci --ignore-scripts`
|
||||
(also removes the malicious-postinstall vector) + persistent npm cache
|
||||
volume `~/ctao-portal-demo/npm-cache`. Validated locally, then on machine.
|
||||
|
||||
### 4. Left on the machine (known, not cleaned — user asked to be told, not tidied)
|
||||
- Orphan systemd entry `ctao-demo-network.service` (not-found/active-exited),
|
||||
ghost of the removed network quadlet. Harmless; clears with
|
||||
`systemctl --user stop ctao-demo-network.service`.
|
||||
- `/tmp/storage-run-1003/` (~136 KB) — podman runroot from ssh calls without
|
||||
`XDG_RUNTIME_DIR`. Disappears on reboot. (`storage-run-1001` is NOT ours.)
|
||||
- Red journal entries from the netavark restart loop (history, not a live fault).
|
||||
- Disk: 11 G → 9.6 G free (images ~590 MB + repo/content). RAM available ~1.3 G.
|
||||
|
||||
### 5. Pipeline proven end-to-end on the machine (2026-07-28)
|
||||
- Pushed `46ec5e2` to the machine's Gitea (incremental bundle, 6 KB — this is
|
||||
what ongoing syncs cost, vs the 78 MB one-off bootstrap).
|
||||
- **The poller published it automatically in 26 s end-to-end** (push →
|
||||
visible), of which the build itself was 21 s. That is the demo's publish
|
||||
latency: **~30 s worst case** on 2 vCPU (vs 44 s for the very first build
|
||||
which included `npm ci`). Cold-cache builds only happen when the lockfile
|
||||
changes.
|
||||
- Verified after publish: `/` 200, `/admin/` 200 (Sveltia), an article page
|
||||
200; neighbours untouched (Strapi 204, Outline 200); RAM available 1.2 G,
|
||||
disk free 9.0 G.
|
||||
|
||||
### 6. Sveltia wired to the machine + editor cycle proven (2026-07-28)
|
||||
- **OAuth app created via API, no UI clicking**: temp `write:user` token from
|
||||
`gitea admin user generate-access-token` (CLI in container) → POST
|
||||
`/user/applications/oauth2` → temp token deleted (needed basic auth from
|
||||
`initial-admin.txt`; token-auth DELETE returns 401). App: "Sveltia CMS
|
||||
(demo)", `client_id cf1c44ac-ebe6-4a97-bf2f-f7f26ef1126c`, PKCE
|
||||
(`confidential_client=false`), redirect_uris: `http://localhost:18080/admin/`
|
||||
and `http://localhost:8080/admin/`.
|
||||
- **Found & fixed**: `public/admin/config.yml` still pointed at the MAC's Gitea
|
||||
(`localhost:3010` + old app_id) — CMS login on the machine could never work.
|
||||
Switched to `localhost:3000` + machine app_id (`bc02012`). localhost:3000 is
|
||||
valid both on the machine and through the tunnel (same-port trick).
|
||||
- **Editor cycle simulated exactly like Sveltia does it** (Gitea contents API):
|
||||
create article via API → **live on the portal in 12 s**; delete via API →
|
||||
**gone in 21 s**. Repo left clean (`29385a5`). Earlier full-push rebuild:
|
||||
16 s build. Publish latency ≈ **10–30 s** depending on poll-tick alignment.
|
||||
- Tunnel run from the Mac with keepalive (`-o ServerAliveInterval=30`), portal
|
||||
mapped to **18080** locally (user request: non-default port), Gitea kept at
|
||||
**3000** (must match ROOT_URL). First tunnel died after idle — keepalive fixed.
|
||||
- Push token scope confirmed minimal (`write:repository` only) — cannot manage
|
||||
users/apps; file format is `<name>:<40-hex>` (grep the hex out).
|
||||
|
||||
## TODO (next agent starts here)
|
||||
|
||||
1. ~~Commit the `--ignore-scripts` fix and mirror to the machine~~ — DONE
|
||||
(`46ec5e2`, and it doubled as the live pipeline test above).
|
||||
2. ~~Measure incremental publish latency on the machine~~ — DONE: **26 s
|
||||
end-to-end**, build 21 s.
|
||||
3. ~~Verify the loop through `ssh -L`~~ — mostly DONE: tunnel
|
||||
`-L 18080:localhost:8080 -L 3000:localhost:3000` (with ServerAlive
|
||||
keepalive) verified with real content. **Remaining: the user clicks
|
||||
"Sign in with Gitea" at `http://localhost:18080/admin/`** and edits an
|
||||
article in the browser — the only step that needs a human + browser
|
||||
(PKCE login). Credentials: `initial-admin.txt` on the machine.
|
||||
4. ~~Sveltia OAuth app~~ — DONE via API (see §6). Config committed and
|
||||
auto-published. Only the browser login test remains (see 3).
|
||||
5. **Message to Hubert** (only after we have seen it working):
|
||||
- two ingress vhosts → `192.168.10.15:8080` (portal) and `:3000` (gitea),
|
||||
same mechanism as `strapi.isl-dev…:1337`;
|
||||
- are vhosts public-internet or VPN-scopable? (Gitea preferably VPN-only);
|
||||
- does Cyfronet offer static-file hosting on the ingress itself? If yes we
|
||||
drop our nginx container and just rsync builds there.
|
||||
6. **After vhosts arrive**: replace both `TODO(vhost)` in
|
||||
`ctao-demo-gitea.container` (ROOT_URL, CORS origin + `GITEA__cors__SCHEME=https`),
|
||||
`daemon-reload` + restart, update Sveltia config, re-test login.
|
||||
7. **Backup**: nightly `tar` of `~/ctao-portal-demo/gitea-data` (the only
|
||||
stateful thing; everything else is rebuildable). Not yet written.
|
||||
8. **Decided but not implemented — split into two repos** (`portal` = code,
|
||||
`portal-content` = markdown + uploads). Rationale: editors get write access
|
||||
ONLY to content, so repo-push cannot alter `package.json`/templates that the
|
||||
build executes; also keeps code history clean. Cost: `build.sh` clones two
|
||||
repos and overlays content (~10 lines), polls two SHAs. Do this for the
|
||||
production version, not mid-demo.
|
||||
9. Demo choreography: the test article is `draft: true`; the user creates one
|
||||
live during the demo. With the poller running, a Sveltia save publishes
|
||||
automatically — no manual rebuild needed (unlike the Mac preview setup).
|
||||
|
||||
## Command cheat-sheet (machine)
|
||||
|
||||
```
|
||||
export XDG_RUNTIME_DIR=/run/user/$(id -u) # needed for systemctl --user over ssh
|
||||
systemctl --user status ctao-demo-gitea ctao-demo-web ctao-portal-build.timer
|
||||
journalctl --user -u ctao-portal-build -n 20 --no-pager # build times land here
|
||||
~/ctao-portal-demo/bin/build.sh # manual build
|
||||
readlink ~/ctao-portal-demo/releases/current # which sha is live
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/
|
||||
```
|
||||
@@ -1,10 +1,10 @@
|
||||
# Deploy: CTAO portal demo on strapi-experimental.cyfronet
|
||||
# Deploy — CTAO portal demo on strapi-experimental.cyfronet
|
||||
|
||||
Everything runs rootless as user `strapi`. Footprint on the machine:
|
||||
ONE directory (`~/ctao-portal-demo/`) + 4 unit files in `~/.config/`.
|
||||
Uninstall restores the machine exactly (see bottom). No secrets in any file.
|
||||
|
||||
Two Gitea repos by design: `ctao/portal` (code, developed by the team) and
|
||||
Two Gitea repos by design — `ctao/portal` (code, developed by the team) and
|
||||
`ctao/content` (Markdown + uploads, committed by the CMS). The build overlays
|
||||
content onto code; a push to EITHER republishes the site.
|
||||
|
||||
@@ -29,44 +29,9 @@ missing). Everything else runs inside containers.
|
||||
| 3000 | Gitea | https://astro-git.isl-dev.grid.cyfronet.pl (ingress vhost) |
|
||||
| 8080 | portal (nginx, static) | https://astro.isl-dev.grid.cyfronet.pl (ingress vhost) |
|
||||
|
||||
## Publish the simplified portal from `main`
|
||||
|
||||
1. Check that `ctao/content:main` contains the four Example files in `pages/`
|
||||
(listed in `src/data/examples.json`) and the uploads they reference. They
|
||||
are already committed there (`a3452d7`, "demo lorem ipsum pages"). News and
|
||||
other page files can remain in the content repository; this portal loads
|
||||
only the four Example files.
|
||||
2. Push the content branch to Gitea as `ctao/content:main`, and the code
|
||||
branch as `ctao/portal:main`. These are separate pushes; Bitbucket is
|
||||
not the deployment remote.
|
||||
3. Copy the updated `build.sh` to `~/ctao-portal-demo/bin/build.sh` and the
|
||||
updated `ctao-portal-build.service` to `~/.config/systemd/user/`. Reload
|
||||
systemd with `systemctl --user daemon-reload`. The service explicitly sets
|
||||
`CODE_BRANCH=main` and `CONTENT_BRANCH=main`; the script defaults agree.
|
||||
4. Start the build service or let its timer run. It overlays `pages/` and
|
||||
`uploads/`, clears the previous news overlay, and runs `npm run check`
|
||||
before the atomic release switch. Missing Example pages fail the build.
|
||||
5. Verify the four `/example/…/` pages and empty `/news/`. Sign in at `/admin/`,
|
||||
save an Example title, and check that a commit appears on
|
||||
`ctao/content:main` and the rebuilt page reflects it.
|
||||
|
||||
The OAuth client and public portal origin stay the same. Sveltia's
|
||||
`backend.branch` in `public/admin/config.yml` must always match
|
||||
`CONTENT_BRANCH`. Branches are selected independently; a single `BRANCH`
|
||||
environment variable is no longer used. For a separate preview hostname,
|
||||
update the site origin, OAuth redirect, and Gitea CORS together.
|
||||
|
||||
To undo the simplification in shared code history, use
|
||||
`git revert -m 1 <simplification-merge-commit>` on `main` and publish the
|
||||
resulting commit. The previous portal code is also preserved at `0772462`.
|
||||
For an immediate deployment rollback, restore the previous release and worker
|
||||
script/service configuration. Content has its own history: reverting a code
|
||||
commit does not revert CMS edits. Keep the old content files available if
|
||||
the previous portal may be restored.
|
||||
|
||||
## Install (each step reviewed before running; [W] = writes to the machine)
|
||||
|
||||
1. **[W]** `loginctl enable-linger $USER`. Without lingering every user unit
|
||||
1. **[W]** `loginctl enable-linger $USER` — without lingering every user unit
|
||||
dies at logout and nothing starts after a reboot. Verify:
|
||||
`loginctl show-user $USER -p Linger` → `Linger=yes`.
|
||||
2. **[W]** `mkdir -p ~/ctao-portal-demo/{gitea-data,gitea-config,releases,state,bin,config} ~/.config/containers/systemd ~/.config/systemd/user`
|
||||
@@ -84,16 +49,15 @@ the previous portal may be restored.
|
||||
(Upgrading later = pick new digests deliberately, update the pins in the
|
||||
`.container` files / `Containerfile.build`, re-pull, re-build.)
|
||||
5. **[W]** `systemctl --user daemon-reload && systemctl --user start ctao-demo-gitea`
|
||||
6. **[W]** Create the Gitea admin. Run it interactively in a terminal so the
|
||||
6. **[W]** Create the Gitea admin — run interactively in a terminal so the
|
||||
password never lands in a file or shell history:
|
||||
`podman exec -it ctao-demo-gitea gitea admin user create --admin --username <you> --email <you@…> --random-password`
|
||||
7. **[W]** In the Gitea UI: create org `ctao` with repos `portal` and
|
||||
`content` (both public read). The content `main` branch needs the four
|
||||
Example pages described above. Then, from your workstation, push both over
|
||||
`content` (both public read). Then, from your workstation, push both over
|
||||
the vhost with a repo-scoped token:
|
||||
`git push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/portal.git main`
|
||||
`git push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git main`
|
||||
8. **[W]** `systemctl --user enable --now ctao-portal-build.timer`. The first run
|
||||
8. **[W]** `systemctl --user enable --now ctao-portal-build.timer` — first run
|
||||
clones + `npm ci` + builds (minutes); later runs are seconds. Wait until
|
||||
`journalctl --user -u ctao-portal-build -n 5` shows `published <sha>`
|
||||
(starting nginx earlier just serves 404s until the first build lands).
|
||||
@@ -102,7 +66,7 @@ the previous portal may be restored.
|
||||
and `journalctl --user -u ctao-portal-build -n 20` (shows measured build times).
|
||||
|
||||
Editor accounts need WRITE access to `ctao/content` (Gitea → repo →
|
||||
Collaborators, or a team); public read alone lets them sign in but every
|
||||
Collaborators, or a team) — public read alone lets them sign in but every
|
||||
save fails.
|
||||
|
||||
## CMS sign-in (Sveltia ↔ Gitea OAuth)
|
||||
@@ -152,12 +116,12 @@ loginctl disable-linger $USER # only if nothing else of yours should survive l
|
||||
lockfile changes. Routine rebuilds are fully offline.
|
||||
- Memory caps (`MemoryHigh`) keep us polite next to Outline + Strapi;
|
||||
the build container is capped at 1 GB via `podman run --memory`. Watch the
|
||||
first `npm ci` + build in the journal; if it OOMs inside its cgroup
|
||||
first `npm ci` + build in the journal — if it OOMs inside its cgroup
|
||||
(contained, just retries), raise the cap.
|
||||
- Build isolation: build containers join the Gitea container's network
|
||||
namespace (`--network=container:ctao-demo-gitea`): repo/npm code sees
|
||||
namespace (`--network=container:ctao-demo-gitea`) — repo/npm code sees
|
||||
Gitea on localhost:3000 but cannot reach host loopback services. (Rootless
|
||||
netavark bridges don't work here: no `ip_tables` kernel module, no sudo.)
|
||||
- Secrets inventory: Gitea admin password (typed interactively, lives only
|
||||
in Gitea's DB) and repo-scoped push tokens (managed in Gitea); that's the
|
||||
in Gitea's DB) and repo-scoped push tokens (managed in Gitea) — that's the
|
||||
complete list. Build/poll/serve use none.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
# Two repositories by design: code (templates/CSS, developed by the team) and
|
||||
# content (Markdown + uploads, committed by the CMS). The build overlays
|
||||
# content onto code, so an editor publishing a page and a developer
|
||||
# content onto code, so an editor publishing an article and a developer
|
||||
# shipping CSS never mix histories — either change republishes the site.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -17,8 +17,7 @@ BASE="${BASE:-$HOME/ctao-portal-demo}"
|
||||
GITEA_URL="${GITEA_URL:-http://localhost:3000}" # published by ctao-demo-gitea
|
||||
CODE_REPO="${CODE_REPO:-ctao/portal}" # owner/repo in Gitea
|
||||
CONTENT_REPO="${CONTENT_REPO:-ctao/content}"
|
||||
CODE_BRANCH="${CODE_BRANCH:-main}"
|
||||
CONTENT_BRANCH="${CONTENT_BRANCH:-main}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}"
|
||||
# The build joins the Gitea container's network namespace: localhost inside
|
||||
# the build = Gitea's loopback (port 3000), host loopback stays unreachable.
|
||||
@@ -27,7 +26,7 @@ BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}"
|
||||
BUILD_NETNS="${BUILD_NETNS:-container:ctao-demo-gitea}"
|
||||
CODE_URL="${CODE_URL:-http://localhost:3000/$CODE_REPO.git}"
|
||||
CONTENT_URL="${CONTENT_URL:-http://localhost:3000/$CONTENT_REPO.git}"
|
||||
KEEP="${KEEP:-3}" # previous releases kept besides `current` (rollback targets)
|
||||
KEEP="${KEEP:-3}" # released builds to retain
|
||||
mkdir -p "$BASE/repo" "$BASE/content" "$BASE/releases" "$BASE/state" "$BASE/npm-cache"
|
||||
|
||||
# Host prerequisites (everything else runs inside containers). Fail loud —
|
||||
@@ -38,16 +37,16 @@ done
|
||||
|
||||
# --- 1. Cheap poll: both branch heads via the local Gitea API ---
|
||||
head_of() {
|
||||
curl -fsS --max-time 5 "$GITEA_URL/api/v1/repos/$1/branches/$2" \
|
||||
curl -fsS --max-time 5 "$GITEA_URL/api/v1/repos/$1/branches/$BRANCH" \
|
||||
| jq -r '.commit.id' || true
|
||||
}
|
||||
code_sha=$(head_of "$CODE_REPO" "$CODE_BRANCH")
|
||||
content_sha=$(head_of "$CONTENT_REPO" "$CONTENT_BRANCH")
|
||||
code_sha=$(head_of "$CODE_REPO")
|
||||
content_sha=$(head_of "$CONTENT_REPO")
|
||||
# Gitea down/unreachable is a transient, not a unit failure — exit 0 quietly
|
||||
# instead of painting the journal red every 10 s. Name the repo: a 404 here
|
||||
# also means "repo/branch missing or renamed", not just "Gitea down".
|
||||
[[ "$code_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "poll failed for $CODE_REPO@$CODE_BRANCH (gitea down, or repo/branch missing) — skipping"; exit 0; }
|
||||
[[ "$content_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "poll failed for $CONTENT_REPO@$CONTENT_BRANCH (gitea down, or repo/branch missing) — skipping"; exit 0; }
|
||||
[[ "$code_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "poll failed for $CODE_REPO@$BRANCH (gitea down, or repo/branch missing) — skipping"; exit 0; }
|
||||
[[ "$content_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "poll failed for $CONTENT_REPO@$BRANCH (gitea down, or repo/branch missing) — skipping"; exit 0; }
|
||||
release="${code_sha:0:12}-${content_sha:0:12}" # code+content pin the release
|
||||
# Skip only if this pair is both recorded AND still present in releases/
|
||||
# (a deleted release dir must trigger a rebuild, not an eternal skip).
|
||||
@@ -74,7 +73,7 @@ podman run --rm --network="$BUILD_NETNS" --memory=1g \
|
||||
-e ASTRO_TELEMETRY_DISABLED=1 \
|
||||
-e CODE_SHA="$code_sha" -e CODE_URL="$CODE_URL" \
|
||||
-e CONTENT_SHA="$content_sha" -e CONTENT_URL="$CONTENT_URL" \
|
||||
-e RELEASE="$release" -e CODE_BRANCH="$CODE_BRANCH" -e CONTENT_BRANCH="$CONTENT_BRANCH" \
|
||||
-e RELEASE="$release" -e BRANCH="$BRANCH" \
|
||||
-v "$BASE/repo:/work/repo:z" \
|
||||
-v "$BASE/content:/work/content:z" \
|
||||
-v "$BASE/releases:/work/releases:z" \
|
||||
@@ -82,20 +81,21 @@ podman run --rm --network="$BUILD_NETNS" --memory=1g \
|
||||
-w /work "$BUILD_IMAGE" sh -ec '
|
||||
git config --global safe.directory "/work/repo"
|
||||
git config --global --add safe.directory "/work/content"
|
||||
sync_clone() { # $1 dir $2 url $3 sha $4 branch
|
||||
[ -d "$1/.git" ] || git clone --branch "$4" "$2" "$1"
|
||||
sync_clone() { # $1 dir $2 url $3 sha
|
||||
[ -d "$1/.git" ] || git clone --branch "$BRANCH" "$2" "$1"
|
||||
git -C "$1" remote set-url origin "$2" # self-heal if the URL changes
|
||||
git -C "$1" fetch --quiet origin "$4"
|
||||
git -C "$1" fetch --quiet origin "$BRANCH"
|
||||
# --force: the working copy is disposable; a stray tracked-file edit
|
||||
# must not wedge every future build.
|
||||
git -C "$1" checkout --quiet --force "$3"
|
||||
}
|
||||
sync_clone repo "$CODE_URL" "$CODE_SHA" "$CODE_BRANCH"
|
||||
sync_clone content "$CONTENT_URL" "$CONTENT_SHA" "$CONTENT_BRANCH"
|
||||
sync_clone repo "$CODE_URL" "$CODE_SHA"
|
||||
sync_clone content "$CONTENT_URL" "$CONTENT_SHA"
|
||||
# Overlay content onto code (these paths are gitignored in the code repo).
|
||||
# mkdir -p: checkout prunes the emptied parent dirs, cp needs them back.
|
||||
rm -rf repo/src/content/news repo/src/content/pages repo/public/uploads
|
||||
mkdir -p repo/src/content repo/public
|
||||
cp -a content/news repo/src/content/news
|
||||
cp -a content/pages repo/src/content/pages
|
||||
cp -a content/uploads repo/public/uploads
|
||||
cd repo
|
||||
@@ -109,9 +109,7 @@ podman run --rm --network="$BUILD_NETNS" --memory=1g \
|
||||
npm ci --ignore-scripts --no-audit --no-fund
|
||||
echo "$lock" > .deps-hash
|
||||
fi
|
||||
# Validate the overlay and links before publishing; never seed demo content
|
||||
# in place of an incomplete content branch.
|
||||
npm run check
|
||||
npm run build
|
||||
rm -rf "../releases/$RELEASE"
|
||||
cp -a dist "../releases/$RELEASE"
|
||||
' || { echo "$release" > "$BASE/state/last-failed"; echo "BUILD FAILED for $release (see above) — will not retry until a new commit"; exit 1; }
|
||||
|
||||
@@ -40,7 +40,7 @@ Environment=GITEA__service__DISABLE_REGISTRATION=true
|
||||
Environment=GITEA__mailer__ENABLED=false
|
||||
# Repos default to public so the build pipeline can clone anonymously —
|
||||
# content is the public site anyway. The `ctao/portal` repo itself is
|
||||
# created in the UI (README step 7).
|
||||
# created in the UI (README step 6).
|
||||
Environment=GITEA__repository__DEFAULT_PRIVATE=public
|
||||
# Sveltia is served from the portal vhost and calls the Gitea API cross-origin
|
||||
# — CORS locked to exactly that origin. ALLOW_DOMAIN takes FULL origins with
|
||||
|
||||
@@ -8,8 +8,6 @@ Description=CTAO portal demo — rebuild if the code or content repo has new com
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=CODE_BRANCH=main
|
||||
Environment=CONTENT_BRANCH=main
|
||||
ExecStart=%h/ctao-portal-demo/bin/build.sh
|
||||
# Polite neighbour on the shared VM:
|
||||
Nice=10
|
||||
|
||||
@@ -8,14 +8,11 @@
|
||||
"name": "ctao-portal",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"astro": "7.2.10"
|
||||
"astro": "^7.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "0.9.10",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@astrojs/check": {
|
||||
@@ -3594,9 +3591,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -3794,9 +3791,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.28",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
|
||||
"integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
|
||||
"version": "8.5.22",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
|
||||
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3813,7 +3810,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.18",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
|
||||
@@ -4,23 +4,16 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"content:link": "node scripts/content.mjs --relink",
|
||||
"predev": "node scripts/content.mjs && node scripts/preview-styles.mjs",
|
||||
"dev": "astro dev",
|
||||
"prebuild": "node scripts/content.mjs && node scripts/preview-styles.mjs",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"precheck": "node scripts/content.mjs && node scripts/preview-styles.mjs",
|
||||
"check": "astro check && astro build && node scripts/check-links.mjs"
|
||||
"check": "astro build && node scripts/check-links.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "7.2.10"
|
||||
"astro": "^7.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "0.9.10",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
# these origins in deploy/ctao-demo-gitea.container.
|
||||
backend:
|
||||
name: gitea
|
||||
# Keep this branch in sync with CONTENT_BRANCH in deploy/build.sh.
|
||||
# Content lives in its OWN repo (editorial history separate from code —
|
||||
# the build overlays it onto the portal code, see deploy/build.sh).
|
||||
repo: ctao/content
|
||||
branch: main
|
||||
base_url: https://astro-git.isl-dev.grid.cyfronet.pl
|
||||
@@ -44,82 +45,36 @@ media_libraries:
|
||||
optimize: true
|
||||
|
||||
collections:
|
||||
- name: home
|
||||
label: "Home page"
|
||||
files:
|
||||
- name: home
|
||||
label: "Home page"
|
||||
file: "pages/home.md"
|
||||
- name: news
|
||||
label: "News"
|
||||
label_singular: "Article"
|
||||
folder: "news"
|
||||
create: true
|
||||
slug: "{{slug}}"
|
||||
extension: md
|
||||
format: frontmatter
|
||||
summary: "{{title}} · {{date}}"
|
||||
fields:
|
||||
- { name: title, label: "Title", widget: string }
|
||||
- { name: introduction, label: "Introduction", widget: text, required: false }
|
||||
- name: images
|
||||
label: "Images"
|
||||
label_singular: "image"
|
||||
widget: list
|
||||
required: false
|
||||
max: 2
|
||||
hint: "Up to two images, shown side by side."
|
||||
fields:
|
||||
- { name: image, label: "Image", widget: image }
|
||||
- { name: alt, label: "Alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
|
||||
- { name: caption, label: "Caption", widget: string, required: false }
|
||||
- { 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: lang, label: "Language (if not English, e.g. pl)", widget: string, required: false }
|
||||
- { name: draft, label: "Draft (unpublished)", widget: boolean, default: false }
|
||||
- { name: body, label: "Body", widget: markdown }
|
||||
- { name: buttonLabel, label: "Button label", widget: string, required: false }
|
||||
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." }
|
||||
- name: examples
|
||||
label: "Example pages"
|
||||
files:
|
||||
- name: lorem-ipsum
|
||||
label: "Lorem ipsum"
|
||||
file: "pages/lorem-ipsum.md"
|
||||
|
||||
- name: pages
|
||||
label: "Pages"
|
||||
label_singular: "Page"
|
||||
folder: "pages"
|
||||
create: true
|
||||
slug: "{{slug}}"
|
||||
extension: md
|
||||
format: frontmatter
|
||||
summary: "{{title}}"
|
||||
fields:
|
||||
- { name: title, label: "Title", widget: string }
|
||||
- { name: introduction, label: "Introduction", widget: text, required: false }
|
||||
- { name: image, label: "Image", widget: image, required: false }
|
||||
- { name: imageAlt, label: "Image alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
|
||||
- { name: imageCaption, label: "Image caption", widget: string, required: false }
|
||||
- { name: description, label: "Description", widget: text, required: false }
|
||||
- { name: body, label: "Body", widget: markdown }
|
||||
- { name: buttonLabel, label: "Button label", widget: string, required: false }
|
||||
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." }
|
||||
- name: dolor-sit-amet
|
||||
label: "Dolor sit amet"
|
||||
file: "pages/dolor-sit-amet.md"
|
||||
format: frontmatter
|
||||
fields:
|
||||
- { name: title, label: "Title", widget: string }
|
||||
- { name: introduction, label: "Introduction", widget: text, required: false }
|
||||
- { name: image, label: "Image", widget: image, required: false }
|
||||
- { name: imageAlt, label: "Image alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
|
||||
- { name: imageCaption, label: "Image caption", widget: string, required: false }
|
||||
- { name: body, label: "Body", widget: markdown }
|
||||
- { name: buttonLabel, label: "Button label", widget: string, required: false }
|
||||
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." }
|
||||
- name: consectetur-adipiscing
|
||||
label: "Consectetur adipiscing"
|
||||
file: "pages/consectetur-adipiscing.md"
|
||||
format: frontmatter
|
||||
fields:
|
||||
- { name: title, label: "Title", widget: string }
|
||||
- { name: introduction, label: "Introduction", widget: text, required: false }
|
||||
- { name: image, label: "Image", widget: image, required: false }
|
||||
- { name: imageAlt, label: "Image alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
|
||||
- { name: imageCaption, label: "Image caption", widget: string, required: false }
|
||||
- { name: body, label: "Body", widget: markdown }
|
||||
- { name: buttonLabel, label: "Button label", widget: string, required: false }
|
||||
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." }
|
||||
- name: sed-do-eiusmod
|
||||
label: "Sed do eiusmod"
|
||||
file: "pages/sed-do-eiusmod.md"
|
||||
format: frontmatter
|
||||
fields:
|
||||
- { name: title, label: "Title", widget: string }
|
||||
- { name: introduction, label: "Introduction", widget: text, required: false }
|
||||
- { name: image, label: "Image", widget: image, required: false }
|
||||
- { name: imageAlt, label: "Image alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
|
||||
- { name: imageCaption, label: "Image caption", widget: string, required: false }
|
||||
- { name: body, label: "Body", widget: markdown }
|
||||
- { name: buttonLabel, label: "Button label", widget: string, required: false }
|
||||
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." }
|
||||
|
||||
@@ -1,470 +1,71 @@
|
||||
/* Generated by scripts/preview-styles.mjs; edit src/styles/global.css. */
|
||||
|
||||
/* ==========================================================================
|
||||
CTAO Science Portal: content page mockup
|
||||
Colours and type follow the CTAO Brand Guidelines (April 2024).
|
||||
The editor preview (public/admin/preview.css) is generated from the
|
||||
hero, prose and button sections below by scripts/preview-styles.mjs.
|
||||
========================================================================== */
|
||||
|
||||
/* Fonts
|
||||
========================================================================== */
|
||||
|
||||
/* Entry-preview styles for the Sveltia editor, registered via the documented
|
||||
CMS.registerPreviewStyle API (bare element selectors, per the official docs
|
||||
example). This file MIRRORS the article styles 1:1 — the design tokens from
|
||||
:root and the `.prose` block in src/styles/global.css — with `.prose`
|
||||
dropped from selectors (the preview pane has no such wrapper).
|
||||
KEEP IN SYNC: when you touch `.prose` or the tokens in global.css, update
|
||||
the corresponding value here (a matching reminder sits next to `.prose`).
|
||||
Fonts: same self-hosted woff2 files the site uses, same origin. (The
|
||||
Sveltia bundle still loads its own UI fonts from jsDelivr — only the
|
||||
PREVIEW pane is CDN-free.) */
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-weight: 400 700;
|
||||
font-display: swap;
|
||||
src: url("/assets/fonts/inter-latin.woff2") format("woff2");
|
||||
font-family: "Inter"; font-weight: 400 700; font-display: swap;
|
||||
src: url("/fonts/inter-latin.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Space Grotesk";
|
||||
font-weight: 500 700;
|
||||
font-display: swap;
|
||||
src: url("/assets/fonts/space-grotesk-latin.woff2") format("woff2");
|
||||
font-family: "Space Grotesk"; font-weight: 500 700; font-display: swap;
|
||||
src: url("/fonts/space-grotesk-latin.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* Tokens
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
/* Brand main colours (D.2.1) */
|
||||
--galaxy-blue: #00004a;
|
||||
--cherenkov-blue: #00e4d8;
|
||||
--moon-gray: #f5f5f5;
|
||||
|
||||
/* Brand secondary colours (D.2.2) */
|
||||
--white: #ffffff;
|
||||
--cosmic-azure: #007aff;
|
||||
--interstellar-indigo: #00009c;
|
||||
|
||||
/* Text */
|
||||
--text: #111111;
|
||||
--text-muted: #555555;
|
||||
/* Cosmic Azure at 84% brightness: 5.4:1 on white, 3.5:1 against body text */
|
||||
--link: #0066d6;
|
||||
|
||||
/* Type: Inter for everything, Space Grotesk for headlines (D.3) */
|
||||
--font-body: "Inter", Arial, sans-serif;
|
||||
--font-head: "Space Grotesk", Arial, sans-serif;
|
||||
|
||||
/* Layout */
|
||||
--content-width: 85rem;
|
||||
--gutter: clamp(1rem, 4vw, 2.5rem);
|
||||
--header-height: 7rem;
|
||||
--header-height-scrolled: 5rem;
|
||||
/* Large decorative corners (hero, header, search panel): grows with the
|
||||
viewport instead of switching at breakpoints */
|
||||
--radius-lg: min(15vw, 180px);
|
||||
--radius-sm: 15px;
|
||||
/* How far photos are pulled up into the hero */
|
||||
--hero-overlap: clamp(6rem, 11.25vw, 9rem);
|
||||
/* Dropdown submenus (Example, language) */
|
||||
--radius-menu: 8px;
|
||||
--transition: 0.3s ease-in-out;
|
||||
|
||||
/* Film grain tile over the hero background, also hides gradient banding */
|
||||
--grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.05'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Base
|
||||
========================================================================== */
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-padding-top: var(--header-height-scrolled);
|
||||
/* tokens copied from global.css :root */
|
||||
--galaxy: #00004a;
|
||||
--moon: #f5f5f5;
|
||||
--text: #101228;
|
||||
--muted: #46536a;
|
||||
--border: #e2e5ee;
|
||||
--link: #0057c2;
|
||||
--font-body: "Inter", Arial, system-ui, sans-serif;
|
||||
--font-display: "Space Grotesk", var(--font-body);
|
||||
--fs-s: 0.9rem;
|
||||
--fs-l: 1.125rem;
|
||||
--fs-xl: 1.5rem;
|
||||
--fs-h2: clamp(1.75rem, 3.5vw, 2.5rem);
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
/* preview shell ≈ article column */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
background: var(--white);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body.no-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus-visible {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: none;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Focus ring: Link blue on light backgrounds, Cherenkov Blue on dark ones */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--link);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.site-header :focus-visible,
|
||||
.page-hero :focus-visible,
|
||||
.site-footer :focus-visible {
|
||||
outline-color: var(--cherenkov-blue);
|
||||
}
|
||||
|
||||
/* Utilities
|
||||
========================================================================== */
|
||||
|
||||
.container {
|
||||
width: min(100% - 2 * var(--gutter), var(--content-width));
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -4rem;
|
||||
left: 1rem;
|
||||
z-index: 100;
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--galaxy-blue);
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-block;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
a:hover .arrow,
|
||||
button:hover .arrow {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
@keyframes panel-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Page hero
|
||||
========================================================================== */
|
||||
|
||||
.page-hero {
|
||||
margin-top: calc(-1 * var(--header-height));
|
||||
padding: calc(var(--header-height) + 3rem) 0 calc(var(--hero-overlap) + 3rem);
|
||||
color: var(--white);
|
||||
background: var(--grain), var(--galaxy-blue);
|
||||
border-bottom-right-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
/* Pages without a photo: no room needed for the figure to overlap */
|
||||
.page-hero--compact {
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.breadcrumb ol {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0;
|
||||
font-size: 0.875rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.breadcrumb li + li::before {
|
||||
content: "›";
|
||||
margin-right: 0.5rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.breadcrumb [aria-current] {
|
||||
font-weight: 500;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* Headline: Space Grotesk medium, white on dark (D.3.4) */
|
||||
.page-hero__title {
|
||||
max-width: 24ch;
|
||||
margin: 0 0 1.25rem;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(2rem, 5vw, 3.75rem);
|
||||
font-weight: 500;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.02em;
|
||||
text-wrap: balance;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.page-hero__lead {
|
||||
max-width: 70ch;
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Photo pulled up over the hero; flat, no shadow */
|
||||
.page-figure {
|
||||
margin: calc(-1 * var(--hero-overlap)) 0 4rem;
|
||||
}
|
||||
|
||||
.page-figure img {
|
||||
width: 100%;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.page-figure figcaption {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Two photos side by side (Home), pulled up over the hero together; they
|
||||
stack by themselves once each would be narrower than 18rem */
|
||||
.page-figures {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
|
||||
gap: 1.5rem;
|
||||
margin: calc(-1 * var(--hero-overlap)) 0 4rem;
|
||||
}
|
||||
|
||||
.page-figures .page-figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Same shape for both photos, whatever the uploads' proportions */
|
||||
.page-figures img {
|
||||
aspect-ratio: 3 / 2;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Prose
|
||||
========================================================================== */
|
||||
|
||||
.prose {
|
||||
padding-bottom: 5rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.prose--no-image {
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.prose > * {
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
/* Galaxy Blue headline on light background (D.3.4) */
|
||||
.prose h2 {
|
||||
margin: 3rem 0 1rem;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(1.75rem, 3vw, 2.375rem);
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--galaxy-blue);
|
||||
}
|
||||
|
||||
.prose h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
margin: 0 0 1.25rem;
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Text links: colour only at rest, underline on hover and focus */
|
||||
.prose a:not(.btn):hover,
|
||||
.prose a:not(.btn):focus-visible {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
}
|
||||
|
||||
/* Cherenkov Blue fails contrast as text on white, so it is the accent bar */
|
||||
.prose .highlight,
|
||||
.prose blockquote {
|
||||
margin: 2.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
color: var(--galaxy-blue);
|
||||
border-left: 4px solid var(--cherenkov-blue);
|
||||
}
|
||||
|
||||
.prose blockquote p {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.prose h3,
|
||||
.prose h4,
|
||||
.prose h5,
|
||||
.prose h6 {
|
||||
font-family: var(--font-head);
|
||||
color: var(--galaxy-blue);
|
||||
}
|
||||
|
||||
.prose li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
background: var(--moon-gray);
|
||||
}
|
||||
|
||||
.prose code {
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
.prose table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.prose th,
|
||||
.prose td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.prose hr {
|
||||
margin: 2rem 0;
|
||||
border: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* Placeholder pages: empty body until their content exists */
|
||||
.placeholder-body {
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
/* Buttons
|
||||
========================================================================== */
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
height: 2.75rem;
|
||||
padding: 0 1.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* Underline the label only: a flex container would pass it on to the arrow */
|
||||
.btn:hover,
|
||||
.btn:focus-visible {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover .btn__label,
|
||||
.btn:focus-visible .btn__label {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
color: var(--white);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--galaxy-blue) 0%,
|
||||
var(--cosmic-azure) 70%,
|
||||
var(--cherenkov-blue) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.btn--primary:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* Preferences
|
||||
========================================================================== */
|
||||
|
||||
/* Windows High Contrast: drop the decorative hero background behind system text */
|
||||
@media (forced-colors: active) {
|
||||
.page-hero {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Preview frame
|
||||
========================================================================== */
|
||||
|
||||
/* There is no site header above the hero */
|
||||
.page-hero {
|
||||
margin-top: 0;
|
||||
padding-top: 3rem;
|
||||
/* = global.css base heading/link rules = */
|
||||
h1, h2 { font-family: var(--font-display); font-weight: 500; letter-spacing: -0.025em; line-height: 1.1; }
|
||||
h3 { font-weight: 600; }
|
||||
h1, h2, h3 { overflow-wrap: break-word; text-wrap: balance; }
|
||||
a { color: var(--link); text-decoration: underline; text-underline-offset: 3px; }
|
||||
|
||||
/* = global.css `.prose` rules, selectors unwrapped = */
|
||||
body { line-height: 1.65; overflow-wrap: break-word; }
|
||||
a { overflow-wrap: anywhere; }
|
||||
h1 { color: var(--galaxy); font-size: var(--fs-h2); } /* mirrors `.article h1` */
|
||||
h2 { color: var(--galaxy); margin: 44px 0 12px; font-size: var(--fs-xl); }
|
||||
h3 { color: var(--galaxy); margin: 30px 0 8px; font-size: var(--fs-l); }
|
||||
p { margin: 0 0 16px; text-wrap: pretty; }
|
||||
ul, ol { margin: 0 0 16px; padding-left: 22px; }
|
||||
li { margin: 6px 0; }
|
||||
blockquote {
|
||||
margin: 22px 0; padding: 12px 20px; border-left: 3px solid var(--galaxy);
|
||||
background: var(--moon); color: var(--text);
|
||||
}
|
||||
img { display: block; max-width: 100%; height: auto; border: 1px solid var(--border); border-radius: var(--radius); }
|
||||
img + em, p > em:only-child { color: var(--muted); font-size: var(--fs-s); }
|
||||
hr { border: 0; border-top: 1px solid var(--border); margin: 32px 0; }
|
||||
table { display: block; width: 100%; overflow-x: auto; border-collapse: collapse; margin: 0 0 16px; font-size: var(--fs-s); }
|
||||
th { text-align: left; font-weight: 600; color: var(--galaxy); }
|
||||
th, td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
||||
thead th { border-bottom: 2px solid var(--galaxy); }
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 153 32" style="enable-background:new 0 0 153 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;}
|
||||
.st2{display:inline;clip-path:url(#SVGID_00000049184525615463858320000015264053225314800771_);}
|
||||
.st3{fill:#FFFFFF;}
|
||||
.st4{fill:#00E5D9;}
|
||||
.st5{fill:#00E4D8;}
|
||||
</style>
|
||||
<g class="st0">
|
||||
<defs>
|
||||
<rect id="SVGID_1_" x="0.9" width="151.4" height="32"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_00000109021271204444604050000000952066022526769593_" class="st1">
|
||||
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
|
||||
</clipPath>
|
||||
<g style="display:inline;clip-path:url(#SVGID_00000109021271204444604050000000952066022526769593_);">
|
||||
<path class="st3" d="M19.3,0c10.9,0,17.1,4.8,18,12.6h-9.1c-0.6-2.8-3.4-5.1-8.8-5.1c-6.3,0-9.4,3.2-9.4,8.5
|
||||
c0,5.3,3.1,8.6,9.4,8.6c5.5,0,8.3-2.4,8.8-5.2h9.1c-0.9,7.9-7,12.7-18,12.7C7.4,32,0.9,26.4,0.9,16C0.9,5.6,7.4,0,19.3,0z"/>
|
||||
<path class="st3" d="M62.3,8.4c0.2-0.1,0.4-0.1,0.6-0.1c4,0,8,0,12.1,0c0.2,0,0.3,0,0.5,0c0.1,0,0.2,0,0.3-0.1c0-0.1,0-0.2,0-0.3
|
||||
c0-2.3,0-4.6,0-6.9c0-0.1,0-0.2,0-0.3c-0.2,0-0.5,0-0.7,0c-11.4,0-22.8,0-34.3,0c-0.2,0-0.3,0-0.4,0c-0.1,0-0.2,0-0.3,0.1
|
||||
c0,0.2,0,0.4,0,0.5c0,0.8,0,1.6,0,2.4c0,1.3,0,2.6,0,3.9c0,0.2,0,0.4,0.1,0.5c0.2,0.1,0.4,0.1,0.6,0.1c4,0,8.1,0,12.1,0
|
||||
c0.2,0,0.3,0,0.5,0c0.1,0,0.2,0,0.3,0.1c0,0.2,0,0.5,0,0.7c0,6.6,0,13.1,0,19.7c0,0.2,0,0.4,0,0.5c0,0.1,0,0.2,0,0.3
|
||||
c0.1,0,0.1,0,0.1,0c2.7-0.5,5.4-1.1,8.1-1.6c0.1,0,0.2-0.1,0.3-0.1c0-0.2,0-0.4,0-0.6c0-6,0-12,0-18.1c0-0.2,0-0.4,0-0.5
|
||||
C62.2,8.6,62.3,8.5,62.3,8.4L62.3,8.4z M53.7,30.2c0,0.3-0.1,0.6,0,0.8c0.4,0.1,6.6,0.1,8.2,0c0.1,0,0.2,0,0.3,0
|
||||
c0-0.1,0.1-0.1,0.1-0.2c0-0.6,0-1.3,0-1.9c0,0,0-0.1-0.1-0.2C59.4,29.2,56.5,29.7,53.7,30.2L53.7,30.2z"/>
|
||||
<path class="st3" d="M151.5,11.4c0.1,0,0.2,0,0.2,0c0.1,0.1,0.1,0.2,0.1,0.3c0.1,0.6,0.2,1.3,0.3,1.9c0.2,1.3,0.2,2.6,0.1,3.8
|
||||
c-0.1,2-0.6,4-1.5,5.9c-1,2.2-2.6,3.9-4.5,5.3c-1.2,0.8-2.5,1.5-3.9,2c-1.1,0.4-2.3,0.7-3.4,0.9c-0.5,0.1-1.1,0.2-1.6,0.3
|
||||
c-0.5,0.1-0.9,0.1-1.4,0.1c-0.1,0-0.1,0-0.2,0c-0.7,0.1-1.5,0.1-2.2,0.1c-0.7,0-1.5,0-2.2,0c-0.3,0-0.5,0-0.8-0.1
|
||||
c-0.1,0-0.3,0-0.4,0c-2.2-0.2-4.3-0.6-6.4-1.3c-1.4-0.5-2.7-1.2-4-2c-2.7-1.9-4.5-4.5-5.4-7.7c-0.1-0.5-0.3-1.1-0.3-1.6
|
||||
c0-0.1,0-0.1,0-0.2c0.1-0.1,0.2-0.1,0.4-0.2c0.5-0.1,1.1-0.2,1.6-0.3c0.9-0.2,1.8-0.4,2.7-0.5c0.9-0.2,1.8-0.4,2.7-0.6
|
||||
c0.3-0.1,0.5-0.1,0.8-0.2c0.2,0,0.3-0.1,0.5-0.1c0.1,0,0.1,0.1,0.2,0.2c0,0.4,0.1,0.8,0.2,1.2c0.2,0.9,0.6,1.8,1.2,2.6
|
||||
c0.6,0.8,1.3,1.4,2.2,1.9c0.7,0.4,1.5,0.7,2.4,0.9c0.6,0.1,1.2,0.3,1.8,0.3c0.8,0.1,1.7,0.2,2.5,0.2c0.4,0,0.7,0,1.1,0
|
||||
c0.1,0,0.3,0,0.4,0c1.3-0.1,2.5-0.3,3.7-0.7c0.5-0.2,1-0.4,1.5-0.7c1.6-0.9,2.7-2.3,3.2-4.1c0.2-0.6,0.3-1.2,0.3-1.8
|
||||
c0-0.5,0.1-1,0.1-1.4c0-0.7-0.1-1.4-0.2-2.1c0-0.1,0-0.2,0-0.3c0,0,0-0.1,0-0.1c0,0,0.1,0,0.1-0.1c0.1,0,0.3-0.1,0.4-0.1
|
||||
c1.5-0.3,2.9-0.6,4.4-0.9c0.7-0.1,1.5-0.3,2.2-0.4c0.2,0,0.4-0.1,0.6-0.1c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0-0.1
|
||||
C151.2,11.3,151.3,11.3,151.5,11.4C151.5,11.4,151.5,11.4,151.5,11.4L151.5,11.4z"/>
|
||||
<path class="st3" d="M151.1,11.5c-0.4,0-0.7,0.1-1.1,0.1c-0.7,0.1-1.4,0.2-2.1,0.4c-0.7,0.1-1.3,0.2-2,0.4c-0.7,0.1-1.3,0.2-2,0.3
|
||||
c-0.1,0-0.3,0.1-0.4,0.1c-0.1,0-0.3,0-0.4,0c-0.1-0.2-0.2-0.3-0.2-0.5c-0.5-1.4-1.4-2.4-2.6-3.2c-0.5-0.3-1-0.6-1.5-0.8
|
||||
c-0.7-0.3-1.5-0.5-2.3-0.6c-1.3-0.2-2.6-0.3-3.8-0.3c-0.7,0-1.4,0.1-2.1,0.2c-0.8,0.1-1.6,0.3-2.4,0.5c-0.5,0.2-1,0.4-1.4,0.6
|
||||
c-1.8,1-3,2.5-3.5,4.5c-0.2,0.7-0.3,1.5-0.3,2.2c0,0.2,0,0.4,0,0.7c0,0.1,0,0.2-0.1,0.2c-0.1,0.1-0.3,0.1-0.4,0.1
|
||||
c-0.6,0.1-1.3,0.2-1.9,0.3c-0.6,0.1-1.3,0.2-1.9,0.3c-0.7,0.1-1.4,0.2-2.1,0.4c-0.7,0.1-1.3,0.2-2,0.4c-0.1,0-0.3,0.1-0.4,0
|
||||
c-0.1,0-0.1-0.1-0.1-0.2c0-0.1,0-0.3,0-0.4c0-0.9-0.1-1.8,0-2.7c0.1-2.3,0.7-4.5,1.8-6.6c1.1-2,2.6-3.6,4.4-4.8
|
||||
c1.3-0.9,2.7-1.5,4.2-2c1.1-0.4,2.3-0.6,3.4-0.8c0.6-0.1,1.3-0.2,1.9-0.3c0.6-0.1,1.3-0.1,1.9-0.1c1.2,0,2.4,0,3.6,0
|
||||
c0.2,0,0.4,0,0.5,0c0.1,0,0.2,0,0.3,0c0.8,0,1.6,0.2,2.5,0.3c1.3,0.2,2.5,0.5,3.7,0.9c1.4,0.5,2.8,1.1,4.1,2
|
||||
c1.9,1.3,3.4,2.9,4.5,5c0.4,0.8,0.8,1.7,1,2.6c0,0.1,0,0.2,0.1,0.2c0,0,0,0.1,0,0.1c-0.1,0-0.1,0.1-0.2,0.1l0,0
|
||||
C151.4,11.5,151.2,11.4,151.1,11.5L151.1,11.5L151.1,11.5z"/>
|
||||
<path class="st3" d="M151,11.5c0.1-0.1,0.1-0.1,0.2-0.1c0.1,0,0.2,0,0.3,0C151.4,11.5,151.2,11.5,151,11.5L151,11.5z"/>
|
||||
<path class="st3" d="M84.5,23.5C84.5,23.4,84.6,23.4,84.5,23.5c2.7-5.4,5.3-10.8,8-16.1c0,0,0-0.1,0.1-0.1c0,0,0-0.1,0.1-0.1
|
||||
c0,0,0,0,0,0c0,0,0,0,0.1,0c0.1,0.1,0.2,0.3,0.2,0.4c0.9,1.8,1.7,3.5,2.6,5.3c1.2,2.4,2.4,4.8,3.6,7.2c0.1,0.2,0.1,0.3,0.3,0.5
|
||||
c0.6-0.1,1.2-0.2,1.8-0.3c1.1-0.2,2.1-0.4,3.2-0.5c1-0.2,2-0.3,3-0.5c0.1,0,0.2-0.1,0.3-0.1c-0.1-0.2-0.2-0.3-0.2-0.5
|
||||
c-1.4-2.8-2.8-5.6-4.2-8.4c-1.5-2.9-2.9-5.8-4.4-8.7c-0.1-0.2-0.1-0.4-0.3-0.5c-0.1,0-0.1,0-0.2,0c-3.8,0-7.6,0-11.4,0
|
||||
c-0.1,0-0.1,0-0.2,0c0,0,0,0-0.1,0c0,0,0,0-0.1,0C82.6,9,78.5,17.2,74.4,25.3c0,0,0,0.1,0,0.1c0,0,0.1,0.1,0.1,0
|
||||
C77.8,24.8,81.2,24.1,84.5,23.5z M100,21.8c0.3,0.7,4.4,9,4.6,9.3c0.1,0,0.2,0,0.3,0c2.9,0,5.8,0,8.7,0c0,0,0,0,0.1,0
|
||||
c0,0,0,0,0.1-0.1c0-0.1-0.1-0.2-0.1-0.2c-0.1-0.2-0.3-0.5-0.4-0.8c-1.6-3.1-3.2-6.3-4.7-9.4c-0.1-0.2-0.1-0.3-0.3-0.5
|
||||
C105.5,20.7,102.8,21.2,100,21.8L100,21.8z M73.7,26.8c-0.7,1.4-1.4,2.7-2,4.1c0,0.1,0,0.2-0.1,0.3h9.1c0.2-0.3,2.6-5,2.9-5.8
|
||||
c0-0.1,0-0.1,0.1-0.2c-0.1,0-0.2,0-0.3,0C80.2,25.6,76.9,26.2,73.7,26.8L73.7,26.8z M93.8,29C93.8,29,93.8,29,93.8,29
|
||||
c0.1-0.5,0.1-1,0-1.5c0-0.5-0.1-1.1,0-1.6c0-0.3,0.1-0.7,0.2-1c0.3-1.2,1.1-2,2.1-2.4c0.5-0.2,1.1-0.4,1.6-0.6
|
||||
c0.4-0.2,0.9-0.3,1.3-0.5c-0.1-0.1-0.2-0.1-0.3-0.1c-1,0-1.9,0-2.9,0c-0.9,0-1.7-0.4-2.4-1c-0.4-0.4-0.8-0.9-1-1.5
|
||||
c-0.3-0.8-0.6-1.6-0.8-2.4c0-0.1,0-0.1-0.2-0.1c0,0.2,0,0.3,0,0.5c0,0.8,0,1.7,0,2.5c0,0.8-0.2,1.5-0.7,2.1
|
||||
c-0.5,0.7-1.1,1.1-1.8,1.3c-0.8,0.3-1.6,0.6-2.4,0.9c-0.1,0-0.1,0.1-0.2,0.1c0.1,0.1,0.3,0.1,0.4,0.1c0.9,0,1.8,0,2.8,0
|
||||
c0.2,0,0.4,0,0.6,0.1c1.3,0.3,2.1,1.1,2.6,2.3c0.1,0.2,0.2,0.5,0.3,0.7C93.3,27.6,93.5,28.3,93.8,29L93.8,29z"/>
|
||||
<path class="st4" d="M93.8,29C93.8,29,93.8,29,93.8,29c0.1-0.5,0.1-1,0-1.5c0-0.5-0.1-1.1,0-1.6c0-0.3,0.1-0.7,0.2-1
|
||||
c0.3-1.2,1.1-2,2.1-2.4c0.5-0.2,1.1-0.4,1.6-0.6c0.4-0.2,0.8-0.3,1.3-0.5c-0.1-0.1-0.2-0.1-0.3-0.1c-1,0-1.9,0-2.9,0
|
||||
c-0.9,0-1.7-0.4-2.4-1c-0.4-0.4-0.8-0.9-1-1.5c-0.3-0.8-0.6-1.6-0.8-2.4c0-0.1,0-0.1-0.2-0.1c0,0.2,0,0.3,0,0.5c0,0.8,0,1.7,0,2.5
|
||||
c0,0.8-0.2,1.5-0.7,2.1c-0.5,0.7-1.1,1.1-1.8,1.3c-0.8,0.3-1.6,0.6-2.4,0.9c-0.1,0-0.1,0.1-0.2,0.1c0.1,0.1,0.3,0.1,0.4,0.1
|
||||
c0.9,0,1.8,0,2.8,0c0.2,0,0.4,0,0.6,0.1c1.3,0.3,2.1,1.1,2.6,2.3c0.1,0.2,0.2,0.5,0.3,0.7C93.3,27.6,93.5,28.3,93.8,29L93.8,29z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Logotype">
|
||||
<g>
|
||||
<g>
|
||||
<polygon class="st3" points="100.4,22.3 104.7,30.8 113.9,30.8 108.6,20.6 "/>
|
||||
<g>
|
||||
<path class="st3" d="M122.9,16.1c0,0,0-0.1,0-0.1c0-5.2,2.9-8.3,10.2-8.3c5.7,0,8.8,1.9,9.8,5.2l8.9-1.4
|
||||
c-1.9-7-8.3-11.1-18.7-11.1c-12.5,0-19.3,5.8-19.3,15.6c0,0.5,0,1.1,0.1,1.6L122.9,16.1L122.9,16.1z"/>
|
||||
<path class="st3" d="M143.1,13.3c0.2,0.8,0.3,1.7,0.3,2.6c0,5.2-3,8.4-10.3,8.4c-6.5,0-9.6-2.5-10.1-6.8l-8.8,1.8
|
||||
c1.4,7.8,8,12.2,18.9,12.2c12.4,0,19.3-5.9,19.3-15.7c0-1.6-0.2-3.1-0.5-4.5L143.1,13.3L143.1,13.3z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st3" points="73.5,27 71.5,30.8 80.8,30.8 83.5,25.4 "/>
|
||||
<polygon class="st3" points="98.6,1.2 86.8,1.2 74.6,24.7 84.9,22.6 92.7,7.2 99.2,19.9 107.6,18.6 "/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon class="st3" points="53.6,30.2 53.6,30.8 62.2,30.8 62.2,28.8 "/>
|
||||
<polygon class="st3" points="53.6,29.1 62.2,27.3 62.2,8.5 75.7,8.5 75.7,1.2 40.1,1.2 40.1,8.5 53.6,8.5 "/>
|
||||
</g>
|
||||
<path class="st3" d="M19.3,0.4c10.9,0,17.1,4.7,18,12.3h-9.1c-0.6-2.7-3.4-5-8.8-5c-6.3,0-9.4,3.1-9.4,8.3
|
||||
c0,5.2,3.1,8.4,9.4,8.4c5.5,0,8.3-2.3,8.8-5.1h9.1c-0.9,7.7-7.1,12.4-18,12.4C7.4,31.6,0.9,26.1,0.9,16S7.4,0.4,19.3,0.4
|
||||
L19.3,0.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st5" 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.8
|
||||
c-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"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.1 KiB |
@@ -1,129 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 153 32" style="enable-background:new 0 0 153 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;}
|
||||
.st2{display:inline;clip-path:url(#SVGID_00000021077498389242353310000002629454631782585475_);}
|
||||
.st3{fill:#00004A;}
|
||||
.st4{fill:#00E5D9;}
|
||||
.st5{fill:#00E4D8;}
|
||||
</style>
|
||||
<g class="st0">
|
||||
<defs>
|
||||
<rect id="SVGID_1_" x="0.9" width="151.4" height="32"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_00000068637063279286702420000005043763006986931103_" class="st1">
|
||||
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
|
||||
</clipPath>
|
||||
<g style="display:inline;clip-path:url(#SVGID_00000068637063279286702420000005043763006986931103_);">
|
||||
<path class="st3" d="M19.3,0c10.9,0,17.1,4.8,18,12.6h-9.1c-0.6-2.8-3.4-5.1-8.8-5.1c-6.3,0-9.4,3.2-9.4,8.5
|
||||
c0,5.3,3.1,8.6,9.4,8.6c5.5,0,8.3-2.4,8.8-5.2h9.1c-0.9,7.9-7,12.7-18,12.7C7.4,32,0.9,26.4,0.9,16C0.9,5.6,7.4,0,19.3,0z"/>
|
||||
<path class="st3" d="M62.3,8.4c0.2-0.1,0.4-0.1,0.6-0.1c4,0,8,0,12.1,0c0.2,0,0.3,0,0.5,0c0.1,0,0.2,0,0.3-0.1c0-0.1,0-0.2,0-0.3
|
||||
c0-2.3,0-4.6,0-6.9c0-0.1,0-0.2,0-0.3c-0.2,0-0.5,0-0.7,0c-11.4,0-22.8,0-34.3,0c-0.2,0-0.3,0-0.4,0c-0.1,0-0.2,0-0.3,0.1
|
||||
c0,0.2,0,0.4,0,0.5c0,0.8,0,1.6,0,2.4c0,1.3,0,2.6,0,3.9c0,0.2,0,0.4,0.1,0.5c0.2,0.1,0.4,0.1,0.6,0.1c4,0,8.1,0,12.1,0
|
||||
c0.2,0,0.3,0,0.5,0c0.1,0,0.2,0,0.3,0.1c0,0.2,0,0.5,0,0.7c0,6.6,0,13.1,0,19.7c0,0.2,0,0.4,0,0.5c0,0.1,0,0.2,0,0.3
|
||||
c0.1,0,0.1,0,0.1,0c2.7-0.5,5.4-1.1,8.1-1.6c0.1,0,0.2-0.1,0.3-0.1c0-0.2,0-0.4,0-0.6c0-6,0-12,0-18.1c0-0.2,0-0.4,0-0.5
|
||||
C62.2,8.6,62.3,8.5,62.3,8.4L62.3,8.4z M53.7,30.2c0,0.3-0.1,0.6,0,0.8c0.4,0.1,6.6,0.1,8.2,0c0.1,0,0.2,0,0.3,0
|
||||
c0-0.1,0.1-0.1,0.1-0.2c0-0.6,0-1.3,0-1.9c0,0,0-0.1-0.1-0.2C59.4,29.2,56.5,29.7,53.7,30.2L53.7,30.2z"/>
|
||||
<path class="st3" d="M151.5,11.4c0.1,0,0.2,0,0.2,0c0.1,0.1,0.1,0.2,0.1,0.3c0.1,0.6,0.2,1.3,0.3,1.9c0.2,1.3,0.2,2.6,0.1,3.8
|
||||
c-0.1,2-0.6,4-1.5,5.9c-1,2.2-2.6,3.9-4.5,5.3c-1.2,0.8-2.5,1.5-3.9,2c-1.1,0.4-2.3,0.7-3.4,0.9c-0.5,0.1-1.1,0.2-1.6,0.3
|
||||
c-0.5,0.1-0.9,0.1-1.4,0.1c-0.1,0-0.1,0-0.2,0c-0.7,0.1-1.5,0.1-2.2,0.1c-0.7,0-1.5,0-2.2,0c-0.3,0-0.5,0-0.8-0.1
|
||||
c-0.1,0-0.3,0-0.4,0c-2.2-0.2-4.3-0.6-6.4-1.3c-1.4-0.5-2.7-1.2-4-2c-2.7-1.9-4.5-4.5-5.4-7.7c-0.1-0.5-0.3-1.1-0.3-1.6
|
||||
c0-0.1,0-0.1,0-0.2c0.1-0.1,0.2-0.1,0.4-0.2c0.5-0.1,1.1-0.2,1.6-0.3c0.9-0.2,1.8-0.4,2.7-0.5c0.9-0.2,1.8-0.4,2.7-0.6
|
||||
c0.3-0.1,0.5-0.1,0.8-0.2c0.2,0,0.3-0.1,0.5-0.1c0.1,0,0.1,0.1,0.2,0.2c0,0.4,0.1,0.8,0.2,1.2c0.2,0.9,0.6,1.8,1.2,2.6
|
||||
c0.6,0.8,1.3,1.4,2.2,1.9c0.7,0.4,1.5,0.7,2.4,0.9c0.6,0.1,1.2,0.3,1.8,0.3c0.8,0.1,1.7,0.2,2.5,0.2c0.4,0,0.7,0,1.1,0
|
||||
c0.1,0,0.3,0,0.4,0c1.3-0.1,2.5-0.3,3.7-0.7c0.5-0.2,1-0.4,1.5-0.7c1.6-0.9,2.7-2.3,3.2-4.1c0.2-0.6,0.3-1.2,0.3-1.8
|
||||
c0-0.5,0.1-1,0.1-1.4c0-0.7-0.1-1.4-0.2-2.1c0-0.1,0-0.2,0-0.3c0,0,0-0.1,0-0.1c0,0,0.1,0,0.1-0.1c0.1,0,0.3-0.1,0.4-0.1
|
||||
c1.5-0.3,2.9-0.6,4.4-0.9c0.7-0.1,1.5-0.3,2.2-0.4c0.2,0,0.4-0.1,0.6-0.1c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0-0.1
|
||||
C151.2,11.3,151.3,11.3,151.5,11.4C151.5,11.4,151.5,11.4,151.5,11.4L151.5,11.4z"/>
|
||||
<path class="st3" d="M151.1,11.5c-0.4,0-0.7,0.1-1.1,0.1c-0.7,0.1-1.4,0.2-2.1,0.4c-0.7,0.1-1.3,0.2-2,0.4c-0.7,0.1-1.3,0.2-2,0.3
|
||||
c-0.1,0-0.3,0.1-0.4,0.1c-0.1,0-0.3,0-0.4,0c-0.1-0.2-0.2-0.3-0.2-0.5c-0.5-1.4-1.4-2.4-2.6-3.2c-0.5-0.3-1-0.6-1.5-0.8
|
||||
c-0.7-0.3-1.5-0.5-2.3-0.6c-1.3-0.2-2.6-0.3-3.8-0.3c-0.7,0-1.4,0.1-2.1,0.2c-0.8,0.1-1.6,0.3-2.4,0.5c-0.5,0.2-1,0.4-1.4,0.6
|
||||
c-1.8,1-3,2.5-3.5,4.5c-0.2,0.7-0.3,1.5-0.3,2.2c0,0.2,0,0.4,0,0.7c0,0.1,0,0.2-0.1,0.2c-0.1,0.1-0.3,0.1-0.4,0.1
|
||||
c-0.6,0.1-1.3,0.2-1.9,0.3c-0.6,0.1-1.3,0.2-1.9,0.3c-0.7,0.1-1.4,0.2-2.1,0.4c-0.7,0.1-1.3,0.2-2,0.4c-0.1,0-0.3,0.1-0.4,0
|
||||
c-0.1,0-0.1-0.1-0.1-0.2c0-0.1,0-0.3,0-0.4c0-0.9-0.1-1.8,0-2.7c0.1-2.3,0.7-4.5,1.8-6.6c1.1-2,2.6-3.6,4.4-4.8
|
||||
c1.3-0.9,2.7-1.5,4.2-2c1.1-0.4,2.3-0.6,3.4-0.8c0.6-0.1,1.3-0.2,1.9-0.3c0.6-0.1,1.3-0.1,1.9-0.1c1.2,0,2.4,0,3.6,0
|
||||
c0.2,0,0.4,0,0.5,0c0.1,0,0.2,0,0.3,0c0.8,0,1.6,0.2,2.5,0.3c1.3,0.2,2.5,0.5,3.7,0.9c1.4,0.5,2.8,1.1,4.1,2
|
||||
c1.9,1.3,3.4,2.9,4.5,5c0.4,0.8,0.8,1.7,1,2.6c0,0.1,0,0.2,0.1,0.2c0,0,0,0.1,0,0.1c-0.1,0-0.1,0.1-0.2,0.1l0,0
|
||||
C151.4,11.5,151.2,11.4,151.1,11.5L151.1,11.5L151.1,11.5z"/>
|
||||
<path class="st3" d="M151,11.5c0.1-0.1,0.1-0.1,0.2-0.1c0.1,0,0.2,0,0.3,0C151.4,11.5,151.2,11.5,151,11.5L151,11.5z"/>
|
||||
<path class="st3" d="M84.5,23.5C84.5,23.4,84.6,23.4,84.5,23.5c2.7-5.4,5.3-10.8,8-16.1c0,0,0-0.1,0.1-0.1c0,0,0-0.1,0.1-0.1
|
||||
c0,0,0,0,0,0c0,0,0,0,0.1,0c0.1,0.1,0.2,0.3,0.2,0.4c0.9,1.8,1.7,3.5,2.6,5.3c1.2,2.4,2.4,4.8,3.6,7.2c0.1,0.2,0.1,0.3,0.3,0.5
|
||||
c0.6-0.1,1.2-0.2,1.8-0.3c1.1-0.2,2.1-0.4,3.2-0.5c1-0.2,2-0.3,3-0.5c0.1,0,0.2-0.1,0.3-0.1c-0.1-0.2-0.2-0.3-0.2-0.5
|
||||
c-1.4-2.8-2.8-5.6-4.2-8.4c-1.5-2.9-2.9-5.8-4.4-8.7c-0.1-0.2-0.1-0.4-0.3-0.5c-0.1,0-0.1,0-0.2,0c-3.8,0-7.6,0-11.4,0
|
||||
c-0.1,0-0.1,0-0.2,0c0,0,0,0-0.1,0c0,0,0,0-0.1,0C82.6,9,78.5,17.2,74.4,25.3c0,0,0,0.1,0,0.1c0,0,0.1,0.1,0.1,0
|
||||
C77.8,24.8,81.2,24.1,84.5,23.5z M100,21.8c0.3,0.7,4.4,9,4.6,9.3c0.1,0,0.2,0,0.3,0c2.9,0,5.8,0,8.7,0c0,0,0,0,0.1,0
|
||||
c0,0,0,0,0.1-0.1c0-0.1-0.1-0.2-0.1-0.2c-0.1-0.2-0.3-0.5-0.4-0.8c-1.6-3.1-3.2-6.3-4.7-9.4c-0.1-0.2-0.1-0.3-0.3-0.5
|
||||
C105.5,20.7,102.8,21.2,100,21.8L100,21.8z M73.7,26.8c-0.7,1.4-1.4,2.7-2,4.1c0,0.1,0,0.2-0.1,0.3h9.1c0.2-0.3,2.6-5,2.9-5.8
|
||||
c0-0.1,0-0.1,0.1-0.2c-0.1,0-0.2,0-0.3,0C80.2,25.6,76.9,26.2,73.7,26.8L73.7,26.8z M93.8,29C93.8,29,93.8,29,93.8,29
|
||||
c0.1-0.5,0.1-1,0-1.5c0-0.5-0.1-1.1,0-1.6c0-0.3,0.1-0.7,0.2-1c0.3-1.2,1.1-2,2.1-2.4c0.5-0.2,1.1-0.4,1.6-0.6
|
||||
c0.4-0.2,0.9-0.3,1.3-0.5c-0.1-0.1-0.2-0.1-0.3-0.1c-1,0-1.9,0-2.9,0c-0.9,0-1.7-0.4-2.4-1c-0.4-0.4-0.8-0.9-1-1.5
|
||||
c-0.3-0.8-0.6-1.6-0.8-2.4c0-0.1,0-0.1-0.2-0.1c0,0.2,0,0.3,0,0.5c0,0.8,0,1.7,0,2.5c0,0.8-0.2,1.5-0.7,2.1
|
||||
c-0.5,0.7-1.1,1.1-1.8,1.3c-0.8,0.3-1.6,0.6-2.4,0.9c-0.1,0-0.1,0.1-0.2,0.1c0.1,0.1,0.3,0.1,0.4,0.1c0.9,0,1.8,0,2.8,0
|
||||
c0.2,0,0.4,0,0.6,0.1c1.3,0.3,2.1,1.1,2.6,2.3c0.1,0.2,0.2,0.5,0.3,0.7C93.3,27.6,93.5,28.3,93.8,29L93.8,29z"/>
|
||||
<path class="st4" d="M93.8,29C93.8,29,93.8,29,93.8,29c0.1-0.5,0.1-1,0-1.5c0-0.5-0.1-1.1,0-1.6c0-0.3,0.1-0.7,0.2-1
|
||||
c0.3-1.2,1.1-2,2.1-2.4c0.5-0.2,1.1-0.4,1.6-0.6c0.4-0.2,0.8-0.3,1.3-0.5c-0.1-0.1-0.2-0.1-0.3-0.1c-1,0-1.9,0-2.9,0
|
||||
c-0.9,0-1.7-0.4-2.4-1c-0.4-0.4-0.8-0.9-1-1.5c-0.3-0.8-0.6-1.6-0.8-2.4c0-0.1,0-0.1-0.2-0.1c0,0.2,0,0.3,0,0.5c0,0.8,0,1.7,0,2.5
|
||||
c0,0.8-0.2,1.5-0.7,2.1c-0.5,0.7-1.1,1.1-1.8,1.3c-0.8,0.3-1.6,0.6-2.4,0.9c-0.1,0-0.1,0.1-0.2,0.1c0.1,0.1,0.3,0.1,0.4,0.1
|
||||
c0.9,0,1.8,0,2.8,0c0.2,0,0.4,0,0.6,0.1c1.3,0.3,2.1,1.1,2.6,2.3c0.1,0.2,0.2,0.5,0.3,0.7C93.3,27.6,93.5,28.3,93.8,29L93.8,29z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Logotype" class="st0">
|
||||
<g class="st1">
|
||||
<g>
|
||||
<polygon class="st3" points="100.4,22.3 104.7,30.8 113.9,30.8 108.6,20.6 "/>
|
||||
<g>
|
||||
<path class="st3" d="M122.9,16.1c0,0,0-0.1,0-0.1c0-5.2,2.9-8.3,10.2-8.3c5.7,0,8.8,1.9,9.8,5.2l8.9-1.4
|
||||
c-1.9-7-8.3-11.1-18.7-11.1c-12.5,0-19.3,5.8-19.3,15.6c0,0.5,0,1.1,0.1,1.6L122.9,16.1L122.9,16.1z"/>
|
||||
<path class="st3" d="M143.1,13.3c0.2,0.8,0.3,1.7,0.3,2.6c0,5.2-3,8.4-10.3,8.4c-6.5,0-9.6-2.5-10.1-6.8l-8.8,1.8
|
||||
c1.4,7.8,8,12.2,18.9,12.2c12.4,0,19.3-5.9,19.3-15.7c0-1.6-0.2-3.1-0.5-4.5L143.1,13.3L143.1,13.3z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st3" points="73.5,27 71.5,30.8 80.8,30.8 83.5,25.4 "/>
|
||||
<polygon class="st3" points="98.6,1.2 86.8,1.2 74.6,24.7 84.9,22.6 92.7,7.2 99.2,19.9 107.6,18.6 "/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon class="st3" points="53.6,30.2 53.6,30.8 62.2,30.8 62.2,28.8 "/>
|
||||
<polygon class="st3" points="53.6,29.1 62.2,27.3 62.2,8.5 75.7,8.5 75.7,1.2 40.1,1.2 40.1,8.5 53.6,8.5 "/>
|
||||
</g>
|
||||
<path class="st3" d="M19.3,0.4c10.9,0,17.1,4.7,18,12.3h-9.1c-0.6-2.7-3.4-5-8.8-5c-6.3,0-9.4,3.1-9.4,8.3
|
||||
c0,5.2,3.1,8.4,9.4,8.4c5.5,0,8.3-2.3,8.8-5.1h9.1c-0.9,7.7-7.1,12.4-18,12.4C7.4,31.6,0.9,26.1,0.9,16S7.4,0.4,19.3,0.4
|
||||
L19.3,0.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st5" 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.8
|
||||
c-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"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Logotype_00000134236662009793237430000016451412871672361145_">
|
||||
<g>
|
||||
<g>
|
||||
<polygon class="st3" points="100,22.3 104.3,30.7 113.5,30.7 108.2,20.5 "/>
|
||||
<g>
|
||||
<path class="st3" d="M122.4,16.1c0,0,0-0.1,0-0.1c0-5.2,2.9-8.2,10.2-8.2c5.7,0,8.7,1.9,9.8,5.2l8.8-1.4c-1.8-7-8.2-11-18.6-11
|
||||
c-12.4,0-19.1,5.7-19.1,15.5c0,0.5,0,1.1,0.1,1.6L122.4,16.1L122.4,16.1z"/>
|
||||
<path class="st3" d="M142.5,13.4c0.2,0.8,0.3,1.7,0.3,2.6c0,5.1-3,8.3-10.2,8.3c-6.4,0-9.5-2.5-10.1-6.7l-8.7,1.8
|
||||
c1.4,7.7,7.9,12.2,18.8,12.2c12.4,0,19.1-5.8,19.1-15.6c0-1.6-0.2-3.1-0.5-4.4L142.5,13.4L142.5,13.4z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st3" points="73.4,26.9 71.4,30.7 80.6,30.7 83.3,25.3 "/>
|
||||
<polygon class="st3" points="98.4,1.3 86.6,1.3 74.5,24.7 84.7,22.5 92.5,7.3 98.9,19.9 107.2,18.6 "/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon class="st3" points="53.7,30.1 53.7,30.7 62.2,30.7 62.2,28.7 "/>
|
||||
<polygon class="st3" points="53.7,29 62.2,27.3 62.2,8.5 75.6,8.5 75.6,1.3 40.2,1.3 40.2,8.5 53.7,8.5 "/>
|
||||
</g>
|
||||
<path class="st3" d="M19.6,0.5c10.8,0,17,4.7,17.8,12.2h-9.1c-0.6-2.7-3.3-5-8.8-5c-6.3,0-9.4,3.1-9.4,8.2s3.1,8.3,9.4,8.3
|
||||
c5.4,0,8.2-2.3,8.8-5.1h9.1c-0.9,7.6-7,12.3-17.8,12.3C7.8,31.5,1.3,26.1,1.3,16S7.8,0.5,19.6,0.5L19.6,0.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st3" d="M94.4,23.7c1-1.4,3.3-2.1,5.1-2.7c-2,0.1-4.4,0.3-5.8-0.6c-1.4-1-2.1-3.3-2.7-5.1c0.1,2,0.3,4.4-0.6,5.8
|
||||
c-1,1.4-3.3,2.1-5.1,2.7c2-0.1,4.4-0.3,5.8,0.6c1.4,1,2.1,3.3,2.7,5.1C93.6,27.5,93.4,25.1,94.4,23.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.6 KiB |
@@ -1,11 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Ebene_1" x="0" y="0" viewBox="0 0 153 46.0">
|
||||
<style>
|
||||
.st3{fill:#ffffff}
|
||||
.st5{fill:#00E4D8}
|
||||
</style>
|
||||
<g id="Logotype_00000134236662009793237430000016451412871672361145_">
|
||||
<path d="m100 22.3 4.3 8.4h9.2l-5.3-10.2zm22.4-6.2V16c0-5.2 2.9-8.2 10.2-8.2 5.7 0 8.7 1.9 9.8 5.2l8.8-1.4c-1.8-7-8.2-11-18.6-11-12.4 0-19.1 5.7-19.1 15.5 0 .5 0 1.1.1 1.6zm20.1-2.7c.2.8.3 1.7.3 2.6 0 5.1-3 8.3-10.2 8.3-6.4 0-9.5-2.5-10.1-6.7l-8.7 1.8c1.4 7.7 7.9 12.2 18.8 12.2 12.4 0 19.1-5.8 19.1-15.6 0-1.6-.2-3.1-.5-4.4zM73.4 26.9l-2 3.8h9.2l2.7-5.4zm25-25.6H86.6L74.5 24.7l10.2-2.2 7.8-15.2 6.4 12.6 8.3-1.3zM53.7 30.1v.6h8.5v-2zm0-1.1 8.5-1.7V8.5h13.4V1.3H40.2v7.2h13.5zM19.6.5c10.8 0 17 4.7 17.8 12.2h-9.1c-.6-2.7-3.3-5-8.8-5-6.3 0-9.4 3.1-9.4 8.2s3.1 8.3 9.4 8.3c5.4 0 8.2-2.3 8.8-5.1h9.1c-.9 7.6-7 12.3-17.8 12.3C7.8 31.5 1.3 26.1 1.3 16S7.8.5 19.6.5" class="st3"/>
|
||||
<path d="M94.4 23.7c1-1.4 3.3-2.1 5.1-2.7-2 .1-4.4.3-5.8-.6-1.4-1-2.1-3.3-2.7-5.1.1 2 .3 4.4-.6 5.8-1 1.4-3.3 2.1-5.1 2.7 2-.1 4.4-.3 5.8.6 1.4 1 2.1 3.3 2.7 5.1-.2-2-.4-4.4.6-5.8" class="st5"/>
|
||||
</g>
|
||||
<path d="M6.48 45.77Q5.41 45.77 4.62 45.44Q3.83 45.1 3.38 44.47Q2.94 43.83 2.9 42.92H4.53Q4.57 43.4 4.83 43.72Q5.09 44.04 5.52 44.2Q5.95 44.36 6.47 44.36Q7.01 44.36 7.43 44.19Q7.85 44.03 8.08 43.72Q8.32 43.42 8.32 43.03Q8.32 42.67 8.11 42.43Q7.9 42.2 7.53 42.03Q7.15 41.87 6.65 41.75L5.6 41.47Q4.45 41.18 3.8 40.57Q3.15 39.97 3.15 38.99Q3.15 38.18 3.59 37.57Q4.03 36.95 4.8 36.61Q5.56 36.28 6.52 36.28Q7.51 36.28 8.25 36.62Q8.99 36.96 9.41 37.56Q9.83 38.16 9.85 38.93H8.25Q8.18 38.33 7.71 38.01Q7.25 37.68 6.5 37.68Q5.98 37.68 5.6 37.84Q5.23 38 5.03 38.27Q4.83 38.55 4.83 38.9Q4.83 39.28 5.06 39.53Q5.3 39.78 5.66 39.93Q6.03 40.08 6.4 40.17L7.26 40.39Q7.77 40.51 8.26 40.72Q8.74 40.92 9.13 41.24Q9.52 41.55 9.75 41.99Q9.97 42.44 9.97 43.04Q9.97 43.85 9.56 44.47Q9.15 45.08 8.37 45.43Q7.58 45.77 6.48 45.77ZM18.24 45.75Q17.03 45.75 16.08 45.19Q15.12 44.63 14.57 43.57Q14.02 42.51 14.02 41.02Q14.02 39.53 14.58 38.47Q15.13 37.4 16.08 36.84Q17.04 36.28 18.24 36.28Q19 36.28 19.66 36.49Q20.32 36.71 20.84 37.12Q21.35 37.53 21.69 38.12Q22.02 38.72 22.13 39.47H20.47Q20.39 39.06 20.19 38.74Q20 38.43 19.71 38.21Q19.41 37.98 19.05 37.87Q18.68 37.76 18.27 37.76Q17.51 37.76 16.93 38.14Q16.34 38.52 16.01 39.25Q15.68 39.97 15.68 41.02Q15.68 42.08 16.02 42.8Q16.35 43.53 16.93 43.9Q17.52 44.27 18.27 44.27Q18.68 44.27 19.04 44.16Q19.41 44.04 19.7 43.82Q20 43.6 20.19 43.28Q20.39 42.97 20.47 42.56H22.14Q22.05 43.22 21.73 43.8Q21.42 44.38 20.91 44.82Q20.41 45.26 19.74 45.51Q19.06 45.75 18.24 45.75ZM28.13 36.4V45.63H26.48V36.4ZM32.81 45.63V36.4H38.81V37.79H34.46V40.28H38.49V41.66H34.46V44.24H38.83V45.63ZM43.31 45.63V36.4H45.18L48.38 41.48Q48.57 41.8 48.8 42.2Q49.02 42.6 49.26 43.09Q49.5 43.58 49.74 44.19H49.55Q49.51 43.66 49.48 43.12Q49.44 42.57 49.43 42.07Q49.41 41.58 49.41 41.24V36.4H51.09V45.63H49.21L46.33 41.05Q46.06 40.61 45.82 40.19Q45.57 39.76 45.3 39.22Q45.02 38.68 44.62 37.92H44.85Q44.89 38.59 44.92 39.2Q44.96 39.8 44.98 40.28Q45 40.75 45 41.05V45.63ZM59.7 45.75Q58.49 45.75 57.53 45.19Q56.58 44.63 56.03 43.57Q55.48 42.51 55.48 41.02Q55.48 39.53 56.03 38.47Q56.58 37.4 57.54 36.84Q58.5 36.28 59.7 36.28Q60.46 36.28 61.12 36.49Q61.78 36.71 62.29 37.12Q62.81 37.53 63.14 38.12Q63.48 38.72 63.59 39.47H61.92Q61.84 39.06 61.65 38.74Q61.45 38.43 61.16 38.21Q60.87 37.98 60.51 37.87Q60.14 37.76 59.73 37.76Q58.97 37.76 58.38 38.14Q57.8 38.52 57.47 39.25Q57.14 39.97 57.14 41.02Q57.14 42.08 57.47 42.8Q57.81 43.53 58.39 43.9Q58.97 44.27 59.72 44.27Q60.14 44.27 60.5 44.16Q60.86 44.04 61.16 43.82Q61.45 43.6 61.65 43.28Q61.85 42.97 61.93 42.56H63.59Q63.5 43.22 63.19 43.8Q62.88 44.38 62.37 44.82Q61.86 45.26 61.19 45.51Q60.52 45.75 59.7 45.75ZM67.93 45.63V36.4H73.93V37.79H69.59V40.28H73.61V41.66H69.59V44.24H73.95V45.63ZM84.45 45.63V36.4H87.91Q88.97 36.4 89.69 36.8Q90.41 37.2 90.78 37.89Q91.15 38.57 91.15 39.46Q91.15 40.34 90.78 41.03Q90.41 41.72 89.68 42.11Q88.95 42.51 87.88 42.51H85.59V41.15H87.66Q88.29 41.15 88.69 40.93Q89.09 40.71 89.28 40.33Q89.46 39.95 89.46 39.46Q89.46 38.96 89.28 38.59Q89.09 38.21 88.69 38Q88.28 37.78 87.65 37.78H86.11V45.63ZM99.41 45.75Q98.2 45.75 97.24 45.19Q96.28 44.63 95.72 43.57Q95.17 42.51 95.17 41.02Q95.17 39.53 95.72 38.47Q96.28 37.4 97.24 36.84Q98.2 36.28 99.41 36.28Q100.62 36.28 101.57 36.84Q102.53 37.4 103.08 38.47Q103.64 39.53 103.64 41.02Q103.64 42.51 103.08 43.57Q102.53 44.63 101.57 45.19Q100.62 45.75 99.41 45.75ZM99.41 44.27Q100.16 44.27 100.74 43.9Q101.32 43.52 101.65 42.8Q101.98 42.07 101.98 41.02Q101.98 39.96 101.65 39.23Q101.32 38.51 100.74 38.13Q100.16 37.76 99.41 37.76Q98.65 37.76 98.07 38.13Q97.49 38.51 97.16 39.24Q96.83 39.97 96.83 41.02Q96.83 42.07 97.16 42.8Q97.49 43.52 98.07 43.9Q98.65 44.27 99.41 44.27ZM108.03 45.63V36.4H111.48Q112.54 36.4 113.26 36.77Q113.99 37.14 114.36 37.81Q114.73 38.47 114.73 39.35Q114.73 40.24 114.35 40.89Q113.98 41.54 113.25 41.89Q112.52 42.24 111.45 42.24H108.99V40.87H111.23Q111.86 40.87 112.26 40.69Q112.66 40.52 112.85 40.18Q113.04 39.84 113.04 39.35Q113.04 38.86 112.85 38.51Q112.66 38.16 112.26 37.97Q111.86 37.78 111.23 37.78H109.68V45.63ZM113.22 45.63 110.98 41.44H112.79L115.07 45.63ZM118.71 37.79V36.4H126.06V37.79H123.21V45.63H121.56V37.79ZM129.7 45.63 132.92 36.4H135.02L138.31 45.63H136.5L134.86 40.84Q134.62 40.09 134.36 39.15Q134.1 38.22 133.78 37.02H134.13Q133.82 38.23 133.57 39.17Q133.32 40.12 133.09 40.84L131.51 45.63ZM131.58 43.34V42.01H136.44V43.34ZM142.37 45.63V36.4H144.02V44.24H148.1V45.63Z" class="st3"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Ebene_1" x="0" y="0" viewBox="0 0 153 46.0">
|
||||
<style>
|
||||
.st3{fill:#00004a}
|
||||
</style>
|
||||
<g id="Logotype_00000134236662009793237430000016451412871672361145_">
|
||||
<path d="m100 22.3 4.3 8.4h9.2l-5.3-10.2zm22.4-6.2V16c0-5.2 2.9-8.2 10.2-8.2 5.7 0 8.7 1.9 9.8 5.2l8.8-1.4c-1.8-7-8.2-11-18.6-11-12.4 0-19.1 5.7-19.1 15.5 0 .5 0 1.1.1 1.6zm20.1-2.7c.2.8.3 1.7.3 2.6 0 5.1-3 8.3-10.2 8.3-6.4 0-9.5-2.5-10.1-6.7l-8.7 1.8c1.4 7.7 7.9 12.2 18.8 12.2 12.4 0 19.1-5.8 19.1-15.6 0-1.6-.2-3.1-.5-4.4zM73.4 26.9l-2 3.8h9.2l2.7-5.4zm25-25.6H86.6L74.5 24.7l10.2-2.2 7.8-15.2 6.4 12.6 8.3-1.3zM53.7 30.1v.6h8.5v-2zm0-1.1 8.5-1.7V8.5h13.4V1.3H40.2v7.2h13.5zM19.6.5c10.8 0 17 4.7 17.8 12.2h-9.1c-.6-2.7-3.3-5-8.8-5-6.3 0-9.4 3.1-9.4 8.2s3.1 8.3 9.4 8.3c5.4 0 8.2-2.3 8.8-5.1h9.1c-.9 7.6-7 12.3-17.8 12.3C7.8 31.5 1.3 26.1 1.3 16S7.8.5 19.6.5" class="st3"/>
|
||||
<path d="M94.4 23.7c1-1.4 3.3-2.1 5.1-2.7-2 .1-4.4.3-5.8-.6-1.4-1-2.1-3.3-2.7-5.1.1 2 .3 4.4-.6 5.8-1 1.4-3.3 2.1-5.1 2.7 2-.1 4.4-.3 5.8.6 1.4 1 2.1 3.3 2.7 5.1-.2-2-.4-4.4.6-5.8" class="st3"/>
|
||||
</g>
|
||||
<path d="M6.48 45.77Q5.41 45.77 4.62 45.44Q3.83 45.1 3.38 44.47Q2.94 43.83 2.9 42.92H4.53Q4.57 43.4 4.83 43.72Q5.09 44.04 5.52 44.2Q5.95 44.36 6.47 44.36Q7.01 44.36 7.43 44.19Q7.85 44.03 8.08 43.72Q8.32 43.42 8.32 43.03Q8.32 42.67 8.11 42.43Q7.9 42.2 7.53 42.03Q7.15 41.87 6.65 41.75L5.6 41.47Q4.45 41.18 3.8 40.57Q3.15 39.97 3.15 38.99Q3.15 38.18 3.59 37.57Q4.03 36.95 4.8 36.61Q5.56 36.28 6.52 36.28Q7.51 36.28 8.25 36.62Q8.99 36.96 9.41 37.56Q9.83 38.16 9.85 38.93H8.25Q8.18 38.33 7.71 38.01Q7.25 37.68 6.5 37.68Q5.98 37.68 5.6 37.84Q5.23 38 5.03 38.27Q4.83 38.55 4.83 38.9Q4.83 39.28 5.06 39.53Q5.3 39.78 5.66 39.93Q6.03 40.08 6.4 40.17L7.26 40.39Q7.77 40.51 8.26 40.72Q8.74 40.92 9.13 41.24Q9.52 41.55 9.75 41.99Q9.97 42.44 9.97 43.04Q9.97 43.85 9.56 44.47Q9.15 45.08 8.37 45.43Q7.58 45.77 6.48 45.77ZM18.24 45.75Q17.03 45.75 16.08 45.19Q15.12 44.63 14.57 43.57Q14.02 42.51 14.02 41.02Q14.02 39.53 14.58 38.47Q15.13 37.4 16.08 36.84Q17.04 36.28 18.24 36.28Q19 36.28 19.66 36.49Q20.32 36.71 20.84 37.12Q21.35 37.53 21.69 38.12Q22.02 38.72 22.13 39.47H20.47Q20.39 39.06 20.19 38.74Q20 38.43 19.71 38.21Q19.41 37.98 19.05 37.87Q18.68 37.76 18.27 37.76Q17.51 37.76 16.93 38.14Q16.34 38.52 16.01 39.25Q15.68 39.97 15.68 41.02Q15.68 42.08 16.02 42.8Q16.35 43.53 16.93 43.9Q17.52 44.27 18.27 44.27Q18.68 44.27 19.04 44.16Q19.41 44.04 19.7 43.82Q20 43.6 20.19 43.28Q20.39 42.97 20.47 42.56H22.14Q22.05 43.22 21.73 43.8Q21.42 44.38 20.91 44.82Q20.41 45.26 19.74 45.51Q19.06 45.75 18.24 45.75ZM28.13 36.4V45.63H26.48V36.4ZM32.81 45.63V36.4H38.81V37.79H34.46V40.28H38.49V41.66H34.46V44.24H38.83V45.63ZM43.31 45.63V36.4H45.18L48.38 41.48Q48.57 41.8 48.8 42.2Q49.02 42.6 49.26 43.09Q49.5 43.58 49.74 44.19H49.55Q49.51 43.66 49.48 43.12Q49.44 42.57 49.43 42.07Q49.41 41.58 49.41 41.24V36.4H51.09V45.63H49.21L46.33 41.05Q46.06 40.61 45.82 40.19Q45.57 39.76 45.3 39.22Q45.02 38.68 44.62 37.92H44.85Q44.89 38.59 44.92 39.2Q44.96 39.8 44.98 40.28Q45 40.75 45 41.05V45.63ZM59.7 45.75Q58.49 45.75 57.53 45.19Q56.58 44.63 56.03 43.57Q55.48 42.51 55.48 41.02Q55.48 39.53 56.03 38.47Q56.58 37.4 57.54 36.84Q58.5 36.28 59.7 36.28Q60.46 36.28 61.12 36.49Q61.78 36.71 62.29 37.12Q62.81 37.53 63.14 38.12Q63.48 38.72 63.59 39.47H61.92Q61.84 39.06 61.65 38.74Q61.45 38.43 61.16 38.21Q60.87 37.98 60.51 37.87Q60.14 37.76 59.73 37.76Q58.97 37.76 58.38 38.14Q57.8 38.52 57.47 39.25Q57.14 39.97 57.14 41.02Q57.14 42.08 57.47 42.8Q57.81 43.53 58.39 43.9Q58.97 44.27 59.72 44.27Q60.14 44.27 60.5 44.16Q60.86 44.04 61.16 43.82Q61.45 43.6 61.65 43.28Q61.85 42.97 61.93 42.56H63.59Q63.5 43.22 63.19 43.8Q62.88 44.38 62.37 44.82Q61.86 45.26 61.19 45.51Q60.52 45.75 59.7 45.75ZM67.93 45.63V36.4H73.93V37.79H69.59V40.28H73.61V41.66H69.59V44.24H73.95V45.63ZM84.45 45.63V36.4H87.91Q88.97 36.4 89.69 36.8Q90.41 37.2 90.78 37.89Q91.15 38.57 91.15 39.46Q91.15 40.34 90.78 41.03Q90.41 41.72 89.68 42.11Q88.95 42.51 87.88 42.51H85.59V41.15H87.66Q88.29 41.15 88.69 40.93Q89.09 40.71 89.28 40.33Q89.46 39.95 89.46 39.46Q89.46 38.96 89.28 38.59Q89.09 38.21 88.69 38Q88.28 37.78 87.65 37.78H86.11V45.63ZM99.41 45.75Q98.2 45.75 97.24 45.19Q96.28 44.63 95.72 43.57Q95.17 42.51 95.17 41.02Q95.17 39.53 95.72 38.47Q96.28 37.4 97.24 36.84Q98.2 36.28 99.41 36.28Q100.62 36.28 101.57 36.84Q102.53 37.4 103.08 38.47Q103.64 39.53 103.64 41.02Q103.64 42.51 103.08 43.57Q102.53 44.63 101.57 45.19Q100.62 45.75 99.41 45.75ZM99.41 44.27Q100.16 44.27 100.74 43.9Q101.32 43.52 101.65 42.8Q101.98 42.07 101.98 41.02Q101.98 39.96 101.65 39.23Q101.32 38.51 100.74 38.13Q100.16 37.76 99.41 37.76Q98.65 37.76 98.07 38.13Q97.49 38.51 97.16 39.24Q96.83 39.97 96.83 41.02Q96.83 42.07 97.16 42.8Q97.49 43.52 98.07 43.9Q98.65 44.27 99.41 44.27ZM108.03 45.63V36.4H111.48Q112.54 36.4 113.26 36.77Q113.99 37.14 114.36 37.81Q114.73 38.47 114.73 39.35Q114.73 40.24 114.35 40.89Q113.98 41.54 113.25 41.89Q112.52 42.24 111.45 42.24H108.99V40.87H111.23Q111.86 40.87 112.26 40.69Q112.66 40.52 112.85 40.18Q113.04 39.84 113.04 39.35Q113.04 38.86 112.85 38.51Q112.66 38.16 112.26 37.97Q111.86 37.78 111.23 37.78H109.68V45.63ZM113.22 45.63 110.98 41.44H112.79L115.07 45.63ZM118.71 37.79V36.4H126.06V37.79H123.21V45.63H121.56V37.79ZM129.7 45.63 132.92 36.4H135.02L138.31 45.63H136.5L134.86 40.84Q134.62 40.09 134.36 39.15Q134.1 38.22 133.78 37.02H134.13Q133.82 38.23 133.57 39.17Q133.32 40.12 133.09 40.84L131.51 45.63ZM131.58 43.34V42.01H136.44V43.34ZM142.37 45.63V36.4H144.02V44.24H148.1V45.63Z" class="st3"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,110 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 640" role="img" aria-label="Architektura klasycznego CMS z bazą danych: aplikacja CMS z publicznym panelem admina działa non stop, obok baza danych i wolumen mediów; czytelnik dostaje strony renderowane na żądanie">
|
||||
<defs>
|
||||
<marker id="arrB" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
|
||||
</marker>
|
||||
<filter id="softB" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<style>
|
||||
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
|
||||
.box { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
|
||||
.sub { fill: #f5f5f5; stroke: #e2e5ee; stroke-width: 1; }
|
||||
.ttl { font-weight: 700; font-size: 15.5px; fill: #00004a; letter-spacing: .04em; }
|
||||
.cap { font-size: 12.5px; fill: #46536a; }
|
||||
.lbl { font-size: 12px; fill: #46536a; paint-order: stroke; stroke: #ffffff; stroke-width: 4; }
|
||||
.chipbg { fill: #e9ebf3; }
|
||||
.chip { font-size: 11.5px; font-weight: 700; fill: #00004a; letter-spacing: .06em; }
|
||||
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arrB); }
|
||||
.pill { fill: #f5f5f5; stroke: #e2e5ee; }
|
||||
.pilltxt { font-size: 12.5px; fill: #101228; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- machine boundary (mirrors the static-demo diagram 1:1) -->
|
||||
<rect x="340" y="40" width="830" height="570" rx="18" fill="#f8f9fc" stroke="#e2e5ee" stroke-width="1.5"/>
|
||||
<text class="t" x="366" y="76" font-size="13" font-weight="700" fill="#00004a" letter-spacing=".18em">MASZYNA · CMS Z BAZĄ DANYCH</text>
|
||||
<text class="t cap" x="366" y="96">np. WordPress, Strapi, Drupal — wspólny schemat tej klasy rozwiązań</text>
|
||||
|
||||
<!-- actor: redaktor -->
|
||||
<g filter="url(#softB)">
|
||||
<rect class="box" x="52" y="120" width="220" height="120" rx="16"/>
|
||||
</g>
|
||||
<circle cx="88" cy="156" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
|
||||
<path d="M72 186 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
|
||||
<text class="t ttl" x="116" y="162">Redaktor</text>
|
||||
<text class="t cap" x="70" y="212">panel admina w przeglądarce</text>
|
||||
<text class="t cap" x="70" y="228">logowanie do aplikacji</text>
|
||||
|
||||
<!-- actor: czytelnik -->
|
||||
<g filter="url(#softB)">
|
||||
<rect class="box" x="52" y="400" width="220" height="120" rx="16"/>
|
||||
</g>
|
||||
<circle cx="88" cy="436" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
|
||||
<path d="M72 466 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
|
||||
<text class="t ttl" x="116" y="442">Czytelnik</text>
|
||||
<text class="t cap" x="70" y="492">publiczna strona portalu</text>
|
||||
<text class="t cap" x="70" y="508">HTML składany na żądanie</text>
|
||||
|
||||
<!-- CMS application (tall box, always running) -->
|
||||
<g filter="url(#softB)">
|
||||
<rect class="box" x="400" y="120" width="320" height="400" rx="16"/>
|
||||
</g>
|
||||
<text class="t ttl" x="424" y="152">APLIKACJA CMS</text>
|
||||
<rect class="chipbg" x="590" y="136" width="106" height="22" rx="11"/>
|
||||
<text class="t chip" x="643" y="151" text-anchor="middle">NON STOP</text>
|
||||
<rect class="sub" x="420" y="176" width="280" height="88" rx="12"/>
|
||||
<text class="t" x="436" y="202" font-size="13.5" font-weight="700" fill="#101228">panel admina</text>
|
||||
<text class="t cap" x="436" y="224">publiczny adres + logowanie</text>
|
||||
<text class="t cap" x="436" y="242">z internetu</text>
|
||||
<rect class="sub" x="420" y="278" width="280" height="88" rx="12"/>
|
||||
<text class="t" x="436" y="304" font-size="13.5" font-weight="700" fill="#101228">renderowanie stron</text>
|
||||
<text class="t cap" x="436" y="326">każde wejście czytelnika =</text>
|
||||
<text class="t cap" x="436" y="344">praca aplikacji i bazy</text>
|
||||
<rect class="sub" x="420" y="380" width="280" height="88" rx="12"/>
|
||||
<text class="t" x="436" y="406" font-size="13.5" font-weight="700" fill="#101228">pluginy / rozszerzenia</text>
|
||||
<text class="t cap" x="436" y="428">własny cykl aktualizacji</text>
|
||||
<text class="t cap" x="436" y="446">i podatności</text>
|
||||
|
||||
<!-- database -->
|
||||
<g filter="url(#softB)">
|
||||
<rect class="box" x="790" y="120" width="340" height="185" rx="16"/>
|
||||
</g>
|
||||
<text class="t ttl" x="814" y="152">BAZA DANYCH</text>
|
||||
<rect class="chipbg" x="972" y="136" width="106" height="22" rx="11"/>
|
||||
<text class="t chip" x="1025" y="151" text-anchor="middle">NON STOP</text>
|
||||
<text class="t cap" x="814" y="188">treść + konta użytkowników</text>
|
||||
<text class="t cap" x="814" y="212">wymaga backupów i testów odtwarzania</text>
|
||||
<text class="t cap" x="814" y="236">migracje schematu przy aktualizacjach</text>
|
||||
<text class="t cap" x="814" y="266">historia zmian: zależnie od produktu,</text>
|
||||
<text class="t cap" x="814" y="284">bywa funkcją płatną</text>
|
||||
|
||||
<!-- media volume -->
|
||||
<g filter="url(#softB)">
|
||||
<rect class="sub" x="790" y="400" width="340" height="120" rx="16"/>
|
||||
</g>
|
||||
<text class="t ttl" x="814" y="432">media/</text>
|
||||
<rect class="chipbg" x="896" y="416" width="82" height="22" rx="11"/>
|
||||
<text class="t chip" x="937" y="431" text-anchor="middle">WOLUMEN</text>
|
||||
<text class="t cap" x="814" y="462">obrazy i pliki poza bazą</text>
|
||||
<text class="t cap" x="814" y="480">osobny backup</text>
|
||||
|
||||
<!-- flows -->
|
||||
<path class="flow" d="M272 180 H396"/>
|
||||
<text class="t lbl" x="334" y="170" text-anchor="middle">logowanie do panelu</text>
|
||||
|
||||
<path class="flow" d="M400 460 H276"/>
|
||||
<text class="t lbl" x="338" y="450" text-anchor="middle">HTML na żądanie</text>
|
||||
|
||||
<path class="flow" d="M720 212 H786"/>
|
||||
<text class="t lbl" x="753" y="202" text-anchor="middle">SQL</text>
|
||||
|
||||
<path class="flow" d="M720 440 H786"/>
|
||||
<text class="t lbl" x="753" y="430" text-anchor="middle">upload</text>
|
||||
|
||||
<!-- summary pills -->
|
||||
<rect class="pill" x="380" y="552" width="360" height="36" rx="18"/>
|
||||
<text class="t pilltxt" x="560" y="575" text-anchor="middle">2 procesy działają bez przerwy, także nocą</text>
|
||||
<rect class="pill" x="770" y="552" width="360" height="36" rx="18"/>
|
||||
<text class="t pilltxt" x="950" y="575" text-anchor="middle">aktualizacje bezpieczeństwa = stały obowiązek</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,111 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 640" role="img" aria-label="Architektura demo na maszynie: redaktor i czytelnik łączą się z kontenerami podmana — Gitea, build Astro, wolumen releases i nginx">
|
||||
<defs>
|
||||
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
|
||||
</marker>
|
||||
<filter id="soft" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<style>
|
||||
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
|
||||
.box { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
|
||||
.sub { fill: #f5f5f5; stroke: #e2e5ee; stroke-width: 1; }
|
||||
.ttl { font-weight: 700; font-size: 15.5px; fill: #00004a; letter-spacing: .04em; }
|
||||
.cap { font-size: 12.5px; fill: #46536a; }
|
||||
.lbl { font-size: 12px; fill: #46536a; paint-order: stroke; stroke: #ffffff; stroke-width: 4; }
|
||||
.chipbg { fill: #d2faf7; }
|
||||
.chip { font-size: 11.5px; font-weight: 700; fill: #00004a; letter-spacing: .06em; }
|
||||
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arr); }
|
||||
.mono { font-family: "SF Mono", Consolas, Menlo, monospace; font-size: 12.5px; fill: #101228; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- machine boundary -->
|
||||
<rect x="340" y="40" width="830" height="570" rx="18" fill="#f8f9fc" stroke="#e2e5ee" stroke-width="1.5"/>
|
||||
<text class="t" x="366" y="76" font-size="13" font-weight="700" fill="#00004a" letter-spacing=".18em">MASZYNA DEMO · PODMAN</text>
|
||||
<text class="t cap" x="366" y="96">osobne kontenery pod systemd · wszystko w jednym katalogu użytkownika</text>
|
||||
|
||||
<!-- actor: redaktor -->
|
||||
<g filter="url(#soft)">
|
||||
<rect class="box" x="52" y="120" width="220" height="120" rx="16"/>
|
||||
</g>
|
||||
<circle cx="88" cy="156" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
|
||||
<path d="M72 186 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
|
||||
<text class="t ttl" x="116" y="162">Redaktor</text>
|
||||
<text class="t cap" x="70" y="212">przeglądarka · <tspan font-weight="600">/admin/</tspan> (Sveltia)</text>
|
||||
<text class="t cap" x="70" y="228">edycja WYSIWYG, podgląd 1:1</text>
|
||||
|
||||
<!-- actor: czytelnik -->
|
||||
<g filter="url(#soft)">
|
||||
<rect class="box" x="52" y="400" width="220" height="120" rx="16"/>
|
||||
</g>
|
||||
<circle cx="88" cy="436" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
|
||||
<path d="M72 466 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
|
||||
<text class="t ttl" x="116" y="442">Czytelnik</text>
|
||||
<text class="t cap" x="70" y="492">publiczna strona portalu</text>
|
||||
<text class="t cap" x="70" y="508">czysty, szybki HTML</text>
|
||||
|
||||
<!-- gitea -->
|
||||
<g filter="url(#soft)">
|
||||
<rect class="box" x="400" y="120" width="320" height="185" rx="16"/>
|
||||
</g>
|
||||
<text class="t ttl" x="424" y="152">GITEA</text>
|
||||
<rect class="chipbg" x="490" y="136" width="52" height="22" rx="11"/>
|
||||
<text class="t chip" x="516" y="151" text-anchor="middle">:3000</text>
|
||||
<rect class="sub" x="420" y="172" width="280" height="112" rx="12"/>
|
||||
<text class="t" x="436" y="198" font-size="13.5" font-weight="700" fill="#101228">repo ctao/portal</text>
|
||||
<text class="t cap" x="436" y="220">treść: Markdown + obrazy (WebP)</text>
|
||||
<text class="t cap" x="436" y="238">każda zmiana = commit</text>
|
||||
<text class="t cap" x="436" y="256">pełna historia + rollback za darmo</text>
|
||||
|
||||
<!-- build (ephemeral) -->
|
||||
<g filter="url(#soft)">
|
||||
<rect x="790" y="120" width="340" height="185" rx="16" fill="#ffffff" stroke="#00004a" stroke-width="1.5" stroke-dasharray="7 5"/>
|
||||
</g>
|
||||
<text class="t ttl" x="814" y="152">BUILD · ASTRO</text>
|
||||
<rect class="chipbg" x="948" y="136" width="164" height="22" rx="11"/>
|
||||
<text class="t chip" x="1030" y="151" text-anchor="middle">KONTENER JEDNORAZOWY</text>
|
||||
<text class="t cap" x="814" y="188">timer systemd (co ~10 s) sprawdza,</text>
|
||||
<text class="t cap" x="814" y="206">czy w repo jest nowy commit</text>
|
||||
<text class="t cap" x="814" y="234">jeśli tak: git fetch → checkout →</text>
|
||||
<text class="t cap" x="814" y="252"><tspan font-weight="700" fill="#101228">Markdown → statyczny HTML</tspan></text>
|
||||
<text class="t cap" x="814" y="280">po buildzie kontener znika</text>
|
||||
|
||||
<!-- releases volume -->
|
||||
<g filter="url(#soft)">
|
||||
<rect class="sub" x="790" y="400" width="340" height="150" rx="16"/>
|
||||
</g>
|
||||
<text class="t ttl" x="814" y="432">releases/</text>
|
||||
<rect class="chipbg" x="908" y="416" width="82" height="22" rx="11"/>
|
||||
<text class="t chip" x="949" y="431" text-anchor="middle">WOLUMEN</text>
|
||||
<text class="t mono" x="814" y="466">9f2c1d/ poprzednia wersja</text>
|
||||
<text class="t mono" x="814" y="488">71b980/ <tspan font-weight="700" fill="#00004a">← current (symlink)</tspan></text>
|
||||
<text class="t cap" x="814" y="518">podmiana atomowa — zero przestoju</text>
|
||||
|
||||
<!-- nginx -->
|
||||
<g filter="url(#soft)">
|
||||
<rect class="box" x="400" y="400" width="320" height="150" rx="16"/>
|
||||
</g>
|
||||
<text class="t ttl" x="424" y="432">NGINX</text>
|
||||
<rect class="chipbg" x="500" y="416" width="52" height="22" rx="11"/>
|
||||
<text class="t chip" x="526" y="431" text-anchor="middle">:8080</text>
|
||||
<text class="t cap" x="424" y="466">serwuje <tspan font-weight="600">releases/current</tspan></text>
|
||||
<text class="t cap" x="424" y="484">wyłącznie statyczne pliki</text>
|
||||
<text class="t cap" x="424" y="502">brak aplikacji · brak bazy · brak PHP</text>
|
||||
|
||||
<!-- flows -->
|
||||
<path class="flow" d="M272 180 H396"/>
|
||||
<text class="t lbl" x="334" y="170" text-anchor="middle">zapis przez API (OAuth)</text>
|
||||
|
||||
<path class="flow" d="M790 212 H724"/>
|
||||
<text class="t lbl" x="757" y="202" text-anchor="middle">git fetch</text>
|
||||
|
||||
<path class="flow" d="M960 305 V396"/>
|
||||
<text class="t lbl" x="968" y="354" text-anchor="start">nowa wersja strony</text>
|
||||
|
||||
<path class="flow" d="M790 475 H724"/>
|
||||
<text class="t lbl" x="757" y="465" text-anchor="middle">current/</text>
|
||||
|
||||
<path class="flow" d="M400 475 H276"/>
|
||||
<text class="t lbl" x="338" y="465" text-anchor="middle">statyczny HTML</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,78 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 900" role="img" aria-label="Grafika: graf commitów w gicie przechodzący w statyczną stronę WWW">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#00004a"/>
|
||||
<stop offset=".55" stop-color="#000063"/>
|
||||
<stop offset="1" stop-color="#00009c"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="glow" cx=".5" cy=".5" r=".5">
|
||||
<stop offset="0" stop-color="#00e4d8" stop-opacity=".55"/>
|
||||
<stop offset="1" stop-color="#00e4d8" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="page" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#ffffff"/>
|
||||
<stop offset="1" stop-color="#eef1f8"/>
|
||||
</linearGradient>
|
||||
<filter id="blur18"><feGaussianBlur stdDeviation="18"/></filter>
|
||||
</defs>
|
||||
|
||||
<rect width="1600" height="900" fill="url(#bg)"/>
|
||||
|
||||
<!-- starfield -->
|
||||
<g fill="#ffffff">
|
||||
<circle cx="140" cy="130" r="2" opacity=".55"/><circle cx="360" cy="80" r="1.4" opacity=".35"/>
|
||||
<circle cx="620" cy="150" r="1.8" opacity=".45"/><circle cx="905" cy="70" r="1.4" opacity=".4"/>
|
||||
<circle cx="1210" cy="120" r="2" opacity=".5"/><circle cx="1430" cy="200" r="1.5" opacity=".35"/>
|
||||
<circle cx="90" cy="420" r="1.5" opacity=".35"/><circle cx="240" cy="700" r="2" opacity=".45"/>
|
||||
<circle cx="520" cy="810" r="1.4" opacity=".3"/><circle cx="1050" cy="840" r="1.8" opacity=".4"/>
|
||||
<circle cx="1330" cy="760" r="1.4" opacity=".35"/><circle cx="1500" cy="560" r="2" opacity=".5"/>
|
||||
<circle cx="760" cy="330" r="1.3" opacity=".3"/><circle cx="1130" cy="300" r="1.5" opacity=".35"/>
|
||||
</g>
|
||||
|
||||
<!-- cherenkov glow behind the merge point -->
|
||||
<circle cx="560" cy="450" r="260" fill="url(#glow)" filter="url(#blur18)"/>
|
||||
|
||||
<!-- git graph: two branches merging -->
|
||||
<g fill="none" stroke-linecap="round">
|
||||
<path d="M220 260 H420 Q500 260 540 350 L560 450" stroke="#3d3dc4" stroke-width="10"/>
|
||||
<path d="M220 640 H420 Q500 640 540 550 L560 450" stroke="#3d3dc4" stroke-width="10"/>
|
||||
<path d="M560 450 H840" stroke="#00e4d8" stroke-width="12"/>
|
||||
</g>
|
||||
<!-- commit nodes -->
|
||||
<g stroke="#00004a" stroke-width="6">
|
||||
<circle cx="260" cy="260" r="22" fill="#ffffff"/>
|
||||
<circle cx="400" cy="260" r="22" fill="#ffffff"/>
|
||||
<circle cx="260" cy="640" r="22" fill="#ffffff"/>
|
||||
<circle cx="400" cy="640" r="22" fill="#ffffff"/>
|
||||
<circle cx="560" cy="450" r="30" fill="#00e4d8"/>
|
||||
</g>
|
||||
|
||||
<!-- flowing into a static page -->
|
||||
<g>
|
||||
<!-- back pages -->
|
||||
<rect x="920" y="212" width="420" height="520" rx="24" fill="#c7cde9" opacity=".45" transform="rotate(-4 1130 470)"/>
|
||||
<rect x="905" y="200" width="420" height="520" rx="24" fill="#c7cde9" opacity=".7" transform="rotate(-2 1115 460)"/>
|
||||
<!-- front page -->
|
||||
<rect x="890" y="190" width="430" height="530" rx="24" fill="url(#page)"/>
|
||||
<!-- browser dots -->
|
||||
<circle cx="930" cy="232" r="8" fill="#e2e5ee"/><circle cx="958" cy="232" r="8" fill="#e2e5ee"/><circle cx="986" cy="232" r="8" fill="#e2e5ee"/>
|
||||
<!-- headline + text lines -->
|
||||
<rect x="930" y="286" width="270" height="26" rx="13" fill="#00004a"/>
|
||||
<rect x="930" y="336" width="350" height="14" rx="7" fill="#9aa3bf"/>
|
||||
<rect x="930" y="366" width="330" height="14" rx="7" fill="#9aa3bf"/>
|
||||
<rect x="930" y="396" width="300" height="14" rx="7" fill="#9aa3bf"/>
|
||||
<!-- image placeholder with cherenkov accent -->
|
||||
<rect x="930" y="440" width="350" height="160" rx="16" fill="#e8ecf6"/>
|
||||
<path d="M950 585 L1030 505 L1090 555 L1140 515 L1260 585 Z" fill="#c7cde9"/>
|
||||
<circle cx="1215" cy="480" r="16" fill="#00e4d8"/>
|
||||
<!-- more text -->
|
||||
<rect x="930" y="630" width="340" height="14" rx="7" fill="#9aa3bf"/>
|
||||
<rect x="930" y="660" width="250" height="14" rx="7" fill="#9aa3bf"/>
|
||||
</g>
|
||||
|
||||
<!-- markdown glyph riding the flow -->
|
||||
<g transform="translate(660 380)">
|
||||
<rect x="0" y="0" width="120" height="84" rx="14" fill="#00004a" stroke="#00e4d8" stroke-width="3"/>
|
||||
<text x="60" y="56" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, sans-serif" font-size="38" font-weight="800" fill="#ffffff">M↓</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,76 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 430" role="img" aria-label="Pięć kroków od zapisu w edytorze do publikacji: zapis, commit, detekcja, build, publikacja">
|
||||
<defs>
|
||||
<marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
|
||||
</marker>
|
||||
<filter id="soft2" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<style>
|
||||
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
|
||||
.card { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
|
||||
.ttl { font-weight: 700; font-size: 15px; fill: #00004a; }
|
||||
.cap { font-size: 12.3px; fill: #46536a; }
|
||||
.num { font-weight: 800; font-size: 15px; fill: #00004a; }
|
||||
.pill { fill: #f5f5f5; stroke: #e2e5ee; }
|
||||
.pilltxt { font-size: 12.5px; fill: #101228; }
|
||||
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arr2); }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- cards -->
|
||||
<g filter="url(#soft2)"><rect class="card" x="55" y="100" width="200" height="175" rx="16"/></g>
|
||||
<g filter="url(#soft2)"><rect class="card" x="285" y="100" width="200" height="175" rx="16"/></g>
|
||||
<g filter="url(#soft2)"><rect class="card" x="515" y="100" width="200" height="175" rx="16"/></g>
|
||||
<g filter="url(#soft2)"><rect class="card" x="745" y="100" width="200" height="175" rx="16"/></g>
|
||||
<g filter="url(#soft2)"><rect class="card" x="975" y="100" width="200" height="175" rx="16"/></g>
|
||||
|
||||
<!-- connectors -->
|
||||
<path class="flow" d="M255 187 H281"/>
|
||||
<path class="flow" d="M485 187 H511"/>
|
||||
<path class="flow" d="M715 187 H741"/>
|
||||
<path class="flow" d="M945 187 H971"/>
|
||||
|
||||
<!-- step numbers -->
|
||||
<circle cx="155" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="155" y="106" text-anchor="middle">1</text>
|
||||
<circle cx="385" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="385" y="106" text-anchor="middle">2</text>
|
||||
<circle cx="615" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="615" y="106" text-anchor="middle">3</text>
|
||||
<circle cx="845" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="845" y="106" text-anchor="middle">4</text>
|
||||
<circle cx="1075" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="1075" y="106" text-anchor="middle">5</text>
|
||||
|
||||
<!-- card 1 -->
|
||||
<text class="t ttl" x="155" y="152" text-anchor="middle">Zapis w edytorze</text>
|
||||
<text class="t cap" x="155" y="182" text-anchor="middle">Sveltia · WYSIWYG</text>
|
||||
<text class="t cap" x="155" y="200" text-anchor="middle">podgląd artykułu 1:1</text>
|
||||
<text class="t cap" x="155" y="218" text-anchor="middle">obrazy → WebP przy uploadzie</text>
|
||||
|
||||
<!-- card 2 -->
|
||||
<text class="t ttl" x="385" y="152" text-anchor="middle">Commit w repo</text>
|
||||
<text class="t cap" x="385" y="182" text-anchor="middle">Gitea · logowanie OAuth</text>
|
||||
<text class="t cap" x="385" y="200" text-anchor="middle">zapis przez API = commit</text>
|
||||
<text class="t cap" x="385" y="218" text-anchor="middle">historia każdej zmiany</text>
|
||||
|
||||
<!-- card 3 -->
|
||||
<text class="t ttl" x="615" y="152" text-anchor="middle">Detekcja zmiany</text>
|
||||
<text class="t cap" x="615" y="182" text-anchor="middle">timer systemd</text>
|
||||
<text class="t cap" x="615" y="200" text-anchor="middle">sprawdza repo co ~10 s</text>
|
||||
<text class="t cap" x="615" y="218" text-anchor="middle">brak zmian = brak pracy</text>
|
||||
|
||||
<!-- card 4 -->
|
||||
<text class="t ttl" x="845" y="152" text-anchor="middle">Build strony</text>
|
||||
<text class="t cap" x="845" y="182" text-anchor="middle">jednorazowy kontener</text>
|
||||
<text class="t cap" x="845" y="200" text-anchor="middle">Astro: Markdown →</text>
|
||||
<text class="t cap" x="845" y="218" text-anchor="middle">statyczny HTML</text>
|
||||
|
||||
<!-- card 5 -->
|
||||
<text class="t ttl" x="1075" y="152" text-anchor="middle">Publikacja</text>
|
||||
<text class="t cap" x="1075" y="182" text-anchor="middle">atomowa podmiana symlinka</text>
|
||||
<text class="t cap" x="1075" y="200" text-anchor="middle">nginx od razu serwuje</text>
|
||||
<text class="t cap" x="1075" y="218" text-anchor="middle">nową wersję</text>
|
||||
|
||||
<!-- reassurance pills -->
|
||||
<rect class="pill" x="240" y="330" width="330" height="40" rx="20"/>
|
||||
<text class="t pilltxt" x="405" y="355" text-anchor="middle">awaria builda? strona działa dalej na starej wersji</text>
|
||||
<rect class="pill" x="630" y="330" width="330" height="40" rx="20"/>
|
||||
<text class="t pilltxt" x="795" y="355" text-anchor="middle">rollback = git revert — jedna komenda</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 360" role="img" aria-label="Porównanie powierzchni ataku: CMS z bazą wystawia publicznie aplikację, bazę i pluginy; rozwiązanie statyczne wystawia tylko pliki HTML, a edycja idzie przez git">
|
||||
<defs>
|
||||
<filter id="soft3" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
|
||||
</filter>
|
||||
<marker id="arr3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
|
||||
</marker>
|
||||
<style>
|
||||
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
|
||||
.hd { font-weight: 800; font-size: 14px; fill: #00004a; letter-spacing: .14em; }
|
||||
.cap { font-size: 12.5px; fill: #46536a; }
|
||||
.item{ font-size: 13px; fill: #101228; }
|
||||
.bad { fill: #fdf0f0; stroke: #e8c5c5; stroke-width: 1.5; }
|
||||
.good{ fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
|
||||
.flow{ stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arr3); }
|
||||
.lbl { font-size: 12px; fill: #46536a; paint-order: stroke; stroke: #ffffff; stroke-width: 4; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- left: classic CMS -->
|
||||
<text class="t hd" x="300" y="52" text-anchor="middle">CMS Z BAZĄ DANYCH</text>
|
||||
<text class="t item" x="60" y="150">internet</text>
|
||||
<path class="flow" d="M130 145 H186"/>
|
||||
<g filter="url(#soft3)"><rect class="bad" x="190" y="80" width="360" height="200" rx="16"/></g>
|
||||
<text class="t item" x="218" y="126">▪ aplikacja CMS (PHP / Node) — publiczna</text>
|
||||
<text class="t item" x="218" y="158">▪ panel logowania — publiczny</text>
|
||||
<text class="t item" x="218" y="190">▪ baza danych z treścią</text>
|
||||
<text class="t item" x="218" y="222">▪ pluginy i zależności</text>
|
||||
<text class="t cap" x="218" y="256" font-weight="700" fill="#8f3b3b">wszystko działa non stop i wymaga patchowania</text>
|
||||
|
||||
<!-- divider -->
|
||||
<line x1="600" y1="60" x2="600" y2="320" stroke="#e2e5ee" stroke-width="1.5"/>
|
||||
|
||||
<!-- right: static -->
|
||||
<text class="t hd" x="900" y="52" text-anchor="middle">TO ROZWIĄZANIE</text>
|
||||
<text class="t item" x="646" y="130">internet</text>
|
||||
<path class="flow" d="M716 125 H772"/>
|
||||
<g filter="url(#soft3)"><rect class="good" x="776" y="88" width="360" height="76" rx="16"/></g>
|
||||
<text class="t item" x="804" y="120">▪ pliki HTML — i nic więcej</text>
|
||||
<text class="t cap" x="804" y="146">nie ma czego zhakować ani patchować</text>
|
||||
|
||||
<text class="t item" x="646" y="252">edycja</text>
|
||||
<path class="flow" d="M716 247 H772"/>
|
||||
<g filter="url(#soft3)"><rect class="good" x="776" y="210" width="360" height="76" rx="16"/></g>
|
||||
<text class="t item" x="804" y="242">▪ git (Gitea) — tylko zalogowani</text>
|
||||
<text class="t cap" x="804" y="268">poza ścieżką czytelnika — awaria nie kładzie strony</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -1,151 +0,0 @@
|
||||
/*
|
||||
* CTAO portal mockup: header behaviour
|
||||
*
|
||||
* 1. The sticky header turns solid Galaxy Blue after 40px of scroll.
|
||||
* 2. Submenus: every button with data-submenu-toggle (Example, language)
|
||||
* toggles the element named in its aria-controls. One open at a time.
|
||||
* 3. The search button toggles the search panel.
|
||||
* 4. The burger button toggles the main nav on small screens.
|
||||
* 5. Escape and clicks outside the header close open menus.
|
||||
*
|
||||
* Review helper: index.html#open=<id> opens a panel on load,
|
||||
* for example #open=submenu-example, #open=submenu-lang,
|
||||
* #open=search-panel or #open=site-nav.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Keep in sync with the small-screen media query in css/styles.css
|
||||
const MOBILE_QUERY = window.matchMedia("(max-width: 1079px)");
|
||||
|
||||
const header = document.getElementById("site-header");
|
||||
const nav = document.getElementById("site-nav");
|
||||
const searchPanel = document.getElementById("search-panel");
|
||||
const searchButton = header.querySelector('[aria-controls="search-panel"]');
|
||||
const menuButton = header.querySelector('[aria-controls="site-nav"]');
|
||||
const submenuButtons = Array.from(
|
||||
header.querySelectorAll("[data-submenu-toggle]"),
|
||||
);
|
||||
|
||||
/* 1. Scrolled state */
|
||||
|
||||
function updateScrolled() {
|
||||
header.classList.toggle("is-scrolled", window.scrollY > 40);
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", updateScrolled, { passive: true });
|
||||
updateScrolled();
|
||||
|
||||
/* Keeps the header solid while a full-width panel is open */
|
||||
function updateHeaderState() {
|
||||
const panelOpen =
|
||||
!searchPanel.hidden || header.classList.contains("is-nav-open");
|
||||
header.classList.toggle("is-open", panelOpen);
|
||||
}
|
||||
|
||||
/* 2. Submenus */
|
||||
|
||||
function setSubmenu(button, open) {
|
||||
const submenu = document.getElementById(
|
||||
button.getAttribute("aria-controls"),
|
||||
);
|
||||
button.setAttribute("aria-expanded", String(open));
|
||||
submenu.hidden = !open;
|
||||
}
|
||||
|
||||
function closeSubmenus(except) {
|
||||
submenuButtons.forEach(function (button) {
|
||||
if (button !== except) setSubmenu(button, false);
|
||||
});
|
||||
}
|
||||
|
||||
submenuButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
const open = button.getAttribute("aria-expanded") !== "true";
|
||||
closeSubmenus(button);
|
||||
setSearch(false);
|
||||
setSubmenu(button, open);
|
||||
});
|
||||
});
|
||||
|
||||
/* 3. Search */
|
||||
|
||||
function setSearch(open) {
|
||||
searchButton.setAttribute("aria-expanded", String(open));
|
||||
searchPanel.hidden = !open;
|
||||
updateHeaderState();
|
||||
if (open) searchPanel.querySelector("input").focus();
|
||||
}
|
||||
|
||||
searchButton.addEventListener("click", function () {
|
||||
const open = searchPanel.hidden;
|
||||
closeSubmenus();
|
||||
setMobileNav(false);
|
||||
setSearch(open);
|
||||
});
|
||||
|
||||
/* 4. Mobile nav */
|
||||
|
||||
function setMobileNav(open) {
|
||||
menuButton.setAttribute("aria-expanded", String(open));
|
||||
header.classList.toggle("is-nav-open", open);
|
||||
document.body.classList.toggle("no-scroll", open);
|
||||
updateHeaderState();
|
||||
}
|
||||
|
||||
menuButton.addEventListener("click", function () {
|
||||
const open = !header.classList.contains("is-nav-open");
|
||||
closeSubmenus();
|
||||
setSearch(false);
|
||||
setMobileNav(open);
|
||||
});
|
||||
|
||||
/* 5. Dismiss */
|
||||
|
||||
function closeAll() {
|
||||
closeSubmenus();
|
||||
setSearch(false);
|
||||
setMobileNav(false);
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
if (!header.contains(event.target)) closeAll();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key !== "Escape") return;
|
||||
|
||||
const openSubmenuButton = submenuButtons.find(function (button) {
|
||||
return button.getAttribute("aria-expanded") === "true";
|
||||
});
|
||||
|
||||
if (openSubmenuButton) {
|
||||
setSubmenu(openSubmenuButton, false);
|
||||
openSubmenuButton.focus();
|
||||
} else if (!searchPanel.hidden) {
|
||||
setSearch(false);
|
||||
searchButton.focus();
|
||||
} else if (header.classList.contains("is-nav-open")) {
|
||||
setMobileNav(false);
|
||||
menuButton.focus();
|
||||
}
|
||||
});
|
||||
|
||||
/* Switching between desktop and small-screen layouts starts clean */
|
||||
MOBILE_QUERY.addEventListener("change", closeAll);
|
||||
|
||||
/* Review helpers */
|
||||
|
||||
const openMatch = location.hash.match(/^#open=([\w-]+)$/);
|
||||
if (openMatch) {
|
||||
const target = header.querySelector(
|
||||
'[aria-controls="' + openMatch[1] + '"]',
|
||||
);
|
||||
if (target) {
|
||||
if (MOBILE_QUERY.matches && nav.contains(target)) menuButton.click();
|
||||
target.click();
|
||||
}
|
||||
}
|
||||
|
||||
if (location.hash === "#scrolled") window.scrollTo(0, 420);
|
||||
})();
|
||||
@@ -0,0 +1,164 @@
|
||||
// Shared client-side news search over /search.json (no dependencies, no modules).
|
||||
// Used by the header search (field + mobile reveal) and /search (full results).
|
||||
// Focused-empty state per Baymard autocomplete guidance: recent searches (local
|
||||
// only — localStorage, no network) plus the newest articles, never a void.
|
||||
(function () {
|
||||
const RKEY = 'ctao-recent-searches';
|
||||
function recents() {
|
||||
try { return JSON.parse(localStorage.getItem(RKEY) || '[]'); } catch { return []; }
|
||||
}
|
||||
function remember(q) {
|
||||
q = (q || '').trim();
|
||||
if (q.length < 2) return;
|
||||
try {
|
||||
const r = [q, ...recents().filter((x) => x.toLowerCase() !== q.toLowerCase())].slice(0, 4);
|
||||
localStorage.setItem(RKEY, JSON.stringify(r));
|
||||
} catch { /* private mode — recents are a nicety */ }
|
||||
}
|
||||
|
||||
window.newsSearch = function (opts) {
|
||||
const input = document.getElementById(opts.input);
|
||||
const list = document.getElementById(opts.list);
|
||||
const status = document.getElementById(opts.status);
|
||||
// Null-safe: a page may render only some search surfaces — bail silently
|
||||
// (returning a no-op) so one missing input can't kill later inits.
|
||||
if (!input || !list || !status) return function () {};
|
||||
let index = null;
|
||||
let timer;
|
||||
async function ensureIndex() {
|
||||
if (index) return index;
|
||||
try {
|
||||
index = await (await fetch('/search.json')).json();
|
||||
} catch {
|
||||
// Network hiccup: say so instead of failing silently; index stays
|
||||
// null so the next keystroke retries.
|
||||
status.textContent = 'Search is unavailable right now.';
|
||||
return null;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
// First title match wrapped in <mark> (Starlight search pattern) — built
|
||||
// from text nodes, never innerHTML.
|
||||
function highlight(text, q) {
|
||||
const i = text.toLowerCase().indexOf(q);
|
||||
if (i < 0) return [text];
|
||||
const mark = document.createElement('mark');
|
||||
mark.textContent = text.slice(i, i + q.length);
|
||||
return [text.slice(0, i), mark, text.slice(i + q.length)];
|
||||
}
|
||||
function label(text) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'suggest-label';
|
||||
li.setAttribute('aria-hidden', 'true'); // the status line announces it
|
||||
li.textContent = text;
|
||||
return li;
|
||||
}
|
||||
function row(children, href, q) {
|
||||
const li = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
a.href = href;
|
||||
if (q) a.dataset.q = q;
|
||||
a.append(...children);
|
||||
li.append(a);
|
||||
return li;
|
||||
}
|
||||
// Dropdowns close with a "Press Enter for all results" row — a real list
|
||||
// element (was a CSS ::after), aria-hidden like the group labels so
|
||||
// screen-reader exposure stays consistent (the status line announces).
|
||||
function hint() {
|
||||
if (!opts.dismiss || !list.firstChild) return;
|
||||
const li = document.createElement('li');
|
||||
li.className = 'suggest-hint';
|
||||
li.setAttribute('aria-hidden', 'true');
|
||||
li.textContent = 'Press Enter for all results';
|
||||
list.append(li);
|
||||
}
|
||||
// Focused-empty state: up to 4 recent queries, topped up to 5 rows with
|
||||
// the newest articles from the same index.
|
||||
async function idle() {
|
||||
if (input.value.trim().length >= 2) return;
|
||||
// Build off-DOM, swap atomically — clearing before the await shrinks the
|
||||
// page mid-flight and the browser clamps the scroll position ("jumps up").
|
||||
const frag = document.createDocumentFragment();
|
||||
const r = recents();
|
||||
if (r.length) {
|
||||
frag.append(label('Recent'));
|
||||
for (const term of r) frag.append(row([term], '/search?q=' + encodeURIComponent(term), term));
|
||||
}
|
||||
const idx = await ensureIndex();
|
||||
if (!idx) return;
|
||||
const fresh = idx.slice(0, Math.max(1, 5 - r.length));
|
||||
if (input.value.trim().length >= 2) return; // typed meanwhile — run() owns the list
|
||||
frag.append(label('Latest news'));
|
||||
for (const p of fresh) {
|
||||
const li = row([p.title], '/news/' + encodeURIComponent(p.slug));
|
||||
const small = document.createElement('small');
|
||||
small.textContent = p.date;
|
||||
li.append(small);
|
||||
frag.append(li);
|
||||
}
|
||||
list.replaceChildren(frag);
|
||||
hint();
|
||||
status.textContent = opts.hint || (r.length ? 'Showing recent searches and latest news' : 'Showing latest news');
|
||||
}
|
||||
async function run() {
|
||||
const q = input.value.trim().toLowerCase();
|
||||
if (q.length < 2) { list.replaceChildren(); idle(); return; }
|
||||
const idx = await ensureIndex();
|
||||
if (!idx) { list.replaceChildren(); return; }
|
||||
const hits = idx
|
||||
.filter((p) => (p.title + ' ' + p.description + ' ' + p.category).toLowerCase().includes(q))
|
||||
.slice(0, opts.limit);
|
||||
const unit = opts.unit || 'result';
|
||||
status.textContent = hits.length
|
||||
? hits.length + ' ' + unit + (hits.length === 1 ? '' : 's') + ' for “' + input.value.trim() + '”'
|
||||
: 'No results for “' + input.value.trim() + '”. Try a shorter or different term.';
|
||||
// Off-DOM build + atomic swap (no transient page-height collapse)
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const p of hits) {
|
||||
const li = row(highlight(p.title, q), '/news/' + encodeURIComponent(p.slug));
|
||||
const small = document.createElement('small');
|
||||
small.textContent = p.date;
|
||||
li.append(small);
|
||||
frag.append(li);
|
||||
}
|
||||
// Dropdown only: an empty list would collapse (.suggest:empty) and the
|
||||
// panel would just vanish — show the no-results line sighted users too
|
||||
// (the status line above is sr-only in the header; /search shows a
|
||||
// visible notice instead).
|
||||
if (!hits.length && opts.dismiss) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'suggest-hint';
|
||||
li.setAttribute('aria-hidden', 'true'); // the status line announces it
|
||||
li.textContent = 'No results for “' + input.value.trim() + '”';
|
||||
frag.append(li);
|
||||
}
|
||||
list.replaceChildren(frag);
|
||||
hint();
|
||||
}
|
||||
input.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(run, 150); });
|
||||
input.addEventListener('focus', () => { if (input.value.trim().length < 2) idle(); });
|
||||
// Remember successful queries: Enter (form submit) and suggestion clicks
|
||||
if (input.form) input.form.addEventListener('submit', () => remember(input.value));
|
||||
list.addEventListener('click', (e) => {
|
||||
const a = e.target.closest && e.target.closest('a');
|
||||
remember((a && a.dataset.q) || input.value);
|
||||
});
|
||||
// Dropdown dismiss (opt-in — /search keeps its results list). The
|
||||
// suggestion list is a NON-MODAL popup (combobox-style; plain
|
||||
// Tab-reachable links, no roving arrow-key focus): interacting outside
|
||||
// the field+list closes it but the interaction is NOT swallowed —
|
||||
// unlike the modal header panels, which scrim-dismiss in Base.astro.
|
||||
if (opts.dismiss) {
|
||||
const box = input.closest('form') || input.parentElement;
|
||||
const hide = () => { clearTimeout(timer); list.replaceChildren(); status.textContent = ''; };
|
||||
document.addEventListener('pointerdown', (e) => { if (list.firstChild && !box.contains(e.target)) hide(); });
|
||||
// relatedTarget may be null mid-click on our own links — pointerdown covers that path
|
||||
box.addEventListener('focusout', (e) => { if (e.relatedTarget && !box.contains(e.relatedTarget)) hide(); });
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && list.firstChild) { hide(); e.stopPropagation(); } // panel Escape stays a second step
|
||||
});
|
||||
}
|
||||
return run; // callers may run() immediately (e.g. /search?q=…)
|
||||
};
|
||||
})();
|
||||
@@ -4,9 +4,8 @@
|
||||
// URLs, mailto:, data: and pure-#fragment links are out of scope.
|
||||
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const DIST = fileURLToPath(new URL('../dist', import.meta.url));
|
||||
const DIST = new URL('../dist', import.meta.url).pathname;
|
||||
if (!existsSync(DIST)) { console.error('dist/ not found — run `npm run build` first'); process.exit(1); }
|
||||
|
||||
const htmlFiles = [];
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// Point the site at a checkout of the ctao/content repository (default: the
|
||||
// sibling ../content clone), the same content the live demo builds from.
|
||||
// src/content/pages and public/uploads become links into that checkout, so
|
||||
// saves from Sveltia's local-repository mode show up in `npm run dev` directly.
|
||||
//
|
||||
// node scripts/content.mjs link if missing, then check the pages (pre-hooks)
|
||||
// npm run content:link -- [dir] replace existing links or copies
|
||||
//
|
||||
// The deploy build copies the content into place instead (deploy/build.sh);
|
||||
// existing directories are left alone unless --relink is given.
|
||||
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, unlinkSync } from 'node:fs';
|
||||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const examples = JSON.parse(readFileSync(join(root, 'src/data/examples.json'), 'utf8'));
|
||||
const relink = process.argv.includes('--relink');
|
||||
const source = resolve(root, process.argv.slice(2).find((arg) => !arg.startsWith('--')) ?? '../content');
|
||||
const targets = [
|
||||
{ from: join(source, 'pages'), to: join(root, 'src/content/pages') },
|
||||
{ from: join(source, 'uploads'), to: join(root, 'public/uploads') },
|
||||
];
|
||||
|
||||
const exists = (path) => {
|
||||
try { lstatSync(path); return true; } catch { return false; }
|
||||
};
|
||||
|
||||
for (const { from, to } of targets) {
|
||||
if (exists(to) && !relink) continue;
|
||||
if (!existsSync(from)) {
|
||||
throw new Error(`Missing ${from}. Clone the content repository first:\n` +
|
||||
` git clone https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git ${relative(root, source) || '.'}\n` +
|
||||
'or pass another checkout: npm run content:link -- <path>');
|
||||
}
|
||||
if (exists(to)) {
|
||||
// Remove only the link itself, never the content it points to.
|
||||
if (lstatSync(to).isSymbolicLink()) unlinkSync(to);
|
||||
else rmSync(to, { recursive: true }); // an old copied overlay
|
||||
}
|
||||
mkdirSync(dirname(to), { recursive: true });
|
||||
symlinkSync(from, to, 'junction'); // a junction needs no admin rights on Windows
|
||||
console.log(`Linked ${relative(root, to)} -> ${from}`);
|
||||
}
|
||||
|
||||
const pages = join(root, 'src/content/pages');
|
||||
for (const slug of ['home', ...examples.map(({ slug }) => slug)]) {
|
||||
if (!existsSync(join(pages, `${slug}.md`))) {
|
||||
throw new Error(`Missing ${join(pages, `${slug}.md`)}. Check the content checkout, or run npm run content:link -- <path>.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local dev: put the content repo's files where the build expects them.
|
||||
# Usage: ./scripts/link-content.sh [path-to-content-clone] (default ../ctao-content)
|
||||
# Copies (not symlinks) so the dev tree behaves exactly like the machine
|
||||
# build overlay; re-run after pulling content changes.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
src="${1:-../ctao-content}"
|
||||
for d in news pages uploads; do
|
||||
[ -d "$src/$d" ] || { echo "content clone incomplete: $src/$d missing (git clone <gitea>/ctao/content.git first)"; exit 1; }
|
||||
done
|
||||
rm -rf src/content/news src/content/pages public/uploads
|
||||
mkdir -p src/content public
|
||||
cp -a "$src/news" src/content/news
|
||||
cp -a "$src/pages" src/content/pages
|
||||
cp -a "$src/uploads" public/uploads
|
||||
echo "content linked from $src"
|
||||
@@ -1,27 +0,0 @@
|
||||
// Derive editor preview styles from the site CSS so the preview cannot drift.
|
||||
// The preview template in src/pages/admin/index.astro renders the same markup
|
||||
// as the Example pages, so the page hero, figure and prose rules apply as-is.
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const css = readFileSync(new URL('../src/styles/global.css', import.meta.url), 'utf8').replace(/\r\n/g, '\n');
|
||||
const between = (start, end) => {
|
||||
const from = css.indexOf(start);
|
||||
const to = end ? css.indexOf(end, from) : css.length;
|
||||
if (from < 0 || to < 0) throw new Error(`preview-styles: marker not found in global.css: ${from < 0 ? start : end}`);
|
||||
return css.slice(from, to).trim();
|
||||
};
|
||||
const preview = [
|
||||
between('/* ===', '/* Header'), // fonts, tokens, base, utilities
|
||||
between('/* Page hero', '/* Footer'), // hero, figure, prose, buttons
|
||||
between('/* Preferences'), // high contrast, reduced motion
|
||||
`/* Preview frame
|
||||
========================================================================== */
|
||||
|
||||
/* There is no site header above the hero */
|
||||
.page-hero {
|
||||
margin-top: 0;
|
||||
padding-top: 3rem;
|
||||
}`,
|
||||
];
|
||||
writeFileSync(new URL('../public/admin/preview.css', import.meta.url),
|
||||
'/* Generated by scripts/preview-styles.mjs; edit src/styles/global.css. */\n\n' + preview.join('\n\n') + '\n');
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
// The one news card — used by the home page and the /news archive.
|
||||
// `featured` (archive page 1, first item) switches the wide lead variant and
|
||||
// eager-loads its cover. Cover alt stays empty by design: the image is
|
||||
// decorative next to the always-visible title.
|
||||
import { fmt, iso, readMin } from '../lib/news.js';
|
||||
const { post, featured = false } = Astro.props;
|
||||
---
|
||||
<article class={featured ? 'card card--featured' : 'card'}>
|
||||
{post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" loading={featured ? 'eager' : 'lazy'} />}
|
||||
<div class="body">
|
||||
<h3><a href={`/news/${post.id}`}>{post.data.title}</a></h3>
|
||||
<p class="desc">{post.data.description}</p>
|
||||
<div class="meta"><span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="16" rx="2" /><path d="M4 10h16M8 3v4M16 3v4" /></svg><time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time></span><span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></svg>{readMin(post)} min read</span></div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
interface Props { title: string; introduction?: string; example?: boolean; breadcrumb?: boolean; hasImage?: boolean }
|
||||
const { title, introduction, example = false, breadcrumb = true, hasImage = false } = Astro.props;
|
||||
---
|
||||
<section class:list={['page-hero', { 'page-hero--compact': !hasImage }]}>
|
||||
<div class="container">
|
||||
{breadcrumb && <nav class="breadcrumb" aria-label="Breadcrumb">
|
||||
<ol>
|
||||
{Astro.url.pathname !== '/' && <li><a href="/">Home</a></li>}
|
||||
{example && <li>Example</li>}
|
||||
<li aria-current="page">{title}</li>
|
||||
</ol>
|
||||
</nav>}
|
||||
<h1 class="page-hero__title">{title}</h1>
|
||||
{introduction && <p class="page-hero__lead">{introduction}</p>}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,33 +1,39 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { z } from 'astro/zod';
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
import examples from './data/examples.json';
|
||||
|
||||
const destination = z.string().refine(
|
||||
(value) => !value || /^(\/(?!\/)|https?:\/\/|mailto:|#)/.test(value),
|
||||
'Use a site path, https:// URL, mailto: address, or fragment.',
|
||||
);
|
||||
const pages = defineCollection({
|
||||
// Only the Home page and the four Example files can become public pages.
|
||||
loader: glob({
|
||||
pattern: `{home,${examples.map(({ slug }) => slug).join(',')}}.md`,
|
||||
base: './src/content/pages',
|
||||
}),
|
||||
// News/articles live as Markdown files in src/content/news/*.md.
|
||||
// This is exactly what Sveltia CMS edits — the CMS writes these files, Astro
|
||||
// renders them to static HTML at build time.
|
||||
const news = defineCollection({
|
||||
// Top-level only ('*.md', not '**'): entry ids feed a non-rest [slug]
|
||||
// route, and an id from a subfolder would contain '/' and break it.
|
||||
loader: glob({ pattern: '*.md', base: './src/content/news' }),
|
||||
schema: z.object({
|
||||
title: z.string().min(1),
|
||||
introduction: z.string().optional(),
|
||||
// Example pages: one photo
|
||||
image: z.string().optional(),
|
||||
imageAlt: z.string().optional(),
|
||||
imageCaption: z.string().optional(),
|
||||
// Home: up to two photos side by side
|
||||
images: z.array(z.object({
|
||||
image: z.string().min(1),
|
||||
alt: z.string().optional(),
|
||||
caption: z.string().optional(),
|
||||
})).max(2).optional(),
|
||||
buttonLabel: z.string().optional(),
|
||||
buttonUrl: destination.optional(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
date: z.coerce.date(),
|
||||
category: z.string().default('news'),
|
||||
author: z.string().default('CTAO'),
|
||||
// Public-path string, e.g. "/uploads/foo.jpg" — media lives in the
|
||||
// content repo's uploads/, overlaid to public/uploads at build time,
|
||||
// which Astro's image() helper can't validate (src/ only).
|
||||
cover: z.string().optional(),
|
||||
// BCP-47 tag when an article is not in English (e.g. "pl") — set as lang
|
||||
// on the <article> element so screen readers pick the right voice.
|
||||
lang: z.string().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
}),
|
||||
});
|
||||
export const collections = { pages };
|
||||
|
||||
// Static portal pages (privacy, disclaimer, contact…) — separate collection
|
||||
// because the content type differs (no dates/covers/categories), and editors
|
||||
// get a distinct "Pages" section in the CMS.
|
||||
const pages = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/pages' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { news, pages };
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
[
|
||||
{
|
||||
"slug": "lorem-ipsum",
|
||||
"label": "Lorem ipsum"
|
||||
},
|
||||
{
|
||||
"slug": "dolor-sit-amet",
|
||||
"label": "Dolor sit amet"
|
||||
},
|
||||
{
|
||||
"slug": "consectetur-adipiscing",
|
||||
"label": "Consectetur adipiscing"
|
||||
},
|
||||
{
|
||||
"slug": "sed-do-eiusmod",
|
||||
"label": "Sed do eiusmod"
|
||||
}
|
||||
]
|
||||
@@ -1,42 +0,0 @@
|
||||
[
|
||||
{
|
||||
"slug": "news",
|
||||
"title": "News"
|
||||
},
|
||||
{
|
||||
"slug": "data",
|
||||
"title": "Data"
|
||||
},
|
||||
{
|
||||
"slug": "proposals",
|
||||
"title": "Proposals"
|
||||
},
|
||||
{
|
||||
"slug": "dashboard",
|
||||
"title": "Dashboard"
|
||||
},
|
||||
{
|
||||
"slug": "login",
|
||||
"title": "Sign in"
|
||||
},
|
||||
{
|
||||
"slug": "search",
|
||||
"title": "Search"
|
||||
},
|
||||
{
|
||||
"slug": "support",
|
||||
"title": "Support"
|
||||
},
|
||||
{
|
||||
"slug": "pages/privacy",
|
||||
"title": "Privacy"
|
||||
},
|
||||
{
|
||||
"slug": "pages/disclaimer",
|
||||
"title": "Disclaimer"
|
||||
},
|
||||
{
|
||||
"slug": "pages/contact",
|
||||
"title": "Contact"
|
||||
}
|
||||
]
|
||||
@@ -1,119 +1,191 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import examples from '../data/examples.json';
|
||||
interface Props { title?: string; description?: string }
|
||||
const { title = 'CTAO Science Portal', description = 'CTAO Science Portal' } = Astro.props;
|
||||
const path = Astro.url.pathname.replace(/\/?$/, '/');
|
||||
const canonical = new URL(Astro.url.pathname, Astro.site);
|
||||
// `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 } = Astro.props;
|
||||
// Absolute URLs for canonical/OG/RSS (head pattern from the official Astro
|
||||
// blog template). `site` comes from astro.config.mjs and is always set.
|
||||
const site = Astro.site;
|
||||
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>
|
||||
{/* Always "en": the chrome (nav/footer) is English; a non-English article
|
||||
sets lang on its <article> element instead ([slug].astro). */}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title === 'CTAO Science Portal' ? title : `${title} – CTAO`}</title>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="theme-color" content="#00004a" />
|
||||
<link rel="canonical" href={canonical} />
|
||||
<link rel="icon" href="/assets/logo-sp-positive.svg" type="image/svg+xml" />
|
||||
{/* 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).
|
||||
Both ship inlined as data: URIs in the render-blocking stylesheet —
|
||||
no font request, no swap (see global.css @font-face). */}
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<header class="site-header" id="site-header">
|
||||
<div class="site-header__inner">
|
||||
|
||||
<a class="site-header__logo" href="/">
|
||||
<img src="/assets/logo-sp-negative.svg" alt="CTAO – Home" width="153" height="46">
|
||||
<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 class="site-nav" id="site-nav" aria-label="Main">
|
||||
<ul class="site-nav__list">
|
||||
<li class="site-nav__item">
|
||||
<a class="site-nav__link" href="/news/" aria-current={path === "/news/" ? "page" : undefined}><span class="site-nav__label">News</span></a>
|
||||
</li>
|
||||
<li class="site-nav__item">
|
||||
<a class="site-nav__link" href="/data/" aria-current={path === "/data/" ? "page" : undefined}><span class="site-nav__label">Data</span></a>
|
||||
</li>
|
||||
<li class="site-nav__item">
|
||||
<a class="site-nav__link" href="/proposals/" aria-current={path === "/proposals/" ? "page" : undefined}><span class="site-nav__label">Proposals</span></a>
|
||||
</li>
|
||||
<li class="site-nav__item">
|
||||
<a class="site-nav__link" href="/dashboard/" aria-current={path === "/dashboard/" ? "page" : undefined}><span class="site-nav__label">Dashboard</span></a>
|
||||
</li>
|
||||
<li class="site-nav__item has-submenu">
|
||||
<button class="site-nav__link site-nav__toggle" type="button" data-submenu-toggle aria-expanded="false" aria-controls="submenu-example">
|
||||
<span class="site-nav__label">Example</span>
|
||||
<svg class="chevron" width="10" height="6" viewBox="0 0 10 6" aria-hidden="true" focusable="false"><path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
</button>
|
||||
<ul class="submenu site-nav__submenu" id="submenu-example" hidden>
|
||||
{examples.map(({ slug, label }) => <li><a href={`/example/${slug}/`} aria-current={path === `/example/${slug}/` ? 'page' : undefined}>{label}</a></li>)}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="site-nav__item site-nav__item--signin">
|
||||
<a class="site-nav__link site-nav__signin" href="/login/">Sign in</a>
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
|
||||
<div class="site-header__tools">
|
||||
|
||||
<a class="pill site-header__signin" href="/login/">Sign in</a>
|
||||
|
||||
<div class="lang has-submenu">
|
||||
<button class="pill lang__toggle" type="button" data-submenu-toggle aria-expanded="false" aria-controls="submenu-lang">
|
||||
<span class="visually-hidden">Language:</span>
|
||||
<span>EN</span>
|
||||
<svg class="chevron" width="10" height="6" viewBox="0 0 10 6" aria-hidden="true" focusable="false"><path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
</button>
|
||||
<ul class="submenu submenu--end" id="submenu-lang" hidden>
|
||||
<li><a href={Astro.url.pathname} lang="en" aria-current="true">English</a></li>
|
||||
<li><span class="submenu__unavailable" lang="de" aria-disabled="true">Deutsch</span></li>
|
||||
<li><span class="submenu__unavailable" lang="es" aria-disabled="true">Español</span></li>
|
||||
<li><span class="submenu__unavailable" lang="fr" aria-disabled="true">Français</span></li>
|
||||
<li><span class="submenu__unavailable" lang="it" aria-disabled="true">Italiano</span></li>
|
||||
<li><span class="submenu__unavailable" lang="ja" aria-disabled="true">日本語</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button class="icon-btn" type="button" aria-label="Search" aria-expanded="false" aria-controls="search-panel">
|
||||
<svg class="icon-btn__open" width="16" height="16" viewBox="0 0 16 15" aria-hidden="true" focusable="false"><path d="M5.917.292a5.417 5.417 0 0 1 4.116 8.942l.225.225h.659l4.166 4.166-1.25 1.25-4.166-4.166v-.659l-.225-.225A5.417 5.417 0 1 1 5.917.292Zm0 1.667a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5Z" fill="currentColor"/></svg>
|
||||
<svg class="icon-btn__close" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3.05 1.636 8 6.586l4.95-4.95 1.414 1.414L9.414 8l4.95 4.95-1.414 1.414L8 9.414l-4.95 4.95-1.414-1.414L6.586 8l-4.95-4.95L3.05 1.636Z" fill="currentColor"/></svg>
|
||||
</button>
|
||||
|
||||
<button class="icon-btn icon-btn--menu" type="button" aria-label="Menu" aria-expanded="false" aria-controls="site-nav">
|
||||
<svg class="icon-btn__open" width="21" height="20" viewBox="0 0 21 20" aria-hidden="true" focusable="false"><path d="M3.415 6h15v1.667h-15V6Zm0 4.167h15v1.666h-15v-1.666Zm0 4.166h15V16h-15v-1.667Z" fill="currentColor"/></svg>
|
||||
<svg class="icon-btn__close" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3.05 1.636 8 6.586l4.95-4.95 1.414 1.414L9.414 8l4.95 4.95-1.414 1.414L8 9.414l-4.95 4.95-1.414-1.414L6.586 8l-4.95-4.95L3.05 1.636Z" fill="currentColor"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-panel" id="search-panel" hidden>
|
||||
<form class="search-panel__form" role="search" action="/search/" method="get">
|
||||
<input class="search-panel__input" type="search" name="q" placeholder="Type Search Text Here" aria-label="Search" autocomplete="off">
|
||||
<button class="search-panel__submit" type="submit" aria-label="Submit search">
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
</button>
|
||||
<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>
|
||||
|
||||
|
||||
<main id="main" tabindex="-1"><slot /></main>
|
||||
<footer class="site-footer">
|
||||
<div class="container site-footer__inner">
|
||||
<img class="site-footer__logo" src="/assets/logo-sp-negative.svg" alt="CTAO" width="153" height="46" />
|
||||
<ul class="site-footer__nav">
|
||||
<li><a href="/pages/privacy/">Privacy</a></li>
|
||||
<li><a href="/pages/disclaimer/">Disclaimer</a></li>
|
||||
<li><a href="/pages/contact/">Contact</a></li>
|
||||
<li><a href="/admin/">Content editor</a></li>
|
||||
</ul>
|
||||
<p class="site-footer__copy">© {new Date().getFullYear()} CTAO</p>
|
||||
{/* 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>
|
||||
<script is:inline src="/header.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
// Editable content page: hero, one photo (Example pages) or two side by side
|
||||
// (Home), Markdown body and optional button. The CMS preview template in
|
||||
// src/pages/admin/index.astro mirrors this markup.
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import { render } from 'astro:content';
|
||||
import Base from './Base.astro';
|
||||
import PageHero from '../components/PageHero.astro';
|
||||
|
||||
interface Props { page: CollectionEntry<'pages'>; example?: boolean; breadcrumb?: boolean }
|
||||
const { page, example = false, breadcrumb = true } = Astro.props;
|
||||
const { Content } = await render(page);
|
||||
const { title, introduction, image, imageAlt, imageCaption, images, buttonLabel, buttonUrl } = page.data;
|
||||
const photos = images ?? (image ? [{ image, alt: imageAlt, caption: imageCaption }] : []);
|
||||
---
|
||||
<Base title={example ? title : undefined} description={introduction}>
|
||||
<PageHero title={title} introduction={introduction} example={example} breadcrumb={breadcrumb} hasImage={photos.length > 0} />
|
||||
{photos.length > 0 && <div class="container">
|
||||
<div class:list={{ 'page-figures': photos.length > 1 }}>
|
||||
{photos.map(({ image, alt, caption }) => (
|
||||
<figure class="page-figure">
|
||||
<img src={image} alt={alt || ''} />
|
||||
{caption && <figcaption>{caption}</figcaption>}
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
<article class:list={['container', 'prose', { 'prose--no-image': photos.length === 0 }]}>
|
||||
<Content />
|
||||
{buttonLabel && buttonUrl && <p>
|
||||
<a class="btn btn--primary" href={buttonUrl}>
|
||||
<span class="btn__label">{buttonLabel}</span><span class="arrow" aria-hidden="true">→</span>
|
||||
</a>
|
||||
</p>}
|
||||
</article>
|
||||
</Base>
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
import Base from './Base.astro';
|
||||
import PageHero from '../components/PageHero.astro';
|
||||
interface Props { title: string }
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
<Base title={title}>
|
||||
<PageHero title={title} />
|
||||
<div class="placeholder-body"></div>
|
||||
</Base>
|
||||
@@ -0,0 +1,11 @@
|
||||
// Shared news helpers — single source for card metadata and archive chunking.
|
||||
export const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d);
|
||||
export 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).
|
||||
export const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220));
|
||||
// Archive chunking: page 1 holds 25 items (1 featured lead + 24 grid = even
|
||||
// 3-column rows), later pages 24. Used by BOTH the /news/[...page] route and
|
||||
// sitemap.xml.js — the page counts cannot drift apart.
|
||||
export const PAGE_FIRST = 25;
|
||||
export const PAGE_REST = 24;
|
||||
@@ -1,8 +1,24 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import PageHero from '../components/PageHero.astro';
|
||||
---
|
||||
<Base title="Page not found">
|
||||
<PageHero title="Page not found" />
|
||||
<div class="container prose prose--no-image"><p><a href="/">Return to Home</a></p></div>
|
||||
<Base title="CTAO Science Portal - Page not found" description="This page does not exist">
|
||||
{/* Document archetype (DESIGN.md): the document IS the page — h1, one line,
|
||||
recovery links, and a real GET search form as the way forward. */}
|
||||
<div class="container article-layout">
|
||||
<article class="article">
|
||||
<h1>Page not found</h1>
|
||||
<div class="prose">
|
||||
{/* Keep "the <a>" on one line: the compiler drops the whitespace at a
|
||||
line break before a tag, which glued "the" to the link text. */}
|
||||
<p>The address may be mistyped or the page may have moved.
|
||||
Go to the <a href="/">home page</a> or browse <a href="/news">all news</a>.</p>
|
||||
</div>
|
||||
<form class="search-form" role="search" action="/search" method="get">
|
||||
<label class="sr-only" for="nf-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="nf-q" name="q" type="search" placeholder="Search news…" autocomplete="off" />
|
||||
<button class="btn" type="submit">Search</button>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
import Placeholder from '../layouts/Placeholder.astro';
|
||||
import placeholders from '../data/placeholders.json';
|
||||
export function getStaticPaths() {
|
||||
return placeholders.map(({ slug, title }) => ({ params: { placeholder: slug }, props: { title } }));
|
||||
}
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
<Placeholder title={title} />
|
||||
@@ -5,9 +5,6 @@
|
||||
// deliberate — see README); note the bundle still fetches its own UI fonts
|
||||
// from jsDelivr at runtime. Kept is:inline so Astro doesn't process/bundle it.
|
||||
// CTAO branding on the sign-in screen comes from the documented `logo` option.
|
||||
import examples from '../../data/examples.json';
|
||||
|
||||
const previewNames = examples.map(({ slug }) => slug);
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -32,67 +29,8 @@ const previewNames = examples.map(({ slug }) => slug);
|
||||
<noscript><p>The CTAO content editor requires JavaScript.<br />Please enable it, or return to the <a href="/">Science Portal</a>.</p></noscript>
|
||||
<script is:inline src="/vendor/sveltia-cms.js"></script>
|
||||
<script is:inline>
|
||||
// Generated from the site's CSS; see scripts/preview-styles.mjs.
|
||||
// Officially supported API: article previews rendered with portal-like styles.
|
||||
window.CMS?.registerPreviewStyle?.('/admin/preview.css');
|
||||
</script>
|
||||
<script is:inline define:vars={{ previewNames }}>
|
||||
// Preview pane: the same markup as src/layouts/ContentPage.astro and
|
||||
// PageHero.astro, without field labels. Sveltia looks templates up by
|
||||
// file name for file collections, so register one per page.
|
||||
const { h, rf } = window;
|
||||
// Our own <img>: Sveltia's image widget keeps the image transparent until
|
||||
// a visibility check that does not complete inside a custom template.
|
||||
// The asset URL starts as the public path and becomes a blob URL once
|
||||
// Sveltia has fetched the file, which covers images not deployed yet.
|
||||
const PreviewImage = ({ asset, fallback, alt }) => h('img', {
|
||||
src: asset ? String(asset) : fallback,
|
||||
alt,
|
||||
onError: ({ currentTarget: img }) => {
|
||||
const retry = (tries) => {
|
||||
const url = asset && String(asset);
|
||||
if (url && url !== img.getAttribute('src')) img.src = url;
|
||||
else if (tries) setTimeout(() => retry(tries - 1), 500);
|
||||
};
|
||||
retry(10);
|
||||
},
|
||||
});
|
||||
const PagePreview = ({ entry, widgetFor, getAsset, home = false }) => {
|
||||
const get = (key) => entry.getIn(['data', key]);
|
||||
const title = get('title') || '';
|
||||
const introduction = get('introduction');
|
||||
const buttonLabel = get('buttonLabel');
|
||||
const buttonUrl = get('buttonUrl');
|
||||
// Home: a list of up to two photos; Example pages: a single photo
|
||||
const photos = home
|
||||
? (get('images')?.toJS?.() ?? []).filter((item) => item?.image)
|
||||
: get('image') ? [{ image: get('image'), alt: get('imageAlt'), caption: get('imageCaption') }] : [];
|
||||
return h(rf, null,
|
||||
h('section', { className: photos.length ? 'page-hero' : 'page-hero page-hero--compact' },
|
||||
h('div', { className: 'container' },
|
||||
!home && h('nav', { className: 'breadcrumb', 'aria-label': 'Breadcrumb' },
|
||||
h('ol', null,
|
||||
h('li', null, h('a', { href: '/' }, 'Home')),
|
||||
h('li', null, 'Example'),
|
||||
h('li', { 'aria-current': 'page' }, title))),
|
||||
h('h1', { className: 'page-hero__title' }, title),
|
||||
introduction && h('p', { className: 'page-hero__lead' }, introduction))),
|
||||
photos.length > 0 && h('div', { className: 'container' },
|
||||
h('div', { className: photos.length > 1 ? 'page-figures' : undefined },
|
||||
photos.map(({ image, alt, caption }, index) => h('figure', { className: 'page-figure', key: index },
|
||||
h(PreviewImage, { key: image, asset: getAsset(image), fallback: image, alt: alt || '' }),
|
||||
caption && h('figcaption', null, caption))))),
|
||||
h('article', { className: photos.length ? 'container prose' : 'container prose prose--no-image' },
|
||||
widgetFor('body'),
|
||||
buttonLabel && buttonUrl && h('p', null,
|
||||
h('a', { className: 'btn btn--primary', href: buttonUrl },
|
||||
h('span', { className: 'btn__label' }, buttonLabel),
|
||||
h('span', { className: 'arrow', 'aria-hidden': 'true' }, '→')))));
|
||||
};
|
||||
const HomePreview = (props) => h(PagePreview, { ...props, home: true });
|
||||
if (window.CMS?.registerPreviewTemplate && h) {
|
||||
window.CMS.registerPreviewTemplate('home', HomePreview);
|
||||
for (const name of previewNames) window.CMS.registerPreviewTemplate(name, PagePreview);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
// Per SPEC §3.3.2 the dashboard aggregates proposal statuses, data products
|
||||
// and help-desk tickets. No integrations exist yet, so panels show empty
|
||||
// states instead of fabricated sample rows (source-documents-only rule).
|
||||
---
|
||||
<Base title="CTAO Science Portal - Dashboard" description="User dashboard (mock)">
|
||||
<section class="band--moon">
|
||||
<header class="container page-head">
|
||||
<h1>Dashboard</h1>
|
||||
<p class="standfirst">Your proposals, data products and support tickets in one place.</p>
|
||||
</header>
|
||||
<div class="container page">
|
||||
<div class="dash-grid">
|
||||
<div class="panel">
|
||||
<h2 class="panel-title">My proposals</h2>
|
||||
<p class="notice">Proposal statuses will appear here once the Proposal Handling System (APC team) is connected.</p>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2 class="panel-title">Recent data products</h2>
|
||||
<p class="notice">Your data products will appear here. Search and download in the <a href="https://padc-ctao-data-explorer.obspm.fr/" target="_blank" rel="noopener" class="external">Data Explorer</a> (external, LUX team).</p>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2 class="panel-title">Support tickets</h2>
|
||||
<p class="notice">Help-desk tickets will appear here once User Support is connected.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import ContentPage from '../../layouts/ContentPage.astro';
|
||||
import examples from '../../data/examples.json';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const pages = await getCollection('pages');
|
||||
return examples.map(({ slug }) => {
|
||||
const page = pages.find(({ id }) => id === slug);
|
||||
if (!page) throw new Error(`Missing Example page: ${slug}. Check the content checkout (npm run content:link -- <path>).`);
|
||||
return { params: { slug }, props: { page } };
|
||||
});
|
||||
}
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
<ContentPage page={page} example />
|
||||
@@ -1,8 +1,93 @@
|
||||
---
|
||||
import { getEntry } from 'astro:content';
|
||||
import ContentPage from '../layouts/ContentPage.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import Base from '../layouts/Base.astro';
|
||||
import NewsCard from '../components/NewsCard.astro';
|
||||
|
||||
const page = await getEntry('pages', 'home');
|
||||
if (!page) throw new Error('Missing Home page: pages/home.md. Check the content checkout (npm run content:link -- <path>).');
|
||||
const posts = (await getCollection('news', ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.slice(0, 3);
|
||||
// Services per the Science Portal spec. Tiles carry no status chrome — honesty
|
||||
// lives at the interaction point (REQUIREMENTS §5): each target page opens with
|
||||
// its own whisper badge. Planned services stay in the grid but subdued (muted,
|
||||
// no link) with availability folded into the description text.
|
||||
// Tile icons: hand-drawn stroke glyphs in the header-icon family (.ico —
|
||||
// currentColor, round caps), aria-hidden. NN/g icon research: icons alone are
|
||||
// ambiguous, but NEXT TO an always-visible label they differentiate otherwise
|
||||
// uniform tiles and speed up grid scanning.
|
||||
const services = [
|
||||
{ title: 'Proposals', href: '/proposals', desc: 'Submit observation proposals, including ToO / MWL requests.',
|
||||
icon: '<path d="M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5z"/><path d="M13.5 3v4.5H18"/><path d="M9 12.5h6M9 16h4"/>' },
|
||||
{ title: 'Data Explorer', href: 'https://padc-ctao-data-explorer.obspm.fr/', ext: true, desc: 'Search and download CTAO science data products.',
|
||||
icon: '<ellipse cx="12" cy="5.5" rx="7" ry="2.5"/><path d="M5 5.5V18c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5V5.5"/><path d="M5 11.8c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5"/>' },
|
||||
{ title: 'Dashboard', href: '/dashboard', desc: 'Your proposals, data products and support tickets in one place.',
|
||||
icon: '<rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/>' },
|
||||
{ title: 'User Support', href: '/support', desc: 'Help desk, FAQ, user forum and mailing lists.',
|
||||
icon: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4"/><path d="m5.7 5.7 3.5 3.5m5.6 0 3.5-3.5m0 12.6-3.5-3.5m-5.6 0-3.5 3.5"/>' },
|
||||
// Two subdued planned tiles complete the 2×3 grid (an orphan 4th tile in a
|
||||
// 3-col grid reads broken); the remaining TBD services stay on the quiet line.
|
||||
{ title: 'Software', planned: true, desc: 'Landing page for CTAO software (planned).',
|
||||
icon: '<rect x="3" y="4.5" width="18" height="15" rx="2"/><path d="m7 9.5 3 2.5-3 2.5M12.5 15H17"/>' },
|
||||
{ title: 'Documentation', planned: true, desc: 'CTAO user documentation (planned).',
|
||||
icon: '<path d="M12 6.7C10.6 5.2 8.6 4.5 5.7 4.5H4v13.6h1.7c2.9 0 4.9.7 6.3 2.2 1.4-1.5 3.4-2.2 6.3-2.2H20V4.5h-1.7c-2.9 0-4.9.7-6.3 2.2z"/><path d="M12 6.7v13.6"/>' },
|
||||
];
|
||||
const planned = ['Scheduling', 'Science alerts'];
|
||||
---
|
||||
<ContentPage page={page} breadcrumb={false} />
|
||||
<Base title="CTAO Science Portal" description="News, observation proposals, data and user services of the Cherenkov Telescope Array Observatory" ambient>
|
||||
{/* Photographic hero: LST-1 under the La Palma night sky (copied from the
|
||||
content library to /brand/hero.jpg) beneath the Galaxy scrim — text
|
||||
contrast computed worst-case in DESIGN.md. Three elements only:
|
||||
headline, brand subtitle, CTA pair (Apple hero restraint). */}
|
||||
<section class="hero-band">
|
||||
<div class="band-bg" aria-hidden="true"></div>
|
||||
<div class="container hero">
|
||||
<div class="eyebrow">Cherenkov Telescope Array Observatory</div>
|
||||
<h1>CTAO Science Portal</h1>
|
||||
<p>Exploring the Universe at the Highest Energies</p>
|
||||
{/* Hero actions = the two REAL things a logged-out visitor can do today:
|
||||
news (SPEC §3.1 lists general information/announcements FIRST; §3.3.1
|
||||
"displays the main information"; calls for proposals arrive VIA news)
|
||||
and the external Data Explorer. Proposals stays in nav + tiles — a
|
||||
mock doesn't earn hero billing. Search lives in the header. */}
|
||||
<div class="hero-cta">
|
||||
<a class="btn" href="/news">Browse news & announcements</a>
|
||||
<a class="btn btn--ghost external" href="https://padc-ctao-data-explorer.obspm.fr/" target="_blank" rel="noopener">Explore the data</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section order is task-first: Services directly under the hero, news after.
|
||||
NN/g homepage principles — the homepage must give clear starting points
|
||||
for the user's top tasks; for scientists that is proposals/data/support,
|
||||
not reading news. Freshness is still signalled above the fold by the
|
||||
one-line hero teaser, so the 3-card news section can sit below.
|
||||
Surface rhythm (DESIGN.md): services on Moon Gray, editorial news on white. */}
|
||||
<section class="band--moon">
|
||||
<div class="container page">
|
||||
<div class="section-head"><h2>Services</h2></div>
|
||||
<div class="grid">
|
||||
{services.map((s) => (
|
||||
<article class={s.planned ? 'card tile tile--planned' : 'card tile'}>
|
||||
<svg class="ico" aria-hidden="true" viewBox="0 0 24 24" set:html={s.icon} />
|
||||
<h3>{s.planned
|
||||
? s.title
|
||||
: (s.ext
|
||||
? <a class="external" href={s.href} target="_blank" rel="noopener">{s.title}</a>
|
||||
: <a href={s.href}>{s.title}</a>)}</h3>
|
||||
<p class="desc">{s.desc}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<p class="planned-note">Planned services: {planned.join(' · ')}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container page">
|
||||
<div class="section-head">
|
||||
<h2>Latest news</h2>
|
||||
<a class="more" href="/news">All news →</a>
|
||||
</div>
|
||||
<div class="grid">
|
||||
{posts.map((post) => <NewsCard post={post} />)}
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="CTAO Science Portal - Sign in" description="Sign in via CTAO AAI (mock)">
|
||||
<section class="auth-band">
|
||||
<div class="band-bg" aria-hidden="true"></div>
|
||||
<div class="auth-card">
|
||||
<img class="auth-logo" src="/brand/CTAO_Logo_positive.svg" alt="CTAO" />
|
||||
<h1>Sign in</h1>
|
||||
{/* Auth pages are scanned, not read — one line, no reassurance prose */}
|
||||
<p class="muted">Use your CTAO account (single sign-on via CTAO AAI).</p>
|
||||
<button class="btn btn-wide" type="button">Continue with your CTAO account</button>
|
||||
<p class="notice">No account? <a>Register via CTAO AAI</a></p>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import NewsCard from '../../components/NewsCard.astro';
|
||||
import { PAGE_FIRST as FIRST, PAGE_REST as REST } from '../../lib/news.js';
|
||||
|
||||
// Paginated archive; the [...page] rest route makes page 1 the bare /news URL,
|
||||
// then /news/2 … /news/N. Custom slicing instead of paginate(): page 1 holds
|
||||
// FIRST items (1 featured lead + 24 grid = even 3-column rows), later pages
|
||||
// REST — paginate() cannot vary page size. Chunk sizes live in lib/news.js,
|
||||
// shared with sitemap.xml.js.
|
||||
export async function getStaticPaths() {
|
||||
const posts = (await getCollection('news', ({ data }) => !data.draft)).sort(
|
||||
(a, b) => b.data.date.valueOf() - a.data.date.valueOf(),
|
||||
);
|
||||
const chunks = [posts.slice(0, FIRST)];
|
||||
for (let i = FIRST; i < posts.length; i += REST) chunks.push(posts.slice(i, i + REST));
|
||||
const lastPage = chunks.length;
|
||||
const hrefOf = (n) => (n === 1 ? '/news' : `/news/${n}`);
|
||||
return chunks.map((data, i) => {
|
||||
const n = i + 1;
|
||||
return {
|
||||
params: { page: n === 1 ? undefined : String(n) },
|
||||
props: { page: {
|
||||
data,
|
||||
currentPage: n,
|
||||
lastPage,
|
||||
url: {
|
||||
prev: n > 1 ? hrefOf(n - 1) : undefined,
|
||||
next: n < lastPage ? hrefOf(n + 1) : undefined,
|
||||
},
|
||||
} },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
const first = page.currentPage === 1;
|
||||
// Windowed page list — 1 … n-1 n n+1 … last (0 marks an ellipsis)
|
||||
const nums = [];
|
||||
for (let n = 1; n <= page.lastPage; n++) {
|
||||
if (n === 1 || n === page.lastPage || Math.abs(n - page.currentPage) <= 1) nums.push(n);
|
||||
else if (nums.at(-1) !== 0) nums.push(0);
|
||||
}
|
||||
const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);
|
||||
---
|
||||
<Base
|
||||
title={first ? 'CTAO Science Portal - News' : `CTAO Science Portal - News, page ${page.currentPage}`}
|
||||
description={first ? 'News and announcements of the CTAO' : `News and announcements of the CTAO, page ${page.currentPage} of ${page.lastPage}`}
|
||||
>
|
||||
{/* Section-landing archetype: white head, display Galaxy h1 + standfirst
|
||||
(the navy band is reserved for / and /login — DESIGN.md). */}
|
||||
<header class="container page-head">
|
||||
<h1>News & Announcements</h1>
|
||||
<p class="standfirst">Science highlights and updates from the observatory and its partners.</p>
|
||||
</header>
|
||||
|
||||
<section class="container page">
|
||||
<h2 class="sr-only">{first ? 'All news' : `All news, page ${page.currentPage}`}</h2>
|
||||
<div class="grid">
|
||||
{page.data.map((post, i) => <NewsCard post={post} featured={first && i === 0} />)}
|
||||
</div>
|
||||
{page.lastPage > 1 && (
|
||||
<nav class="pagination" aria-label="News pages">
|
||||
{page.url.prev
|
||||
? <a class="page-link page-step" href={page.url.prev} rel="prev">← Newer</a>
|
||||
: <span class="page-link page-step" aria-hidden="true">← Newer</span>}
|
||||
<ol>
|
||||
{nums.map((n) => (
|
||||
<li>
|
||||
{n === 0
|
||||
? <span class="page-gap" aria-hidden="true">…</span>
|
||||
: <a class="page-link" href={hrefFor(n)} aria-current={n === page.currentPage ? 'page' : undefined}><span class="sr-only">Page </span>{n}</a>}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
{page.url.next
|
||||
? <a class="page-link page-step" href={page.url.next} rel="next">Older →</a>
|
||||
: <span class="page-link page-step" aria-hidden="true">Older →</span>}
|
||||
</nav>
|
||||
)}
|
||||
</section>
|
||||
</Base>
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import { fmt, readMin as readMinOf } from '../../lib/news.js';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
// 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, newer, older } = Astro.props;
|
||||
const { Content, headings } = await render(post);
|
||||
const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
||||
const readMin = readMinOf(post);
|
||||
---
|
||||
<Base title={`${post.data.title} - CTAO`} description={post.data.description} type="article" image={post.data.cover && encodeURI(post.data.cover)}>
|
||||
{/* Reading progress (scroll-driven CSS, no JS) — styled only where
|
||||
animation-timeline is supported; elsewhere it stays an empty div. */}
|
||||
<div class="read-progress" aria-hidden="true"></div>
|
||||
{/* TOC pattern (MDN/Stripe/NN-g): sticky right rail ≥1200px, collapsed <details>
|
||||
under the title below that. CSS shows exactly one of the two (DESIGN.md). */}
|
||||
{/* lang sits on the article, not <html>: the chrome (nav/footer) stays
|
||||
English for screen readers even when the article body is not. */}
|
||||
<div class="container article-layout">
|
||||
<article class="article" lang={post.data.lang}>
|
||||
<a class="back" href="/news">← All news</a>
|
||||
{/* Editorial anatomy (Guardian/BBC/Reuters convention): headline →
|
||||
standfirst → byline/meta → lead image → body. The standfirst is the
|
||||
description made visible — cards already show it, the article should too. */}
|
||||
<h1>{post.data.title}</h1>
|
||||
<p class="standfirst">{post.data.description}</p>
|
||||
{/* Meta — one quiet muted line: category · author · date · reading time */}
|
||||
<div class="meta">
|
||||
<span class="cat"><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><path d="M20 13.2 13.2 20a2 2 0 0 1-2.9 0L4 13.7V4h9.7l6.3 6.3a2 2 0 0 1 0 2.9Z" stroke-linejoin="round" /><circle cx="8.5" cy="8.5" r="1" /></svg>{post.data.category}</span>·
|
||||
<span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4" /><path d="M4.5 20c1.6-3.8 4.6-5.5 7.5-5.5s5.9 1.7 7.5 5.5" /></svg>{post.data.author}</span>·
|
||||
<time datetime={post.data.date.toISOString().slice(0, 10)}><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="16" rx="2" /><path d="M4 10h16M8 3v4M16 3v4" /></svg>{fmt(post.data.date)}</time>·
|
||||
<span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></svg>{readMin} min read</span>
|
||||
</div>
|
||||
{post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" />}
|
||||
{/* TOC sits between lead image and body (Wikipedia/GOV.UK contents
|
||||
position): the press head above stays unbroken, and the list is
|
||||
adjacent to the content it indexes. */}
|
||||
{toc.length >= 3 && (
|
||||
<details class="toc toc--inline">
|
||||
<summary>On this page <svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><path d="m5 9 7 7 7-7" /></svg></summary>
|
||||
<ol>
|
||||
{toc.map((h) => (
|
||||
<li class={h.depth === 3 ? 'd3' : undefined}><a href={`#${h.slug}`}>{h.text}</a></li>
|
||||
))}
|
||||
</ol>
|
||||
</details>
|
||||
)}
|
||||
<div class="prose">
|
||||
<Content />
|
||||
</div>
|
||||
{(newer || older) && (
|
||||
<nav class="post-nav" aria-label="More news">
|
||||
{newer && (
|
||||
<a class="post-nav-prev" rel="prev" href={`/news/${newer.id}`}>
|
||||
<small>← Newer</small>
|
||||
<span class="post-nav-title">{newer.data.title}</span>
|
||||
</a>
|
||||
)}
|
||||
{older && (
|
||||
<a class="post-nav-next" rel="next" href={`/news/${older.id}`}>
|
||||
<small>Older →</small>
|
||||
<span class="post-nav-title">{older.data.title}</span>
|
||||
</a>
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
</article>
|
||||
{toc.length >= 3 && (
|
||||
<nav class="toc toc--rail" aria-label="On this page">
|
||||
<strong>On this page</strong>
|
||||
<ol>
|
||||
{toc.map((h) => (
|
||||
<li class={h.depth === 3 ? 'd3' : undefined}><a href={`#${h.slug}`}>{h.text}</a></li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
<script>
|
||||
// TOC scrollspy — minimal vanilla take on Starlight's starlight-toc.ts:
|
||||
// an IntersectionObserver band near the viewport top; whichever prose block
|
||||
// enters it is mapped back to its governing h2/h3, whose TOC link(s) get
|
||||
// aria-current="true" (set in BOTH renderings — inline <details> and rail).
|
||||
// Progressive enhancement: without JS the TOC is plain working anchors.
|
||||
const links = [...document.querySelectorAll('.toc a[href^="#"]')];
|
||||
if (links.length) {
|
||||
let current = [];
|
||||
const setCurrent = (id) => {
|
||||
const next = id ? links.filter((a) => decodeURIComponent(a.hash.slice(1)) === id) : [];
|
||||
if (next[0] === current[0]) return;
|
||||
for (const a of current) a.removeAttribute('aria-current');
|
||||
for (const a of next) a.setAttribute('aria-current', 'true');
|
||||
current = next;
|
||||
};
|
||||
const blocks = [...document.querySelectorAll('.prose > *')];
|
||||
// Governing heading = the block itself or the nearest h2/h3[id] above it
|
||||
const headingFor = (el) => {
|
||||
for (let i = blocks.indexOf(el); i >= 0; i--) {
|
||||
if (blocks[i].matches('h2[id], h3[id]')) return blocks[i].id;
|
||||
}
|
||||
return null; // intro before the first heading — nothing highlighted
|
||||
};
|
||||
const onIntersect = (entries) => {
|
||||
for (const e of entries) {
|
||||
if (e.isIntersecting) { setCurrent(headingFor(e.target)); break; }
|
||||
}
|
||||
};
|
||||
let observer;
|
||||
const observe = () => {
|
||||
observer?.disconnect();
|
||||
// Narrow band below the sticky header (--header-h 56px + 12px slack;
|
||||
// JS can't read the token cheaply, keep in sync) — Starlight's rootMargin trick
|
||||
const top = 68, band = 64;
|
||||
observer = new IntersectionObserver(onIntersect, {
|
||||
rootMargin: `-${top}px 0px ${top + band - document.documentElement.clientHeight}px`,
|
||||
});
|
||||
blocks.forEach((b) => observer.observe(b));
|
||||
};
|
||||
observe();
|
||||
let timer;
|
||||
addEventListener('resize', () => { clearTimeout(timer); timer = setTimeout(observe, 200); });
|
||||
// Short final section: at page bottom, the last heading wins
|
||||
addEventListener('scroll', () => {
|
||||
if (innerHeight + scrollY >= document.documentElement.scrollHeight - 4) {
|
||||
setCurrent(headingFor(blocks.at(-1)));
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const pages = await getCollection('pages');
|
||||
return pages.map((page) => ({ params: { slug: page.id }, props: { page } }));
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
const { Content } = await render(page);
|
||||
---
|
||||
<Base title={`${page.data.title} - CTAO Science Portal`} description={page.data.description}>
|
||||
{/* Same wrapper pattern as news/[slug] (never .container and .article on
|
||||
one element — both set max-width and would depend on rule order). */}
|
||||
<div class="container article-layout">
|
||||
<article class="article">
|
||||
{/* Static pages are footer-reached documents; same back affordance as
|
||||
articles ("← All news") so deep links aren't dead ends. */}
|
||||
<a class="back" href="/">← Home</a>
|
||||
<h1>{page.data.title}</h1>
|
||||
<div class="prose">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="CTAO Science Portal - Proposals" description="Observation proposals: Proposal Handling System integration (planned)">
|
||||
<section class="band--moon">
|
||||
<header class="container page-head">
|
||||
<h1>Observation proposals</h1>
|
||||
<p class="standfirst">Submit observation proposals, including ToO and MWL requests.</p>
|
||||
</header>
|
||||
<div class="container page">
|
||||
{/* Integration landing, not a form: the PHS is APC's product — the spec
|
||||
(§3.3.3) scopes this portal to integrating it, so we describe the
|
||||
hand-off instead of imitating their submission UI. */}
|
||||
<div class="panel panel--narrow">
|
||||
<h2 class="panel-title">Proposal Handling System</h2>
|
||||
<p>Submitting observation proposals, including Target of Opportunity (ToO)
|
||||
and multi-wavelength (MWL) requests, happens in the Proposal Handling
|
||||
System. That system is built by the APC team and integrated into this portal.</p>
|
||||
<p>What the portal contributes:</p>
|
||||
<ul class="panel-list">
|
||||
<li>This menu entry, with a single sign-on hand-off (CTAO AAI).</li>
|
||||
<li>Proposal status feedback in your <a href="/dashboard">Dashboard</a>.</li>
|
||||
</ul>
|
||||
<button class="btn" type="button" disabled>Open the Proposal Handling System (integration planned)</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -0,0 +1,43 @@
|
||||
// RSS 2.0 feed — hand-rolled static endpoint like search.json.js (no @astrojs/rss
|
||||
// dependency; the spec is 20 lines of XML). Newest 30 items, absolute URLs from
|
||||
// the configured `site`.
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const esc = (s = '') =>
|
||||
s.replace(/[<>&'"]/g, (c) => ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"' })[c]);
|
||||
|
||||
export async function GET(context) {
|
||||
const site = context.site; // always set in astro.config.mjs
|
||||
const posts = (await getCollection('news', ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.slice(0, 30);
|
||||
|
||||
const items = posts
|
||||
.map((p) => {
|
||||
const url = new URL(`/news/${p.id}`, site).href;
|
||||
return ` <item>
|
||||
<title>${esc(p.data.title)}</title>
|
||||
<link>${url}</link>
|
||||
<guid isPermaLink="true">${url}</guid>
|
||||
<pubDate>${p.data.date.toUTCString()}</pubDate>
|
||||
<description>${esc(p.data.description)}</description>
|
||||
<category>${esc(p.data.category)}</category>
|
||||
</item>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>CTAO Science Portal News</title>
|
||||
<link>${new URL('/news', site).href}</link>
|
||||
<description>News and announcements of the Cherenkov Telescope Array Observatory</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>${(posts[0]?.data.date ?? new Date()).toUTCString()}</lastBuildDate>
|
||||
<atom:link href="${new URL('/rss.xml', site).href}" rel="self" type="application/rss+xml"/>
|
||||
${items}
|
||||
</channel>
|
||||
</rss>
|
||||
`;
|
||||
return new Response(xml, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' } });
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="CTAO Science Portal - Search" description="Search CTAO news and announcements">
|
||||
{/* Utility archetype: ONE surface per page — Moon canvas from the landing
|
||||
head down (borderless cards/panels pop against it); editorial pages stay
|
||||
all-white. No mid-page surface seams (DESIGN.md). */}
|
||||
<section class="band--moon">
|
||||
<header class="container page-head">
|
||||
<h1>Search</h1>
|
||||
<p class="standfirst">Find news and announcements.</p>
|
||||
</header>
|
||||
<div class="container page">
|
||||
<form class="search-form" role="search" action="/search" method="get">
|
||||
<label class="sr-only" for="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="q" name="q" type="search" placeholder="e.g. LST camera, Council, data…" autocomplete="off" />
|
||||
<button class="btn" type="submit">Search</button>
|
||||
</form>
|
||||
<p class="notice" id="search-status" role="status" aria-live="polite">Type to search the news archive.</p>
|
||||
<ol class="search-results" id="search-results"></ol>
|
||||
<noscript><p class="notice">Search requires JavaScript. Browse all news on the <a href="/news">news page</a>.</p></noscript>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
{/* search-client.js is loaded once (deferred) by the Base layout; this inline
|
||||
module runs after it in the shared after-parse queue — order guaranteed. */}
|
||||
<script is:inline type="module">
|
||||
const run = newsSearch({ input: 'q', list: 'search-results', status: 'search-status', limit: 20, hint: 'Type at least 2 characters.' });
|
||||
// Support /search?q=… deep links (hero search form, footer, bookmarks)
|
||||
const q0 = new URLSearchParams(location.search).get('q');
|
||||
const q = document.getElementById('q');
|
||||
if (q0) { q.value = q0; run(); }
|
||||
q.focus();
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
// Build-time search index (Astro static endpoint — no server, no dependencies).
|
||||
// Kept lean: title/description/category/date/slug only (~60 KB for 200+ articles).
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function GET() {
|
||||
const posts = await getCollection('news', ({ data }) => !data.draft);
|
||||
const index = posts
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.map((p) => ({
|
||||
slug: p.id,
|
||||
title: p.data.title,
|
||||
description: p.data.description,
|
||||
category: p.data.category,
|
||||
date: p.data.date.toISOString().slice(0, 10),
|
||||
}));
|
||||
return new Response(JSON.stringify(index), {
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,40 @@
|
||||
import examples from '../data/examples.json';
|
||||
import placeholders from '../data/placeholders.json';
|
||||
// Sitemap — hand-rolled static endpoint (no @astrojs/sitemap dependency; our URL
|
||||
// set is fully known at build time). /admin is intentionally excluded.
|
||||
import { getCollection } from 'astro:content';
|
||||
import { PAGE_FIRST, PAGE_REST } from '../lib/news.js';
|
||||
|
||||
// Chunk sizes are imported from lib/news.js — the same values the
|
||||
// /news/[...page] route slices with, so the page count here cannot drift.
|
||||
const newsPageCount = (n) => (n <= PAGE_FIRST ? 1 : 1 + Math.ceil((n - PAGE_FIRST) / PAGE_REST));
|
||||
|
||||
export async function GET(context) {
|
||||
const site = context.site; // always set in astro.config.mjs
|
||||
const news = (await getCollection('news', ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
const pages = await getCollection('pages');
|
||||
|
||||
// Static pages come from the actual route files, so a new src/pages/*.astro
|
||||
// can never be forgotten here. 404 is excluded; /admin (subdirectory) is
|
||||
// intentionally unlisted. Go-live note: /login and /dashboard are mock
|
||||
// pages — filter them out before robots.txt stops disallowing everything.
|
||||
const staticPages = Object.keys(import.meta.glob('./*.astro'))
|
||||
.map((p) => p.slice(1, -'.astro'.length))
|
||||
.filter((p) => p !== '/404')
|
||||
.map((p) => (p === '/index' ? '/' : p))
|
||||
.sort();
|
||||
const urls = [
|
||||
...staticPages, '/news',
|
||||
...Array.from({ length: newsPageCount(news.length) - 1 }, (_, i) => `/news/${i + 2}`),
|
||||
...pages.map((p) => `/pages/${p.id}`),
|
||||
].map((path) => ` <url><loc>${new URL(path, site).href}</loc></url>`);
|
||||
const articles = news.map(
|
||||
(p) => ` <url><loc>${new URL(`/news/${p.id}`, site).href}</loc><lastmod>${p.data.date.toISOString().slice(0, 10)}</lastmod></url>`,
|
||||
);
|
||||
|
||||
export function GET({ site }) {
|
||||
const paths = ['/', ...placeholders.map(({ slug }) => `/${slug}/`),
|
||||
...examples.map(({ slug }) => `/example/${slug}/`)];
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${paths.map((path) => ` <url><loc>${new URL(path, site).href}</loc></url>`).join('\n')}
|
||||
</urlset>`;
|
||||
${[...urls, ...articles].join('\n')}
|
||||
</urlset>
|
||||
`;
|
||||
return new Response(xml, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
// User support channels per the Science Portal spec — all mock for now.
|
||||
const channels = [
|
||||
{ title: 'Help desk', desc: 'Submit and track support tickets.' },
|
||||
{ title: 'FAQ & troubleshooting', desc: 'Common questions and step-by-step guides.' },
|
||||
{ title: 'User forum', desc: 'Discuss with the CTAO user community.' },
|
||||
{ title: 'Mailing lists', desc: 'Announcements and topical mailing lists.' },
|
||||
];
|
||||
---
|
||||
<Base title="CTAO Science Portal - Support" description="User support: help desk, FAQ, forum, mailing lists (mock)">
|
||||
<section class="band--moon">
|
||||
<header class="container page-head">
|
||||
<h1>User Support</h1>
|
||||
<p class="standfirst">Help desk, FAQ, user forum and mailing lists.</p>
|
||||
</header>
|
||||
<div class="container page">
|
||||
<h2 class="sr-only">Support channels</h2>
|
||||
<div class="grid">
|
||||
{channels.map((c) => (
|
||||
<article class="card tile">
|
||||
<h3>{c.title}</h3>
|
||||
<p class="desc">{c.desc}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"extends": "astro/tsconfigs/base",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist", "public"]
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
|
||||