import { Temporal } from "~/common/temporal.js"; import "@awesome.me/webawesome/dist/components/split-panel/split-panel.js"; import "@awesome.me/webawesome/dist/components/dialog/dialog.js"; import "@awesome.me/webawesome/dist/components/button/button.js"; import "@awesome.me/webawesome/dist/components/input/input.js"; import "@awesome.me/webawesome/dist/components/icon/icon.js"; import "@awesome.me/webawesome/dist/components/select/select.js"; import "@awesome.me/webawesome/dist/components/option/option.js"; import "~/common/webawesome/detect-dark.js"; import foundation from "~/common/foundation.js"; import * as Output from "~/common/output.js"; // Set doc title document.title = "Split View | Diffuse"; /** * @import { default as WaSplitPanel } from "@awesome.me/webawesome/dist/components/split-panel/split-panel.js" * @import { default as WaDialog } from "@awesome.me/webawesome/dist/components/dialog/dialog.js" * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" * @import { default as WaIcon } from "@awesome.me/webawesome/dist/components/icon/icon.js" * @import { default as WaSelect } from "@awesome.me/webawesome/dist/components/select/select.js" */ /** * @typedef {{ type: "pane", facet: string | null }} PaneNode * @typedef {{ type: "split", orientation: "horizontal" | "vertical", position: number, start: Node, end: Node }} SplitNode * @typedef {PaneNode | SplitNode} Node */ const STORAGE_KEY = "diffuse/facets/misc/split-view/builder/layout"; // ─── State ─────────────────────────────────────────────────────────────────── /** @type {Node} */ let state = loadState(); let editMode = false; /** @type {string | null} */ let pendingPaneId = null; /** @returns {Node} */ function loadState() { try { return /** @type {Node} */ (JSON.parse( localStorage.getItem(STORAGE_KEY) ?? "null", )) ?? defaultState(); } catch { return defaultState(); } } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } /** @returns {PaneNode} */ function defaultState() { return { type: "pane", facet: null }; } // ─── Tree helpers ───────────────────────────────────────────────────────────── // Nodes are identified by a dot-separated path: "root", "root.start", "root.end.start" /** * @param {string} path * @returns {Node} */ function getNode(path) { if (path === "root") return state; const parts = path.replace(/^root\./, "").split("."); /** @type {any} */ let node = state; for (const part of parts) node = node[part]; return /** @type {Node} */ (node); } /** * @param {string} path * @param {Node} newNode */ function replaceNode(path, newNode) { if (path === "root") { state = newNode; return; } const parts = path.split("."); const parentPath = parts.slice(0, -1).join("."); const childKey = parts[parts.length - 1]; const parent = /** @type {any} */ (getNode(parentPath)); parent[childKey] = newNode; } // ─── Mutations ──────────────────────────────────────────────────────────────── /** * @param {string} nodePath * @param {"horizontal" | "vertical"} orientation */ function splitPane(nodePath, orientation) { const existing = getNode(nodePath); replaceNode(nodePath, { type: "split", orientation, position: 50, start: existing, end: { type: "pane", facet: null }, }); saveState(); render(); } /** @param {string} nodePath */ function removePane(nodePath) { if (nodePath === "root") return; const parts = nodePath.split("."); const parentPath = parts.slice(0, -1).join("."); const childKey = parts[parts.length - 1]; const siblingKey = childKey === "start" ? "end" : "start"; const parent = /** @type {any} */ (getNode(parentPath)); const sibling = /** @type {Node} */ (parent[siblingKey]); replaceNode(parentPath, sibling); saveState(); render(); } /** * @param {string} nodePath * @param {string} facetPath */ function setFacet(nodePath, facetPath) { const pane = /** @type {PaneNode} */ (getNode(nodePath)); pane.facet = facetPath; saveState(); render(); } // ─── Rendering ──────────────────────────────────────────────────────────────── const layout = /** @type {HTMLElement} */ (document.querySelector("#layout")); function render() { layout.innerHTML = ""; layout.appendChild(renderNode(state, "root", true)); layout.classList.toggle("edit-mode", editMode); } /** * @param {Node} node * @param {string} path * @param {boolean} isRoot * @returns {HTMLElement} */ function renderNode(node, path, isRoot) { if (node.type === "split") { const panel = /** @type {WaSplitPanel} */ (document.createElement("wa-split-panel")); panel.position = node.position; if (node.orientation === "vertical") panel.orientation = "vertical"; const startSlot = document.createElement("div"); startSlot.slot = "start"; startSlot.appendChild(renderNode(node.start, path + ".start", false)); const endSlot = document.createElement("div"); endSlot.slot = "end"; endSlot.appendChild(renderNode(node.end, path + ".end", false)); panel.appendChild(startSlot); panel.appendChild(endSlot); panel.addEventListener("wa-reposition", () => { const splitNode = /** @type {SplitNode} */ (getNode(path)); splitNode.position = panel.position; saveState(); }); return panel; } // Pane const pane = document.createElement("div"); pane.className = "pane" + (node.facet ? "" : " pane--empty"); if (node.facet) { const iframe = document.createElement("iframe"); const uri = node.facet.includes("://") ? node.facet : `diffuse://${node.facet}`; iframe.src = "l/?uri=" + encodeURIComponent(uri); iframe.allow = "autoplay"; pane.appendChild(iframe); } const overlay = document.createElement("div"); overlay.className = "pane-overlay"; if (node.facet) { const title = document.querySelector(`#facet-select wa-option[value="${node.facet}"]`) ?.textContent?.trim() ?? node.facet; const label = document.createElement("div"); label.className = "pane-name"; label.style.fontWeight = "700"; label.textContent = title; overlay.appendChild(label); } overlay.appendChild( makeWaButton( node.facet ? "Change facet" : "+ Add facet", "neutral", "filled", () => openPicker(path), ), ); overlay.appendChild( makeWaButton( "Split left / right", "neutral", "outlined", () => splitPane(path, "horizontal"), ), ); overlay.appendChild( makeWaButton( "Split top / bottom", "neutral", "outlined", () => splitPane(path, "vertical"), ), ); if (!isRoot) { overlay.appendChild( makeWaButton("Remove", "danger", "outlined", () => removePane(path)), ); } pane.appendChild(overlay); return pane; } /** * @param {string} text * @param {string} variant * @param {string} appearance * @param {() => void} onClick * @returns {HTMLElement} */ function makeWaButton(text, variant, appearance, onClick) { const btn = document.createElement("wa-button"); btn.setAttribute("variant", variant); btn.setAttribute("appearance", appearance); btn.setAttribute("size", "small"); btn.style.width = "100%"; btn.textContent = text; btn.addEventListener("click", (e) => { e.stopPropagation(); onClick(); }); return btn; } // ─── Divider drag: disable iframe pointer events while dragging ─────────────── document.addEventListener("mousedown", (e) => { const isDivider = e.composedPath().some( (el) => el instanceof Element && el.getAttribute("part") === "divider", ); if (isDivider) layout.classList.add("dragging"); }, { capture: true }); document.addEventListener("mouseup", () => { layout.classList.remove("dragging"); }); // ─── Edit mode ──────────────────────────────────────────────────────────────── const editToggle = /** @type {HTMLElement} */ (document.querySelector("#edit-toggle")); const editIcon = /** @type {WaIcon} */ (document.querySelector("#edit-icon")); const saveCopyBtn = /** @type {HTMLElement} */ (document.querySelector("#save-copy-btn")); const saveCopyIcon = /** @type {WaIcon} */ (document.querySelector("#save-copy-icon")); editToggle.addEventListener("click", () => { editMode = !editMode; editToggle.setAttribute("aria-label", editMode ? "Done" : "Edit layout"); editIcon.name = editMode ? "xmark" : "border-all"; layout.classList.toggle("edit-mode", editMode); saveCopyBtn.style.display = editMode ? "" : "none"; }); saveCopyBtn.addEventListener("click", async () => { await saveSimplifiedCopy(); saveCopyIcon.name = "check"; setTimeout(() => { saveCopyIcon.name = "floppy-disk"; }, 2000); }); // ─── Facet picker ───────────────────────────────────────────────────────────── const pickerDialog = /** @type {WaDialog} */ (document.querySelector("#facet-picker")); const facetSelect = /** @type {WaSelect} */ (document.querySelector("#facet-select")); const customPath = /** @type {WaInput} */ (document.querySelector("#custom-path")); const customConfirm = /** @type {HTMLElement} */ (document.querySelector("#custom-confirm")); customConfirm.addEventListener("click", () => { const val = customPath.value?.trim() || /** @type {string} */ (facetSelect.value) || ""; if (val && pendingPaneId !== null) setFacet(pendingPaneId, val); pendingPaneId = null; pickerDialog.open = false; }); pickerDialog.addEventListener("wa-hide", (e) => { if (e.target === pickerDialog) pendingPaneId = null; }); /** @param {string} nodePath */ function openPicker(nodePath) { pendingPaneId = nodePath; facetSelect.value = null; customPath.value = ""; pickerDialog.open = true; } // ─── Save simplified copy ───────────────────────────────────────────────────── /** * @param {Node} node * @param {string} indent * @returns {string} */ function generateNodeHTML(node, indent = "") { const inner = indent + " "; if (node.type === "split") { const orientationAttr = node.orientation === "vertical" ? ' orientation="vertical"' : ""; return `${indent} ${inner}
${generateNodeHTML(node.start, inner + " ")} ${inner}
${inner}
${generateNodeHTML(node.end, inner + " ")} ${inner}
${indent}
`; } if (node.facet) { const uri = node.facet.includes("://") ? node.facet : `diffuse://${node.facet}`; const src = "l/?uri=" + encodeURIComponent(uri); return `${indent}
${inner} ${indent}
`; } return `${indent}
`; } /** * @param {string} id */ function generateSimplifiedHTML(id) { const scriptClose = ""; return `\
${generateNodeHTML(state, " ")}