diff --git a/.gitignore b/.gitignore index fba1a8c..99173fd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,9 @@ Thumbs.db # Playwright MCP session artifacts (console logs + page snapshots) .playwright-mcp/ + +# Generated at build time by scripts/fetch-*.py — never committed. +# Schema-only examples (*.example.json) are committed alongside as documentation. +src/data/plugins_metadata.json +src/data/verdi-cli.json +src/data/pypi-stats.json diff --git a/package-lock.json b/package-lock.json index 73f17e3..efa6d21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "astro": "^5.17.1", + "fuse.js": "^7.3.0", "js-yaml": "^4.1.1", "react": "^19.2.4", "react-dom": "^19.2.4" @@ -2936,6 +2937,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fuse.js": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", + "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index 6f93ffc..db2679c 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,16 @@ "type": "module", "version": "0.0.1", "scripts": { + "fetch-data": "python3 scripts/fetch-verdi-cli.py && python3 scripts/fetch-pypi-stats.py && python3 scripts/fetch-registry.py", + "predev": "test -f src/data/verdi-cli.json && test -f src/data/pypi-stats.json && test -f src/data/plugins_metadata.json || npm run fetch-data", "dev": "astro dev", - "prebuild": "python3 scripts/fetch-verdi-cli.py", + "prebuild": "npm run fetch-data", "build": "astro build", "preview": "astro preview", "astro": "astro", "update-cli": "python3 scripts/fetch-verdi-cli.py", - "update-stats": "python3 scripts/fetch-pypi-stats.py" + "update-stats": "python3 scripts/fetch-pypi-stats.py", + "update-registry": "python3 scripts/fetch-registry.py" }, "dependencies": { "@astrojs/react": "^4.4.2", @@ -17,6 +20,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "astro": "^5.17.1", + "fuse.js": "^7.3.0", "js-yaml": "^4.1.1", "react": "^19.2.4", "react-dom": "^19.2.4" diff --git a/public/plugin-registry/aiida-logo.png b/public/plugin-registry/aiida-logo.png new file mode 100644 index 0000000..996f7b7 Binary files /dev/null and b/public/plugin-registry/aiida-logo.png differ diff --git a/scripts/fetch-pypi-stats.py b/scripts/fetch-pypi-stats.py index a576df7..b1b88d6 100644 --- a/scripts/fetch-pypi-stats.py +++ b/scripts/fetch-pypi-stats.py @@ -2,12 +2,20 @@ """Fetch monthly PyPI download stats for aiida-core. Run: python scripts/fetch-pypi-stats.py -Output: src/data/pypi-stats.json +Output: src/data/pypi-stats.json (gitignored — generated fresh on every build) -Intended to run at build time or periodically. -The generated JSON is committed to the repo and imported statically. +Wired into: + - `npm run prebuild` — always re-fetches before every build (deploy, CI). + - `npm run predev` — re-fetches only when any of the three data files is + missing (first clone). Subsequent `npm run dev` is + instant; refresh manually with `npm run fetch-data`. + +Build/dev fails loudly if pypistats.org is unreachable or returns zero +downloads — there is deliberately no graceful fallback to stale data. +Expected output schema: src/data/pypi-stats.example.json. """ import json +import sys import urllib.request from datetime import datetime, timedelta, timezone from pathlib import Path @@ -52,10 +60,17 @@ def fetch_stats(): "package": "aiida-core", } + if monthly_total <= 0: + raise RuntimeError("pypistats returned zero downloads — refusing to overwrite with empty data.") + out_path = Path(__file__).resolve().parent.parent / "src" / "data" / "pypi-stats.json" out_path.write_text(json.dumps(result, indent=2) + "\n") print(f"PyPI stats: {display} downloads/month ({result['period_start']} to {result['period_end']})") return result if __name__ == "__main__": - fetch_stats() + try: + fetch_stats() + except Exception as e: + print(f"fetch-pypi-stats: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scripts/fetch-registry.py b/scripts/fetch-registry.py new file mode 100644 index 0000000..ccc7a4a --- /dev/null +++ b/scripts/fetch-registry.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Fetch the AiiDA plugin registry metadata. + +Run: python3 scripts/fetch-registry.py +Output: src/data/plugins_metadata.json (gitignored — generated fresh on every build) + +Pulls the pre-built metadata from the upstream aiida-registry, which +runs its own daily cron to scrape PyPI, parse pyproject.toml/setup.cfg, +test installation, and emit JSON. We just consume that artefact so this +site stays a thin presentation layer over the canonical registry. + +Wired into: + - `npm run prebuild` — always re-fetches before every build (deploy, CI). + - `npm run predev` — re-fetches only when any of the three data files is + missing (first clone). Subsequent `npm run dev` is + instant; refresh manually with `npm run fetch-data`. + +For nightly freshness in production, a Cloudflare Pages deploy hook re-triggers +the build, which re-runs prebuild. Build/dev fails loudly if upstream is +unreachable or the schema drifts — there is deliberately no graceful fallback +to stale data. Expected output schema: src/data/plugins_metadata.example.json. +""" +import json +import sys +import urllib.request +from pathlib import Path + +UPSTREAM = "https://aiidateam.github.io/aiida-registry/plugins_metadata.json" +TIMEOUT_SEC = 30 + + +def fetch(): + req = urllib.request.Request(UPSTREAM, headers={"User-Agent": "aiida-website-build"}) + with urllib.request.urlopen(req, timeout=TIMEOUT_SEC) as resp: + raw = resp.read() + data = json.loads(raw.decode()) + + expected = {"plugins", "globalsummary", "status_dict", "entrypointtypes"} + missing = expected - set(data.keys()) + if missing: + raise RuntimeError(f"upstream JSON missing keys: {missing}") + if not data["plugins"]: + raise RuntimeError("upstream JSON contains zero plugins — refusing to overwrite with empty data.") + + out_path = Path(__file__).resolve().parent.parent / "src" / "data" / "plugins_metadata.json" + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(data, indent=2) + "\n") + + n_plugins = len(data["plugins"]) + size_kb = len(raw) // 1024 + print(f"Plugin registry: {n_plugins} plugins, {size_kb} KB") + + +if __name__ == "__main__": + try: + fetch() + except Exception as e: + print(f"fetch-registry: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scripts/fetch-verdi-cli.py b/scripts/fetch-verdi-cli.py index 6c02801..8d0252e 100644 --- a/scripts/fetch-verdi-cli.py +++ b/scripts/fetch-verdi-cli.py @@ -2,13 +2,20 @@ """Generate verdi CLI tree from aiida-core for the website. Run: python scripts/fetch-verdi-cli.py -Output: src/data/verdi-cli.json +Output: src/data/verdi-cli.json (gitignored — generated fresh on every build) -- Developer mode: uses locally installed aiida-core -- CI / deploy: fetches the latest stable release from PyPI automatically +- Developer mode: uses locally installed aiida-core (fast, no install). +- CI / fresh clone without aiida-core: creates a temp venv, installs aiida-core + from PyPI, and re-runs this script inside it (slow first time, ~30 seconds). -This only needs to be re-run when aiida-core updates its CLI. -The generated JSON is committed to the repo and imported statically. +Wired into: + - `npm run prebuild` — always re-fetches before every build (deploy, CI). + - `npm run predev` — re-fetches only when any of the three data files is + missing (first clone). Subsequent `npm run dev` is + instant; refresh manually with `npm run fetch-data`. + +Build/dev fails loudly if generation fails — there is deliberately no graceful +fallback to stale data. Expected output schema: src/data/verdi-cli.example.json. """ import json import os @@ -60,6 +67,9 @@ def walk_group(group: click.MultiCommand, prefix: str = "verdi") -> dict: "help": tree["help"], } + if not output["commands"]: + raise RuntimeError("verdi command tree came back empty — aborting rather than write an empty CLI reference.") + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) OUT_PATH.write_text(json.dumps(output, indent=2, sort_keys=True) + "\n") print(f"Generated {OUT_PATH} with {len(output['commands'])} commands, {len(output['subcommands'])} groups") @@ -93,6 +103,10 @@ def fetch_and_generate(): if __name__ == "__main__": try: - generate() - except ImportError: - fetch_and_generate() + try: + generate() + except ImportError: + fetch_and_generate() + except Exception as e: + print(f"fetch-verdi-cli: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/components/Docs.tsx b/src/components/Docs.tsx index 8c76183..a5a59ae 100644 --- a/src/components/Docs.tsx +++ b/src/components/Docs.tsx @@ -1,5 +1,7 @@ import type { ReactNode } from 'react'; +const base = import.meta.env.BASE_URL?.replace(/\/$/, '') || ''; + const ExtIcon = () => ( @@ -168,7 +170,7 @@ export default function Docs(): ReactNode {

Source code, issues, and contributing

- +
diff --git a/src/components/Ecosystem.tsx b/src/components/Ecosystem.tsx index 46d89d1..8d885d0 100644 --- a/src/components/Ecosystem.tsx +++ b/src/components/Ecosystem.tsx @@ -35,7 +35,7 @@ const NODES: EcoNode[] = [ name: 'Plugin registry', tagline: '100+ community plugins', desc: 'A curated directory of all AiiDA plugins — from simulation codes (Quantum ESPRESSO, VASP, CP2K, FLEUR, Siesta, …) to data types, schedulers, and transports. Plugins extend AiiDA via Python entry points.', - url: 'https://aiidateam.github.io/aiida-registry/', + url: `${base}/plugin-registry/`, color: '#0096de', category: 'core', }, @@ -193,7 +193,7 @@ export default function Ecosystem(): ReactNode { {/* Bottom row: extensions and tools */}
-
diff --git a/src/components/LandingPage.tsx b/src/components/LandingPage.tsx index eac5052..06b5293 100644 --- a/src/components/LandingPage.tsx +++ b/src/components/LandingPage.tsx @@ -652,7 +652,7 @@ function PluginShowcase(): ReactNode {
))} - + Browse all 100+ plugins in the registry →
@@ -3112,7 +3112,7 @@ function Numbers(): ReactNode { 1000+ Publications - + 100+ Plugins diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 9422195..43ccfc6 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -56,7 +56,7 @@ export default function Navbar() {
  • Ecosystem
  • Docs
  • News
  • -
  • Plugins
  • +
  • Plugins
  • Community
  • + diff --git a/src/pages/plugin-registry/[key].astro b/src/pages/plugin-registry/[key].astro new file mode 100644 index 0000000..112e053 --- /dev/null +++ b/src/pages/plugin-registry/[key].astro @@ -0,0 +1,400 @@ +--- +import '../../styles/registry.css'; +import Layout from '../../layouts/Layout.astro'; +import data from '../../data/plugins_metadata.json'; + +interface SummaryInfo { text: string; count: number; colorclass: string; } +interface PluginMetadata { + description?: string; + author?: string; + author_email?: string; + version?: string; + release_date?: string; + classifiers?: string[]; +} +interface PluginEntryPointSpec { + inputs?: { name: string; required: boolean; valid_types: string; info: string }[]; + outputs?: { name: string; required: boolean; valid_types: string; info: string }[]; + exit_codes?: { status: number; message: string }[]; +} +interface PluginEntryPointDetail { + class?: string; + description?: string[]; + spec?: PluginEntryPointSpec; +} +type PluginEntryPointVal = string | PluginEntryPointDetail; +interface PluginRecord { + name: string; + package_name?: string; + code_home: string; + documentation_url?: string; + pip_url?: string; + pip_install_cmd?: string; + development_status: string; + entry_point_prefix?: string; + aiida_version?: string; + is_installable?: string; + metadata: PluginMetadata; + summaryinfo?: SummaryInfo[]; + warnings?: string[]; + errors?: string[]; + entry_points?: Record>; +} + +export async function getStaticPaths() { + const plugins = data.plugins as Record; + return Object.keys(plugins).map(key => ({ params: { key } })); +} + +const { key } = Astro.params; +const plugins = data.plugins as Record; +const statusDict = data.status_dict as Record; +const entryPointTypes = data.entrypointtypes as Record; +const plugin = plugins[key as string]; + +/* Static (SSG) build: getStaticPaths enumerates every plugin key, so this branch + is unreachable in production. Throwing here makes a misconfiguration loud at + build time rather than producing a silent broken page. */ +if (!plugin) { + throw new Error(`Plugin "${key}" rendered without a matching entry in plugins_metadata.json`); +} + +const base = import.meta.env.BASE_URL?.replace(/\/$/, '') || ''; + +const STATUS_COLOR: Record = { + planning: 'red', + 'pre-alpha': 'red', + alpha: 'orange', + beta: 'yellow', + stable: 'green', + mature: 'brightgreen', + inactive: 'gray', +}; +function statusColorClass(statusKey: string): string { + return STATUS_COLOR[statusKey] ?? 'gray'; +} + +function splitOther(text: string): { label: string; tooltip?: string } { + const m = text.match(/^Other\s*\(([^)]*)\)\s*$/); + return m ? { label: 'Other', tooltip: m[1].trim() } : { label: text }; +} + +/* Upstream registry checks come as HTML strings shaped like + `W001: ` + where the text portion can include data sourced from third-party plugin + metadata (pyproject.toml, setup.cfg). To avoid set:html on author-controlled + content, parse the well-known shape and render it as JSX with text escaping. */ +function parseCheck(raw: string): { code: string | null; codeUrl: string | null; text: string } { + const m = raw.match(/^([WE]\d{3,})<\/a>:\s*([\s\S]*)$/); + if (m) { + return { codeUrl: m[1], code: m[2], text: m[3].trim() }; + } + /* Fallback: strip any tags so unexpected shapes can't inject HTML either. */ + return { code: null, codeUrl: null, text: raw.replace(/<[^>]*>/g, '').trim() }; +} + +const statusKey = plugin.development_status; +const statusTooltip = statusDict[statusKey]?.[0] ?? statusKey; +const description = plugin.metadata?.description ?? ''; +const hasEntryPoints = plugin.entry_points && Object.keys(plugin.entry_points).length > 0; +const warnings = plugin.warnings ?? []; +const errors = plugin.errors ?? []; +const allClean = warnings.length === 0 && errors.length === 0; +--- + + +
    +
    + + + + + All plugins + + +

    + {key} +

    + {description &&

    {description}

    } + +
    + + status + {statusKey} + + {plugin.aiida_version && ( + + + + AiiDA + + {plugin.aiida_version} + + )} + {plugin.is_installable === 'True' && ( + + + + )} +
    + +

    General information

    +
    + {plugin.pip_install_cmd && ( +
    + Install + {plugin.pip_install_cmd} +
    + )} +
    + Source code + + {plugin.code_home} + +
    +
    + Documentation + + {plugin.documentation_url ? ( + {plugin.documentation_url} + ) : ( + No documentation provided by the package author. + )} + +
    + {plugin.metadata?.author && ( +
    + Author(s) + {plugin.metadata.author} +
    + )} + {plugin.metadata?.author_email && ( +
    + Contact + + {plugin.metadata.author_email.split(',').map(email => ( + {email.trim()} + ))} + +
    + )} + {plugin.package_name && ( +
    + Python import + import {plugin.package_name} +
    + )} + {plugin.metadata?.version && ( +
    + Latest version + {plugin.metadata.version} +
    + )} + {plugin.metadata?.release_date && ( +
    + Released + {plugin.metadata.release_date} +
    + )} +
    + +

    Registry checks

    + {allClean ? ( +
    + + + + All checks passed +
    + ) : ( + <> + {warnings.map(w => { + const c = parseCheck(w); + return ( +
    + + + + + + + {c.code && c.codeUrl && ( + <>{c.code}: + )} + {c.text} + +
    + ); + })} + {errors.map(e => { + const c = parseCheck(e); + return ( +
    + + + + + + + {c.code && c.codeUrl && ( + <>{c.code}: + )} + {c.text} + +
    + ); + })} +
    + + Click any code (W001, E001…) to jump to{' '} + + troubleshooting instructions + . + +
    + + )} + + {plugin.summaryinfo && plugin.summaryinfo.length > 0 && ( + <> +

    Plugins provided

    +
    + {plugin.summaryinfo.map(s => { + const { label, tooltip } = splitOther(s.text); + return ( + + {label} + {s.count} + + ); + })} +
    + + )} + + {hasEntryPoints && ( + <> +

    Entry points

    + {Object.entries(plugin.entry_points!).map(([groupKey, eps]) => ( +
    +
    + {entryPointTypes[groupKey] ?? groupKey} + {groupKey} +
    +
      + {Object.entries(eps).map(([epName, epVal]) => ( +
    • +

      {epName}

      + {typeof epVal === 'string' ? ( + {epVal} + ) : ( + <> + {epVal.class && ( + class: {epVal.class} + )} + {epVal.description && epVal.description.length > 0 && ( +

      + {epVal.description.join(' ').trim()} +

      + )} + {epVal.spec?.inputs && epVal.spec.inputs.length > 0 && ( + + + + {epVal.spec.inputs.map(s => ( + + + + + + + ))} + +
      InputRequiredValid typesDescription
      {s.name}{String(s.required)}{s.valid_types}{s.info}
      + )} + {epVal.spec?.outputs && epVal.spec.outputs.length > 0 && ( + + + + {epVal.spec.outputs.map(s => ( + + + + + + + ))} + +
      OutputRequiredValid typesDescription
      {s.name}{String(s.required)}{s.valid_types}{s.info}
      + )} + {epVal.spec?.exit_codes && epVal.spec.exit_codes.length > 0 && ( + + + + {epVal.spec.exit_codes.map(c => ( + + + + + ))} + +
      Exit statusMessage
      {c.status}{c.message}
      + )} + + )} +
    • + ))} +
    +
    + ))} + + )} +
    + + {hasEntryPoints && ( + + )} +
    +
    diff --git a/src/pages/plugin-registry/index.astro b/src/pages/plugin-registry/index.astro new file mode 100644 index 0000000..bb008a8 --- /dev/null +++ b/src/pages/plugin-registry/index.astro @@ -0,0 +1,45 @@ +--- +import '../../styles/registry.css'; +import Layout from '../../layouts/Layout.astro'; +import PluginRegistryList from '../../components/PluginRegistryList'; +import data from '../../data/plugins_metadata.json'; + +const plugins = data.plugins as Record; +const statusDict = data.status_dict as Record; +const globalSummary = data.globalsummary as { name: string; total_num: number; num_entries: number; colorclass: string; tooltip?: string }[]; +const totalPackages = Object.keys(plugins).length; +--- + + +
    +
    +

    Plugin registry

    +

    + Community plugins extending AiiDA — simulation codes, data types, + schedulers, transports, and workflows. Register your plugin{' '} + + here + +

    +
    + +
    +

    Registered plugin packages: {totalPackages}

    +
    + {globalSummary.map(s => ( + + {s.name} + + {s.total_num} plugin{s.total_num === 1 ? '' : 's'} in {s.num_entries} package{s.num_entries === 1 ? '' : 's'} + + + ))} +
    +
    + + +
    +
    diff --git a/src/pages/science.md b/src/pages/science.md index eb9507c..c362d75 100644 --- a/src/pages/science.md +++ b/src/pages/science.md @@ -35,7 +35,7 @@ description: "Research publications powered by AiiDA." ## AiiDA-powered research surveys -The AiiDA team conducts periodically a **survey** in parallel with each development milestone, formerly on the AiiDA mailing list and now on [Discourse](https://aiida.discourse.group/), to gather information on research projects using AiiDA, including papers published, [AiiDA plugins](https://aiidateam.github.io/aiida-registry/) used, and further database indicators that inform future development choices. +The AiiDA team conducts periodically a **survey** in parallel with each development milestone, formerly on the AiiDA mailing list and now on [Discourse](https://aiida.discourse.group/), to gather information on research projects using AiiDA, including papers published, [AiiDA plugins](/plugin-registry/) used, and further database indicators that inform future development choices. - **2024**: [results](/survey/Questionnaire_results_2024.pdf) - **2020**: [results](/survey/aiida-powered-research-projects-2020_Updated.pdf) diff --git a/src/pages/team.astro b/src/pages/team.astro index a1bdd9f..c11066b 100644 --- a/src/pages/team.astro +++ b/src/pages/team.astro @@ -93,7 +93,7 @@ const formerMembers = [

    Besides the AiiDA development team listed below, we thank our numerous external code contributors - as well as all plugin developers + as well as all plugin developers for making the AiiDA ecosystem what it is today.

    diff --git a/src/styles/global.css b/src/styles/global.css index 84b25e3..0c8a138 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -55,13 +55,21 @@ html { /* Design system — all three AiiDA brand colors */ --color-accent-orange: #e06800; --color-accent-green: #28a006; + --color-accent-seafoam: #9fe2bf; + --color-text-on-seafoam: #1a1d2e; --color-bg-tinted: #f0f5fa; --color-bg-tinted-alt: #eaf0f8; --color-bg-tinted-green: #eef8ec; --color-bg-tinted-orange: #fdf3ea; + --color-bg-tinted-red: #fde8ea; --color-border-accent: rgba(0, 150, 222, 0.18); --color-border-accent-green: rgba(48, 184, 8, 0.15); --color-border-accent-orange: rgba(255, 125, 23, 0.15); + --color-border-accent-red: rgba(193, 18, 31, 0.18); + /* Foreground text designed for legibility on the matching tinted backgrounds */ + --color-text-on-tint-green: #186b07; + --color-text-on-tint-orange: #934d00; + --color-text-on-tint-red: #8c1018; --color-shadow-accent: rgba(0, 150, 222, 0.08); --color-heading-accent: #0077b3; /* Syntax highlighting (light mode — darker variants for contrast on @@ -94,9 +102,14 @@ html { --color-bg-tinted-alt: #14162c; --color-bg-tinted-green: #101a10; --color-bg-tinted-orange: #1a1410; + --color-bg-tinted-red: #2a1416; --color-border-accent: rgba(0, 150, 222, 0.15); --color-border-accent-green: rgba(48, 184, 8, 0.12); --color-border-accent-orange: rgba(255, 125, 23, 0.12); + --color-border-accent-red: rgba(193, 18, 31, 0.22); + --color-text-on-tint-green: #6dd14e; + --color-text-on-tint-orange: #f7b369; + --color-text-on-tint-red: #f0a4ab; --color-shadow-accent: rgba(0, 150, 222, 0.08); --color-heading-accent: #4ab8f0; --screen-prompt-interactive: #f9e2af; @@ -503,3 +516,58 @@ a:hover .navbar-ext-icon { .footer-copy a:hover { color: var(--color-primary); } + +#back-to-top { + position: fixed; + right: 1.25rem; + bottom: 1.25rem; + width: 2.5rem; + height: 2.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border-radius: 50%; + border: 1px solid var(--color-primary-dark); + background: var(--color-primary); + color: #fff; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25); + cursor: pointer; + opacity: 0; + transform: translateY(8px); + transition: + opacity 180ms ease, + transform 180ms ease, + background-color 150ms ease, + color 150ms ease, + border-color 150ms ease; + z-index: 90; +} + +#back-to-top.is-visible { + opacity: 1; + transform: translateY(0); +} + +#back-to-top:hover { + background: var(--color-primary-dark); + border-color: var(--color-primary-darker); +} + +#back-to-top:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +#back-to-top svg { + width: 1.1rem; + height: 1.1rem; +} + +@media (prefers-reduced-motion: reduce) { + #back-to-top { + transition: + opacity 0ms, + transform 0ms; + } +} diff --git a/src/styles/registry.css b/src/styles/registry.css new file mode 100644 index 0000000..15ccb5e --- /dev/null +++ b/src/styles/registry.css @@ -0,0 +1,902 @@ +/* ===== PLUGIN REGISTRY PAGES ===== */ + +/* Local tokens scoped to the registry. Kept here (not in global.css) because + they describe shields.io-style badge chrome that is intentionally + theme-neutral and only used on registry pages. */ +:where(.reg-page, .reg-detail) { + --reg-badge-label-bg: #555; + --reg-badge-label-color: #fff; +} + +.reg-page { + max-width: 1100px; + margin: 0 auto; + padding: 0 2rem 4rem; +} + +/* --- Hero --- */ + +.reg-hero { + text-align: center; + padding: 3rem 0 1.25rem; +} + +.reg-hero h1 { + font-size: 2.4rem; + font-weight: 800; + margin: 0 0 0.6rem; +} + +.reg-hero-sub { + font-size: 1.05rem; + color: var(--color-text-secondary); + max-width: 720px; + margin: 0 auto; + line-height: 1.6; +} + +.reg-hero-sub a { + color: var(--color-primary); + text-decoration: none; +} + +.reg-hero-sub a:hover { + text-decoration: underline; +} + +/* --- Global summary badges --- */ + +.reg-summary { + margin: 2rem 0 1rem; + padding: 1.25rem 1.5rem; + background: var(--color-bg-alt); + border: 1px solid var(--color-border); + border-radius: 12px; +} + +.reg-summary-title { + font-size: 0.95rem; + font-weight: 600; + color: var(--color-text-secondary); + margin: 0 0 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.reg-summary-grid { + display: grid; + grid-template-columns: max-content max-content; + gap: 0.4rem 0; + align-items: center; + justify-content: start; +} + +.reg-sum-label, +.reg-sum-count { + font-family: var(--font-mono); + font-size: 0.8rem; + padding: 0.28rem 0.6rem; + white-space: nowrap; + border: 1px solid var(--color-border); + line-height: 1.2; +} + +.reg-sum-label { + color: #fff; + font-weight: 600; + border-radius: 4px 0 0 4px; + text-align: right; + justify-self: end; + border-right: none; +} + +.reg-sum-count { + background: var(--color-border); + color: var(--color-text); + border-radius: 0 4px 4px 0; + border-left: none; + text-align: right; +} + +.reg-badge { + display: inline-flex; + font-family: var(--font-mono); + font-size: 0.72rem; + border-radius: 3px; + overflow: hidden; + border: 1px solid var(--color-border); + line-height: 1.1; +} + +.reg-badge-left, +.reg-badge-right { + padding: 0.18rem 0.42rem; + white-space: nowrap; + display: inline-flex; + align-items: center; +} + +/* Shields.io-style default: dark-gray label on the left, lighter value on the right. */ +.reg-badge-left { + background: var(--reg-badge-label-bg); + color: var(--reg-badge-label-color); + font-weight: 600; +} + +.reg-badge-right { + background: var(--color-border); + color: var(--color-text); + font-weight: 600; +} + +/* Shared colour palette — works on either side and on the summary chips. */ +.reg-badge-left.gray, +.reg-badge-right.gray, +.reg-sum-label.gray { + background: #6b7280; + color: #fff; +} +.reg-badge-left.red, +.reg-badge-right.red, +.reg-sum-label.red { + background: #c1121f; + color: #fff; +} +.reg-badge-left.orange, +.reg-badge-right.orange, +.reg-sum-label.orange { + background: #d97706; + color: #fff; +} +.reg-badge-left.yellow, +.reg-badge-right.yellow, +.reg-sum-label.yellow { + background: #ca8a04; + color: #fff; +} +.reg-badge-left.green, +.reg-badge-right.green, +.reg-sum-label.green { + background: #16a34a; + color: #fff; +} +.reg-badge-left.blue, +.reg-badge-right.blue, +.reg-sum-label.blue { + background: var(--color-primary); + color: #fff; +} +.reg-badge-left.lightblue, +.reg-badge-right.lightblue, +.reg-sum-label.lightblue { + background: var(--color-primary-light); + color: #fff; +} +.reg-badge-left.seafoam, +.reg-badge-right.seafoam, +.reg-sum-label.seafoam { + background: var(--color-accent-seafoam); + color: var(--color-text-on-seafoam); +} +.reg-badge-left.brightgreen, +.reg-badge-right.brightgreen, +.reg-sum-label.brightgreen { + background: var(--color-accent-green); + color: #fff; +} +.reg-badge-left.brown, +.reg-badge-right.brown, +.reg-sum-label.brown { + background: #92400e; + color: #fff; +} +.reg-badge-left.purple, +.reg-badge-right.purple, +.reg-sum-label.purple { + background: #6f3aad; + color: #fff; +} + +/* Original upstream registry palette for entry-point category chips + (Calculations, Data, Workflows, Parsers, Console scripts, Other). Applies to + both the per-card summary and the top inventory grid, in light and dark mode. */ +.reg-card-summary .reg-badge-left.blue, +.reg-summary-grid .reg-sum-label.blue { + background: #377eb8; /* Calculations */ +} +.reg-card-summary .reg-badge-left.red, +.reg-summary-grid .reg-sum-label.red { + background: #e41a1c; /* Data */ +} +.reg-card-summary .reg-badge-left.green, +.reg-summary-grid .reg-sum-label.green { + background: #4daf4a; /* Workflows */ +} +.reg-card-summary .reg-badge-left.orange, +.reg-summary-grid .reg-sum-label.orange { + background: #ff7f00; /* Other */ +} +.reg-card-summary .reg-badge-left.brown, +.reg-summary-grid .reg-sum-label.brown { + background: #a65628; /* Parsers */ +} +.reg-card-summary .reg-badge-left.purple, +.reg-summary-grid .reg-sum-label.purple { + background: #984ea3; /* Console scripts */ +} + +/* --- Toolbar (search + sort) --- */ + +.reg-toolbar { + display: flex; + gap: 1rem; + margin: 1.5rem 0 1.25rem; + flex-wrap: wrap; + align-items: center; +} + +.reg-search { + position: relative; + flex: 1; + min-width: 240px; +} + +.reg-search-input { + width: 100%; + padding: 0.7rem 0.9rem 0.7rem 2.4rem; + border: 1.5px solid var(--color-border); + border-radius: 8px; + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-base); + font-size: 0.95rem; + transition: + border-color 0.15s, + box-shadow 0.15s; +} + +.reg-search-input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-shadow-accent); +} + +.reg-search-icon { + position: absolute; + left: 0.85rem; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + color: var(--color-text-muted); + pointer-events: none; +} + +.reg-suggestions { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); + max-height: 320px; + overflow-y: auto; + z-index: 30; + padding: 0.4rem; +} + +.reg-suggestion { + display: block; + padding: 0.5rem 0.7rem; + border-radius: 6px; + text-decoration: none; + color: var(--color-text); + font-size: 0.9rem; +} + +.reg-suggestion:hover, +.reg-suggestion[data-active="true"] { + background: var(--color-bg-alt); +} + +.reg-suggestion-name { + font-weight: 600; + color: var(--color-primary-dark); +} + +.reg-suggestion-snippet { + font-size: 0.8rem; + color: var(--color-text-muted); + margin-top: 0.15rem; + line-height: 1.4; +} + +.reg-suggestion-snippet mark { + background: #fff3a3; + padding: 0 2px; + border-radius: 2px; + color: var(--color-text); +} + +[data-theme="dark"] .reg-suggestion-snippet mark { + background: #4a4011; + color: #ffeb84; +} + +.reg-sort { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.reg-sort label { + font-size: 0.85rem; + color: var(--color-text-secondary); +} + +.reg-sort select { + padding: 0.55rem 0.8rem; + border: 1.5px solid var(--color-border); + border-radius: 8px; + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-base); + font-size: 0.9rem; + cursor: pointer; +} + +.reg-sort select:focus { + outline: none; + border-color: var(--color-primary); +} + +.reg-results-count { + font-size: 0.85rem; + color: var(--color-text-muted); + margin: 0 0 1rem; +} + +/* --- Plugin list --- */ + +.reg-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.reg-card { + padding: 1.25rem 1.5rem; + border: 1px solid var(--color-border); + border-radius: 12px; + background: var(--color-bg); + transition: + border-color 0.15s, + box-shadow 0.15s; +} + +.reg-card:hover { + border-color: var(--color-primary); + box-shadow: 0 4px 18px var(--color-shadow-accent); +} + +.reg-card-head { + display: flex; + align-items: center; + gap: 0.6rem; + flex-wrap: wrap; + margin-bottom: 0.4rem; +} + +.reg-card-name { + font-size: 1.15rem; + font-weight: 700; + margin: 0; + font-family: var(--font-mono); +} + +.reg-card-name a { + color: var(--color-primary-dark); + text-decoration: none; +} + +.reg-card-name a:hover { + color: var(--color-primary); + text-decoration: underline; +} + +/* Bare green-circle check icon (no pill, no label) — the upstream registry's + single-glyph "installable" indicator. Hovering shows the tooltip via title. */ +.reg-installable { + display: inline-flex; + align-items: center; + line-height: 0; + cursor: help; +} + +.reg-installable-icon { + width: 22px; + height: 22px; + vertical-align: middle; + /* Drives the circle's `fill="currentColor"` via the design token. */ + color: var(--color-accent-green); + transition: transform 0.15s; +} + +.reg-installable:hover .reg-installable-icon { + transform: scale(1.08); +} + +/* Small inline AiiDA logo inside a shields.io-style label. */ +.reg-badge-logo { + width: 15px; + height: 15px; + vertical-align: middle; + margin-right: 0.32rem; + flex-shrink: 0; +} + +.reg-card-badges { + display: flex; + gap: 0.4rem; + flex-wrap: wrap; + margin: 0.4rem 0 0.6rem; +} + +.reg-card-desc { + margin: 0.4rem 0 0.6rem; + font-size: 0.93rem; + color: var(--color-text-secondary); + line-height: 1.5; +} + +.reg-card-links { + display: flex; + gap: 1rem; + font-size: 0.82rem; + color: var(--color-text-muted); +} + +/* Source Code / Documentation / Plugin details list under each card, + mirroring the upstream `.plugin-info` arrow bullets. */ +.reg-plugin-info { + list-style: none; + margin: 0.35rem 0 0.5rem; + padding: 0; + font-size: 0.92rem; +} + +.reg-plugin-info li { + padding-left: 1em; + text-indent: -1em; + line-height: 1.6; +} + +.reg-plugin-info li::before { + content: "→"; + padding-right: 0.35rem; + color: var(--color-text-muted); +} + +.reg-plugin-info a { + color: var(--color-primary); + text-decoration: none; +} + +.reg-plugin-info a:hover { + text-decoration: underline; +} + +.reg-card-links span { + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.reg-card-summary { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: 0.7rem; +} + +/* --- Empty state --- */ + +.reg-empty { + text-align: center; + padding: 3rem 1rem; + color: var(--color-text-muted); +} + +.reg-empty a { + color: var(--color-primary); + text-decoration: none; +} + +.reg-empty a:hover { + text-decoration: underline; +} + +/* --- Detail page --- */ + +.reg-detail { + max-width: 1100px; + margin: 0 auto; + padding: 0 2rem 4rem; + display: grid; + grid-template-columns: minmax(0, 1fr) 260px; + gap: 2.5rem; +} + +@media (max-width: 900px) { + .reg-detail { + grid-template-columns: 1fr; + } +} + +.reg-detail-main { + min-width: 0; +} + +.reg-back { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.88rem; + color: var(--color-text-muted); + text-decoration: none; + margin: 1.5rem 0 1rem; +} + +.reg-back:hover { + color: var(--color-primary); +} + +.reg-detail h1 { + font-size: 2rem; + font-weight: 800; + margin: 0 0 0.4rem; + font-family: var(--font-mono); + word-break: break-word; +} + +.reg-detail h1 a { + color: var(--color-primary-dark); + text-decoration: none; +} + +.reg-detail h1 a:hover { + text-decoration: underline; +} + +.reg-detail h2 { + font-size: 1.3rem; + font-weight: 700; + margin: 2rem 0 1rem; + padding-bottom: 0.4rem; + border-bottom: 1px solid var(--color-border-light); +} + +.reg-detail h3 { + font-size: 1.05rem; + font-weight: 700; + margin: 1.5rem 0 0.6rem; +} + +.reg-meta { + display: flex; + flex-direction: column; + gap: 0.4rem; + margin: 0.5rem 0 1rem; + font-size: 0.95rem; +} + +.reg-meta-row { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + align-items: baseline; +} + +.reg-meta-key { + font-weight: 600; + color: var(--color-text-secondary); + min-width: 130px; +} + +.reg-meta-val { + color: var(--color-text); +} + +.reg-meta-val code, +.reg-detail code { + font-family: var(--font-mono); + font-size: 0.88em; + background: var(--color-bg-alt); + border: 1px solid var(--color-border-light); + border-radius: 4px; + padding: 0.1rem 0.35rem; +} + +.reg-meta-val a { + color: var(--color-primary); + text-decoration: none; +} + +.reg-meta-val a:hover { + text-decoration: underline; +} + +/* --- Alert boxes (warnings / errors / success) --- */ + +.reg-alert { + display: flex; + gap: 0.7rem; + padding: 0.75rem 1rem; + border-radius: 8px; + margin: 0.5rem 0; + font-size: 0.9rem; + line-height: 1.5; +} + +.reg-alert > span { + min-width: 0; + overflow-wrap: anywhere; +} + +.reg-alert a { + color: inherit; + font-weight: 600; + text-decoration: underline; +} + +/* All four alert variants share the same shape: tinted background, tinted + border, and a text colour designed for legibility on that tint. The tokens + below auto-swap between light and dark via global.css. */ +.reg-alert--success { + background: var(--color-bg-tinted-green); + border: 1px solid var(--color-border-accent-green); + color: var(--color-text-on-tint-green); +} + +.reg-alert--warning { + background: var(--color-bg-tinted-orange); + border: 1px solid var(--color-border-accent-orange); + color: var(--color-text-on-tint-orange); +} + +.reg-alert--error { + background: var(--color-bg-tinted-red); + border: 1px solid var(--color-border-accent-red); + color: var(--color-text-on-tint-red); +} + +.reg-alert--info { + background: var(--color-bg-tinted); + border: 1px solid var(--color-border-accent); + color: var(--color-primary-darker); +} + +[data-theme="dark"] .reg-alert--info { + color: var(--color-primary-light); +} + +.reg-alert-icon { + flex-shrink: 0; + width: 18px; + height: 18px; + margin-top: 1px; +} + +/* --- Entry points --- */ + +.reg-ep-group { + margin: 1.25rem 0; + border: 1px solid var(--color-border); + border-radius: 10px; + background: var(--color-bg); +} + +.reg-ep-group-head { + padding: 0.7rem 1rem; + background: var(--color-bg-alt); + border-bottom: 1px solid var(--color-border); + border-radius: 10px 10px 0 0; + font-weight: 600; + font-size: 0.95rem; + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 0.5rem; + flex-wrap: wrap; +} + +.reg-ep-group-raw { + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--color-text-muted); + font-weight: 400; +} + +.reg-ep-list { + list-style: none; + margin: 0; + padding: 0; +} + +.reg-ep-item { + padding: 0.7rem 1rem; + border-top: 1px solid var(--color-border-light); +} + +.reg-ep-item:first-child { + border-top: none; +} + +.reg-ep-name { + font-family: var(--font-mono); + font-size: 0.92rem; + font-weight: 600; + color: var(--color-primary-dark); + margin: 0 0 0.3rem; + overflow-wrap: anywhere; +} + +.reg-ep-class { + font-family: var(--font-mono); + font-size: 0.82rem; + color: var(--color-text-muted); + overflow-wrap: anywhere; +} + +.reg-ep-table { + width: 100%; + border-collapse: collapse; + margin: 0.5rem 0; + font-size: 0.85rem; + /* Allow horizontal scroll on narrow viewports rather than truncating long valid_types cells. */ + display: block; + overflow-x: auto; +} + +.reg-ep-table th, +.reg-ep-table td { + padding: 0.4rem 0.6rem; + border: 1px solid var(--color-border-light); + text-align: left; + vertical-align: top; +} + +.reg-ep-table th { + background: var(--color-bg-alt); + font-weight: 600; +} + +.reg-ep-table code { + font-size: 0.82rem; +} + +/* --- Sidebar (TOC) --- */ + +.reg-sidebar { + position: sticky; + top: 6rem; + align-self: start; + font-size: 0.88rem; + border-left: 1px solid var(--color-border-light); + padding-left: 1.25rem; + max-height: calc(100vh - 8rem); + overflow-y: auto; +} + +.reg-sidebar-title { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted); + margin: 0 0 0.6rem; + font-weight: 600; +} + +.reg-sidebar-list { + list-style: none; + padding: 0; + margin: 0; +} + +.reg-sidebar-list li { + margin: 0.2rem 0; +} + +.reg-sidebar-list a { + display: block; + padding: 0.2rem 0.4rem; + color: var(--color-text-secondary); + text-decoration: none; + border-left: 2px solid transparent; + border-radius: 0 4px 4px 0; +} + +.reg-sidebar-list a:hover { + color: var(--color-primary); + background: var(--color-bg-alt); + border-left-color: var(--color-primary); +} + +.reg-sidebar-list a.reg-sidebar-sub { + padding-left: 1rem; + font-size: 0.83rem; +} + +@media (max-width: 900px) { + .reg-sidebar { + display: none; + } +} + +@media (max-width: 600px) { + .reg-page, + .reg-detail { + padding: 0 1rem 3rem; + } + .reg-toolbar { + flex-direction: column; + align-items: stretch; + } + .reg-sort { + justify-content: flex-end; + } + .reg-detail h1 { + font-size: 1.5rem; + } + + /* Stack entry-point tables into per-row cards so the rightmost column + (Description) isn't pushed off-screen by horizontal scroll. */ + .reg-ep-table, + .reg-ep-table tbody, + .reg-ep-table tr, + .reg-ep-table td { + display: block; + overflow-x: visible; + } + .reg-ep-table thead { + display: none; + } + .reg-ep-table tr { + margin: 0.5rem 0; + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border-light); + border-radius: 6px; + background: var(--color-bg-alt); + } + .reg-ep-table td { + padding: 0.2rem 0; + border: none; + display: grid; + grid-template-columns: 6.5rem 1fr; + gap: 0.5rem; + align-items: baseline; + } + .reg-ep-table td::before { + content: attr(data-label); + font-weight: 600; + font-size: 0.78rem; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.03em; + } + .reg-ep-table td:first-child { + display: block; + padding: 0.1rem 0 0.45rem; + margin-bottom: 0.4rem; + border-bottom: 1px solid var(--color-border-light); + font-weight: 600; + } + .reg-ep-table td:first-child::before { + display: none; + } + .reg-ep-table td code { + word-break: break-word; + } +}