diff --git a/assets/main.scss b/assets/main.scss index 7bdb074..4a72eb1 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -1138,6 +1138,89 @@ body { } } +// Table of contents sidebar, used by the long pages (see partials/toc.html) + +.with-toc, +.with-toc-body { + display: flex; + flex-direction: column; + min-width: 0; +} + +.toc { + display: none; +} + +// Only worth a column when there is room for one beside the content +@media (min-width: 75rem) { + #page-content:has(.with-toc) { + max-width: $maxwidthwide; + width: $contentwidthwide; + } + + .with-toc { + display: grid; + grid-template-columns: 13rem minmax(0, 1fr); + gap: 3.5rem; + align-items: start; + } + + .toc { + display: block; + position: sticky; + top: 2rem; + max-height: calc(100vh - 4rem); + overflow-y: auto; + // The body column starts with the h1, so drop the list to roughly its baseline + margin-top: 4.5rem; + + &[hidden] { + display: none; + } + + .toc-title { + margin: 0 0 0.6rem 0.75rem; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: $greydesc; + } + + ul { + margin: 0; + padding: 0; + list-style: none; + border-left: 1px solid $overline; + } + + li a { + display: block; + padding: 0.35rem 0.75rem; + margin-left: -1px; + border-left: 2px solid transparent; + font-size: 0.9rem; + line-height: 1.35; + color: $greydesc; + text-decoration: none; + transition: all 200ms ease-in-out; + + &:hover { + color: black; + } + &.is-current { + color: black; + border-left-color: $tiletext; + } + } + + .toc-h3 a { + padding-left: 1.5rem; + font-size: 0.85rem; + } + } +} + // Specific to /people #people-content .team .team-list { diff --git a/layouts/learn/list.html b/layouts/learn/list.html index e7cea9a..eff578b 100644 --- a/layouts/learn/list.html +++ b/layouts/learn/list.html @@ -1,64 +1,69 @@ {{ define "main" }} -

{{ .Title }}

-
-
- {{ if .Params.tutorials }} - {{ .Params.tutorials_intro | markdownify }} +
+ {{ partial "toc.html" . }} +
+

{{ .Title }}

+
+
+ {{ if .Params.tutorials }} + {{ .Params.tutorials_intro | markdownify }} - {{ $jsonURL := "https://scverse.org/scverse-tutorials/tutorials.json" }} - {{ $remoteData := resources.GetRemote $jsonURL }} - {{ $content := transform.Unmarshal $remoteData.Content }} - {{ if $content }} + {{ $jsonURL := "https://scverse.org/scverse-tutorials/tutorials.json" }} + {{ $remoteData := resources.GetRemote $jsonURL }} + {{ $content := transform.Unmarshal $remoteData.Content }} + {{ if $content }} -

Tutorials

+

Tutorials

-
- +
+ - {{ range $cat := $content.categories }} - {{ range $catname, $catmore := $cat }} - {{ $ts := where $content.tutorials "primary_category" $catname }} - {{ if (gt ($ts | len) 0) }} -

{{ $catname }}

-

{{ $catmore.description }}

+ {{ range $cat := $content.categories }} + {{ range $catname, $catmore := $cat }} + {{ $ts := where $content.tutorials "primary_category" $catname }} + {{ if (gt ($ts | len) 0) }} +

{{ $catname }}

+

{{ $catmore.description }}

- - {{ end }} - - {{ end }} + {{ end }} +
+
-
+ {{ end }} diff --git a/layouts/packages/list.html b/layouts/packages/list.html index c913803..16779d7 100644 --- a/layouts/packages/list.html +++ b/layouts/packages/list.html @@ -1,104 +1,109 @@ {{ define "main" }} -

{{ .Title }}

-
- {{ $sections := index .Params.sections 0 }} -
- {{ if .Params.packages }} -

Packages maintained by core team

- {{ $sections.core_packages | markdownify }} - {{ if .Params.datastructures }} -

Data structures

- {{ $sections.datastructures | markdownify }} -
- {{ $datastructures := .Params.datastructures }} - {{ range $i, $e := $datastructures }} -
-
- Logo for {{ .name }} -
-
-
-
- {{ .name }} - {{ .details | markdownify }} +
+ {{ partial "toc.html" . }} +
+

{{ .Title }}

+
+ {{ $sections := index .Params.sections 0 }} +
+ {{ if .Params.packages }} +

Packages maintained by core team

+ {{ $sections.core_packages | markdownify }} + {{ if .Params.datastructures }} +

Data structures

+ {{ $sections.datastructures | markdownify }} +
+ {{ $datastructures := .Params.datastructures }} + {{ range $i, $e := $datastructures }} +
+
+ Logo for {{ .name }} +
+
+
+
+ {{ .name }} + {{ .details | markdownify }} +
+
+
- -
+ {{ end }}
{{ end }} -
- {{ end }} -

Analysis task-specific extensions

-

{{ $sections.datastructures_post | markdownify }}

-

Frameworks

- {{ $sections.frameworks | markdownify }} -
- {{ $packages := .Params.packages }} - {{ range $i, $e := $packages }} -
-
- Logo for {{ .name }} -
-
-
-
- {{ .name }} - {{ .details | markdownify }} +

Analysis task-specific extensions

+

{{ $sections.datastructures_post | markdownify }}

+

Frameworks

+ {{ $sections.frameworks | markdownify }} +
+ {{ $packages := .Params.packages }} + {{ range $i, $e := $packages }} +
+
+ Logo for {{ .name }} +
+
+
+
+ {{ .name }} + {{ .details | markdownify }} +
+
+
- -
+ {{ end }}
{{ end }} -
- {{ end }} - {{ $jsonURL := "https://scverse.org/ecosystem-packages/packages.json" }} - {{ $remoteData := resources.GetRemote $jsonURL }} - {{ $pkgs := transform.Unmarshal $remoteData.Content }} -

Ecosystem packages maintained by scverse community

-
-

{{ $sections.ecosystem | markdownify }}

-
- {{ if $pkgs }} -
- - - - - - - - - - {{ range $lib := $pkgs }} - - - - - {{ end }} - -
PackageDescription
+ {{ $jsonURL := "https://scverse.org/ecosystem-packages/packages.json" }} + {{ $remoteData := resources.GetRemote $jsonURL }} + {{ $pkgs := transform.Unmarshal $remoteData.Content }} +

Ecosystem packages maintained by scverse community

+
+

{{ $sections.ecosystem | markdownify }}

+
+ {{ if $pkgs }} +
+ + + + + + + + + + {{ range $lib := $pkgs }} + + + + + {{ end }} + +
PackageDescription
+
+ {{ end }}
- {{ end }} +
- + {{ end }} diff --git a/layouts/partials/toc.html b/layouts/partials/toc.html new file mode 100644 index 0000000..821384d --- /dev/null +++ b/layouts/partials/toc.html @@ -0,0 +1,7 @@ +{{/* The headings on these pages are rendered by templates rather than by markdown, so Hugo's + .TableOfContents cannot see them. initTableOfContents in main.js fills this in and unhides it. +*/}} + diff --git a/static/main.js b/static/main.js index 5eb34b9..799fc1d 100644 --- a/static/main.js +++ b/static/main.js @@ -12,6 +12,56 @@ const filterPackages = () => { trs.forEach(toggleTrs); } +// Table of contents for long pages. These headings come from templates rather than markdown, so +// Hugo's .TableOfContents cannot see them and the list is built from the rendered page instead. +const initTableOfContents = () => { + const toc = document.querySelector('.toc'); + const body = document.querySelector('.with-toc-body'); + if (!toc || !body) return; + + const headings = [...body.querySelectorAll('h2, h3')].filter(heading => heading.textContent.trim()); + if (headings.length < 3) return; // too short to be worth a sidebar + + const list = toc.querySelector('ul'); + const used = new Set(); + const links = new Map(); + + headings.forEach(heading => { + if (!heading.id) { + const slug = heading.textContent.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + let id = slug; + for (let n = 2; used.has(id) || document.getElementById(id); n += 1) id = `${slug}-${n}`; + heading.id = id; + } + used.add(heading.id); + + const link = document.createElement('a'); + link.href = `#${heading.id}`; + link.textContent = heading.textContent.trim(); + + const item = document.createElement('li'); + item.className = `toc-${heading.tagName.toLowerCase()}`; + item.append(link); + list.append(item); + links.set(heading, link); + }); + + toc.hidden = false; + + // Highlight the last heading scrolled past. A handful of rect reads per scroll is cheap enough + // to do directly, and avoids a throttle that can wedge if its callback never runs. + const markCurrent = () => { + let current = headings[0]; + for (const heading of headings) { + if (heading.getBoundingClientRect().top > 120) break; + current = heading; + } + links.forEach((link, heading) => link.classList.toggle('is-current', heading === current)); + }; + window.addEventListener('scroll', markCurrent, { passive: true }); + markCurrent(); +}; + const filterTutorials = () => { const trs = document.querySelectorAll('.tutorial-item'); const filter = document.querySelector('#tutorial-filter').value; @@ -258,6 +308,8 @@ document.addEventListener('DOMContentLoaded', function() { tutorialFilter.addEventListener('input', filterTutorials) } + initTableOfContents() + // Initialize interactive visualization if on home page initInteractiveViz() })