diff --git a/docs/post/grove/file-preview.js b/docs/post/grove/file-preview.js
new file mode 100644
index 0000000..6cece87
--- /dev/null
+++ b/docs/post/grove/file-preview.js
@@ -0,0 +1,38 @@
+/* global document, window */
+
+if (window.location.protocol === "file:") {
+ const setText = (selector, value) => {
+ const element = document.querySelector(selector);
+ if (element) element.textContent = value;
+ };
+
+ const showLocalPreview = () => {
+ document.body.classList.remove("is-checking");
+ document.body.classList.add("is-local-preview");
+
+ setText("[data-view-state]", "Local preview · open the hosted Grove for live stats");
+ setText("[data-snapshot-state]", "Local preview");
+ setText("[data-network]", "Sepolia");
+ setText("[data-view-time]", "Not connected");
+ setText("[data-snapshot-cadence]", "15 min");
+ setText("[data-node-count]", "—");
+ setText("[data-view-age]", "Not connected");
+ setText("[data-node-hours]", "—");
+ setText("[data-history-low]", "—");
+ setText("[data-history-high]", "—");
+ setText("[data-history-samples]", "0");
+ setText("[data-history-coverage]", "0%");
+
+ const chart = document.querySelector("[data-history-chart]");
+ chart?.setAttribute(
+ "aria-label",
+ "Local file preview. Open the hosted Grove for live signed count history.",
+ );
+ };
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", showLocalPreview, { once: true });
+ } else {
+ showLocalPreview();
+ }
+}
diff --git a/docs/post/grove/grove.css b/docs/post/grove/grove.css
index 0345acc..256aff4 100644
--- a/docs/post/grove/grove.css
+++ b/docs/post/grove/grove.css
@@ -6,7 +6,7 @@
background:
radial-gradient(circle at 50% 16%, rgba(101, 125, 95, 0.14), transparent 31rem),
linear-gradient(180deg, rgba(7, 12, 9, 0.86), rgba(7, 12, 9, 0.98)),
- url("/fig/shade-tree-banner.webp") center / cover no-repeat;
+ url("../fig/shade-tree-banner.webp") center / cover no-repeat;
}
.network-hero {
@@ -761,7 +761,7 @@
}
@media (max-width: 360px) {
- .network-page .nav-links a[href="/research/"] {
+ .network-page .nav-links a[href="../research/index.html"] {
display: none;
}
diff --git a/docs/post/grove/index.html b/docs/post/grove/index.html
index c311da5..4af0230 100644
--- a/docs/post/grove/index.html
+++ b/docs/post/grove/index.html
@@ -13,10 +13,11 @@
-
-
-
-
+
+
+
+
+
Shade Tree Grove Network Stats
@@ -24,13 +25,13 @@
@@ -134,8 +135,8 @@ 24 hours of shade
diff --git a/docs/post/site.css b/docs/post/site.css
index 8aec1a5..92c3263 100644
--- a/docs/post/site.css
+++ b/docs/post/site.css
@@ -51,7 +51,7 @@ body:not(.home-page)::before {
content: "";
background:
linear-gradient(180deg, rgba(7, 16, 12, 0.8), rgba(7, 16, 12, 0.97)),
- url("/fig/shade-tree-banner.webp") center / cover no-repeat;
+ url("./fig/shade-tree-banner.webp") center / cover no-repeat;
filter: saturate(0.72) contrast(1.05);
transform: scale(1.02);
}
diff --git a/test/site.selftest.mjs b/test/site.selftest.mjs
index 4bf8796..b3dac94 100644
--- a/test/site.selftest.mjs
+++ b/test/site.selftest.mjs
@@ -65,6 +65,7 @@ const labCss = read("lab/lab.css");
const labScript = read("lab/lab.js");
const groveCss = read("grove/grove.css");
const groveLoader = read("grove/network.js");
+const groveFilePreview = read("grove/file-preview.js");
const groveScene = read("grove/scene.js");
const groveFreshness = read("grove/freshness.js");
const groveHistory = read("grove/history.js");
@@ -97,6 +98,7 @@ check("Protocol Lab keeps sensitive fields out of its public card", /IP, target,
check("Protocol Lab route is responsive and reduced-motion safe", /@media \(max-width: 650px\)[\s\S]*?\.route-stages\s*\{[\s\S]*?grid-template-columns:\s*1fr/.test(labCss) && /@media \(prefers-reduced-motion: reduce\)/.test(labCss) && /matchMedia\("\(prefers-reduced-motion: reduce\)"\)/.test(labScript));
check("Protocol Lab has replay, local-run, and post-success share actions", /Walk this route/.test(labPage) && /data-share>Share this route/.test(labPage) && /data-copy-link>Copy route link/.test(labPage) && /href="\.\.\/agent\/index\.html">Run locally/.test(labPage) && /result\.hidden = true/.test(labScript) && /result\.hidden = false/.test(labScript));
check("Protocol Lab opens as a styled app from direct-file and clean hosted URLs", /href="\.\/lab\/index\.html">Lab/.test(landing) && /href="\.\.\/site\.css"/.test(labPage) && /href="\.\.\/lab\/lab\.css"/.test(labPage) && /src="\.\.\/lab\/lab\.js"/.test(labPage) && /href="\.\.\/index\.html"/.test(labPage) && !/(?:href|src)="\/(?:site|favicon|lab|agent|grove|research)/.test(labPage) && /new URL\(canonicalLabUrl\)/.test(labScript) && /history\.replaceState\(null, "", `\?scenario=/.test(labScript));
+check("Grove opens as a styled, honest preview from direct-file and a live app when hosted", /href="\.\.\/site\.css"/.test(grovePage) && /href="\.\/grove\.css"/.test(grovePage) && /src="\.\/network\.js"/.test(grovePage) && /src="\.\/file-preview\.js"/.test(grovePage) && /href="\.\.\/index\.html"/.test(grovePage) && /href="\.\.\/research\/index\.html"/.test(grovePage) && !/(?:href|src)="\/(?:site|favicon|fig|grove|research)/.test(grovePage) && /window\.location\.protocol === "file:"/.test(groveFilePreview) && /Local preview · open the hosted Grove for live stats/.test(groveFilePreview) && /data-node-count\]", "—"/.test(groveFilePreview));
check("Protocol Lab has a dedicated static social image", /og:image" content="https:\/\/shade-tree-node\.vercel\.app\/fig\/shade-tree-lab-og\.png"/.test(labPage));
check("landing has one H1 and one decorative canvas", (landing.match(/]+aria-hidden="true"/g) || []).length === 1);
check("Grove has one H1, one focused detail section, and one live status", (grovePage.match(/