Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 186 additions & 61 deletions web/index.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<!doctype html>
<html lang="en" data-flavor="relay">
<head>
Expand Down Expand Up @@ -107,35 +107,59 @@
}
.btn-ghost:hover { border-color: color-mix(in srgb, var(--af-accent) 50%, transparent); }

/* ---- Live check demo ---- */
.feed-caption { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; font-size: 0.8rem; color: var(--af-text-muted); }
/* ---- Directory tree centerpiece ---- */
.diagram-caption { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; font-size: 0.8rem; color: var(--af-text-muted); }
.pulse-dot { position: relative; width: 8px; height: 8px; border-radius: 50%; background: var(--af-accent); flex: none; }
.pulse-dot::after {
content: ""; position: absolute; inset: 0; border-radius: 50%; background: var(--af-accent);
animation: pulse 2s ease-out infinite;
}
@keyframes pulse { 0% { opacity: 0.6; transform: scale(1); } 100% { opacity: 0; transform: scale(2.8); } }
#feed-window { box-shadow: 0 30px 60px -20px rgb(0 0 0 / 0.6); max-width: 100%; }
#feed-body { min-height: 15.5rem; max-height: 15.5rem; padding: 0.85rem 1rem; overflow: hidden; font-family: var(--af-font-mono); font-size: 0.78rem; }
.feed-line {
display: grid; grid-template-columns: auto minmax(0, 10.5rem) minmax(0, 1fr) minmax(0, 7rem); gap: 0.6rem; align-items: baseline;
padding: 0.3rem 0; color: var(--af-text-muted);
animation: feed-in 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.feed-line .title { overflow-wrap: anywhere; }
@keyframes feed-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
.feed-line .sev { text-transform: none; font-size: 0.7rem; overflow-wrap: anywhere; }
.feed-line .title { color: var(--af-text); }
.feed-line .who { color: var(--af-text-muted); opacity: 0.75; text-align: right; overflow-wrap: anywhere; }
.feed-tone { width: 6px; height: 6px; border-radius: 50%; align-self: center; }
.feed-tone { width: 6px; height: 6px; border-radius: 50%; align-self: center; flex: none; }
.feed-tone-info { background: #22d3ee; }
.feed-tone-warn { background: #ffb000; }
.feed-tone-critical { background: var(--af-accent); box-shadow: 0 0 8px color-mix(in srgb, var(--af-accent) 60%, transparent); }
.feed-summary { color: var(--af-text-muted); }
.feed-summary .ok { color: #22d3ee; }
.feed-summary .bad { color: var(--af-accent); }

.tree-panel { padding: 1.35rem 1.4rem 1.15rem; box-shadow: 0 30px 60px -20px rgb(0 0 0 / 0.6); max-width: 100%; overflow-x: auto; }
.file-tree, .file-tree ul { list-style: none; margin: 0; padding: 0; }
.file-tree { font-family: var(--af-font-mono); font-size: 0.86rem; }
.t-row { display: flex; align-items: stretch; flex-wrap: wrap; min-height: 1.9rem; }
.t-row.t-root { min-height: 1.7rem; }
.t-rail, .t-elbow { position: relative; width: 1.05rem; flex: none; }
.t-vline {
position: absolute; left: 50%; top: 0; width: 1px; background: var(--af-line);
transform: translateX(-50%) scaleY(1); transform-origin: top;
}
.t-rail .t-vline, .t-tee .t-vline { height: 100%; }
.t-corner .t-vline { height: 50%; }
.t-hline {
position: absolute; left: 50%; top: 50%; height: 1px; width: 0.6rem; background: var(--af-line);
transform: scaleX(1); transform-origin: left;
}
.node {
align-self: center; display: inline-flex; align-items: center; padding: 0.12rem 0.5rem; margin: 0.18rem 0;
border-radius: 4px; white-space: nowrap; color: var(--af-text-muted);
}
.node-dir { color: var(--af-text); font-weight: 600; }
.node-file { color: var(--af-text-muted); }
.node-violation {
color: var(--af-text); font-weight: 600;
background: color-mix(in srgb, var(--af-accent) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--af-accent) 55%, transparent);
}
.node-violation.is-pulsing { box-shadow: 0 0 0 4px color-mix(in srgb, var(--af-accent) 25%, transparent); }
.violation-flag {
align-self: center; flex-basis: 100%; margin: 0.1rem 0 0.2rem calc(1.05rem + 0.5rem);
display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.72rem; color: var(--af-text-muted);
overflow-wrap: anywhere;
}
.tree-verdict {
margin-top: 0.9rem; padding-top: 0.85rem; border-top: 1px solid var(--af-line);
font-family: var(--af-font-mono); font-size: 0.8rem; color: var(--af-text-muted);
display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
}
.tree-verdict strong { color: var(--af-accent); font-weight: 700; }
@media (prefers-reduced-motion: reduce) {
.feed-line { animation: none; }
.pulse-dot::after { animation: none; opacity: 0; }
}

Expand Down Expand Up @@ -200,8 +224,9 @@
@media (max-width: 480px) {
.topbar .wrap { flex-wrap: wrap; row-gap: 0.6rem; padding-top: 0.75rem; padding-bottom: 0.75rem; min-height: 0; }
.topbar { min-height: 0; }
.feed-line { grid-template-columns: auto minmax(0, 1fr); }
.feed-line .sev, .feed-line .who { display: none; }
.file-tree { font-size: 0.76rem; }
.t-rail, .t-elbow { width: 0.85rem; }
.violation-flag { margin-left: calc(0.85rem + 0.4rem); }
}
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
</style>
Expand Down Expand Up @@ -242,13 +267,46 @@
</div>

<div>
<div class="feed-caption"><span class="pulse-dot"></span> Live demo (simulated output, not a real run)</div>
<div class="terminal-window" id="feed-window">
<div class="terminal-header">
<span class="dot dot-red"></span><span class="dot dot-yellow"></span><span class="dot dot-green"></span>
<span style="margin-left: 0.5rem; color: var(--af-text-muted);">structlint validate</span>
</div>
<div id="feed-body"></div>
<div class="diagram-caption"><span class="pulse-dot" aria-hidden="true"></span> Sample project, one <code>.structlint.yaml</code> run</div>
<div class="glass-card tree-panel">
<ul class="file-tree" aria-label="Sample directory tree for a Go project, with one file flagged as a violation">
<li class="t-row t-root">
<span class="node node-dir">myapp/</span>
</li>
<li class="t-row">
<span class="t-elbow t-tee" aria-hidden="true"><span class="t-vline"></span><span class="t-hline"></span></span>
<span class="node node-dir">cmd/</span>
</li>
<li class="t-row">
<span class="t-rail" aria-hidden="true"><span class="t-vline"></span></span>
<span class="t-elbow t-corner" aria-hidden="true"><span class="t-vline"></span><span class="t-hline"></span></span>
<span class="node node-file">main.go</span>
</li>
<li class="t-row">
<span class="t-elbow t-tee" aria-hidden="true"><span class="t-vline"></span><span class="t-hline"></span></span>
<span class="node node-dir">internal/</span>
</li>
<li class="t-row">
<span class="t-rail" aria-hidden="true"><span class="t-vline"></span></span>
<span class="t-elbow t-tee" aria-hidden="true"><span class="t-vline"></span><span class="t-hline"></span></span>
<span class="node node-dir">domain/</span>
</li>
<li class="t-row">
<span class="t-rail" aria-hidden="true"><span class="t-vline"></span></span>
<span class="t-elbow t-corner" aria-hidden="true"><span class="t-vline"></span><span class="t-hline"></span></span>
<span class="node node-dir">db/</span>
</li>
<li class="t-row">
<span class="t-elbow t-tee" aria-hidden="true"><span class="t-vline"></span><span class="t-hline"></span></span>
<span class="node node-file">go.mod</span>
</li>
<li class="t-row">
<span class="t-elbow t-corner" aria-hidden="true"><span class="t-vline"></span><span class="t-hline"></span></span>
<span class="node node-file node-violation" title="disallowed_file_pattern: matches *.env*">.env.local</span>
<span class="violation-flag"><span class="feed-tone feed-tone-critical" aria-hidden="true"></span>disallowed_file_pattern</span>
</li>
</ul>
<div class="tree-verdict"><strong aria-hidden="true">&#10007;</strong> 1 violation found &mdash; <code>disallowed_file_pattern</code> on <code>.env.local</code></div>
</div>
</div>
</div>
Expand Down Expand Up @@ -400,43 +458,110 @@
<a href="https://axeforging.github.io/gumshoe/">gumshoe</a>.</p>
</footer>

<script src="https://unpkg.com/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://unpkg.com/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
<script>
(function () {
var FINDINGS = [
{ code: "unallowed_directory", tone: "info", title: "Directory not in allowed list: tmp", who: "tmp/" },
{ code: "disallowed_file_pattern", tone: "warn", title: "Disallowed file naming pattern found: .env.local", who: ".env.local" },
{ code: "disallowed_file_pattern", tone: "warn", title: "Disallowed file naming pattern found: debug.log", who: "debug.log" },
{ code: "placement_violation", tone: "warn", title: "expected under migrations/**", who: "scripts/2024_add_users.sql" },
{ code: "missing_required_file", tone: "warn", title: "No file matching go.mod found", who: "(root)" },
{ code: "boundary_violation", tone: "critical", title: "internal/domain imports internal/db", who: "internal/domain/order.go" }
];

var body = document.getElementById("feed-body");
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var MAX_LINES = 6;
var i = 0;

function addLine() {
var f = FINDINGS[i % FINDINGS.length];
i++;
var line = document.createElement("div");
line.className = "feed-line";
line.innerHTML =
'<span class="feed-tone feed-tone-' + f.tone + '"></span>' +
'<span class="sev" style="color: var(--af-text-muted);">' + f.code + '</span>' +
'<span class="title">' + f.title + '</span>' +
'<span class="who">' + f.who + '</span>';
body.appendChild(line);
while (body.children.length > MAX_LINES) {
body.removeChild(body.firstChild);
}
}
// No GSAP (CDN blocked/offline)? Nothing in the CSS starts hidden, so the
// page is already fully visible and fully usable -- no-op fallback.
if (typeof gsap === "undefined") return;

for (var seed = 0; seed < 4; seed++) addLine();
gsap.registerPlugin(ScrollTrigger);

if (!reduced) {
setInterval(addLine, 2400);
}
var mm = gsap.matchMedia();

mm.add(
{
reduce: "(prefers-reduced-motion: reduce)",
noPreference: "(prefers-reduced-motion: no-preference)"
},
function (context) {
var reduce = context.conditions.reduce;

var heroTargets = [".kicker", ".hero h1", ".hero p.lede", ".hero .ctas", ".tree-panel"];
var vlines = gsap.utils.toArray(".t-vline");
var hlines = gsap.utils.toArray(".t-hline");
var nodes = gsap.utils.toArray(".file-tree .node");
var verdict = document.querySelector(".tree-verdict");
var violation = document.querySelector(".node-violation");

var sectionGroups = gsap.utils.toArray("section[id]").map(function (sec) {
var items = sec.querySelectorAll(".feature-card, .guide-step, .usecase-card, .install-step, tr");
var targets = items.length ? gsap.utils.toArray(items) : [sec.querySelector(".section-head")];
return { sec: sec, targets: targets };
});

if (reduce) {
// Hard a11y requirement: land every element in its final visible
// state immediately, with no animation, no matter what state
// it would otherwise start from.
gsap.set(heroTargets, { autoAlpha: 1, y: 0, clearProps: "transform" });
gsap.set(vlines, { scaleY: 1 });
gsap.set(hlines, { scaleX: 1 });
gsap.set(nodes, { autoAlpha: 1, x: 0 });
if (verdict) gsap.set(verdict, { autoAlpha: 1, y: 0 });
sectionGroups.forEach(function (g) { gsap.set(g.targets, { autoAlpha: 1, y: 0 }); });
return;
}

// ---- Hero entrance: kicker -> h1 -> lede -> ctas -> tree panel ----
var heroTl = gsap.timeline({ defaults: { ease: "power3.out" } });
heroTl
.from(".kicker", { autoAlpha: 0, y: 14, duration: 0.5 })
.from(".hero h1", { autoAlpha: 0, y: 18, duration: 0.6 }, "-=0.32")
.from(".hero p.lede", { autoAlpha: 0, y: 14, duration: 0.5 }, "-=0.34")
.from(".hero .ctas", { autoAlpha: 0, y: 12, duration: 0.45 }, "-=0.3")
.from(".tree-panel", { autoAlpha: 0, y: 20, duration: 0.6 }, "-=0.25");

// ---- Tree draws in top-down: each branch extends, then its node
// fades in, row by row; the flagged node pulses once revealed ----
gsap.set(vlines, { scaleY: 0 });
gsap.set(hlines, { scaleX: 0 });
gsap.set(nodes, { autoAlpha: 0, x: -6 });
if (verdict) gsap.set(verdict, { autoAlpha: 0, y: 8 });

var rows = gsap.utils.toArray(".t-row");
var treeTl = gsap.timeline();
rows.forEach(function (row, i) {
var v = row.querySelectorAll(".t-vline");
var h = row.querySelectorAll(".t-hline");
var node = row.querySelector(".node");
var t = i * 0.13;
if (v.length) treeTl.to(v, { scaleY: 1, duration: 0.22, ease: "power1.out" }, t);
if (h.length) treeTl.to(h, { scaleX: 1, duration: 0.14, ease: "power1.out" }, t + 0.16);
if (node) treeTl.to(node, { autoAlpha: 1, x: 0, duration: 0.2, ease: "power2.out" }, t + 0.18);
});
if (violation) {
treeTl.to(violation, {
scale: 1.1,
duration: 0.2,
ease: "power2.out",
yoyo: true,
repeat: 1,
transformOrigin: "center",
onStart: function () { violation.classList.add("is-pulsing"); },
onComplete: function () { violation.classList.remove("is-pulsing"); }
}, "+=0.1");
}
if (verdict) treeTl.to(verdict, { autoAlpha: 1, y: 0, duration: 0.35, ease: "power2.out" }, "-=0.1");

heroTl.add(treeTl, "-=0.2");

// ---- Scroll-triggered reveals, once per section, staggered across
// the cards within it ----
sectionGroups.forEach(function (g) {
gsap.set(g.targets, { autoAlpha: 0, y: 24 });
ScrollTrigger.create({
trigger: g.sec,
start: "top 82%",
once: true,
onEnter: function () {
gsap.to(g.targets, { autoAlpha: 1, y: 0, duration: 0.6, ease: "power2.out", stagger: 0.08 });
}
});
});
}
);
})();
</script>
</body>
Expand Down
Loading