A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: improved dashboard header + nav design

+191 -64
+3 -2
src/_components/grid.vto
··· 7 7 8 8 <div style="flex: 1"></div> 9 9 10 - <span class="grid-filter--label grid-filter--label-output">Userdata from</span> 11 - <span class="grid-filter--output"></span> 10 + <span class="grid-filter--label">Category</span> 11 + <div class="grid-filter--categories"></div> 12 12 </div> 13 13 14 14 <ul class="grid" style="margin-top: var(--space-xs);"> ··· 33 33 data-active-color="{{color}}" 34 34 data-description="{{item.desc.trim()}}" 35 35 data-name="{{item.title}}" 36 + data-category="{{ item.category ?? `` }}" 36 37 data-kind="{{item.kind ?? `interface`}}" 37 38 data-uri="{{ item.url |> facetURI }}" 38 39 >
+6 -23
src/_components/nav.vto
··· 24 24 </span> 25 25 </a> 26 26 27 - <a href="code/" class="button {{ colorClass("code/") }} button--border"> 27 + <a href="catalogue/" class="button {{ colorClass("catalogue/") }} button--border"> 28 28 <span> 29 - <i class="ph-fill ph-hammer"></i> 30 - Code 29 + <i class="ph-fill ph-storefront"></i> 30 + Catalogue 31 31 </span> 32 32 </a> 33 33 34 - <div class="divider"></div> 35 - 36 - <a href="data/" class="button {{ colorClass("data/") }} button--border"> 37 - <span>Input & Output</span> 38 - </a> 39 - 40 - <a href="playback/" class="button {{ colorClass("playback/") }} button--border"> 41 - <span>Playback</span> 42 - </a> 43 - 44 - <a href="browsing/" class="button {{ colorClass("browsing/") }} button--border"> 45 - <span>Browsing</span> 46 - </a> 47 - 48 - <a href="themes/" class="button {{ colorClass("themes/") }} button--border"> 49 - <span>Themes</span> 50 - </a> 51 - 52 - <a href="misc/" class="button {{ colorClass("misc/") }} button--border"> 34 + <a href="code/" class="button {{ colorClass("code/") }} button--border"> 53 35 <span> 54 - <i class="ph-fill ph-treasure-chest"></i> 36 + <i class="ph-fill ph-hammer"></i> 37 + Code 55 38 </span> 56 39 </a> 57 40 </nav>
+9 -13
src/_includes/layouts/kitchen.vto
··· 15 15 - common/pages/version-upgrade.js 16 16 --- 17 17 18 - <header style="overflow: hidden"> 19 - <div> 20 - <div class="diffuse-logo-container diffuse-logo-container--condensed"> 21 - <a href="./" style="display: inline-block;"> 22 - {{ await comp.diffuse.logo() }} 23 - </a> 24 - </div> 25 - </div> 26 - <div class="dither-mask filler"> 27 - <div id="status--filler-container"> 28 - {{ await comp.diffuse.status() }} 18 + <header> 19 + <div class="header__nav"> 20 + <div style="flex-shrink: 0;"> 21 + <div class="diffuse-logo-container diffuse-logo-container--condensed"> 22 + <a href="./" style="display: inline-block;"> 23 + {{ await comp.diffuse.logo() }} 24 + </a> 25 + </div> 29 26 </div> 27 + {{ await comp.nav({ url, facets }) }} 30 28 </div> 31 29 </header> 32 30 33 31 <main> 34 - {{ await comp.nav({ url, facets }) }} 35 - 36 32 {{ content }} 37 33 </main>
+11
src/catalogue.vto
··· 1 + --- 2 + layout: layouts/kitchen.vto 3 + base: ../ 4 + title: Catalogue | Diffuse 5 + --- 6 + 7 + <h1 hidden>Featured</h1> 8 + 9 + <section> 10 + {{ await comp.grid({ id: "catalogue", items: facets }) }} 11 + </section>
+83 -16
src/common/pages/grid.js
··· 10 10 11 11 export function setupFilter() { 12 12 /** @type {NodeListOf<HTMLElement>} */ 13 - const buttons = document.querySelectorAll(".grid-filter button[data-filter]"); 13 + const kindButtons = document.querySelectorAll( 14 + ".grid-filter button[data-filter]", 15 + ); 14 16 15 17 /** @type {NodeListOf<HTMLElement>} */ 16 18 const items = document.querySelectorAll(".grid-item"); 17 19 18 - /** @param {string} filter */ 19 - function applyFilter(filter) { 20 - buttons.forEach((b) => { 21 - if (b.dataset.filter === filter) b.classList.remove("button--transparent"); 22 - else b.classList.add("button--transparent"); 20 + // Build category buttons from the categories present in the current grid 21 + const categoriesEl = document.querySelector(".grid-filter--categories"); 22 + const categories = /** @type {string[]} */ ( 23 + [...new Set([...items].map((i) => i.dataset.category).filter(Boolean))] 24 + .sort() 25 + ); 26 + 27 + /** @type {HTMLElement | null} */ 28 + let categoryLabelEl = null; 29 + /** @type {HTMLElement | null} */ 30 + let categoryMenuEl = null; 31 + 32 + if (categoriesEl && categories.length > 1) { 33 + categoryLabelEl = document.createElement("span"); 34 + categoryLabelEl.textContent = "All"; 35 + 36 + const triggerBtn = document.createElement("button"); 37 + triggerBtn.className = "button--border button--tiny button--transparent"; 38 + triggerBtn.setAttribute("popovertarget", "grid-category-menu"); 39 + const span = document.createElement("span"); 40 + span.className = "with-icon"; 41 + span.appendChild(categoryLabelEl); 42 + const caret = document.createElement("i"); 43 + caret.className = "ph-bold ph-caret-down"; 44 + span.appendChild(caret); 45 + triggerBtn.appendChild(span); 46 + 47 + categoryMenuEl = document.createElement("div"); 48 + categoryMenuEl.id = "grid-category-menu"; 49 + categoryMenuEl.className = "dropdown"; 50 + categoryMenuEl.setAttribute("popover", ""); 51 + 52 + for (const cat of ["all", ...categories]) { 53 + const item = document.createElement("button"); 54 + item.dataset.category = cat; 55 + item.textContent = cat === "all" ? "All" : cat; 56 + item.addEventListener("click", () => { 57 + activeCategory = cat; 58 + const url = new URL(location.href); 59 + if (cat === "all") url.searchParams.delete("category"); 60 + else url.searchParams.set("category", cat); 61 + history.replaceState(null, "", url); 62 + categoryMenuEl?.hidePopover(); 63 + applyFilter(activeKind, activeCategory); 64 + }); 65 + categoryMenuEl.appendChild(item); 66 + } 67 + 68 + categoriesEl.appendChild(triggerBtn); 69 + categoriesEl.appendChild(categoryMenuEl); 70 + } 71 + 72 + let activeKind = "all"; 73 + let activeCategory = "all"; 74 + 75 + /** 76 + * @param {string} kind 77 + * @param {string} category 78 + */ 79 + function applyFilter(kind, category) { 80 + kindButtons.forEach((b) => { 81 + const transparent = b.dataset.filter !== kind; 82 + if (b.classList.contains("button--transparent") !== transparent) { 83 + b.classList.toggle("button--transparent", transparent); 84 + } 23 85 }); 86 + if (categoryLabelEl) { 87 + categoryLabelEl.textContent = category === "all" ? "All" : category; 88 + } 24 89 items.forEach((item) => { 25 - const kind = item.dataset.kind; 26 - const show = filter === "all" || kind === filter; 27 - item.hidden = !show; 90 + const kindMatch = kind === "all" || item.dataset.kind === kind; 91 + const catMatch = category === "all" || item.dataset.category === category; 92 + item.hidden = !(kindMatch && catMatch); 28 93 }); 29 94 } 30 95 31 - buttons.forEach((b) => { 96 + kindButtons.forEach((b) => { 32 97 b.addEventListener("click", () => { 33 - const filter = b.dataset.filter ?? "all"; 98 + activeKind = b.dataset.filter ?? "all"; 34 99 const url = new URL(location.href); 35 - if (filter === "all") url.searchParams.delete("filter"); 36 - else url.searchParams.set("filter", filter); 100 + if (activeKind === "all") url.searchParams.delete("filter"); 101 + else url.searchParams.set("filter", activeKind); 37 102 history.replaceState(null, "", url); 38 - applyFilter(filter); 103 + applyFilter(activeKind, activeCategory); 39 104 }); 40 105 }); 41 106 42 - const initial = new URL(location.href).searchParams.get("filter") ?? "all"; 43 - applyFilter(initial); 107 + const params = new URL(location.href).searchParams; 108 + activeKind = params.get("filter") ?? "all"; 109 + activeCategory = params.get("category") ?? "all"; 110 + applyFilter(activeKind, activeCategory); 44 111 } 45 112 46 113 ////////////////////////////////////////////
+43 -5
src/common/pages/nav.js
··· 1 + /** 2 + * Updates the active state of nav links based on the current URL. 3 + * Required because the nav now lives in <header> and is not replaced by PPR. 4 + */ 5 + export function updateActiveLinks() { 6 + const nav = document.getElementById("diffuse-nav"); 7 + const menu = document.getElementById("nav-overflow-menu"); 8 + 9 + for (const container of [nav, menu]) { 10 + if (!container) continue; 11 + for (const link of container.querySelectorAll("a[href]")) { 12 + const a = /** @type {HTMLAnchorElement} */ (link); 13 + const isActive = new URL(a.href).pathname === location.pathname; 14 + a.classList.toggle("button--transparent", !isActive); 15 + } 16 + } 17 + } 18 + 1 19 export function update() { 2 20 const nav = document.getElementById("diffuse-nav"); 3 21 const btn = document.getElementById("nav-overflow-btn"); ··· 11 29 for (const item of items) item.style.display = ""; 12 30 btn.style.display = "none"; 13 31 32 + // Available width is the nav-container's width (nav may be content-sized in column layouts) 33 + const availableWidth = (nav.parentElement ?? nav).clientWidth; 34 + 35 + // Measure total content width directly — scrollWidth is unreliable with 36 + // justify-content: flex-end because overflow spills left (negative direction) 37 + // and isn't reflected in scrollWidth. 38 + const gap = parseFloat(getComputedStyle(nav).columnGap) || 0; 39 + const contentWidth = () => { 40 + const visible = items.filter((el) => el.style.display !== "none"); 41 + return visible.reduce((acc, el) => acc + el.offsetWidth, 0) 42 + + gap * Math.max(0, visible.length - 1); 43 + }; 44 + 14 45 // No overflow — nothing to do 15 - if (nav.scrollWidth <= nav.clientWidth) return; 46 + if (contentWidth() <= availableWidth) return; 16 47 17 - // Show button (nav shrinks to accommodate it via flex) 48 + // Show button (takes up space; subtract its width from available) 18 49 btn.style.display = ""; 19 50 20 - // Hide items from right until nav content fits 51 + // Hide items from right until content fits 21 52 const hidden = []; 22 53 for (let i = items.length - 1; i >= 0; i--) { 23 - if (nav.scrollWidth <= nav.clientWidth) break; 54 + if (contentWidth() <= availableWidth - btn.offsetWidth) break; 24 55 items[i].style.display = "none"; 25 56 hidden.unshift(items[i]); 26 57 } ··· 43 74 } 44 75 } 45 76 77 + let _observer = /** @type {ResizeObserver | undefined} */ (undefined); 78 + 46 79 export function watchResize() { 47 80 const nav = document.getElementById("diffuse-nav"); 48 - if (nav) new ResizeObserver(update).observe(nav); 81 + if (!nav) return; 82 + _observer?.disconnect(); 83 + _observer = new ResizeObserver(update); 84 + // Observe the container — in column layouts the nav is content-sized and 85 + // won't resize, but the container does as the header width changes. 86 + _observer.observe(nav.parentElement ?? nav); 49 87 }
+2
src/common/pages/ppr.js
··· 28 28 const path = relativePathname(url.pathname); 29 29 30 30 Nav.update(); 31 + Nav.updateActiveLinks(); 31 32 Nav.watchResize(); 32 33 33 34 Grid.setupFilter(); ··· 72 73 73 74 const url = new URL(event.destination.url); 74 75 if (url.origin !== location.origin) return; 76 + if (url.pathname === location.pathname) return; 75 77 76 78 // Only intercept paths one level deep 77 79 const relative = relativePathname(url.pathname);
+34 -5
src/styles/diffuse/page.css
··· 75 75 display: flex; 76 76 gap: var(--space-lg); 77 77 } 78 + 79 + .header__nav { 80 + align-items: center; 81 + border-bottom: 1px solid var(--border-color); 82 + display: flex; 83 + flex: 1; 84 + gap: var(--space-lg); 85 + } 78 86 } 79 87 80 88 header, ··· 197 205 background: transparent; 198 206 } 199 207 200 - & > a { 208 + & > a, 209 + & > button { 201 210 cursor: pointer; 202 211 display: flex; 203 212 min-width: var(--space-3xl); ··· 209 218 } 210 219 } 211 220 212 - & > a:not(:last-child) { 221 + & > button { 222 + background: none; 223 + border: 0; 224 + border-radius: 0; 225 + color: inherit; 226 + font-family: inherit; 227 + font-size: inherit; 228 + font-weight: inherit; 229 + text-align: left; 230 + width: 100%; 231 + } 232 + 233 + & > a:not(:last-child), 234 + & > button:not(:last-child) { 213 235 border-bottom: 1px solid var(--border-color); 214 236 } 215 237 ··· 406 428 font-weight: 600; 407 429 margin-right: var(--space-2xs); 408 430 opacity: 0.4; 431 + white-space: nowrap; 409 432 410 433 @media (min-width: 31.5rem) { 411 434 display: block; ··· 424 447 .grid-filter--output { 425 448 opacity: 0; 426 449 transition: opacity 250ms; 450 + } 451 + 452 + .grid-filter--categories { 453 + align-items: center; 454 + display: flex; 455 + gap: var(--space-2xs); 427 456 } 428 457 } 429 458 ··· 760 789 761 790 .nav-container { 762 791 align-items: center; 763 - border-bottom: 1px solid var(--border-color); 764 792 display: flex; 793 + flex: 1; 765 794 gap: var(--space-xs); 766 - margin-top: var(--space-md); 767 - padding-bottom: var(--space-md); 795 + min-width: 0; 768 796 } 769 797 770 798 nav { ··· 774 802 flex-wrap: nowrap; 775 803 font-size: var(--fs-sm); 776 804 gap: var(--space-xs); 805 + justify-content: flex-end; 777 806 overflow: hidden; 778 807 779 808 .button {