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
+7
View File
@@ -1,5 +1,12 @@
# Changelog
## 0.8.4 - 2026-07-26
- added interactive animated release-flow illustrations to high-value project surfaces;
- introduced cursor-responsive depth, travelling deployment signals and living status nodes;
- added compact repository illustration watermarks without reducing usable header space;
- made every illustration theme-aware, responsive, semantic and reduced-motion safe.
## 0.8.3 - 2026-07-26
- enriched light mode with layered color, depth and stronger navigation hierarchy;
+10 -9
View File
@@ -1,4 +1,4 @@
ForgeFlow 0.8.3 source manifest
ForgeFlow 0.8.4 source manifest
SHA-256 BYTES PATH
(The manifest excludes itself, dependencies and generated release artifacts.)
755f4db7d76bfec0963ef051748a82810c0d58acd4ffd823aa6928a5167fceb4 58 .gitignore
@@ -12,7 +12,7 @@ ca32a76e708d565c4af659f0f4d2615fc32114c3f75aec1454862a3ed1e72c41 2263
4633990a4b055bb3d00fef915ee29e85be5ee8413f809334728ad9688973c183 3364 build/icon-64.png
25048ed854e8ce8fece115e555c98d25507b002f8019b6ae717b54604c868c50 46223 build/icon.ico
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 build/icon.png
02c39f04f01394f687eac1d8bae0cdd85018d8e95ff941b6ce6d9f9d7b19d697 8606 CHANGELOG.md
30857d5b5ab786f2f7a2b7c15f329b209a5759ea6f34e851898c19c70a01a95d 8985 CHANGELOG.md
21cb96e7afe71b1dc791c818dedd244d92f9a6ed4d9ffbb3022ccb187e1bdf0f 852 docs/ACCEPTANCE.md
a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md
30a92bcf5daadb019efa2f82cb820ea302490dd1d68fb772674dc3faccd3e594 2045 docs/DEPLOYMENT_SETUP.md
@@ -41,6 +41,7 @@ d7bdc61d9b617ad5acf0b2d468eda547fd7509d4af08f33d2661393f25bdcb5a 576
1e056bfcf2105843402f4b14c63480240cc55456a4a63e229b3fdbaf3156b803 754 docs/RELEASE_NOTES_0.8.1.md
7f1d7c8bc895d309dad2f8ab444d6d2ba3e68c8daa240fecd2abdd9d8a56ba20 729 docs/RELEASE_NOTES_0.8.2.md
c2802fa5dbff392c846b82b55e84a8bfb8e1625546fd1eba39f7129318bece96 654 docs/RELEASE_NOTES_0.8.3.md
8c13279987672314332f648889f52338bcdcb243249f9e1c20fb09d85d7808f5 505 docs/RELEASE_NOTES_0.8.4.md
2b631b9d6d973bdd70869d84886ff339da351e29e17598970b3b27915674661d 4175 docs/ROADMAP.md
1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png
b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png
@@ -64,8 +65,8 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532
4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE
6765015bdf27b288a250192272750b243c3cb8d1326b752d056d1e43317b6344 12935 main.cjs
91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md
8f1b45693760790cca87c0643a0e43adb38b1b7182a592b0ac5e1f3883d47566 130466 package-lock.json
17ec2a34f0182f0752212765674afd79df9aa47c6b833065b943743737ce6bc8 3721 package.json
6c74e7d16379b92028a17b9d8b5b843734710ce8c2813da75e5cda2e822fa913 130466 package-lock.json
05bead9ffc27397b7854f7bda8150806ca433342613834448b28e72dcdd6ccea 3758 package.json
3d2ac366a13e9418e3ec6d13ce95b611f30f0228eb3a80ef9e7a936ce9578e24 9080 preload.cjs
b31c43d9355c13b5ae4efc0f3649d8cb8d509b2bb7ebb042ff546b7820fb7de8 8411 Publish-ForgeFlow-Release.ps1
a6d32a742412b7836606be00f17be0465f1b6f55d3911f6c73a14029787ba206 14037 README.md
@@ -78,7 +79,7 @@ f8359a69d20deb2dfe10042d1bec7b12a95e76e58e36bc5f265f073c3111d056 10287
74433d8a6b24afe368197a469e2fe0c5050c239d7250b84c2f3f598c304778b0 4736 scripts/publish-binary-release.cjs
444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs
42203f9e0fd4aae517284d387f265cf1b0b180379bc253a092b5c3c5c4caef0a 2992 scripts/validate-installed-connections.cjs
a2f19d7c45132973b3db3a56b57bd4a8e34de98e7c446b19cfaf3c4904fe5715 12209 scripts/verify.mjs
1b03d3144b911863a688ec4be2ae471afd09214ee901bd76c42429c0f5285062 12242 scripts/verify.mjs
0079701b5acbfef07b71a9623613d1940805ccd20649d77e3f34c37e79df7655 735 scripts/write-release-checksums.mjs
619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1
dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs
@@ -98,14 +99,14 @@ e89b54e7e3174b4b0a1dcd9058d8344e29431f9d16d0e6bb8d11559b691440a0 2508
b31a63bf8cb1807b3e838e2bf8a0e742738f119d13de8ca9f42e471f072217d3 8328 src/main/ssh-service.cjs
8cf5013de91737dd9345b121586ceec38a6fc8518648975d60593d81fc225c4b 67376 src/main/unraid-deployment-service.cjs
36cc05deda3395e5e9de92b880c8315f508cb88e9b080ae34705057ae696804f 20696 src/main/update-service.cjs
982039f1243c3e48b589b8ad6f8fe712b5b8dde3d23f6f171ad31b09ff1a70c2 192055 src/renderer/app.js
ef029189791024421d78b3ac92981f03b20f2086fc4846eaa95d509f65e096a9 194049 src/renderer/app.js
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png
e1c463d6cda9f2b9b78c468845c0a7e8688f0362be5642074a1a5f7122dfe811 762 src/renderer/index.html
68e565f0dc3d8a7fbce0d57fc55535c0fe022fd96ebd716d1adf279f995b0256 50922 src/renderer/mock-bridge.js
09c832aaea5994558eddcd17b20e7edd6840df5fc80e50354c627c41989711d9 63138 src/renderer/styles.css
342faf4acf1464c871114ca1c82ef926a64cb612a93d022dba401873925f3177 50922 src/renderer/mock-bridge.js
ebdd804b55c0b13cbf57e51c6d51a60dd838d67bb4c4eaf17ca34c86938a6865 67440 src/renderer/styles.css
0a1e9d9d6cd4d190eb7f85dbc6668d80600b1cf2749cc0c2c51cc428f506f20d 1121 src/shared/clone-target.cjs
5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs
029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs
@@ -133,7 +134,7 @@ e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690
caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs
96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs
c0f8f5a3784835f19d9ff1015185ccb385840b6fa1c9ec19f233393a7d952b65 3718 tests/preflight.test.mjs
128a3b87c26db9edd4413d6ab8c06bdfada311d2135d342ea298ebea89e40712 7061 tests/renderer-workflow.test.mjs
bc29c4e0c08305b2d00bf59ae9d486cad323c94879cf2a35d596a6b58fc17a0b 7784 tests/renderer-workflow.test.mjs
2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.test.mjs
f679072548554a64974f0452337ce5e7b0c567343c287223770cc0974b905348 1068 tests/repository-monitor.test.mjs
75b5b83836c75675bb9a48fe4363fcb8a24fc425e6af6f822d7955c6f3c79eac 2265 tests/repository-service.test.mjs
+10
View File
@@ -0,0 +1,10 @@
# ForgeFlow 0.8.4
ForgeFlow 0.8.4 adds interactive project illustrations where they reinforce the
release story. The overview now visualizes commits travelling through a release
topology, with animated status nodes and cursor-responsive depth.
Repository headers reuse the same visual language as a subtle animated
watermark. Illustrations adapt to light and dark themes, collapse gracefully at
compact widths and disable non-essential movement when reduced motion is
requested by the operating system.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "forgeflow",
"version": "0.8.3",
"version": "0.8.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "forgeflow",
"version": "0.8.3",
"version": "0.8.4",
"dependencies": {
"ssh2": "1.17.0"
},
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "forgeflow",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"description": "Desktop release cockpit for local Git, Gitea Actions and controlled exact-commit deployments.",
"main": "main.cjs",
@@ -76,6 +76,7 @@
"docs/RELEASE_NOTES_0.8.1.md",
"docs/RELEASE_NOTES_0.8.2.md",
"docs/RELEASE_NOTES_0.8.3.md",
"docs/RELEASE_NOTES_0.8.4.md",
"docs/ACCEPTANCE.md"
],
"asarUnpack": [
+3 -2
View File
@@ -64,6 +64,7 @@ const required = [
"docs/RELEASE_NOTES_0.8.1.md",
"docs/RELEASE_NOTES_0.8.2.md",
"docs/RELEASE_NOTES_0.8.3.md",
"docs/RELEASE_NOTES_0.8.4.md",
"docs/UPDATING.md",
"docs/DIAGNOSTICS.md",
"docs/DEPLOYMENT_SETUP.md",
@@ -102,9 +103,9 @@ for (const file of required) await access(path.join(root, file));
const packageJson = JSON.parse(
await readFile(path.join(root, "package.json"), "utf8"),
);
if (packageJson.version !== "0.8.3")
if (packageJson.version !== "0.8.4")
throw new Error(
`Expected package version 0.8.3, got ${packageJson.version}.`,
`Expected package version 0.8.4, got ${packageJson.version}.`,
);
const sourceManifest = await readFile(
path.join(root, "SOURCE_MANIFEST.txt"),
+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;
}
}
+22 -1
View File
@@ -182,7 +182,10 @@ test("one-click troubleshooting excludes destructive or publishing Git actions",
assert.match(ipc, /action:\s*["']abort-operation["'],\s*safe:\s*false/);
assert.match(ipc, /action:\s*["']push["'],\s*safe:\s*false/);
assert.match(ipc, /const stale = lock\.ageMs >= 10_000/);
assert.match(ipc, /\[\s*["']fast-forward["'],\s*["']fetch["']\s*\]\.includes\(issue\.action\)/);
assert.match(
ipc,
/\[\s*["']fast-forward["'],\s*["']fetch["']\s*\]\.includes\(issue\.action\)/,
);
assert.doesNotMatch(
ipc,
/\[\s*["']fast-forward["'],\s*["']push["'],\s*["']fetch["']\s*\]\.includes\(issue\.action\)/,
@@ -205,3 +208,21 @@ test("premium repository workspace reserves separate rows for actions and releas
assert.match(styles, /prefers-reduced-motion/);
assert.match(styles, /ForgeFlow 0\.8 premium visual system/);
});
test("interactive project illustrations are semantic, responsive and motion-safe", async () => {
const renderer = await readFile(
new URL("../src/renderer/app.js", import.meta.url),
"utf8",
);
const styles = await readFile(
new URL("../src/renderer/styles.css", import.meta.url),
"utf8",
);
assert.match(renderer, /function projectIllustration/);
assert.match(renderer, /data-project-illustration/);
assert.match(renderer, /document\.addEventListener\("pointermove"/);
assert.match(styles, /\.project-illustration/);
assert.match(styles, /@keyframes signal-travel/);
assert.match(styles, /prefers-reduced-motion/);
assert.match(styles, /transform: none !important/);
});