diff --git a/README.md b/README.md index 3b2ebf5..a0eb48e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,19 @@ The website of [TissUUmaps](https://github.com/TissUUmaps/TissUUmaps), a GPU-acc The website hosts the TissUUmaps application and its documentation, both versioned by MAJOR.MINOR line. The documentation is written in the TissUUmaps repository; changes to it go there. This repository holds the rest of the website. +## Editing the website + +Most pages are Markdown files that can be edited on GitHub in the browser, through their "Edit this page" link. + +| Page | File | +| --------------- | -------------------------------------------------------------------------------- | +| Gallery | one folder per project in [`gallery/`](gallery/README.md) | +| Getting started | [`src/pages/getting-started.mdx`](src/pages/getting-started.mdx) | +| About | [`src/pages/about.mdx`](src/pages/about.mdx) | +| Documentation | `docs/` in the [TissUUmaps](https://github.com/TissUUmaps/TissUUmaps) repository | + +The Markdown pages can use a few components for their layout, such as ``, ``, `` or ``, without an import (see [`src/components/markdown`](src/components/markdown/index.ts)). Image paths start with `/`, from `static/`. The landing page is [`src/pages/index.tsx`](src/pages/index.tsx). + ## Releases On every application release, the TissUUmaps release workflow pushes the released docs to `releases//`: diff --git a/docusaurus.config.ts b/docusaurus.config.ts index b049df8..88b71f9 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -3,14 +3,10 @@ import type { Config } from "@docusaurus/types"; import { themes as prismThemes } from "prism-react-renderer"; import semver from "semver"; -import { docsPath, loadVersions } from "./tissuumaps-docs"; +import { docsPath, loadTypedocSidebar, loadVersions } from "./tissuumaps-docs"; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) -// The versioned TissUUmaps app is served next to the website as static files, -// so link to it by pathname instead of as a Docusaurus route -const appUrl = "pathname:///app/"; - const versions = loadVersions(); if (versions.length === 0 && !docsPath) { throw new Error( @@ -26,6 +22,11 @@ const config: Config = { tagline: "Spatial Biology Visualization", favicon: "img/favicon.ico", + // Lato, the SciLifeLab typeface + stylesheets: [ + "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,300;0,400;0,700;1,400&display=swap", + ], + // Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future future: { v4: true, // Improve compatibility with the upcoming Docusaurus v4 @@ -59,6 +60,26 @@ const config: Config = { docs: { path: docsPath, sidebarPath: "./sidebars.ts", + sidebarItemsGenerator: async ({ + defaultSidebarItemsGenerator, + ...args + }) => { + const items = await defaultSidebarItemsGenerator(args); + return items.map((item) => + item.type === "category" && + item.link?.type === "doc" && + item.link.id === "api/index" + ? { + ...item, + label: "API", + link: { type: "generated-index", slug: "/category/api" }, + items: loadTypedocSidebar( + args.version.contentPath, + ) as typeof items, + } + : item, + ); + }, includeCurrentVersion: docsPath !== undefined, lastVersion: docsPath ? "current" : lastVersion?.line, editUrl: ({ docPath }) => @@ -66,7 +87,32 @@ const config: Config = { ? undefined : `https://github.com/TissUUmaps/TissUUmaps/tree/main/docs/${docPath}`, }, - blog: false, + // Markdown pages in src/pages get an "Edit this page" link + pages: { + editUrl: + "https://github.com/TissUUmaps/tissuumaps.github.io/edit/main/", + }, + // The gallery: one Markdown file per project in gallery/, shown as + // cards by our own list page (see gallery/README.md) + blog: { + path: "gallery", + routeBasePath: "gallery", + blogTitle: "Gallery", + blogDescription: + "Example projects. Each one opens directly in TissUUmaps, in your browser.", + blogListComponent: "@site/src/components/pages/GalleryListPage", + blogTagsPostsComponent: "@site/src/components/pages/GalleryListPage", + blogPostComponent: "@site/src/components/pages/GalleryProjectPage", + exclude: ["**/_*/**", "**/_*.md", "README.md"], + postsPerPage: "ALL", + blogSidebarCount: 0, + showReadingTime: false, + archiveBasePath: null, + feedOptions: { type: null }, + onUntruncatedBlogPosts: "ignore", + editUrl: + "https://github.com/TissUUmaps/tissuumaps.github.io/edit/main/", + }, theme: { customCss: "./src/css/custom.css", }, @@ -78,6 +124,14 @@ const config: Config = { colorMode: { respectPrefersColorScheme: true, }, + // Points TissUUmaps 3 users to the legacy website while 4 is in beta + announcementBar: { + id: "tissuumaps3-legacy", + content: + 'TissUUmaps 4 is in beta. Looking for TissUUmaps 3? Go to the TissUUmaps 3 website', + backgroundColor: "var(--scilifelab-lime)", + textColor: "var(--scilifelab-dark-gray)", + }, navbar: { title: "TissUUmaps", logo: { @@ -85,6 +139,11 @@ const config: Config = { src: "img/logo.svg", }, items: [ + { + to: "/getting-started", + label: "Getting started", + position: "left", + }, { type: "docSidebar", sidebarId: "docsSidebar", @@ -92,67 +151,27 @@ const config: Config = { label: "Documentation", }, { - type: "docSidebar", - sidebarId: "apiSidebar", + to: "/gallery", + label: "Gallery", position: "left", - label: "Packages", }, { - type: "docsVersionDropdown", - position: "right", + to: "/about", + label: "About", + position: "left", }, { - href: "https://github.com/TissUUmaps/TissUUmaps", - label: "GitHub", + type: "docsVersionDropdown", position: "right", }, { - href: appUrl, - label: "App", + type: "custom-partnerLogos", position: "right", }, ], }, footer: { - style: "dark", - links: [ - { - title: "Docs", - items: [ - { - label: "Documentation", - to: "/docs/", - }, - { - label: "Packages", - to: "/docs/api", - }, - ], - }, - { - title: "Community", - items: [ - { - label: "SciLifeLab BioImage Informatics Unit", - href: "https://www.scilifelab.se/units/bioimage-informatics/", - }, - ], - }, - { - title: "More", - items: [ - { - label: "GitHub", - href: "https://github.com/TissUUmaps/TissUUmaps", - }, - { - label: "App", - href: appUrl, - }, - ], - }, - ], - copyright: `Copyright © ${new Date().getFullYear()} TissUUmaps Core Team. Built with Docusaurus.`, + copyright: `Copyright © ${new Date().getFullYear()} TissUUmaps Core Team.`, }, prism: { theme: prismThemes.github, diff --git a/gallery/2026-10-09-luca-7color/index.md b/gallery/2026-10-09-luca-7color/index.md new file mode 100644 index 0000000..9a60b17 --- /dev/null +++ b/gallery/2026-10-09-luca-7color/index.md @@ -0,0 +1,12 @@ +--- +title: LuCa 7-color multiplex scan +description: Multiplex immunofluorescence whole-slide scan (DAPI, FITC, CY3, Texas Red, CY5), opened directly from the 2 GB QPTIFF file. +image: ./thumbnail.jpg +project: https://tissuumaps4.serve.scilifelab.se/examples/luca-7color_qptiff.tm4 +tags: [Multiplex IF, QPTIFF] +data_provider: PerkinElmer (now Akoya), via the OME sample images +data_url: https://downloads.openmicroscopy.org/images/Vectra-QPTIFF/perkinelmer/PKI_scans/LuCa-7color_Scan1.qptiff +data_license: CC BY 4.0, © PerkinElmer +--- + +A whole-slide scan from a PerkinElmer Vectra multiplex imaging system, read directly from the QPTIFF file without conversion. diff --git a/gallery/2026-10-09-luca-7color/thumbnail.jpg b/gallery/2026-10-09-luca-7color/thumbnail.jpg new file mode 100644 index 0000000..32b9af0 Binary files /dev/null and b/gallery/2026-10-09-luca-7color/thumbnail.jpg differ diff --git a/gallery/2026-10-10-xenium-lung/index.md b/gallery/2026-10-10-xenium-lung/index.md new file mode 100644 index 0000000..c792ca0 --- /dev/null +++ b/gallery/2026-10-10-xenium-lung/index.md @@ -0,0 +1,12 @@ +--- +title: Human lung cancer (Xenium) +description: H&E and morphology images, cell and nucleus segmentation, 12.2M transcripts and cell boundaries, read from a SpatialData store. +image: ./thumbnail.jpg +project: https://tissuumaps4.serve.scilifelab.se/examples/xenium-lung_ome-zarr_table-per-element.tm4 +tags: [Xenium, SpatialData, OME-Zarr, Parquet] +data_provider: 10x Genomics +data_url: https://www.10xgenomics.com/datasets/preview-data-ffpe-human-lung-cancer-with-xenium-multimodal-cell-segmentation-1-standard +data_license: CC BY 4.0 +--- + +A Xenium 2.0 section of human lung cancer, stored as a SpatialData store with one table per element. diff --git a/gallery/2026-10-10-xenium-lung/thumbnail.jpg b/gallery/2026-10-10-xenium-lung/thumbnail.jpg new file mode 100644 index 0000000..4fa05f9 Binary files /dev/null and b/gallery/2026-10-10-xenium-lung/thumbnail.jpg differ diff --git a/gallery/README.md b/gallery/README.md new file mode 100644 index 0000000..c66441f --- /dev/null +++ b/gallery/README.md @@ -0,0 +1,26 @@ +# Gallery + +Every folder here is one project of the Gallery page of the website. Folders starting with `_` are not shown. + +## Add a project + +1. Make a screenshot of the project in TissUUmaps, cropped to 16:10 (for example 800 × 500 pixels), without the side panel. +2. On GitHub, open this folder and choose **Add file > Create new file**. Name it `YYYY-MM-DD-my-project/index.md`: today's date, then a short name with lowercase letters and dashes, for example `2026-10-12-mouse-brain/index.md`. The newest projects are shown first. +3. Copy the contents of [`_template/index.md`](_template/index.md) into it and fill in the fields (see below). +4. Upload the screenshot into the new folder as `thumbnail.jpg` (**Add file > Upload files**). +5. Commit to a new branch and open a pull request. Its checks build the website; a missing field fails them with a message that names the file and the field. Once the pull request is merged, the website is updated within a few minutes. + +## Fields + +| Field | What to write | +| --------------- | ------------------------------------------------------------------ | +| `title` | Short name of the project | +| `description` | One or two sentences for the card | +| `image` | `./thumbnail.jpg` | +| `project` | Link to the project file (`.tm4`); the card opens it in TissUUmaps | +| `tags` | Technologies and formats, in square brackets, separated by commas | +| `data_provider` | Who made the data available | +| `data_url` | Where the data comes from | +| `data_license` | The license of the data, for example `CC BY 4.0` | + +The text below the fields is optional. It appears on the project's own page and can use [Markdown](https://www.markdownguide.org/basic-syntax/): paragraphs, **bold**, links, lists and images. diff --git a/gallery/_template/index.md b/gallery/_template/index.md new file mode 100644 index 0000000..c1216e8 --- /dev/null +++ b/gallery/_template/index.md @@ -0,0 +1,12 @@ +--- +title: Short name of the project +description: One or two sentences shown on the gallery card. +image: ./thumbnail.jpg +project: https://example.org/path/to/project.tm4 +tags: [Technology, File format] +data_provider: Who made the data available +data_url: https://example.org/where/the/data/comes/from +data_license: CC BY 4.0 +--- + +Optional longer text, shown on the project's own page. Remove this paragraph if there is nothing more to say. diff --git a/scripts/prepare.mts b/scripts/prepare.mts index f17a3c3..309c674 100644 --- a/scripts/prepare.mts +++ b/scripts/prepare.mts @@ -119,7 +119,8 @@ export function readReleases(releasesDir: string): string[] { * * @param options.siteDir - The site directory * @param options.versions - The versions to prepare - * @param options.latestVersion - The version `app/` redirects to + * @param options.latestVersion - The version `app/` redirects to, keeping the + * query (e.g. `?project=…`) and the fragment * @param options.customHtml - The HTML to insert into every app page * @param options.assetsDir - A directory holding the app assets * (`tissuumaps-.zip`); downloaded from the releases if omitted @@ -177,6 +178,9 @@ export function prepareApps(options: { + TissUUmaps diff --git a/scripts/prepare.test.mts b/scripts/prepare.test.mts index 9910fbb..220dea1 100644 --- a/scripts/prepare.test.mts +++ b/scripts/prepare.test.mts @@ -138,13 +138,16 @@ void test("prepares the selected versions' docs and apps", () => { assert.ok(!existsSync(join(siteDir, "versioned_docs/version-4.0"))); const sidebars = JSON.parse( read("versioned_sidebars/version-4.1-sidebars.json"), - ) as Record; - assert.deepEqual(sidebars.docsSidebar, [ - { type: "autogenerated", dirName: "." }, - ]); - assert.deepEqual(sidebars.apiSidebar?.[0].items, [ - { type: "doc", id: "api/core" }, - ]); + ) as unknown; + assert.deepEqual(sidebars, { + docsSidebar: [{ type: "autogenerated", dirName: "." }], + }); + // The version's typedoc sidebar, read when building its docs sidebar + assert.ok( + existsSync( + join(siteDir, "versioned_docs/version-4.1/api/typedoc-sidebar.cjs"), + ), + ); // Apps with the custom HTML, and app/ redirecting to the latest stable line assert.equal( read("static/app/4.1/index.html"), @@ -153,6 +156,10 @@ void test("prepares the selected versions' docs and apps", () => { assert.ok(existsSync(join(siteDir, "static/app/4.3/index.html"))); assert.ok(!existsSync(join(siteDir, "static/app/4.0"))); assert.match(read("static/app/index.html"), /url=4\.1\/"/); + assert.match( + read("static/app/index.html"), + /location\.replace\("4\.1\/" \+ location\.search \+ location\.hash\)/, + ); } finally { rmSync(dir, { recursive: true, force: true }); } diff --git a/sidebars.ts b/sidebars.ts index 3be83c0..b5f7f3d 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -1,46 +1,16 @@ import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; -import { existsSync } from "node:fs"; -import { createRequire } from "node:module"; -import { join } from "node:path"; - -import { docsPath } from "./tissuumaps-docs"; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) /** - * Creates the sidebars of the current version, which `docusaurus docs:version` - * also saves for every released version (see scripts/prepare.mts) - * - * @param docsPath - The docs folder, with the API docs generated by typedoc - * @returns The sidebars + * The sidebars of the current version, which `docusaurus docs:version` also + * saves for every released version (see scripts/prepare.mts); the API docs + * generated by typedoc are a category of the docs sidebar (see + * `sidebarItemsGenerator` in docusaurus.config.ts) */ -function createSidebars(docsPath: string): SidebarsConfig { - const typedocSidebarFile = join(docsPath, "api", "typedoc-sidebar.cjs"); - if (!existsSync(typedocSidebarFile)) { - throw new Error( - `${typedocSidebarFile} not found, generate the API docs in TissUUmaps first`, - ); - } - const require = createRequire(import.meta.url); - return { - // By default, Docusaurus generates a sidebar from the docs folder structure - docsSidebar: [{ type: "autogenerated", dirName: "." }], - - // The API reference has its own sidebar, generated by typedoc - apiSidebar: [ - { - type: "category", - label: "API", - link: { type: "doc", id: "api/index" }, - items: require(typedocSidebarFile) as Extract< - SidebarsConfig[string], - unknown[] - >, - }, - ], - }; -} - -const sidebars: SidebarsConfig = docsPath ? createSidebars(docsPath) : {}; +const sidebars: SidebarsConfig = { + // By default, Docusaurus generates a sidebar from the docs folder structure + docsSidebar: [{ type: "autogenerated", dirName: "." }], +}; export default sidebars; diff --git a/src/components/markdown/Box.module.css b/src/components/markdown/Box.module.css new file mode 100644 index 0000000..b790d34 --- /dev/null +++ b/src/components/markdown/Box.module.css @@ -0,0 +1,31 @@ +.columns { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + margin-bottom: 1rem; +} + +.box { + padding: 1.25rem 1.5rem 0.25rem; + border-radius: 0.75rem; + background: var(--tm-band-background); +} + +.box h3 { + margin-bottom: 0.5rem; +} + +.note { + display: inline-block; + padding: 0.2rem 0.75rem; + border-radius: 0.5rem; + background: color-mix(in srgb, var(--scilifelab-lime) 25%, transparent); + font-size: 0.85rem; + font-weight: 700; +} + +@media screen and (max-width: 600px) { + .columns { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/src/components/markdown/Box.tsx b/src/components/markdown/Box.tsx new file mode 100644 index 0000000..f247826 --- /dev/null +++ b/src/components/markdown/Box.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from "react"; + +import styles from "./Box.module.css"; + +/** Boxes side by side, two per row */ +export function Columns({ children }: { children: ReactNode }): ReactNode { + return
{children}
; +} + +/** A box with a title and an optional highlighted note */ +export function Box({ + title, + note, + children, +}: { + title: string; + note?: string; + children: ReactNode; +}): ReactNode { + return ( +
+

{title}

+ {note &&

{note}

} + {children} +
+ ); +} diff --git a/src/components/markdown/Button.module.css b/src/components/markdown/Button.module.css new file mode 100644 index 0000000..0f75ca9 --- /dev/null +++ b/src/components/markdown/Button.module.css @@ -0,0 +1,30 @@ +.button { + display: inline-flex; + align-items: center; + gap: 1rem; + padding: 0.45rem 1.25rem; + border-radius: 999px; + background: var(--scilifelab-teal); + color: #fff; + font-size: 0.85rem; + letter-spacing: 0.1em; + transition: background 0.15s; +} + +.button::after { + content: "⟶"; +} + +.button:hover { + background: var(--scilifelab-grape); + color: #fff; + text-decoration: none; +} + +.large { + gap: 1.5rem; + padding: 1.1rem 2.75rem; + box-shadow: 0 0.75rem 1.5rem + color-mix(in srgb, var(--scilifelab-teal) 25%, transparent); + font-size: 1.35rem; +} diff --git a/src/components/markdown/Button.tsx b/src/components/markdown/Button.tsx new file mode 100644 index 0000000..7b1219a --- /dev/null +++ b/src/components/markdown/Button.tsx @@ -0,0 +1,29 @@ +import Link from "@docusaurus/Link"; +import clsx from "clsx"; +import type { ReactNode } from "react"; + +import styles from "./Button.module.css"; + +/** + * A rounded button linking to a page of this site or to another site, with + * spaced letters and a long arrow; `size="large"` for the main action of a + * page. Also used by the landing page and the gallery. + */ +export function Button({ + to, + size, + children, +}: { + to: string; + size?: "large"; + children: ReactNode; +}): ReactNode { + return ( + + {children} + + ); +} diff --git a/src/components/markdown/Figure.module.css b/src/components/markdown/Figure.module.css new file mode 100644 index 0000000..1803127 --- /dev/null +++ b/src/components/markdown/Figure.module.css @@ -0,0 +1,27 @@ +.figure { + float: right; + width: 40%; + margin: 0 0 1.5rem 2.5rem; +} + +.figure img { + display: block; + width: 100%; + border-radius: 0.75rem; + box-shadow: 0 1rem 2rem + color-mix(in srgb, var(--scilifelab-teal) 20%, transparent); +} + +.figure figcaption { + margin-top: 0.5rem; + color: var(--ifm-color-emphasis-700); + font-size: 0.85rem; +} + +@media screen and (max-width: 996px) { + .figure { + float: none; + width: 100%; + margin: 0 0 1.5rem; + } +} diff --git a/src/components/markdown/Figure.tsx b/src/components/markdown/Figure.tsx new file mode 100644 index 0000000..1d31286 --- /dev/null +++ b/src/components/markdown/Figure.tsx @@ -0,0 +1,22 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import type { ReactNode } from "react"; + +import styles from "./Figure.module.css"; + +/** An image on the right of the text, with a caption */ +export function Figure({ + src, + alt, + caption, +}: { + src: string; + alt: string; + caption?: string; +}): ReactNode { + return ( +
+ {alt} + {caption &&
{caption}
} +
+ ); +} diff --git a/src/components/markdown/Logo.module.css b/src/components/markdown/Logo.module.css new file mode 100644 index 0000000..785ab09 --- /dev/null +++ b/src/components/markdown/Logo.module.css @@ -0,0 +1,18 @@ +.logos { + display: flex; + flex-wrap: wrap; + gap: 2.5rem; + align-items: center; + margin: 2rem 0; + padding: 1.5rem 2rem; + border-radius: 0.75rem; + background: #fff; +} + +.logos img { + height: 4.5rem; +} + +.logos .small { + height: 2.75rem; +} diff --git a/src/components/markdown/Logo.tsx b/src/components/markdown/Logo.tsx new file mode 100644 index 0000000..96b00dd --- /dev/null +++ b/src/components/markdown/Logo.tsx @@ -0,0 +1,29 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import clsx from "clsx"; +import type { ReactNode } from "react"; + +import styles from "./Logo.module.css"; + +/** A row of logos on a white background */ +export function Logos({ children }: { children: ReactNode }): ReactNode { + return
{children}
; +} + +/** A logo; `size="small"` for wide logos such as a word mark */ +export function Logo({ + src, + alt, + size, +}: { + src: string; + alt: string; + size?: "small"; +}): ReactNode { + return ( + {alt} + ); +} diff --git a/src/components/markdown/Publication.module.css b/src/components/markdown/Publication.module.css new file mode 100644 index 0000000..c3c1929 --- /dev/null +++ b/src/components/markdown/Publication.module.css @@ -0,0 +1,51 @@ +/* Full-width rows with the figure on the left */ +.publications { + display: grid; + gap: 1.25rem; + margin-bottom: 2rem; +} + +.publication { + display: grid; + grid-template-columns: 11rem minmax(0, 1fr); + gap: 1.5rem; + align-items: center; + padding: 1rem; + border-radius: 0.75rem; + background: var(--ifm-background-surface-color); + box-shadow: 0 0.25rem 1rem + color-mix(in srgb, var(--scilifelab-teal) 12%, transparent); +} + +.publication img { + width: 100%; + aspect-ratio: 4 / 3; + border-radius: 0.5rem; + background: #fff; + object-fit: contain; +} + +.publication a { + font-size: 1.1rem; + font-weight: 700; +} + +.publication p { + margin: 0.4rem 0 0; + font-size: 0.9rem; +} + +.authors { + color: var(--ifm-color-emphasis-700); +} + +.journal { + font-style: italic; +} + +@media screen and (max-width: 600px) { + .publication { + grid-template-columns: 6rem minmax(0, 1fr); + gap: 1rem; + } +} diff --git a/src/components/markdown/Publication.tsx b/src/components/markdown/Publication.tsx new file mode 100644 index 0000000..78cf1ca --- /dev/null +++ b/src/components/markdown/Publication.tsx @@ -0,0 +1,36 @@ +import Link from "@docusaurus/Link"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import type { ReactNode } from "react"; + +import styles from "./Publication.module.css"; + +/** A list of publications */ +export function Publications({ children }: { children: ReactNode }): ReactNode { + return
{children}
; +} + +/** A publication with a figure */ +export function Publication({ + title, + authors, + journal, + link, + image, +}: { + title: string; + authors: string; + journal: string; + link: string; + image: string; +}): ReactNode { + return ( +
+ +
+ {title} +

{authors}

+

{journal}

+
+
+ ); +} diff --git a/src/components/markdown/Steps.module.css b/src/components/markdown/Steps.module.css new file mode 100644 index 0000000..147576a --- /dev/null +++ b/src/components/markdown/Steps.module.css @@ -0,0 +1,42 @@ +/* A lime number in front of every step heading */ +.steps { + padding-left: 4.5rem; + counter-reset: step; +} + +.steps h2 { + position: relative; + margin-top: 3rem; + counter-increment: step; +} + +.steps h2::before { + content: counter(step); + position: absolute; + top: 50%; + left: -4.5rem; + display: flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + border-radius: 50%; + background: var(--scilifelab-lime); + color: var(--scilifelab-dark-gray); + font-size: 1.35rem; + transform: translateY(-50%); +} + +@media screen and (max-width: 600px) { + .steps { + padding-left: 0; + } + + .steps h2 { + padding-left: 4rem; + } + + .steps h2::before { + left: 0; + } +} diff --git a/src/components/markdown/Steps.tsx b/src/components/markdown/Steps.tsx new file mode 100644 index 0000000..534cc03 --- /dev/null +++ b/src/components/markdown/Steps.tsx @@ -0,0 +1,8 @@ +import type { ReactNode } from "react"; + +import styles from "./Steps.module.css"; + +/** Numbered steps: every `##` heading inside starts a new step */ +export function Steps({ children }: { children: ReactNode }): ReactNode { + return
{children}
; +} diff --git a/src/components/markdown/index.ts b/src/components/markdown/index.ts new file mode 100644 index 0000000..0e3a3fc --- /dev/null +++ b/src/components/markdown/index.ts @@ -0,0 +1,8 @@ +// The components available in every Markdown (MDX) file without an import +// (see src/theme/MDXComponents.tsx); image paths start with "/", from static/ +export { Box, Columns } from "./Box"; +export { Button } from "./Button"; +export { Figure } from "./Figure"; +export { Logo, Logos } from "./Logo"; +export { Publication, Publications } from "./Publication"; +export { Steps } from "./Steps"; diff --git a/src/components/pages/GalleryListPage/ProjectCard.tsx b/src/components/pages/GalleryListPage/ProjectCard.tsx new file mode 100644 index 0000000..5083eac --- /dev/null +++ b/src/components/pages/GalleryListPage/ProjectCard.tsx @@ -0,0 +1,49 @@ +import Link from "@docusaurus/Link"; +import Heading from "@theme/Heading"; +import type { ReactNode } from "react"; + +import type { GalleryProject } from "../../../gallery"; +import { Button } from "../../markdown/Button"; +import styles from "./styles.module.css"; + +export function ProjectCard({ + project, +}: { + project: GalleryProject; +}): ReactNode { + return ( +
+ + + +
+ + {project.title} + +

{project.description}

+ +

+ Data: {project.dataProvider},{" "} + {project.dataLicense} +

+ +
+
+ ); +} + +export function ProjectTags({ + project, +}: { + project: GalleryProject; +}): ReactNode { + return ( +
    + {project.tags.map(({ label, permalink }) => ( +
  • + {label} +
  • + ))} +
+ ); +} diff --git a/src/components/pages/GalleryListPage/index.tsx b/src/components/pages/GalleryListPage/index.tsx new file mode 100644 index 0000000..46d5d0b --- /dev/null +++ b/src/components/pages/GalleryListPage/index.tsx @@ -0,0 +1,55 @@ +import Link from "@docusaurus/Link"; +import type { Props as ListProps } from "@theme/BlogListPage"; +import type { Props as TagProps } from "@theme/BlogTagsPostsPage"; +import Heading from "@theme/Heading"; +import Layout from "@theme/Layout"; +import clsx from "clsx"; +import type { ReactNode } from "react"; + +import { readGalleryProject } from "../../../gallery"; +import { ProjectCard } from "./ProjectCard"; +import styles from "./styles.module.css"; + +// The gallery is the blog plugin's content (see docusaurus.config.ts), listed +// as cards; the same page lists the projects with one tag +export default function GalleryListPage( + props: ListProps | TagProps, +): ReactNode { + const metadata = "tag" in props ? props.listMetadata : props.metadata; + const title = + "tag" in props ? `Projects with "${props.tag.label}"` : metadata.blogTitle; + const projects = props.items.map(({ content }) => + readGalleryProject(content), + ); + return ( + +
+
+
+ + {title} + +

+ {"tag" in props ? ( + All projects + ) : ( + metadata.blogDescription + )} +

+
+
+
+
+ {projects.map((project) => ( + + ))} +
+
+
+
+ ); +} diff --git a/src/components/pages/GalleryListPage/styles.module.css b/src/components/pages/GalleryListPage/styles.module.css new file mode 100644 index 0000000..a6e9c7d --- /dev/null +++ b/src/components/pages/GalleryListPage/styles.module.css @@ -0,0 +1,138 @@ +/* The project grid stretches down to the footer */ +.wrapper, +.main { + display: flex; + flex-direction: column; +} + +.main { + flex: 1; +} + +.header { + padding: 4rem 0 2.5rem; +} + +.title { + margin-bottom: 0.75rem; + color: var(--ifm-color-primary); + font-size: 3rem; +} + +.lead { + max-width: 45rem; + margin: 0; + font-size: 1.15rem; +} + +.band { + flex: 1; + padding: 3rem 0 4rem; + background: var(--tm-band-background); +} + +.container { + max-width: 1280px; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 2rem; +} + +/* Card: screenshot on top, the button at the bottom so all cards line up */ +.card { + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 0.75rem; + background: var(--ifm-background-surface-color); + box-shadow: 0 0.5rem 1.5rem + color-mix(in srgb, var(--scilifelab-teal) 12%, transparent); + transition: + transform 0.15s, + box-shadow 0.15s; +} + +.card:hover { + box-shadow: 0 1rem 2rem + color-mix(in srgb, var(--scilifelab-teal) 20%, transparent); + transform: translateY(-3px); +} + +.card img { + display: block; + width: 100%; + aspect-ratio: 16 / 10; + background: #000; + object-fit: cover; +} + +.body { + display: flex; + flex: 1; + flex-direction: column; + align-items: flex-start; + padding: 1.25rem 1.5rem 1.5rem; +} + +.body h2 { + margin-bottom: 0.5rem; + font-size: 1.3rem; +} + +.body h2 a { + color: inherit; +} + +.body p { + margin-bottom: 1rem; + font-size: 0.95rem; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin: 0 0 1rem; + padding: 0; + list-style: none; +} + +.tags a { + display: block; + padding: 0.1rem 0.65rem; + border-radius: 999px; + background: color-mix(in srgb, var(--scilifelab-lime) 25%, transparent); + color: var(--ifm-font-color-base); + font-size: 0.8rem; +} + +.tags a:hover { + background: color-mix(in srgb, var(--scilifelab-lime) 50%, transparent); + text-decoration: none; +} + +/* The source line takes the free space, which pushes the button down */ +.body .source { + flex: 1; + color: var(--ifm-color-emphasis-700); + font-size: 0.8rem; +} + +@media screen and (max-width: 996px) { + .header { + padding: 2.5rem 0 1.5rem; + } + + .grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media screen and (max-width: 600px) { + .grid { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/src/components/pages/GalleryProjectPage/index.tsx b/src/components/pages/GalleryProjectPage/index.tsx new file mode 100644 index 0000000..99a43a7 --- /dev/null +++ b/src/components/pages/GalleryProjectPage/index.tsx @@ -0,0 +1,56 @@ +import Link from "@docusaurus/Link"; +import type { Props } from "@theme/BlogPostPage"; +import Heading from "@theme/Heading"; +import Layout from "@theme/Layout"; +import MDXContent from "@theme/MDXContent"; +import type { ReactNode } from "react"; + +import { readGalleryProject } from "../../../gallery"; +import { Button } from "../../markdown/Button"; +import { ProjectTags } from "../GalleryListPage/ProjectCard"; +import styles from "./styles.module.css"; + +// The own page of a gallery project: its card details and the Markdown text +// below its front matter +export default function GalleryProjectPage({ content }: Props): ReactNode { + const project = readGalleryProject(content); + const ProjectContent = content; + return ( + +
+
+

+ ← Gallery +

+
+ + + +
+ + {project.title} + +

{project.description}

+ +

+ Data: {project.dataProvider} + , {project.dataLicense} +

+ +
+
+
+ + + +
+ {project.editUrl && ( +

+ Edit this project +

+ )} +
+
+
+ ); +} diff --git a/src/components/pages/GalleryProjectPage/styles.module.css b/src/components/pages/GalleryProjectPage/styles.module.css new file mode 100644 index 0000000..f2bb5c0 --- /dev/null +++ b/src/components/pages/GalleryProjectPage/styles.module.css @@ -0,0 +1,57 @@ +.main { + padding: 2rem 0 4rem; +} + +.main > :global(.container) { + max-width: 1280px; +} + +.grid { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); + gap: 3rem; + align-items: center; + margin: 1rem 0 2.5rem; +} + +.image { + display: block; + width: 100%; + aspect-ratio: 16 / 10; + border-radius: 0.75rem; + background: #000; + box-shadow: 0 1rem 2rem + color-mix(in srgb, var(--scilifelab-teal) 20%, transparent); + object-fit: cover; +} + +.title { + margin-bottom: 0.75rem; + color: var(--ifm-color-primary); + font-size: 2.5rem; +} + +.lead { + font-size: 1.15rem; +} + +.source { + color: var(--ifm-color-emphasis-700); + font-size: 0.9rem; +} + +.text { + max-width: 50rem; +} + +.edit { + margin-top: 2rem; + font-size: 0.9rem; +} + +@media screen and (max-width: 996px) { + .grid { + grid-template-columns: minmax(0, 1fr); + gap: 1.5rem; + } +} diff --git a/src/css/custom.css b/src/css/custom.css index 3859140..2c0fe8e 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -4,27 +4,98 @@ * work well for content-centric websites. */ -/* You can override the default Infima variables here. */ +@import "./scilifelab.css"; + +/* Infima's primary color and its shades, from the SciLifeLab colors */ :root { - --ifm-color-primary: #2e8555; - --ifm-color-primary-dark: #29784c; - --ifm-color-primary-darker: #277148; - --ifm-color-primary-darkest: #205d3b; - --ifm-color-primary-light: #33925d; - --ifm-color-primary-lighter: #359962; - --ifm-color-primary-lightest: #3cad6e; + --ifm-color-primary: var(--scilifelab-teal); + --ifm-color-primary-dark: color-mix( + in srgb, + var(--ifm-color-primary) 90%, + black + ); + --ifm-color-primary-darker: color-mix( + in srgb, + var(--ifm-color-primary) 85%, + black + ); + --ifm-color-primary-darkest: color-mix( + in srgb, + var(--ifm-color-primary) 70%, + black + ); + --ifm-color-primary-light: color-mix( + in srgb, + var(--ifm-color-primary) 90%, + white + ); + --ifm-color-primary-lighter: color-mix( + in srgb, + var(--ifm-color-primary) 85%, + white + ); + --ifm-color-primary-lightest: color-mix( + in srgb, + var(--ifm-color-primary) 70%, + white + ); + --ifm-font-family-base: + var(--scilifelab-font-family), system-ui, -apple-system, "Segoe UI", Roboto, + sans-serif; + --ifm-heading-font-weight: 700; + --ifm-heading-color: var(--scilifelab-dark-gray); + --ifm-navbar-height: 4.5rem; + --ifm-navbar-link-color: var(--scilifelab-teal); + --tm-band-background: #f2f2f2; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme="dark"] { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #21af90; - --ifm-color-primary-darker: #1fa588; - --ifm-color-primary-darkest: #1a8870; - --ifm-color-primary-light: #29d5b0; - --ifm-color-primary-lighter: #32d8b4; - --ifm-color-primary-lightest: #4fddbf; + --ifm-color-primary: var(--scilifelab-lime); + --ifm-heading-color: var(--ifm-font-color-base); + --ifm-navbar-link-color: var(--ifm-font-color-base); + --tm-band-background: #262829; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } + +.navbar { + box-shadow: none; + border-bottom: 1px solid var(--ifm-toc-border-color); +} + +.navbar__logo { + height: 2.5rem; +} + +.navbar__title { + font-size: 1.3rem; + font-weight: 700; +} + +/* Footer: a single slim copyright line */ +.footer { + --ifm-footer-padding-vertical: 0.6rem; + --ifm-footer-background-color: var(--scilifelab-teal); + --ifm-footer-color: rgb(255 255 255 / 80%); + font-size: 0.8rem; +} + +/* Markdown pages (src/pages/*.mdx): readable width, titles like the gallery */ +.mdx-page main.container { + max-width: 1280px; + padding-top: 2rem; + padding-bottom: 2rem; +} + +.mdx-page article h1 { + color: var(--ifm-color-primary); + font-size: 3rem; +} + +.mdx-page article h2 { + clear: both; + margin-top: 3rem; + font-size: 2rem; +} diff --git a/src/css/scilifelab.css b/src/css/scilifelab.css new file mode 100644 index 0000000..5c4c4b9 --- /dev/null +++ b/src/css/scilifelab.css @@ -0,0 +1,16 @@ +/** + * The SciLifeLab visual identity, as variables only; update it here when + * SciLifeLab updates it. Graphic Manual 2025, see + * https://www.scilifelab.se/community-pages/resources/visual-identity/ + * Lime is the main color, Grape only for small details ("color pop"). + */ +:root { + --scilifelab-lime: #a7c947; + --scilifelab-teal: #045c64; + --scilifelab-aqua: #4c979f; + --scilifelab-grape: #491f53; + --scilifelab-light-gray: #e5e5e5; + --scilifelab-medium-gray: #a6a6a6; + --scilifelab-dark-gray: #3f3f3f; + --scilifelab-font-family: "Lato"; +} diff --git a/src/gallery.ts b/src/gallery.ts new file mode 100644 index 0000000..60f6c4d --- /dev/null +++ b/src/gallery.ts @@ -0,0 +1,69 @@ +import type { Content } from "@theme/BlogPostPage"; + +/** + * The application the gallery projects open in: the latest release, served + * next to the website as static files (see scripts/prepare.mts), so linked + * by pathname instead of as a route + */ +const galleryAppUrl = "pathname:///app/"; + +/** A gallery project, read from the front matter of its Markdown file */ +export interface GalleryProject { + title: string; + description: string; + /** URL of the screenshot */ + image: string; + /** URL that opens the project in TissUUmaps */ + appUrl: string; + /** URL of the project's own page */ + pageUrl: string; + tags: { label: string; permalink: string }[]; + dataProvider: string; + dataUrl: string; + dataLicense: string; + editUrl: string | undefined; +} + +const requiredFields = [ + "title", + "description", + "image", + "project", + "data_provider", + "data_url", + "data_license", +] as const; + +/** + * Reads a gallery project from its blog post content + * + * @param content - The blog post content of the project + * @returns The project + * @throws Error if a required front matter field is missing, naming the file + * and the field, so that the build fails with a message editors understand + */ +export function readGalleryProject(content: Content): GalleryProject { + const { frontMatter, assets, metadata } = content; + const fields = frontMatter as Record; + for (const field of requiredFields) { + if (typeof fields[field] !== "string" || fields[field] === "") { + throw new Error( + `Gallery project ${metadata.source}: the field "${field}" is missing`, + ); + } + } + const field = (name: (typeof requiredFields)[number]) => + fields[name] as string; + return { + title: field("title"), + description: field("description"), + image: assets.image ?? field("image"), + appUrl: `${galleryAppUrl}?project=${encodeURIComponent(field("project"))}`, + pageUrl: metadata.permalink, + tags: metadata.tags.map(({ label, permalink }) => ({ label, permalink })), + dataProvider: field("data_provider"), + dataUrl: field("data_url"), + dataLicense: field("data_license"), + editUrl: metadata.editUrl, + }; +} diff --git a/src/pages/about.mdx b/src/pages/about.mdx new file mode 100644 index 0000000..cbee097 --- /dev/null +++ b/src/pages/about.mdx @@ -0,0 +1,76 @@ +--- +title: About +description: The history of TissUUmaps, its publications and funding +hide_table_of_contents: true +--- + +# About + +TissUUmaps is a community-driven, open-source project. Everyone is welcome to [contribute](https://github.com/TissUUmaps/TissUUmaps). + +The [SciLifeLab BioImage Informatics Unit (BIIF)](https://www.scilifelab.se/units/bioimage-informatics/), part of [SciLifeLab](https://www.scilifelab.se/) and [NBIS](https://nbis.se/), is a core contributor to the project. + +## History + +
+ +TissUUmaps goes back to 2011. Carolina Wählby, then at the Broad Institute, was contacted by Mats Nilsson at Stockholm University, who had invented a method to sequence mRNA directly in tissue sections and needed computational tools to decode the image data. This led to a joint [publication](https://www.nature.com/articles/nmeth.2563) on in situ sequencing in Nature Methods in 2013, and a collaboration that is still ongoing. + +In 2015, TissUUmaps became a research project of the Wählby lab at the [Department of Information Technology](https://www.it.uu.se/), Uppsala University. The first paper describing the tool followed in 2020, and TissUUmaps 3, published in 2023, extended it from in situ sequencing to large-scale spatial omics data in general. Along the way, the project also produced new methods for pre- and post-processing of in situ sequencing data. + +Over time, the [SciLifeLab BioImage Informatics Unit (BIIF)](https://www.scilifelab.se/units/bioimage-informatics/), originally founded by Carolina Wählby and now headed by Anna Klemm, took on a growing role in the project. In 2025, the BIIF began an architectural overhaul of the tool, which has since led to today's TissUUmaps 4. + +## Publications + + + + + + + +## Funding + +TissUUmaps is currently funded by: + +- the [SciLifeLab BioImage Informatics Unit (BIIF)](https://www.scilifelab.se/units/bioimage-informatics/), through technology development projects (TDPs); +- the [SciLifeLab and Wallenberg National Program for Data-Driven Life Science (DDLS)](https://www.scilifelab.se/data-driven/), as a [Research Data Community Project (RDCP)](https://www.scilifelab.se/news/four-collaborative-projects-selected-in-the-rdcp-call/). + +Previously, it was funded by: + +- the European Research Council, through an ERC consolidator grant to Carolina Wählby (ERC CoG 682810); +- the Swedish Foundation for Strategic Research (SSF), through the [SysMic](https://sysmic.ki.se/) project (SB16-0046), with Staffan Strömblad as PI and Carolina Wählby as co-PI, and through the Big Data grant [‘Hierarchical Analysis of Temporal and Spatial Image Data - From intelligent data acquisition via smart data-management to confident predictions’](http://haste.research.it.uu.se/), with Carolina Wählby as PI. + + + + + + + diff --git a/src/pages/getting-started.mdx b/src/pages/getting-started.mdx new file mode 100644 index 0000000..1c5cdb0 --- /dev/null +++ b/src/pages/getting-started.mdx @@ -0,0 +1,78 @@ +--- +title: Getting started +description: Open TissUUmaps, explore an example, add your own data and share it +hide_table_of_contents: true +--- + +# Getting started + +From your first look to sharing your own data. + + + +## Open TissUUmaps + +TissUUmaps runs in your browser, with nothing to install. + + + +## Explore an example + +Open a project from the gallery. Pan and zoom in the viewer, and show, hide or restyle each image, label, point and shape layer in the side panel. + + + +## Add your own data + + + + +Drag the folder with your data onto the **Project** tab to connect it, then click **Start empty**. Now drag files or folders, such as an OME-Zarr store, from that folder onto the **Images**, **Labels**, **Points**, **Shapes** or **Tables** tab. TissUUmaps recognizes the format and fills in the add dialog. + + + + +Click **Start empty**, then **Add image** (or labels, points, shapes, table) in the matching tab and paste the URL of your data. The server must allow cross-origin requests (CORS). + + + + +See the [supported data](/docs/supported-data) for the formats and their settings. + +## Open a SpatialData store + +:::info + +The SpatialData plugin is still in development. It will become part of TissUUmaps itself in a future release. + +::: + +The SpatialData plugin reads a whole SpatialData store. Open the plugins menu (**⋮** in the tab bar), choose **Load plugin from URL…** and paste: + +``` +https://tissuumaps4.serve.scilifelab.se/plugins/spatialdata.js +``` + +In the new **SpatialData** tab, paste the URL of a store and click **Load**. For a store on your computer, first connect the folder that contains it (see Local data above), then click **Choose a store in the connected folder** and pick the `.zarr` folder. + +The tab lists the images, labels, points and shapes of the store by coordinate system. Add the ones you want and color them by the store's tables. + +## Save and share + +**Download project** in the project menu saves a project file (`.tm4`). It describes your data and how to show it, but does not contain the data. Drop it on the **Project** tab to open it again. + +To share a project, put the project file and the data on a web server, and add the project file's URL to the TissUUmaps link: + +``` +https://tissuumaps.github.io/app/?project=https://example.org/project.tm4 +``` + +Paths in the project file can be relative to it, so the project folder can move without editing the file. See [Projects](/docs/concepts/projects). + + + +## Learn more + +- [Concepts](/docs/category/concepts): projects, data types and coordinate systems. +- [Supported data](/docs/supported-data): the file formats TissUUmaps reads, and how to configure them. +- [Questions and bugs](https://github.com/TissUUmaps/TissUUmaps/issues): ask a question or report a problem on GitHub. diff --git a/src/pages/index.module.css b/src/pages/index.module.css index 19a5bfb..41bd64f 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -3,21 +3,168 @@ * and scoped locally. */ -.heroBanner { - padding: 4rem 0; - text-align: center; +/* The last band stretches down to the footer */ +.wrapper, +.main { + display: flex; + flex-direction: column; +} + +.main { + flex: 1; +} + +.hero { + padding: 4.5rem 0; +} + +.grid { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); + gap: 4rem; + align-items: center; + max-width: 1440px; +} + +/* Video placeholder: screenshot with a play badge */ +.video { position: relative; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 16 / 9; overflow: hidden; + box-shadow: 0 1.5rem 3rem + color-mix(in srgb, var(--scilifelab-teal) 25%, transparent); } -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } +.video img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; } -.content { +.badge { + position: relative; display: flex; align-items: center; + gap: 1rem; + padding: 0.6rem 1.5rem 0.6rem 0.6rem; + border-radius: 999px; + background: color-mix(in srgb, var(--scilifelab-teal) 90%, transparent); + color: #fff; + font-size: 1.15rem; + letter-spacing: 0.05em; +} + +.play { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + background: var(--scilifelab-lime); + color: var(--scilifelab-teal); +} + +/* Intro text */ +.title { + margin-bottom: 1rem; + color: var(--ifm-color-primary); + font-size: 3.5rem; + line-height: 1.1; +} + +.lead { + margin-bottom: 0.75rem; + color: var(--ifm-color-primary); + font-size: 1.5rem; + line-height: 1.35; +} + +/* Check-marked features, each a main point and a muted detail */ +.features { + margin: 1.5rem 0 2.5rem; + padding: 0; + list-style: none; + font-size: 1.15rem; +} + +.features li { + position: relative; + margin: 0 0 0.5rem; + padding-left: 2rem; + font-weight: 700; +} + +.features li::before { + content: "✓"; + position: absolute; + left: 0; + color: var(--scilifelab-lime); +} + +.features span { + color: var(--ifm-color-emphasis-700); + font-weight: 400; +} + +.actions { + display: flex; justify-content: center; } + +/* Links into the site, on a band like the SciLifeLab section blocks */ +.band { + flex: 1; + padding: 3.5rem 0; + background: var(--tm-band-background); +} + +.cards { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 3rem; + max-width: 1440px; +} + +.cards h2 { + margin-bottom: 0.75rem; + font-size: 1.35rem; +} + +/* Each card is a column whose text takes the free space, so the buttons of + all cards line up */ +.cards > div { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.cards p { + flex: 1; + margin-bottom: 1rem; +} + +@media screen and (max-width: 996px) { + .hero { + padding: 2rem 0; + } + + .grid { + grid-template-columns: minmax(0, 1fr); + gap: 2rem; + } + + .title { + font-size: 2.5rem; + } + + .cards { + grid-template-columns: minmax(0, 1fr); + gap: 2rem; + } +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 7b856ae..9d8d65e 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,24 +1,116 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Heading from "@theme/Heading"; import Layout from "@theme/Layout"; import clsx from "clsx"; import type { ReactNode } from "react"; +import { Button } from "../components/markdown/Button"; import styles from "./index.module.css"; +// The latest release of the app, served next to the website as static files +// (see scripts/prepare.mts), so linked by pathname instead of as a route +const appUrl = "pathname:///app/"; + +// Main point and detail, as on the TissUUmaps 4 poster +const features = [ + ["Browser-based", "no server, nothing to install"], + ["Intuitive and powerful", "like a plotting tool"], + ["Multi-modal", "images, labels, points, shapes and tables"], + ["Your own data", "local files or the cloud"], + ["Open and extensible", "with plugins"], +]; + +const cards = [ + { + title: "Getting started", + text: "Open an example, then load your own data.", + label: "Get started", + to: "/getting-started", + }, + { + title: "Documentation", + text: "Concepts, supported data formats and plugins.", + label: "Read the docs", + to: "/docs/category/concepts", + }, + { + title: "Gallery", + text: "Example projects made with TissUUmaps.", + label: "Gallery", + to: "/gallery", + }, + { + title: "Source code", + text: "TissUUmaps is open source. Report issues and contribute on GitHub.", + label: "GitHub", + to: "https://github.com/TissUUmaps/TissUUmaps/", + }, +]; + export default function Home(): ReactNode { const { siteConfig } = useDocusaurusContext(); return ( - -
-
- - {siteConfig.title} - -

{siteConfig.tagline}

-
-
-
+ +
+
+
+
+ TissUUmaps showing a Xenium dataset +
+ + ▶ + + Video coming soon +
+
+
+ + {siteConfig.title} + +

+ An interactive web application for visualizing spatial biology + data. +

+

+ Originally developed in the Wählby Group at Uppsala University, + TissUUmaps is developed and maintained by the SciLifeLab + BioImage Informatics Unit. +

+
    + {features.map(([feature, detail]) => ( +
  • + {feature} {detail} +
  • + ))} +
+
+ +
+
+
+
+
+
+ {cards.map(({ title, text, label, to }) => ( +
+ {title} +

{text}

+ +
+ ))} +
+
+
); } diff --git a/src/theme/MDXComponents.tsx b/src/theme/MDXComponents.tsx new file mode 100644 index 0000000..250b86f --- /dev/null +++ b/src/theme/MDXComponents.tsx @@ -0,0 +1,10 @@ +import MDXComponents from "@theme-original/MDXComponents"; + +import * as markdownComponents from "../components/markdown"; + +// Makes the custom components usable in every Markdown (MDX) file without an +// import, so that pages can be edited without knowing about imports +export default { + ...MDXComponents, + ...markdownComponents, +}; diff --git a/src/theme/NavbarItem/ComponentTypes.tsx b/src/theme/NavbarItem/ComponentTypes.tsx new file mode 100644 index 0000000..fb320bd --- /dev/null +++ b/src/theme/NavbarItem/ComponentTypes.tsx @@ -0,0 +1,10 @@ +import ComponentTypes from "@theme-original/NavbarItem/ComponentTypes"; + +import PartnerLogosNavbarItem from "./PartnerLogosNavbarItem"; + +// Adds our navbar item types to the ones of Docusaurus (custom types must be +// prefixed with "custom-") +export default { + ...ComponentTypes, + "custom-partnerLogos": PartnerLogosNavbarItem, +}; diff --git a/src/theme/NavbarItem/PartnerLogosNavbarItem.module.css b/src/theme/NavbarItem/PartnerLogosNavbarItem.module.css new file mode 100644 index 0000000..773deee --- /dev/null +++ b/src/theme/NavbarItem/PartnerLogosNavbarItem.module.css @@ -0,0 +1,38 @@ +/** + * Each logo is sized so that the capitals of the names have the same height + * (1.18rem) and their tops line up (0.7rem from the top); NBIS has its + * subtitle below the name. + */ +.partners { + display: flex; + align-items: flex-start; + gap: 2rem; + margin-right: 1rem; +} + +.partners a { + display: flex; +} + +/* Capitals from 31 % to 84 % of the height */ +.scilifelab { + height: 2.25rem; +} + +/* Name in the top 65 %, subtitle below */ +.nbis { + height: 1.82rem; + margin-top: 0.7rem; +} + +/* Capitals fill the height */ +.biif { + height: 1.18rem; + margin-top: 0.7rem; +} + +@media screen and (max-width: 1200px) { + .partners { + display: none; + } +} diff --git a/src/theme/NavbarItem/PartnerLogosNavbarItem.tsx b/src/theme/NavbarItem/PartnerLogosNavbarItem.tsx new file mode 100644 index 0000000..1e779d8 --- /dev/null +++ b/src/theme/NavbarItem/PartnerLogosNavbarItem.tsx @@ -0,0 +1,69 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import ThemedImage from "@theme/ThemedImage"; +import type { ReactNode } from "react"; + +import styles from "./PartnerLogosNavbarItem.module.css"; + +/** The partner logos, with a dark variant where the logo has black text */ +const partners = [ + { + name: "SciLifeLab", + url: "https://www.scilifelab.se/", + logo: "/img/scilifelab.svg", + darkLogo: "/img/scilifelab-dark.svg", + className: styles.scilifelab, + }, + { + name: "NBIS", + url: "https://nbis.se/", + logo: "/img/nbis.svg", + darkLogo: "/img/nbis.svg", + className: styles.nbis, + }, + { + name: "BIIF", + url: "https://www.scilifelab.se/units/bioimage-informatics/", + logo: "/img/biif.svg", + darkLogo: "/img/biif-dark.svg", + className: styles.biif, + }, +]; + +function PartnerLogo({ + name, + url, + logo, + darkLogo, + className, +}: (typeof partners)[number]): ReactNode { + return ( + + + + ); +} + +/** + * The partner logos on the right of the navbar (navbar item type + * `custom-partnerLogos`, see ComponentTypes.tsx); not in the mobile menu + */ +export default function PartnerLogosNavbarItem({ + mobile, +}: { + mobile?: boolean; +}): ReactNode { + if (mobile) { + return null; + } + return ( +
+ {partners.map((partner) => ( + + ))} +
+ ); +} diff --git a/static/img/about/funding/erc.jpg b/static/img/about/funding/erc.jpg new file mode 100644 index 0000000..5b0db7f Binary files /dev/null and b/static/img/about/funding/erc.jpg differ diff --git a/static/img/about/funding/ssf.jpg b/static/img/about/funding/ssf.jpg new file mode 100644 index 0000000..eea0951 Binary files /dev/null and b/static/img/about/funding/ssf.jpg differ diff --git a/static/img/about/funding/wallenberg.png b/static/img/about/funding/wallenberg.png new file mode 100644 index 0000000..bd5cfd3 Binary files /dev/null and b/static/img/about/funding/wallenberg.png differ diff --git a/static/img/about/hackathon.jpg b/static/img/about/hackathon.jpg new file mode 100644 index 0000000..0d7b4d1 Binary files /dev/null and b/static/img/about/hackathon.jpg differ diff --git a/static/img/about/publications/plugins.jpg b/static/img/about/publications/plugins.jpg new file mode 100644 index 0000000..cb1f9a3 Binary files /dev/null and b/static/img/about/publications/plugins.jpg differ diff --git a/static/img/about/publications/tissuumaps.jpg b/static/img/about/publications/tissuumaps.jpg new file mode 100644 index 0000000..3013167 Binary files /dev/null and b/static/img/about/publications/tissuumaps.jpg differ diff --git a/static/img/about/publications/tissuumaps3.jpg b/static/img/about/publications/tissuumaps3.jpg new file mode 100644 index 0000000..143c40d Binary files /dev/null and b/static/img/about/publications/tissuumaps3.jpg differ diff --git a/static/img/biif-dark.svg b/static/img/biif-dark.svg new file mode 100644 index 0000000..1f3c831 --- /dev/null +++ b/static/img/biif-dark.svg @@ -0,0 +1,85 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/static/img/biif.svg b/static/img/biif.svg new file mode 100644 index 0000000..ce48e4d --- /dev/null +++ b/static/img/biif.svg @@ -0,0 +1,85 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/static/img/logo.svg b/static/img/logo.svg index 27106a1..c59a866 100644 --- a/static/img/logo.svg +++ b/static/img/logo.svg @@ -1,104 +1 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/static/img/nbis.svg b/static/img/nbis.svg new file mode 100644 index 0000000..6aaf43c --- /dev/null +++ b/static/img/nbis.svg @@ -0,0 +1,288 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/static/img/scilifelab-dark.svg b/static/img/scilifelab-dark.svg new file mode 100644 index 0000000..637c25c --- /dev/null +++ b/static/img/scilifelab-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/img/scilifelab.svg b/static/img/scilifelab.svg new file mode 100644 index 0000000..b1eaff9 --- /dev/null +++ b/static/img/scilifelab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/img/video-preview.jpg b/static/img/video-preview.jpg new file mode 100644 index 0000000..2f1983a Binary files /dev/null and b/static/img/video-preview.jpg differ diff --git a/tissuumaps-docs.ts b/tissuumaps-docs.ts index 43c5ca0..e7c7368 100644 --- a/tissuumaps-docs.ts +++ b/tissuumaps-docs.ts @@ -1,4 +1,5 @@ import { existsSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -34,3 +35,22 @@ export function loadVersions(): { line: string; version: string }[] { return { line, version: release.version }; }); } + +/** + * Loads the sidebar items that typedoc generated for the API docs of a docs + * version + * + * @param docsDir - The docs folder of the version + * @returns The sidebar items + * @throws Error if the docs folder has no typedoc sidebar + */ +export function loadTypedocSidebar(docsDir: string): unknown[] { + const typedocSidebarFile = join(docsDir, "api", "typedoc-sidebar.cjs"); + if (!existsSync(typedocSidebarFile)) { + throw new Error( + `${typedocSidebarFile} not found, generate the API docs in TissUUmaps first`, + ); + } + const require = createRequire(import.meta.url); + return require(typedocSidebarFile) as unknown[]; +}