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: partial page rendering for facets pages

+506 -359
-2
src/_includes/layouts/facets-category.vto
··· 5 5 <section> 6 6 {{ await comp.facets.grid({ id: slug, items: categoryFacets }) }} 7 7 </section> 8 - 9 - <script src="facets/common/grid-toggle.js" type="module"></script>
+4
src/_includes/layouts/facets.vto
··· 6 6 styles: 7 7 - styles/base.css 8 8 - styles/diffuse/page.css 9 + - styles/diffuse/code-editor.css 9 10 - vendor/@phosphor-icons/bold/style.css 10 11 - vendor/@phosphor-icons/fill/style.css 12 + 13 + scripts: 14 + - facets/common/ppr.js 11 15 --- 12 16 13 17 <style>
-221
src/facets/build.js
··· 1 - import { basicSetup, EditorView } from "codemirror"; 2 - import { css as langCss } from "@codemirror/lang-css"; 3 - import { html as langHtml } from "@codemirror/lang-html"; 4 - import { javascript as langJs } from "@codemirror/lang-javascript"; 5 - import { autocompletion } from "@codemirror/autocomplete"; 6 - 7 - import * as TID from "@atcute/tid"; 8 - 9 - import * as CID from "~/common/cid.js"; 10 - import * as Output from "~/common/output.js"; 11 - import foundation from "~/common/facets/foundation.js"; 12 - import { facetFromURI } from "~/common/facets/utils.js"; 13 - import { loadURI } from "~/common/loader.js"; 14 - import { signal } from "~/common/signal.js"; 15 - 16 - /** 17 - * @import {Facet} from "~/definitions/types.d.ts" 18 - */ 19 - 20 - //////////////////////////////////////////// 21 - // BUILD 22 - //////////////////////////////////////////// 23 - 24 - const output = foundation.orchestrator.output(); 25 - const $editingFacet = signal(/** @type {Facet | null} */ (null)); 26 - 27 - // Code editor 28 - const editorContainer = document.body.querySelector("#html-input-container"); 29 - if (!editorContainer) throw new Error("Editor container not found"); 30 - 31 - const editor = new EditorView({ 32 - parent: editorContainer, 33 - doc: ` 34 - <main> 35 - <h1 id="now-playing"> 36 - Waiting on tracks &amp; queue to load ... 37 - </h1> 38 - </main> 39 - 40 - <style> 41 - @import "./styles/base.css"; 42 - @import "./styles/diffuse/page.css"; 43 - </style> 44 - 45 - <script type="module"> 46 - import foundation from "~/common/facets/foundation.js"; 47 - import { effect } from "~/common/signal.js"; 48 - 49 - const components = foundation.features.fillQueueAutomatically(); 50 - const myHtmlElement = document.querySelector("#now-playing"); 51 - 52 - effect(() => { 53 - const now = components.engine.queue.now(); 54 - const currentlyPlaying = now ? components.orchestrator.output.tracks.collection().find(t => t.id === now.id) : undefined; 55 - if (currentlyPlaying && myHtmlElement) { 56 - myHtmlElement.innerText = \`\$\{currentlyPlaying.tags.artist} - \$\{currentlyPlaying.tags.title}\`; 57 - } 58 - }) 59 - </script> 60 - `.trim(), 61 - extensions: [ 62 - basicSetup, 63 - langHtml(), 64 - langCss(), 65 - langJs(), 66 - autocompletion(), 67 - ], 68 - }); 69 - 70 - // Form submit 71 - document.querySelector("#build-form")?.addEventListener( 72 - "submit", 73 - onBuildSubmit, 74 - ); 75 - 76 - /** 77 - * @param {Event} event 78 - */ 79 - async function onBuildSubmit(event) { 80 - event.preventDefault(); 81 - 82 - const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 83 - "#name-input", 84 - )); 85 - 86 - const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( 87 - document.querySelector("#description-input") 88 - ); 89 - 90 - const html = editor.state.doc.toString(); 91 - const cid = await CID.create(0x55, new TextEncoder().encode(html)); 92 - const name = nameEl?.value ?? "nameless"; 93 - const description = descriptionEl?.value ?? ""; 94 - 95 - /** @type {Facet} */ 96 - const facet = $editingFacet.value 97 - ? { 98 - ...$editingFacet.value, 99 - cid, 100 - description, 101 - html, 102 - name, 103 - } 104 - : { 105 - $type: "sh.diffuse.output.facet", 106 - id: TID.now(), 107 - cid, 108 - description, 109 - html, 110 - name, 111 - }; 112 - 113 - switch (/** @type {any} */ (event).submitter.name) { 114 - case "save": 115 - await saveFacet(facet); 116 - break; 117 - case "save+open": 118 - await saveFacet(facet); 119 - globalThis.open(`./facets/l/?id=${facet.id}`, "blank"); 120 - break; 121 - } 122 - } 123 - 124 - /** 125 - * @param {Facet} ogFacet 126 - */ 127 - async function editFacet(ogFacet) { 128 - const facet = { ...ogFacet }; 129 - const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 130 - "#name-input", 131 - )); 132 - 133 - const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( 134 - document.querySelector("#description-input") 135 - ); 136 - 137 - if (!nameEl) return; 138 - 139 - // Scroll to builder 140 - document.querySelector("#build")?.scrollIntoView(); 141 - 142 - // Make sure HTML is loaded 143 - if (!facet.html && facet.uri) { 144 - const html = await loadURI(facet.uri); 145 - const cid = await CID.create(0x55, new TextEncoder().encode(html)); 146 - 147 - facet.html = html; 148 - facet.cid = cid; 149 - } 150 - 151 - $editingFacet.value = facet; 152 - nameEl.value = facet.name; 153 - 154 - if (descriptionEl) { 155 - descriptionEl.value = facet.description ?? ""; 156 - } 157 - 158 - editor.dispatch({ 159 - changes: { from: 0, to: editor.state.doc.length, insert: facet.html }, 160 - }); 161 - } 162 - 163 - /** 164 - * @param {Facet} facet 165 - */ 166 - 167 - async function saveFacet(facet) { 168 - await Output.waitUntilLoaded(output.facets); 169 - 170 - const col = output.facets.collection(); 171 - const colWithoutId = col.filter((c) => c.id !== facet.id); 172 - await output.facets.save([...colWithoutId, { 173 - ...facet, 174 - updatedAt: new Date().toISOString(), 175 - }]); 176 - } 177 - 178 - //////////////////////////////////////////// 179 - // SAVE & FORK 180 - //////////////////////////////////////////// 181 - 182 - document.body.addEventListener( 183 - "click", 184 - /** 185 - * @param {MouseEvent} event 186 - */ 187 - async (event) => { 188 - const target = /** @type {HTMLElement} */ (event.target); 189 - const rel = target.getAttribute("rel"); 190 - if (!rel) return; 191 - 192 - const uri = target.closest("li")?.getAttribute("data-uri"); 193 - if (!uri) return; 194 - 195 - const name = target.closest("li")?.getAttribute("data-name"); 196 - if (!name) return; 197 - 198 - switch (rel) { 199 - case "edit": { 200 - const facet = await facetFromURI({ name, uri }, { fetchHTML: true }); 201 - editFacet(facet); 202 - document.querySelector("#build")?.scrollIntoView(); 203 - break; 204 - } 205 - } 206 - }, 207 - ); 208 - 209 - //////////////////////////////////////////// 210 - // 🚀 211 - //////////////////////////////////////////// 212 - 213 - await Output.waitUntilLoaded(output.facets); 214 - 215 - // Load facet from url 216 - const idParam = new URLSearchParams(location.search).get("id"); 217 - 218 - if (idParam) { 219 - const facet = output.facets.collection().find((f) => f.id === idParam); 220 - if (facet) await editFacet(facet); 221 - }
-10
src/facets/build.vto
··· 3 3 base: ../../ 4 4 title: Build | Facets | Diffuse 5 5 6 - styles: 7 - - styles/base.css 8 - - styles/diffuse/page.css 9 - - styles/diffuse/code-editor.css 10 - - vendor/@phosphor-icons/bold/style.css 11 - - vendor/@phosphor-icons/fill/style.css 12 - 13 - scripts: 14 - - facets/build.js 15 - 16 6 examples: 17 7 - url: "facets/examples/now-playing/index.html" 18 8 title: "Now playing"
+245
src/facets/common/build.js
··· 1 + import { basicSetup, EditorView } from "codemirror"; 2 + import { css as langCss } from "@codemirror/lang-css"; 3 + import { html as langHtml } from "@codemirror/lang-html"; 4 + import { javascript as langJs } from "@codemirror/lang-javascript"; 5 + import { autocompletion } from "@codemirror/autocomplete"; 6 + 7 + import * as TID from "@atcute/tid"; 8 + 9 + import * as CID from "~/common/cid.js"; 10 + import * as Output from "~/common/output.js"; 11 + import foundation from "~/common/facets/foundation.js"; 12 + import { facetFromURI } from "~/common/facets/utils.js"; 13 + import { loadURI } from "~/common/loader.js"; 14 + import { signal } from "~/common/signal.js"; 15 + 16 + import { saveFacet } from "./crud.js"; 17 + 18 + /** 19 + * @import {Facet} from "~/definitions/types.d.ts" 20 + */ 21 + 22 + const $editor = signal(/** @type {EditorView | null} */ (null)); 23 + const $editingFacet = signal(/** @type {Facet | null} */ (null)); 24 + const output = foundation.orchestrator.output(); 25 + 26 + //////////////////////////////////////////// 27 + // EDITOR 28 + //////////////////////////////////////////// 29 + 30 + export function renderEditor() { 31 + // Code editor 32 + const editorContainer = document.body.querySelector("#html-input-container"); 33 + if (!editorContainer) throw new Error("Editor container not found"); 34 + 35 + const editor = new EditorView({ 36 + parent: editorContainer, 37 + doc: ` 38 + <main> 39 + <h1 id="now-playing"> 40 + Waiting on tracks &amp; queue to load ... 41 + </h1> 42 + </main> 43 + 44 + <style> 45 + @import "./styles/base.css"; 46 + @import "./styles/diffuse/page.css"; 47 + </style> 48 + 49 + <script type="module"> 50 + import foundation from "~/common/facets/foundation.js"; 51 + import { effect } from "~/common/signal.js"; 52 + 53 + const components = foundation.features.fillQueueAutomatically(); 54 + const myHtmlElement = document.querySelector("#now-playing"); 55 + 56 + effect(() => { 57 + const now = components.engine.queue.now(); 58 + const currentlyPlaying = now ? components.orchestrator.output.tracks.collection().find(t => t.id === now.id) : undefined; 59 + if (currentlyPlaying && myHtmlElement) { 60 + myHtmlElement.innerText = \`\$\{currentlyPlaying.tags.artist} - \$\{currentlyPlaying.tags.title}\`; 61 + } 62 + }) 63 + </script> 64 + `.trim(), 65 + extensions: [ 66 + basicSetup, 67 + langHtml(), 68 + langCss(), 69 + langJs(), 70 + autocompletion(), 71 + ], 72 + }); 73 + 74 + $editor.value = editor; 75 + return editor; 76 + } 77 + 78 + //////////////////////////////////////////// 79 + // FORM 80 + //////////////////////////////////////////// 81 + 82 + /** 83 + * @param {EditorView} editor 84 + */ 85 + const onBuildSubmit = (editor) => 86 + /** 87 + * @param {Event} event 88 + */ 89 + async (event) => { 90 + event.preventDefault(); 91 + 92 + const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 93 + "#name-input", 94 + )); 95 + 96 + const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( 97 + document.querySelector("#description-input") 98 + ); 99 + 100 + const html = editor.state.doc.toString(); 101 + const cid = await CID.create(0x55, new TextEncoder().encode(html)); 102 + const name = nameEl?.value ?? "nameless"; 103 + const description = descriptionEl?.value ?? ""; 104 + 105 + /** @type {Facet} */ 106 + const facet = $editingFacet.value 107 + ? { 108 + ...$editingFacet.value, 109 + cid, 110 + description, 111 + html, 112 + name, 113 + } 114 + : { 115 + $type: "sh.diffuse.output.facet", 116 + id: TID.now(), 117 + cid, 118 + description, 119 + html, 120 + name, 121 + }; 122 + 123 + switch (/** @type {any} */ (event).submitter.name) { 124 + case "save": 125 + await saveFacet(facet); 126 + break; 127 + case "save+open": 128 + await saveFacet(facet); 129 + globalThis.open(`./facets/l/?id=${facet.id}`, "blank"); 130 + break; 131 + } 132 + }; 133 + 134 + /** 135 + * @param {Facet} ogFacet 136 + */ 137 + async function editFacet(ogFacet) { 138 + const facet = { ...ogFacet }; 139 + const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 140 + "#name-input", 141 + )); 142 + 143 + const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( 144 + document.querySelector("#description-input") 145 + ); 146 + 147 + if (!nameEl) return; 148 + 149 + // Reset url — remove `id` param if not matching the facet 150 + const url = new URL(location.href); 151 + const id = url.searchParams.get("id"); 152 + 153 + if (id && facet.id !== id) { 154 + url.searchParams.delete("id"); 155 + history.replaceState(null, "", url); 156 + } 157 + 158 + // Scroll to builder 159 + document.querySelector("#build")?.scrollIntoView(); 160 + 161 + // Make sure HTML is loaded 162 + if (!facet.html && facet.uri) { 163 + const html = await loadURI(facet.uri); 164 + const cid = await CID.create(0x55, new TextEncoder().encode(html)); 165 + 166 + facet.html = html; 167 + facet.cid = cid; 168 + } 169 + 170 + $editingFacet.value = facet; 171 + nameEl.value = facet.name; 172 + 173 + if (descriptionEl) { 174 + descriptionEl.value = facet.description ?? ""; 175 + } 176 + 177 + const editor = $editor.value; 178 + editor?.dispatch({ 179 + changes: { from: 0, to: editor.state.doc.length, insert: facet.html }, 180 + }); 181 + } 182 + 183 + export function handleBuildFormSubmit() { 184 + const editor = $editor.value; 185 + if (!editor) return; 186 + 187 + document.querySelector("#build-form")?.addEventListener( 188 + "submit", 189 + onBuildSubmit(editor), 190 + ); 191 + } 192 + 193 + //////////////////////////////////////////// 194 + // EDIT EXAMPLES 195 + //////////////////////////////////////////// 196 + 197 + let isListening = false; 198 + 199 + export function listenForExamplesEdit() { 200 + if (isListening) return; 201 + isListening = true; 202 + 203 + document.body.addEventListener( 204 + "click", 205 + /** 206 + * @param {MouseEvent} event 207 + */ 208 + async (event) => { 209 + const target = /** @type {HTMLElement} */ (event.target); 210 + const rel = target.getAttribute("rel"); 211 + if (!rel) return; 212 + 213 + const uri = target.closest("li")?.getAttribute("data-uri"); 214 + if (!uri) return; 215 + 216 + const name = target.closest("li")?.getAttribute("data-name"); 217 + if (!name) return; 218 + 219 + switch (rel) { 220 + case "edit": { 221 + const facet = await facetFromURI({ name, uri }, { fetchHTML: true }); 222 + editFacet(facet); 223 + document.querySelector("#build")?.scrollIntoView(); 224 + break; 225 + } 226 + } 227 + }, 228 + ); 229 + } 230 + 231 + //////////////////////////////////////////// 232 + // EDIT FACET FROM URL 233 + //////////////////////////////////////////// 234 + 235 + export async function editFacetFromURL() { 236 + await Output.waitUntilLoaded(output.facets); 237 + 238 + // Load facet from url 239 + const idParam = new URLSearchParams(location.search).get("id"); 240 + 241 + if (idParam) { 242 + const facet = output.facets.collection().find((f) => f.id === idParam); 243 + if (facet) await editFacet(facet); 244 + } 245 + }
+38
src/facets/common/crud.js
··· 1 + import * as Output from "~/common/output.js"; 2 + import foundation from "~/common/facets/foundation.js"; 3 + 4 + /** 5 + * @import {Facet} from "~/definitions/types.d.ts" 6 + */ 7 + 8 + /** 9 + * @param {{ id: string }} _ 10 + */ 11 + export function deleteFacet({ id }) { 12 + return async () => { 13 + const c = confirm("Are you sure you want to delete this facet?"); 14 + if (!c) return; 15 + 16 + const output = foundation.orchestrator.output(); 17 + await Output.waitUntilLoaded(output.facets); 18 + 19 + output.facets.save( 20 + output.facets.collection().filter((c) => !(c.id === id)), 21 + ); 22 + }; 23 + } 24 + 25 + /** 26 + * @param {Facet} facet 27 + */ 28 + export async function saveFacet(facet) { 29 + const output = foundation.orchestrator.output(); 30 + await Output.waitUntilLoaded(output.facets); 31 + 32 + const col = output.facets.collection(); 33 + const colWithoutId = col.filter((c) => c.id !== facet.id); 34 + await output.facets.save([...colWithoutId, { 35 + ...facet, 36 + updatedAt: new Date().toISOString(), 37 + }]); 38 + }
-79
src/facets/common/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(".grid-item__title"); 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 - });
+92
src/facets/common/grid.js
··· 1 + import * as Output from "~/common/output.js"; 2 + import foundation from "~/common/facets/foundation.js"; 3 + import { facetFromURI } from "~/common/facets/utils.js"; 4 + import { effect } from "~/common/signal.js"; 5 + 6 + //////////////////////////////////////////// 7 + // TOGGLE BUTTONS 8 + //////////////////////////////////////////// 9 + 10 + export function insertToggleButtons() { 11 + const gridItems = /** @type {NodeListOf<HTMLLIElement>} */ ( 12 + document.querySelectorAll(".grid li") 13 + ); 14 + 15 + for (const li of gridItems) { 16 + const container = li.querySelector(".grid-item__title"); 17 + if (!container) continue; 18 + 19 + const button = document.createElement("button"); 20 + button.className = "button--transparent"; 21 + button.style.cssText = "font-size: var(--fs-md); opacity: 0; padding: 0;"; 22 + button.innerHTML = `<i class="ph-fill ph-toggle-left"></i>`; 23 + 24 + button.addEventListener("click", async (event) => { 25 + event.preventDefault(); 26 + 27 + const uri = li.getAttribute("data-uri"); 28 + const name = li.getAttribute("data-name"); 29 + if (!uri || !name) return; 30 + 31 + const out = foundation.orchestrator.output(); 32 + await Output.waitUntilLoaded(out.facets); 33 + 34 + const collection = out.facets.collection(); 35 + const isActive = collection.some((f) => f.uri === uri); 36 + 37 + if (isActive) { 38 + out.facets.save(collection.filter((f) => f.uri !== uri)); 39 + } else { 40 + const facet = await facetFromURI({ name, uri }, { fetchHTML: false }); 41 + out.facets.save([...collection, facet]); 42 + } 43 + }); 44 + 45 + container.appendChild(button); 46 + } 47 + } 48 + 49 + //////////////////////////////////////////// 50 + // SYNC ACTIVE STATES 51 + //////////////////////////////////////////// 52 + 53 + /** @type {() => void | undefined} */ 54 + let stopMonitor; 55 + 56 + export async function monitorToggleButtonStates() { 57 + if (stopMonitor) stopMonitor(); 58 + 59 + const out = foundation.orchestrator.output(); 60 + await Output.waitUntilLoaded(out.facets); 61 + 62 + stopMonitor = effect(() => { 63 + const gridItems = /** @type {NodeListOf<HTMLLIElement>} */ ( 64 + document.querySelectorAll(".grid li") 65 + ); 66 + 67 + const collection = out.facets.collection(); 68 + const activeURIs = new Set(collection.map((f) => f.uri)); 69 + 70 + for (const li of gridItems) { 71 + const uri = li.getAttribute("data-uri"); 72 + const button = 73 + /** @type {HTMLElement | null} */ (li.querySelector("button")); 74 + const icon = button?.querySelector("i"); 75 + 76 + if (!button || !icon || !uri) continue; 77 + 78 + button.style.opacity = "revert-layer"; 79 + 80 + const isActive = activeURIs.has(uri); 81 + button.title = isActive 82 + ? "Remove from your collection" 83 + : "Add to your collection"; 84 + icon.className = isActive 85 + ? "ph-fill ph-toggle-right" 86 + : "ph-fill ph-toggle-left"; 87 + /** @type {HTMLElement} */ (icon).style.color = isActive 88 + ? "var(--accent-twist-2)" 89 + : ""; 90 + } 91 + }); 92 + }
+99
src/facets/common/ppr.js
··· 1 + import * as Build from "./build.js"; 2 + import * as Grid from "./grid.js"; 3 + import * as You from "./you.js"; 4 + 5 + /** 6 + * @param {URL} url 7 + */ 8 + async function initJsBasedOnPage(url) { 9 + const path = url.pathname.replace(/(\/$)/, ""); 10 + 11 + Grid.insertToggleButtons(); 12 + await Grid.monitorToggleButtonStates(); 13 + 14 + switch (path) { 15 + case "/facets/build": 16 + Build.renderEditor(); 17 + Build.handleBuildFormSubmit(); 18 + Build.listenForExamplesEdit(); 19 + await Build.editFacetFromURL(); 20 + break; 21 + case "/facets/you": 22 + await You.renderList(); 23 + break; 24 + default: 25 + break; 26 + } 27 + } 28 + 29 + initJsBasedOnPage(new URL(location.href)); 30 + 31 + // Partial page updates for facets navigation using the Navigation API. 32 + // Intercepts nav link clicks, fetches the new page, and swaps <main> content 33 + // instead of doing a full page load. 34 + 35 + if ("navigation" in globalThis) { 36 + /** @type {any} */ (globalThis).navigation.addEventListener( 37 + "navigate", 38 + navigateHandler, 39 + ); 40 + } 41 + 42 + /** @param {any} event */ 43 + function navigateHandler(event) { 44 + if (!event.canIntercept) return; 45 + 46 + const url = new URL(event.destination.url); 47 + if (url.origin !== location.origin) return; 48 + 49 + // Only intercept /facets/[section]/ paths (not deeper sub-paths like /facets/tools/*) 50 + const parts = url.pathname.split("/").filter(Boolean); 51 + if (parts[0] !== "facets") return; 52 + if (parts.length > 2) return; 53 + 54 + // Skip the loader page 55 + if (parts[1] === "l") return; 56 + 57 + event.intercept({ 58 + scroll: "manual", 59 + async handler() { 60 + let html; 61 + 62 + try { 63 + const response = await fetch(url); 64 + if (!response.ok) throw new Error(`${response.status}`); 65 + html = await response.text(); 66 + } catch { 67 + location.href = url.href; 68 + return; 69 + } 70 + 71 + const parser = new DOMParser(); 72 + const doc = parser.parseFromString(html, "text/html"); 73 + 74 + const newMain = doc.querySelector("main"); 75 + const currentMain = document.querySelector("main"); 76 + 77 + if (!newMain || !currentMain) { 78 + location.href = url.href; 79 + return; 80 + } 81 + 82 + document.title = doc.title; 83 + 84 + // Replace <main> content 85 + const range = document.createRange(); 86 + range.selectNode(currentMain); 87 + const documentFragment = range.createContextualFragment( 88 + newMain.innerHTML ?? "", 89 + ); 90 + 91 + currentMain.innerHTML = ""; 92 + currentMain.append(documentFragment); 93 + 94 + initJsBasedOnPage(url); 95 + 96 + window.scrollTo({ top: 0, behavior: "instant" }); 97 + }, 98 + }); 99 + }
-2
src/facets/index.vto
··· 9 9 <section> 10 10 {{ await comp.facets.grid({ id: "featured", items: facets.filter(f => f.featured) }) }} 11 11 </section> 12 - 13 - <script src="facets/common/grid-toggle.js" type="module"></script>
+28 -43
src/facets/you.js src/facets/common/you.js
··· 5 5 6 6 import * as Output from "~/common/output.js"; 7 7 import foundation from "~/common/facets/foundation.js"; 8 - import { effect } from "~/common/signal.js"; 9 8 import { nothing } from "~/common/element.js"; 10 9 11 - //////////////////////////////////////////// 12 - // YOUR COLLECTION 13 - //////////////////////////////////////////// 10 + import { deleteFacet } from "./crud.js"; 14 11 15 - /** @type {HTMLElement | null} */ 16 - const listEl = document.querySelector("#list"); 17 - if (!listEl) throw new Error("List element not found"); 12 + const EMPTY_FACETS_LIST = html` 13 + <div> 14 + <i class="ph-fill ph-info"></i> You have not saved any facets yet. 15 + </div> 16 + `; 18 17 19 - const output = foundation.orchestrator.output(); 18 + /** */ 19 + export async function renderList() { 20 + /** @type {HTMLElement | null} */ 21 + const listEl = document.querySelector("#list"); 22 + if (!listEl) throw new Error("List element not found"); 23 + listEl.innerHTML = ""; 20 24 21 - listEl.innerHTML = ""; 25 + const output = foundation.orchestrator.output(); 22 26 23 - effect(() => { 27 + if (output.facets.state() !== "loaded") { 28 + const loading = html` 29 + <div class="with-icon"> 30 + <i class="ph-bold ph-spinner-gap"></i> 31 + Loading items 32 + </div> 33 + `; 34 + 35 + render(loading, listEl); 36 + } 37 + 38 + await Output.waitUntilLoaded(output.facets); 39 + 24 40 const col = output.facets.collection().sort((a, b) => { 25 41 return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()); 26 42 }); 27 43 28 - const state = output.facets.state(); 29 - 30 - const h = col.length && state === "loaded" 44 + const h = col.length 31 45 ? html` 32 46 <ul class="grid" style="margin: 0"> 33 47 ${col.map((c, index) => ··· 95 109 )} 96 110 </ul> 97 111 ` 98 - : state === "loaded" 99 - ? emptyFacetsList 100 - : html` 101 - <div class="with-icon"> 102 - <i class="ph-bold ph-spinner-gap"></i> 103 - Loading items 104 - </div> 105 - `; 112 + : EMPTY_FACETS_LIST; 106 113 107 114 render(h, listEl); 108 - }); 109 - 110 - const emptyFacetsList = html` 111 - <div> 112 - <i class="ph-fill ph-info"></i> You have not saved any facets yet. 113 - </div> 114 - `; 115 - 116 - /** 117 - * @param {{ id: string }} _ 118 - */ 119 - function deleteFacet({ id }) { 120 - return async () => { 121 - const c = confirm("Are you sure you want to delete this facet?"); 122 - if (!c) return; 123 - 124 - await Output.waitUntilLoaded(output.facets); 125 - 126 - output.facets.save( 127 - output.facets.collection().filter((c) => !(c.id === id)), 128 - ); 129 - }; 130 115 }
-2
src/facets/you.vto
··· 16 16 </div> 17 17 </section> 18 18 </div> 19 - 20 - <script src="facets/you.js" type="module"></script>