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

feat: facets grid toggle button to add/remove from collection

+103 -48
+2 -33
src/_components/facets/grid.vto
··· 1 1 <ul class="grid" style="margin-top: var(--space-lg);"> 2 2 {{ for index, item of items }} 3 3 <li data-uri="{{ item.url |> facetOrThemeURI }}" data-name="{{item.title}}"> 4 - <div style="position: relative;"> 5 - <a href="{{ item.url |> facetLoaderURL }}"> 4 + <div style="display: flex; align-items: center; gap: var(--space-xs); justify-content: space-between;"> 5 + <a href="{{ item.url |> facetLoaderURL }}" style="padding: var(--space-3xs) 0"> 6 6 {{item.title}} 7 7 </a> 8 - <button 9 - class="button--fixed button--transparent" 10 - popovertarget="facet-menu-{{id}}-{{index}}" 11 - style="anchor-name: --facet-anchor-{{id}}-{{index}}; position: absolute; right: 0; top: 50%; transform: translateY(-50%);" 12 - > 13 - <i class="ph-fill ph-dots-three-circle"></i> 14 - </button> 15 8 </div> 16 9 <div class="list-description"> 17 10 {{ item.desc |> md(true) }} 18 - </div> 19 - 20 - <!-- Dropdown Menu --> 21 - <div 22 - id="facet-menu-{{id}}-{{index}}" 23 - class="dropdown" 24 - style="position-anchor: --facet-anchor-{{id}}-{{index}}" 25 - popover 26 - > 27 - <a href="{{ item.url |> facetLoaderURL }}"> 28 - <span class="with-icon"> 29 - <i class="ph-fill ph-globe"></i> Open 30 - </span> 31 - </a> 32 - <a rel="save"> 33 - <span class="with-icon"> 34 - <i class="ph-fill ph-download"></i> Save 35 - </span> 36 - </a> 37 - <a rel="fork"> 38 - <span class="with-icon"> 39 - <i class="ph-fill ph-cursor-text"></i> Edit 40 - </span> 41 - </a> 42 11 </div> 43 12 </li> 44 13 {{ /for }}
+1 -1
src/_includes/layouts/facets-category.vto
··· 6 6 {{ await comp.facets.grid({ id: slug, items: categoryFacets }) }} 7 7 </section> 8 8 9 - <script src="facets/pages/save-fork.js" type="module"></script> 9 + <script src="facets/pages/grid-toggle.js" type="module"></script>
+79
src/facets/pages/grid-toggle.js
··· 1 + import { effect } from "~/common/signal.js"; 2 + import * as Output from "~/common/output.js"; 3 + import foundation from "~/common/facets/foundation.js"; 4 + import { facetFromURI } from "~/common/facets/utils.js"; 5 + 6 + //////////////////////////////////////////// 7 + // TOGGLE BUTTONS 8 + //////////////////////////////////////////// 9 + 10 + const gridItems = /** @type {NodeListOf<HTMLLIElement>} */ ( 11 + document.querySelectorAll(".grid li") 12 + ); 13 + 14 + for (const li of gridItems) { 15 + const container = li.querySelector("div[style]"); 16 + if (!container) continue; 17 + 18 + const button = document.createElement("button"); 19 + button.className = "button--transparent"; 20 + button.style.cssText = "font-size: var(--fs-md); opacity: 0; padding: 0;"; 21 + button.innerHTML = `<i class="ph-fill ph-toggle-left"></i>`; 22 + 23 + button.addEventListener("click", async (event) => { 24 + event.preventDefault(); 25 + 26 + const uri = li.getAttribute("data-uri"); 27 + const name = li.getAttribute("data-name"); 28 + if (!uri || !name) return; 29 + 30 + const out = foundation.orchestrator.output(); 31 + await Output.waitUntilLoaded(out.facets); 32 + 33 + const collection = out.facets.collection(); 34 + const isActive = collection.some((f) => f.uri === uri); 35 + 36 + if (isActive) { 37 + out.facets.save(collection.filter((f) => f.uri !== uri)); 38 + } else { 39 + const facet = await facetFromURI({ name, uri }, { fetchHTML: false }); 40 + out.facets.save([...collection, facet]); 41 + } 42 + }); 43 + 44 + container.appendChild(button); 45 + } 46 + 47 + //////////////////////////////////////////// 48 + // SYNC ACTIVE STATES 49 + //////////////////////////////////////////// 50 + 51 + const out = foundation.orchestrator.output(); 52 + 53 + effect(() => { 54 + const collection = out.facets.collection(); 55 + if (out.facets.state() !== "loaded") return; 56 + 57 + const activeURIs = new Set(collection.map((f) => f.uri)); 58 + 59 + for (const li of gridItems) { 60 + const uri = li.getAttribute("data-uri"); 61 + const button = 62 + /** @type {HTMLElement | null} */ (li.querySelector("button")); 63 + const icon = button?.querySelector("i"); 64 + if (!button || !icon || !uri) continue; 65 + 66 + button.style.opacity = "revert-layer"; 67 + 68 + const isActive = activeURIs.has(uri); 69 + button.title = isActive 70 + ? "Remove from your collection" 71 + : "Add to your collection"; 72 + icon.className = isActive 73 + ? "ph-fill ph-toggle-right" 74 + : "ph-fill ph-toggle-left"; 75 + /** @type {HTMLElement} */ (icon).style.color = isActive 76 + ? "var(--accent-twist-2)" 77 + : ""; 78 + } 79 + });
+13 -7
src/facets/you.js
··· 29 29 30 30 const h = col.length && state === "loaded" 31 31 ? html` 32 - <ul style="margin: 0; max-width: none;"> 33 - ${col.map((c) => 32 + <ul class="grid" style="margin: 0"> 33 + ${col.map((c, index) => 34 34 keyed( 35 35 c.id, 36 36 html` 37 - <li style="margin-bottom: var(--space-sm)"> 38 - <div style="position: relative;"> 39 - <a href="facets/l/?id=${c.id}"> 37 + <li> 38 + <div 39 + style="display: flex; align-items: center; gap: var(--space-xs); justify-content: space-between; position: relative;" 40 + > 41 + <a 42 + href="facets/l/?id=${c 43 + .id}" 44 + style="display: inline-block; padding: var(--space-3xs) 0" 45 + > 40 46 ${c.name} 41 47 </a> 42 48 <button 43 - class="button--fixed button--transparent" 49 + class="button--transparent" 44 50 popovertarget="facet-menu-col-${c.id}" 45 51 style="anchor-name: --facet-anchor-col-${c 46 - .id}; position: absolute; right: 0; top: 50%; transform: translateY(-50%);" 52 + .id}; font-size: var(--fs-base); padding: var(--space-3xs) 0;" 47 53 > 48 54 <i class="ph-fill ph-dots-three-circle"></i> 49 55 </button>
+2 -5
src/facets/you.vto
··· 6 6 7 7 <h1 hidden>Your collection</h1> 8 8 9 - <div class="columns" style="margin-top: var(--space-xl);"> 10 - <section class="flex"> 9 + <div style="margin-top: var(--space-lg);"> 10 + <section> 11 11 <div id="list"> 12 12 <div class="with-icon"> 13 13 <i class="ph-bold ph-spinner-gap"></i> 14 14 Loading items 15 15 </div> 16 16 </div> 17 - </section> 18 - 19 - <section class="flex"> 20 17 </section> 21 18 </div> 22 19
+6 -2
src/styles/diffuse/page.css
··· 137 137 */ 138 138 139 139 .dropdown { 140 - background: oklch(from var(--bg-color) calc(l - 0.05) c h); 140 + background: oklch(from var(--bg-color) calc(l + 0.2) c h); 141 141 border: 0; 142 142 border-radius: var(--radius-md); 143 - box-shadow: var(--box-shadow-lg); 143 + box-shadow: var(--box-shadow-xl); 144 144 color: var(--text-color); 145 145 font-size: var(--fs-sm); 146 146 margin: 0; ··· 149 149 position: fixed; 150 150 position-area: bottom span-left; 151 151 text-align: left; 152 + 153 + @media (prefers-color-scheme: dark) { 154 + background: oklch(from var(--bg-color) calc(l - 0.05) c h); 155 + } 152 156 153 157 &::backdrop { 154 158 background: transparent;