A music player that connects to your cloud/distributed storage.
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 */
5export 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
19export function update() {
20 const nav = document.getElementById("diffuse-nav");
21 const btn = document.getElementById("nav-overflow-btn");
22 const menu = document.getElementById("nav-overflow-menu");
23
24 if (!nav || !btn || !menu) return;
25
26 const items = /** @type {HTMLElement[]} */ ([...nav.children]);
27
28 // Reset: show all items, hide button, restore default icon
29 for (const item of items) item.style.display = "";
30 btn.style.display = "none";
31 const span = btn.querySelector("span");
32 if (span) span.innerHTML = `<i class="ph-fill ph-dots-three-outline"></i>`;
33
34 // Available width is the nav-container's width (nav may be content-sized in column layouts)
35 const availableWidth = (nav.parentElement ?? nav).clientWidth;
36
37 // Measure total content width directly — scrollWidth is unreliable with
38 // justify-content: flex-end because overflow spills left (negative direction)
39 // and isn't reflected in scrollWidth.
40 const gap = parseFloat(getComputedStyle(nav).columnGap) || 0;
41 const contentWidth = () => {
42 const visible = items.filter((el) => el.style.display !== "none");
43 return visible.reduce((acc, el) => acc + el.offsetWidth, 0) +
44 gap * Math.max(0, visible.length - 1);
45 };
46
47 // No overflow — nothing to do
48 if (contentWidth() <= availableWidth) return;
49
50 // Show button (takes up space; subtract its width + container gap from available)
51 btn.style.display = "";
52 const containerGap = parseFloat(getComputedStyle(nav.parentElement ?? nav).columnGap) || 0;
53
54 // Hide items from right until content fits
55 const hidden = [];
56 for (let i = items.length - 1; i >= 0; i--) {
57 if (contentWidth() <= availableWidth - btn.offsetWidth - containerGap) break;
58 items[i].style.display = "none";
59 hidden.unshift(items[i]);
60 }
61
62 // Update button label: show "Menu" when all items are hidden
63 const allHidden = hidden.length === items.length;
64 if (span) {
65 span.innerHTML = allHidden
66 ? `<i class="ph-bold ph-list"></i> Menu`
67 : `<i class="ph-fill ph-dots-three-outline"></i>`;
68 }
69
70 // Populate dropdown with clones (stripped of button styling)
71 menu.innerHTML = "";
72 for (const el of hidden) {
73 if (el.classList.contains("divider")) continue;
74
75 const clone = /** @type {HTMLElement} */ (el.cloneNode(true));
76 clone.style.display = "";
77 clone.classList.remove(
78 "button",
79 "button--transparent",
80 "button--border",
81 "button--bg-twist-2",
82 );
83
84 menu.appendChild(clone);
85 }
86}
87
88let _observer = /** @type {ResizeObserver | undefined} */ (undefined);
89
90export function watchResize() {
91 const nav = document.getElementById("diffuse-nav");
92 if (!nav) return;
93 _observer?.disconnect();
94 _observer = new ResizeObserver(update);
95 // Observe the container — in column layouts the nav is content-sized and
96 // won't resize, but the container does as the header width changes.
97 _observer.observe(nav.parentElement ?? nav);
98}