+ {{ $e.name }} +
+ +{{ $e.desc }}
+ + {{ with $e.normtags }} + + {{ end }} + + {{ with $links }} +diff --git a/assets/main.scss b/assets/main.scss index 7bdb074..90fe366 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -1027,9 +1027,365 @@ body { } } -#eco-table tbody { - // TODO: I think this shouldn't be neccesary - border-top: 0px; +// Specific to /packages#ecosystem — the community package registry + +$eco-radius: 0.75rem; +$eco-hues: + $anndataorange, $scanpycerise, $mudatagreen, $spatialdatablue, $squidpyviolet, + $scirpypurple, $scviyellow, $muonaquamarine; + +#ecosystem-packages { + margin: 2rem 0 4rem; + + .eco-sprite { + display: none; + } + + svg { + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; + } + + .eco-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 1.5rem; + } + + .eco-search { + position: relative; + display: flex; + align-items: center; + flex: 1 1 22rem; + + .eco-search-icon { + position: absolute; + left: 1.1rem; + width: 1.1rem; + height: 1.1rem; + color: $greydesc; + pointer-events: none; + } + + input { + width: 100%; + height: 3.25rem; + padding: 0 2.9rem; + font-size: 1rem; + color: $tiletext; + background-color: $backtickbg; + border: 1px solid $overline; + border-radius: 2rem; + outline: none; + transition: all 200ms ease-in-out; + text-overflow: ellipsis; + + &::placeholder { + color: $greydesc; + } + &:focus { + background-color: white; + border-color: $greydesc; + box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.04); + } + // The native affordance duplicates our own clear button + &::-webkit-search-cancel-button { + display: none; + } + } + } + + .eco-search-clear { + position: absolute; + right: 0.9rem; + display: flex; + align-items: center; + justify-content: center; + width: 1.6rem; + height: 1.6rem; + padding: 0; + border: 0; + border-radius: 50%; + color: $tiletext2; + background-color: $tilebg; + cursor: pointer; + transition: all 200ms ease-in-out; + + svg { + width: 0.7rem; + height: 0.7rem; + } + &:hover { + color: white; + background-color: $tiletext; + } + } + + .eco-count { + margin: 0; + font-size: 0.95rem; + color: $greydesc; + white-space: nowrap; + } + + .eco-chips { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin: 1.25rem 0 2rem; + } + + .eco-chip { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.85rem; + font-size: 0.85rem; + color: $tiletext2; + background-color: white; + border: 1px solid $overline; + border-radius: 2rem; + cursor: pointer; + transition: all 200ms ease-in-out; + + &:hover { + color: $tiletext3; + border-color: $tilebg2; + background-color: $tilebg4; + } + &.is-active { + color: white; + background-color: $tiletext; + border-color: $tiletext; + } + .eco-chip-count { + font-size: 0.75rem; + opacity: 0.55; + } + } + + .eco-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); + gap: 1.25rem; + } + + .eco-card { + position: relative; + display: flex; + flex-direction: column; + gap: 0.8rem; + padding: 1.35rem; + background-color: white; + border: 1px solid $overline; + border-radius: $eco-radius; + transition: all 200ms ease-in-out; + + &:hover { + border-color: $tilebg2; + box-shadow: 0 0.6rem 1.5rem rgba(0, 0, 0, 0.06); + transform: translateY(-2px); + } + &[hidden] { + display: none; + } + } + + .eco-card-head { + display: flex; + align-items: center; + gap: 0.85rem; + } + + .eco-logo { + display: grid; + place-items: center; + flex: 0 0 auto; + width: 2.75rem; + height: 2.75rem; + overflow: hidden; + border-radius: 0.6rem; + background-color: $tilebg4; + + img { + width: 100%; + height: 100%; + padding: 0.3rem; + object-fit: contain; + } + } + + .eco-logo--mono { + font-size: 1.3rem; + font-weight: 800; + color: $tiletext; + + @each $hue in $eco-hues { + &[data-hue="#{index($eco-hues, $hue) - 1}"] { + background-color: rgba($hue, 0.18); + } + } + } + + .eco-card-heading { + min-width: 0; + } + + // Element selector included to beat the generic `#page-content .post h3` margins + .eco-card h3.eco-card-name { + margin: 0; + font-size: 1.15rem; + font-weight: 800; + line-height: 1.3; + overflow-wrap: anywhere; + + a { + color: black; + text-decoration: none; + + // Make the whole card clickable without nesting interactive elements + &::after { + content: ""; + position: absolute; + inset: 0; + border-radius: $eco-radius; + } + } + } + + .eco-card-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.4rem; + margin: 0.25rem 0 0; + font-size: 0.75rem; + color: $greydesc; + + .eco-badge { + padding: 0.05rem 0.35rem; + font-size: 0.65rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: white; + background-color: $tiletext; + border-radius: 0.25rem; + } + .eco-badge--lang { + color: $tiletext; + background-color: $tilebg2; + } + .eco-version { + font-family: "JetBrains Mono", Menlo, monospace; + } + .eco-version + .eco-license::before { + content: "·"; + margin-right: 0.4rem; + } + } + + .eco-card-desc { + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + margin: 0; + font-size: 0.9rem; + line-height: 1.55; + color: $tiletext2; + } + + .eco-card-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin: 0; + padding: 0; + list-style: none; + } + + .eco-tag { + position: relative; + z-index: 1; + padding: 0.2rem 0.5rem; + font-size: 0.72rem; + color: $greydesc; + background-color: $tilebg4; + border: 0; + border-radius: 0.35rem; + cursor: pointer; + transition: all 200ms ease-in-out; + + &:hover { + color: $tiletext3; + background-color: $tilebg; + } + &.is-active { + color: white; + background-color: $tiletext; + } + } + + .eco-card-links { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: auto; + padding-top: 0.3rem; + + a { + position: relative; + z-index: 1; + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.6rem; + font-size: 0.78rem; + color: $tiletext2; + text-decoration: none; + background-color: $tilebg4; + border-radius: 0.4rem; + transition: all 200ms ease-in-out; + + svg { + width: 0.85rem; + height: 0.85rem; + } + &:hover { + color: white; + background-color: $tiletext; + } + } + } + + .eco-empty { + padding: 4rem 0; + text-align: center; + color: $greydesc; + + .eco-empty-reset { + padding: 0; + color: $tiletext; + background: none; + border: 0; + text-decoration: underline; + cursor: pointer; + } + } + + @media (max-width: 50rem) { + .eco-grid { + grid-template-columns: 1fr; + } + .eco-count { + order: -1; + } + } } // Specific to /tutorials diff --git a/layouts/packages/list.html b/layouts/packages/list.html index c913803..969c1fc 100644 --- a/layouts/packages/list.html +++ b/layouts/packages/list.html @@ -62,41 +62,250 @@
{{ $sections.ecosystem | markdownify }}
-| Package | -Description | -
|---|---|
| - {{ $lib.name }} - | -{{ $lib.description | markdownify }} | -
{{ $e.desc }}
+ + {{ with $e.normtags }} + + {{ end }} + + {{ with $links }} ++ No packages match your filters. + +
{{ end }} diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..b83ddc5 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,5 @@ +# Deploy previews used to build with Netlify's default Hugo (0.97.2, from 2022), so they did +# not reflect what gets published: .github/workflows/gh-pages.yml builds with `latest`, which +# currently resolves to 0.164.0. Pin previews to that, and bump both together from here on. +[build.environment] +HUGO_VERSION = "0.164.0" diff --git a/static/main.js b/static/main.js index 5eb34b9..0c36a2f 100644 --- a/static/main.js +++ b/static/main.js @@ -1,16 +1,83 @@ -const filterPackages = () => { - const trs = document.querySelectorAll('.eco-table-row'); - const filter = document.querySelector('#eco-filter').value; - const regex = new RegExp(filter, 'i'); - const tdFound = td => regex.test(td.innerHTML); - const pkgFound = childrenArr => childrenArr.some(tdFound); - const toggleTrs = ({ style, children }) => { - style.display = pkgFound([ - ...children - ]) ? '' : 'none' ; +// Ecosystem package registry: free-text search, one active category, one active tag. +// Categories and tags both come from the controlled vocabulary in the registry schema. +const initEcosystemRegistry = () => { + const root = document.querySelector('#ecosystem-packages'); + if (!root) return; + + const input = root.querySelector('#eco-filter'); + const chipRow = root.querySelector('#eco-chips'); + const grid = root.querySelector('#eco-grid'); + const counter = root.querySelector('#eco-count'); + const empty = root.querySelector('#eco-empty'); + const clearButton = root.querySelector('#eco-clear'); + const cards = Array.from(grid.querySelectorAll('.eco-card')); + let activeCategory = ''; + let activeTag = ''; + + const apply = () => { + const terms = input.value.toLowerCase().split(/\s+/).filter(Boolean); + let shown = 0; + cards.forEach(card => { + const visible = + terms.every(term => card.dataset.search.includes(term)) && + (!activeCategory || card.dataset.category === activeCategory) && + (!activeTag || card.dataset.tags.includes(`|${activeTag}|`)); + card.hidden = !visible; + if (visible) shown += 1; + }); + counter.textContent = shown; + empty.hidden = shown !== 0; + clearButton.hidden = !terms.length && !activeCategory && !activeTag; + root.querySelectorAll('.eco-tag').forEach(tag => { + tag.classList.toggle('is-active', tag.dataset.tag === activeTag); + }); }; - trs.forEach(toggleTrs); -} + + const syncChips = () => { + chipRow.querySelectorAll('.eco-chip').forEach(chip => { + const isActive = chip.dataset.category === activeCategory; + chip.classList.toggle('is-active', isActive); + chip.setAttribute('aria-pressed', String(isActive)); + }); + }; + + const reset = () => { + input.value = ''; + activeCategory = ''; + activeTag = ''; + syncChips(); + apply(); + }; + + input.addEventListener('input', apply); + input.addEventListener('keydown', event => { + if (event.key === 'Escape') reset(); + }); + clearButton.addEventListener('click', () => { + reset(); + input.focus(); + }); + empty.querySelector('.eco-empty-reset').addEventListener('click', reset); + + chipRow.addEventListener('click', event => { + const chip = event.target.closest('.eco-chip'); + if (!chip) return; + activeCategory = activeCategory === chip.dataset.category ? '' : chip.dataset.category; + syncChips(); + apply(); + }); + + // Tags are not in the chip row — there are too many — so they filter from the cards themselves. + grid.addEventListener('click', event => { + const tag = event.target.closest('.eco-tag'); + if (!tag) return; + event.preventDefault(); + activeTag = activeTag === tag.dataset.tag ? '' : tag.dataset.tag; + apply(); + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + root.scrollIntoView({ behavior: reduceMotion ? 'auto' : 'smooth', block: 'start' }); + }); +}; const filterTutorials = () => { const trs = document.querySelectorAll('.tutorial-item'); @@ -247,11 +314,7 @@ const initInteractiveViz = () => { // Initialize everything when DOM is loaded document.addEventListener('DOMContentLoaded', function() { - // Set up event listeners for filters if they exist - const ecoFilter = document.querySelector('#eco-filter') - if (ecoFilter) { - ecoFilter.addEventListener('input', filterPackages) - } + initEcosystemRegistry() const tutorialFilter = document.querySelector('#tutorial-filter') if (tutorialFilter) {