Release ForgeFlow 0.8.4 interactive illustrations

This commit is contained in:
NuklearRabbit
2026-07-26 02:28:41 +02:00
parent cb95c407d9
commit 095342b882
10 changed files with 294 additions and 18 deletions
+36 -2
View File
@@ -699,6 +699,19 @@ function queueActionFor(repository) {
return mapping[action.kind] || mapping.clean;
}
function projectIllustration(kind = "flow") {
return `<div class="project-illustration ${attr(kind)}" data-project-illustration aria-hidden="true">
<div class="illustration-glow"></div><svg viewBox="0 0 260 150" role="presentation">
<path class="orbit orbit-a" d="M32 92 C72 20 190 18 230 82"/><path class="orbit orbit-b" d="M42 116 C98 150 190 136 222 60"/>
<g class="illustration-core"><rect x="83" y="39" width="94" height="74" rx="17"/><path d="M103 66h54M103 79h36M103 92h45"/><circle cx="160" cy="92" r="5"/></g>
<g class="illustration-node node-a"><circle cx="37" cy="92" r="12"/><path d="m32 92 4 4 7-9"/></g>
<g class="illustration-node node-b"><circle cx="226" cy="82" r="12"/><path d="M221 82h10M226 77v10"/></g>
<g class="illustration-node node-c"><circle cx="74" cy="31" r="8"/></g>
<circle class="signal signal-a" cx="0" cy="0" r="4"/><circle class="signal signal-b" cx="0" cy="0" r="3"/>
</svg><span class="illustration-label">${kind === "deploy" ? "Live release topology" : kind === "repo" ? "Project signal" : "Release flow"}</span>
</div>`;
}
function renderOverview() {
const changed = ui.repositories.filter(
(repository) => repository.localStatus?.counts.changed,
@@ -720,7 +733,7 @@ function renderOverview() {
(operation) => !isTerminalOperation(operation.status),
);
return `<div class="page">
<div class="page-header"><div><div class="eyebrow">Coding flow</div><h1>Release overview</h1><p>One decision surface for local work, Gitea synchronization and the exact version running on your server.</p></div><button class="button" data-action="refresh">${icon("refresh")}Refresh all</button></div>
<div class="page-header visual-page-header"><div><div class="eyebrow">Coding flow</div><h1>Release overview</h1><p>One decision surface for local work, Gitea synchronization and the exact version running on your server.</p></div>${projectIllustration("flow")}<button class="button" data-action="refresh">${icon("refresh")}Refresh all</button></div>
${ui.refreshError ? `<div class="notice danger">${icon("error")} ${escapeHtml(ui.refreshError)}</div>` : ""}
<div class="summary-grid">
${renderSummaryCard("Local work", changed, changed === 1 ? "repository has changes" : "repositories have changes", "file", changed ? "warning" : "success")}
@@ -951,7 +964,7 @@ function renderRepositoryWorkspace(repository) {
settings: renderRepositorySettings,
}[ui.repositoryTab] || renderChanges
)(repository);
return `<div class="repo-workspace"><header class="repo-header"><div class="repo-heading"><h1><button class="favorite-button ${repository.favorite ? "active" : ""}" data-action="toggle-favorite" title="Toggle favorite">${icon("star")}</button>${escapeHtml(repository.fullName)}</h1><p>${escapeHtml(repository.localPath || "No local working tree linked")}</p></div><div class="repo-header-actions"><button class="button" data-action="fetch" ${!repository.localPath ? "disabled" : ""}>${icon("refresh")}Fetch</button><button class="button" data-action="open-path" ${!repository.localPath ? "disabled" : ""}>${icon("folder")}Folder</button><button class="button" data-action="open-gitea" ${!repository.htmlUrl ? "disabled" : ""}>${icon("external")}Gitea</button></div></header>
return `<div class="repo-workspace"><header class="repo-header illustrated-repo-header"><div class="repo-heading"><h1><button class="favorite-button ${repository.favorite ? "active" : ""}" data-action="toggle-favorite" title="Toggle favorite">${icon("star")}</button>${escapeHtml(repository.fullName)}</h1><p>${escapeHtml(repository.localPath || "No local working tree linked")}</p></div>${projectIllustration("repo")}<div class="repo-header-actions"><button class="button" data-action="fetch" ${!repository.localPath ? "disabled" : ""}>${icon("refresh")}Fetch</button><button class="button" data-action="open-path" ${!repository.localPath ? "disabled" : ""}>${icon("folder")}Folder</button><button class="button" data-action="open-gitea" ${!repository.htmlUrl ? "disabled" : ""}>${icon("external")}Gitea</button></div></header>
${repository.localPath ? `<div class="repo-quick-actions"><button class="button" data-action="open-editor">${icon("external")}Open in editor</button><button class="button" data-action="open-terminal">${icon("terminal")}Open terminal</button><button class="button" data-action="check-branch-protection">${icon("shield")}Check branch protection</button><button class="button primary" data-action="open-pull-request">${icon("git")}Create pull request</button>${ui.branchProtection ? `<span class="status-pill ${ui.branchProtection.protected ? "warning" : "success"}">${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}</span>` : ""}</div>` : ""}
<div class="release-rail">${releaseNode("Local", status?.shortHead || "Not linked", status ? `${status.counts.changed} changes · ${status.branch.head}` : "No working tree", localTone)}${releaseNode("Gitea", status?.shortHead || "Unknown", status?.branch.upstream ? `${status.branch.ahead} ahead · ${status.branch.behind} behind` : "Branch not published", remoteTone)}${releaseNode(`Server${profile ? ` · ${profile.environment}` : ""}`, serverState.liveSha ? shortSha(serverState.liveSha) : "Unknown", profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "not checked") : "No deployment profile", serverTone)}</div>
<nav class="tabs">${[
@@ -3244,6 +3257,27 @@ document.addEventListener("keydown", (event) => {
}
});
document.addEventListener("pointermove", (event) => {
const illustration = event.target.closest?.("[data-project-illustration]");
if (!illustration) return;
const bounds = illustration.getBoundingClientRect();
illustration.style.setProperty(
"--tilt-x",
`${((event.clientY - bounds.top) / bounds.height - 0.5) * -7}deg`,
);
illustration.style.setProperty(
"--tilt-y",
`${((event.clientX - bounds.left) / bounds.width - 0.5) * 9}deg`,
);
});
document.addEventListener("pointerout", (event) => {
const illustration = event.target.closest?.("[data-project-illustration]");
if (!illustration || illustration.contains(event.relatedTarget)) return;
illustration.style.removeProperty("--tilt-x");
illustration.style.removeProperty("--tilt-y");
});
window.addEventListener("error", (event) => {
window.forgeflow
.reportRendererEvent?.("error", "uncaught-error", {
+1 -1
View File
@@ -564,7 +564,7 @@
await wait(80);
snapshot();
return {
appVersion: "0.8.3-demo",
appVersion: "0.8.4-demo",
platform: "win32",
state: clone(state),
git: { available: true, version: "git version 2.47.3" },
+201
View File
@@ -3205,6 +3205,197 @@ select:focus {
backdrop-filter: blur(16px);
}
.visual-page-header {
min-height: 142px;
position: relative;
overflow: hidden;
padding: 20px 22px;
border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--line));
border-radius: 16px;
background:
linear-gradient(
105deg,
var(--surface-1) 0 52%,
color-mix(in srgb, var(--primary-soft) 62%, var(--surface-1))
),
var(--surface-1);
box-shadow: var(--shadow);
}
.project-illustration {
--tilt-x: 0deg;
--tilt-y: 0deg;
width: 238px;
height: 128px;
position: relative;
flex: 0 0 238px;
perspective: 700px;
cursor: crosshair;
isolation: isolate;
}
.project-illustration svg {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
overflow: visible;
transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
filter: drop-shadow(0 16px 24px rgba(10, 20, 48, 0.2));
transition: transform 140ms ease-out;
}
.illustration-glow {
width: 150px;
height: 85px;
position: absolute;
inset: 24px auto auto 49px;
border-radius: 50%;
background: color-mix(in srgb, var(--primary) 24%, transparent);
filter: blur(24px);
animation: illustration-breathe 4.8s ease-in-out infinite;
}
.project-illustration .orbit {
fill: none;
stroke: color-mix(in srgb, var(--primary) 48%, var(--line));
stroke-width: 1.5;
stroke-dasharray: 4 7;
}
.project-illustration .orbit-b {
stroke: color-mix(in srgb, var(--success) 48%, var(--line));
}
.illustration-core rect {
fill: color-mix(in srgb, var(--surface-1) 86%, var(--primary));
stroke: color-mix(in srgb, var(--primary) 58%, var(--line));
stroke-width: 1.5;
}
.illustration-core path {
fill: none;
stroke: var(--text-muted);
stroke-width: 3;
stroke-linecap: round;
}
.illustration-core circle {
fill: var(--success);
animation: illustration-pulse 2.4s ease-in-out infinite;
}
.illustration-node {
animation: illustration-float 4s ease-in-out infinite;
}
.illustration-node circle {
fill: var(--surface-1);
stroke: var(--primary);
stroke-width: 2;
}
.illustration-node path {
fill: none;
stroke: var(--primary);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.illustration-node.node-b {
animation-delay: -1.4s;
}
.illustration-node.node-c {
animation-delay: -2.7s;
}
.illustration-node.node-c circle {
fill: var(--warning);
stroke: color-mix(in srgb, var(--warning) 55%, var(--surface-1));
}
.project-illustration .signal {
fill: var(--primary);
}
.project-illustration .signal-a {
offset-path: path("M32 92 C72 20 190 18 230 82");
animation: signal-travel 3.6s linear infinite;
}
.project-illustration .signal-b {
fill: var(--success);
offset-path: path("M42 116 C98 150 190 136 222 60");
animation: signal-travel 4.4s -2s linear infinite;
}
.illustration-label {
position: absolute;
right: 8px;
bottom: 3px;
z-index: 2;
color: var(--text-faint);
font: 650 9px var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.project-illustration.repo {
width: 150px;
height: 82px;
position: absolute;
top: -9px;
left: 43%;
z-index: 0;
margin: 0;
opacity: 0.28;
pointer-events: none;
}
.project-illustration.repo .illustration-label {
display: none;
}
.illustrated-repo-header {
position: relative;
overflow: hidden;
}
.illustrated-repo-header > :not(.project-illustration) {
position: relative;
z-index: 1;
}
@keyframes signal-travel {
to {
offset-distance: 100%;
}
}
@keyframes illustration-float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
@keyframes illustration-breathe {
0%,
100% {
opacity: 0.55;
transform: scale(0.92);
}
50% {
opacity: 0.95;
transform: scale(1.08);
}
}
@keyframes illustration-pulse {
0%,
100% {
opacity: 0.55;
}
50% {
opacity: 1;
}
}
html[data-theme="light"] .visual-page-header {
background:
radial-gradient(
circle at 74% 20%,
rgba(102, 82, 235, 0.12),
transparent 28%
),
linear-gradient(
105deg,
rgba(255, 255, 255, 0.96) 0 52%,
rgba(229, 239, 255, 0.94)
);
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
@@ -3214,6 +3405,9 @@ select:focus {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
.project-illustration svg {
transform: none !important;
}
}
@media (max-width: 1180px) {
@@ -3227,4 +3421,11 @@ select:focus {
.summary-grid {
gap: 8px;
}
.project-illustration {
width: 190px;
flex-basis: 190px;
}
.project-illustration.repo {
display: none;
}
}