Release ForgeFlow 0.8.4 interactive illustrations
This commit is contained in:
+36
-2
@@ -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", {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user