app.addEventListener("click", async (event) => { const target = event.target.closest("[data-action]"); if (!target) return; const action = target.dataset.action; let repository = selectedRepository(); if (target.dataset.repositoryId) { const actionRepository = ui.repositories.find( (item) => String(item.id) === String(target.dataset.repositoryId), ); if (actionRepository) repository = actionRepository; } const handlers = [handleShellActions, handleInventoryActions, handleDeploymentProfileActions, handleDeploymentOperationActions, handleSetupAndSettingsActions, handleRecoveryActions, handleCommandActions]; for (const handler of handlers) if (await handler(event, target, action, repository)) return; }); app.addEventListener("input", (event) => { if (event.target.id === "global-search") { ui.search = event.target.value; scheduleInputRender(); } else if (event.target.id === "repo-filter") { ui.repoSearch = event.target.value; scheduleInputRender(); } else if (event.target.id === "commit-message") { ui.commitMessage = event.target.value; const repository = selectedRepository(); const hasSelection = Boolean(ui.selectedFiles.size || repository?.localStatus?.counts?.staged); const ready = Boolean(hasSelection && ui.commitMessage.trim()); const blocker = !hasSelection ? "Select files or stage one or more hunks." : ready ? ui.selectedFiles.size ? "Ready to commit. ForgeFlow stages the selected files automatically." : "Ready to commit only the reviewed staged hunks." : "Enter a commit message to enable commit and push."; const readiness = document.querySelector(".commit-readiness"); if (readiness) { readiness.classList.toggle("ready", ready); readiness.classList.toggle("blocked", !ready); readiness.innerHTML = `${icon(ready ? "check" : "warning")}${escapeHtml(blocker)}`; } for (const button of document.querySelectorAll('[data-action="commit-push"], [data-action="commit-only"]')) { button.disabled = !ready; if (ready) button.removeAttribute("title"); else button.title = blocker; } } else if (event.target.id === "setup-url") ui.setupDraft.baseUrl = event.target.value; else if (event.target.id === "setup-token") ui.setupDraft.token = event.target.value; else if (event.target.id === "palette-input") { ui.paletteQuery = event.target.value; scheduleInputRender(60); } else if (event.target.id === "help-search") { ui.helpQuery = event.target.value; scheduleInputRender(60); } }); app.addEventListener("change", async (event) => { if (event.target.matches("[data-file-select]")) { const filePath = event.target.dataset.fileSelect; if (event.target.checked) ui.selectedFiles.add(filePath); else ui.selectedFiles.delete(filePath); render(); } else if (event.target.id === "appearance-select") { ui.boot.state = await window.forgeflow.setAppearance(event.target.value); applyTheme(event.target.value); render(); } else if (event.target.id === "action-profile-select") { ui.selectedProfileId = event.target.value; render(); } else if (event.target.id === "validator-policy") { await handleShellActions(event, event.target, "git-validator-policy", selectedRepository()); } else if (event.target.id === "profile-provider") { ui.modal.provider = event.target.value; render(); } else if (event.target.id === "server-auth-type") { ui.modal.authType = event.target.value; render(); } }); document.addEventListener("keydown", (event) => { if ( (event.key === "Enter" || event.key === " ") && event.target.matches('.file-row[data-action="select-file"]') ) { event.preventDefault(); event.target.click(); return; } if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") { event.preventDefault(); ui.paletteQuery = ""; ui.modal = { type: "command-palette" }; render(); return; } if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "f" && ui.currentView === "help") { event.preventDefault(); document.querySelector("#help-search")?.focus(); return; } if ( (event.ctrlKey || event.metaKey) && event.key === "Enter" && ui.currentView === "repository" ) { const button = document.querySelector( '[data-action="commit-push"]:not(:disabled)', ); if (button) button.click(); } if (event.key === "F5") { event.preventDefault(); refreshRepositories(true); } if (event.key === "Escape" && ui.modal) { ui.modal = null; render(); } }); let pointerAnimationFrame = null; let pendingPointer = null; document.addEventListener("pointermove", (event) => { pendingPointer = { target: event.target, clientX: event.clientX, clientY: event.clientY }; if (pointerAnimationFrame) return; pointerAnimationFrame = requestAnimationFrame(() => { pointerAnimationFrame = null; const current = pendingPointer; pendingPointer = null; if (!current) return; const illustration = current.target.closest?.("[data-project-illustration]"); if (illustration) { const bounds = illustration.getBoundingClientRect(); illustration.style.setProperty("--tilt-x", `${((current.clientY - bounds.top) / bounds.height - 0.5) * -7}deg`); illustration.style.setProperty("--tilt-y", `${((current.clientX - bounds.left) / bounds.width - 0.5) * 9}deg`); } const diffPanel = current.target.closest?.(".diff-panel"); const atmosphere = diffPanel?.querySelector("[data-diff-atmosphere]"); if (atmosphere) { const bounds = diffPanel.getBoundingClientRect(); atmosphere.style.setProperty("--diff-tilt-x", `${((current.clientY - bounds.top) / bounds.height - 0.5) * -3}deg`); atmosphere.style.setProperty("--diff-tilt-y", `${((current.clientX - bounds.left) / bounds.width - 0.5) * 4}deg`); } }); }); document.addEventListener("pointerout", (event) => { const illustration = event.target.closest?.("[data-project-illustration]"); if (illustration && !illustration.contains(event.relatedTarget)) { illustration.style.removeProperty("--tilt-x"); illustration.style.removeProperty("--tilt-y"); } const diffPanel = event.target.closest?.(".diff-panel"); if (diffPanel && !diffPanel.contains(event.relatedTarget)) { const atmosphere = diffPanel.querySelector("[data-diff-atmosphere]"); atmosphere?.style.removeProperty("--diff-tilt-x"); atmosphere?.style.removeProperty("--diff-tilt-y"); } }); window.addEventListener("error", (event) => { window.forgeflow .reportRendererEvent?.("error", "uncaught-error", { message: event.message, filename: event.filename, line: event.lineno, column: event.colno, stack: event.error?.stack, }) .catch(() => {}); }); window.addEventListener("unhandledrejection", (event) => { const reason = event.reason; window.forgeflow .reportRendererEvent?.("error", "unhandled-rejection", { message: reason?.message || String(reason || "Unknown rejection"), stack: reason?.stack, }) .catch(() => {}); }); bootstrap();