diff --git a/app.js b/app.js index 3e2849e..a5a31ce 100644 --- a/app.js +++ b/app.js @@ -88,6 +88,7 @@ import { renderMarkdownAudit, } from "./src/auditRenderer.js"; import { initComposer } from "./src/composerAdapter.js"; +import { cancelSurfaceMotion, currentSurfaceRect, morphSurface, exitHero } from "./src/surfaceMotion.js"; // --- CONFIGURATION --- const IS_DEV = import.meta.env.DEV @@ -5008,6 +5009,7 @@ function restoreAndShowReplacementFailure(error, { previous, stage, runId, retry setGenerationStageVisible(true); showResultsView(); updateSelectedArtifactPanels(); + cancelPipelineMorph(); showReplacementFailure(error, { stage, runId, retry }); updateDeployButtonVisibility(); } @@ -5364,8 +5366,10 @@ async function runThinkingPipeline() { // Show pipeline progress bar. The outline morph runs over the panel once // the panel itself is in place, so busy feedback is immediate either way. + const composerRect = document.getElementById("composer")?.getBoundingClientRect(); + const heroRect = document.getElementById("hero")?.getBoundingClientRect(); showPipelineProgress({ prompt: userInput, runId }); - morphComposerToPipeline(); + morphComposerToPipeline(composerRect, heroRect); // Step 1: Prompt Architect selectWorkflowStep(1); @@ -5445,7 +5449,6 @@ async function runThinkingPipeline() { // panel into it and hand the outline morph over to the expansion. hidePipelineProgress(); const auditHtml = renderMarkdownAudit(pipelineState.step3Result); - morphPipelineToResults(); showResultsView(cleanStep2, auditHtml); } catch (error) { // A cancelled request belongs to a run the user already abandoned; it is @@ -7139,6 +7142,7 @@ document.addEventListener("DOMContentLoaded", async () => { // button's listener exists while auth/subscription requests are still in // flight; runThinkingPipeline waits on sessionReadiness before checking // entitlements. + initExactShell(); composerControls = initComposer({ onSubmit: runThinkingPipeline }); // Startup auth + subscription share one promise: the pipeline awaits it so @@ -8080,6 +8084,7 @@ window.advanceWalkthrough = advanceWalkthrough; window.commitToFlutterFlow = commitToFlutterFlow; function focusPromptInput() { + if (!pipelineState.isRunning) setGenerationStageVisible(false); const input = document.getElementById("pipeline-input"); if (input) { input.focus(); @@ -8087,9 +8092,8 @@ function focusPromptInput() { } function openModelSelector() { - const details = document.getElementById("advanced-settings"); - if (details) details.open = true; - const select = document.getElementById("code-generator-model"); + openComposerSettings(); + const select = document.getElementById("composer-settings-model"); if (select) { select.scrollIntoView({ behavior: "smooth", block: "center" }); select.focus(); @@ -8395,106 +8399,29 @@ function pipelineMorphDurationMs() { return Number.isFinite(parsed) && parsed > 0 ? parsed : PIPELINE_MORPH_FALLBACK_MS; } -let pipelineMorphGhost = null; -let pipelineMorphAnimation = null; - function prefersReducedMotion() { return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; } function cancelPipelineMorph() { - if (pipelineMorphAnimation) { - try { - pipelineMorphAnimation.cancel(); - } catch { - /* an already-finished animation cannot be cancelled */ - } - pipelineMorphAnimation = null; - } - if (pipelineMorphGhost) { - pipelineMorphGhost.remove(); - pipelineMorphGhost = null; - } - window.removeEventListener("resize", cancelPipelineMorph); + cancelSurfaceMotion(); } -function runOutlineMorph(fromEl, toEl) { - cancelPipelineMorph(); - if (prefersReducedMotion() || !fromEl || !toEl || !document.body) return; - if (typeof Element.prototype.animate !== "function") return; - - const from = fromEl.getBoundingClientRect(); - const to = toEl.getBoundingClientRect(); - if (!from.width || !from.height || !to.width || !to.height) return; - - const ghost = document.createElement("div"); - ghost.className = "composer-morph"; - ghost.setAttribute("aria-hidden", "true"); - ghost.style.cssText = `left:${to.left}px;top:${to.top}px;width:${to.width}px;height:${to.height}px;opacity:0;`; - document.body.appendChild(ghost); - - const dx = from.left + from.width / 2 - (to.left + to.width / 2); - const dy = from.top + from.height / 2 - (to.top + to.height / 2); - const sx = from.width / to.width; - const sy = from.height / to.height; - const base = `translate(${dx}px,${dy}px) scale(${sx},${sy})`; - - // 0-14%: the outline eases in over the surface it is leaving. 14-28%: the - // anticipatory shrink. 28-100%: one longer growth that settles with weight. - const animation = ghost.animate( - [ - { transform: base, opacity: 0, offset: 0, easing: "cubic-bezier(.2,0,0,1)" }, - { transform: base, opacity: 1, offset: 0.14, easing: "cubic-bezier(.4,0,.2,1)" }, - { - transform: `translate(${dx}px,${dy}px) scale(${sx * 0.93},${sy * 0.93})`, - opacity: 1, - offset: 0.28, - easing: "cubic-bezier(.32,1.38,.5,1)", - }, - { transform: "translate(0px,0px) scale(1,1)", opacity: 1, offset: 1 }, - ], - { duration: pipelineMorphDurationMs(), fill: "both" }, - ); - - pipelineMorphGhost = ghost; - pipelineMorphAnimation = animation; - window.addEventListener("resize", cancelPipelineMorph); - - animation.onfinish = () => { - if (pipelineMorphGhost !== ghost) return; - const out = ghost.animate([{ opacity: 1 }, { opacity: 0 }], { - duration: 260, - fill: "forwards", - }); - out.onfinish = () => { - if (pipelineMorphGhost === ghost) cancelPipelineMorph(); - }; - }; +function pipelineRevealNodes() { + return [".pipeline-prompt-kicker", "#pipeline-submitted-prompt", "#pipeline-edit-prompt", ".pipeline-glyph", "#progress-title-text", "#progress-description-text", ".progress-track", ".progress-meta"].map((selector) => document.querySelector(selector)); } -/** Composer -> generation panel. */ -function morphComposerToPipeline() { - const from = document.getElementById("composer"); - const fromRect = from?.getBoundingClientRect(); - if (!fromRect) return; - // The generation stage only gains a box once it is revealed a frame later, - // so the outline is measured against the panel that is actually on screen. - requestAnimationFrame(() => { - requestAnimationFrame(() => { - runOutlineMorph( - { getBoundingClientRect: () => fromRect }, - document.getElementById("main-stage-container"), - ); - }); - }); +function togglePipelineStageNavigation() { + const nav = document.getElementById("pipeline-stage-nav"); + const open = nav.classList.toggle("is-open"); + document.getElementById("progress-stage-count").setAttribute("aria-expanded", String(open)); } +window.togglePipelineStageNavigation = togglePipelineStageNavigation; -/** Generation panel -> expanded Results view. */ -function morphPipelineToResults() { - runOutlineMorph( - document.getElementById("pipeline-progress"), - document.getElementById("main-stage-container"), - ); +/** Composer -> generation panel. */ +function morphComposerToPipeline(from, heroRect) { + morphSurface(from, document.getElementById("main-stage-container"), pipelineRevealNodes()); + exitHero(document.getElementById("hero"), heroRect); } // Navigating away abandons the hand-off rather than leaving an orphaned @@ -8507,6 +8434,7 @@ function showPipelineProgress(options = {}) { if (!isCurrentPipelineRun(runId)) return; // A previous run's delayed hide must not blank this run's panel. cancelPipelineHideTimer(); + cancelPipelineMorph(); setGenerationStageVisible(true); const progress = document.getElementById("pipeline-progress"); @@ -8517,6 +8445,8 @@ function showPipelineProgress(options = {}) { if (resultsView) resultsView.classList.remove("visible"); document.body.classList.remove("results-fullscreen", "results-with-sidebar"); if (progress) progress.classList.add("visible"); + document.getElementById("pipeline-stage-nav")?.classList.remove("is-open"); + document.getElementById("progress-stage-count")?.setAttribute("aria-expanded", "false"); setPipelineRunState("running"); if (prompt !== null) { @@ -8833,16 +8763,16 @@ function renderSummaryDetail(presentation) { const manualSteps = presentation.manualSteps.length ? `
-

${reviewStatusIcon("info")} Complete in FlutterFlow

-

For your information — these don't block the deploy. Finish them by hand in the FlutterFlow editor once the code is pushed.

- +
` : ""; @@ -8850,10 +8780,16 @@ function renderSummaryDetail(presentation) { summaryDetail.innerHTML = `
+

${escapeHtml(presentation.title)}

${escapeHtml(presentation.summary)}

+
+
Artifact${presentation.artifacts.length === 1 ? "" : "s"}
${presentation.artifacts.map((artifact) => escapeHtml(artifact.artifactType || artifact.type || artifact.name)).join(" · ")}
+
Public API
${presentation.artifacts.flatMap((artifact) => artifact.publicApi || []).map(escapeHtml).join(" · ") || "No parameters returned"}
+
Manual Steps Required
${manualSteps || "None reported"}
+
Review
${escapeHtml(presentation.status || "Not reported")}
+
${findings} ${bundleWarnings} - ${manualSteps}