Landing page: remove terminal demo and features grid, clean up ISL iframe layout

Anton Kaminsky22d agod6f1620a1802parent 7e5aa77
1 file changed+18-224
web/app/landing.tsx
@@ -4,115 +4,6 @@
44import { GroveLogo } from "@/app/components/grove-logo";
55import { useTheme } from "@/lib/theme";
66
7const TERMINAL_LINES = [
8 { prompt: true, text: "grove init --owner letterpress-labs", delay: 40 },
9 { prompt: false, text: "┌ grove init grove-cli", delay: 0 },
10 { prompt: false, text: "◇ Created letterpress-labs/grove-cli", delay: 800 },
11 { prompt: false, text: "◇ Sapling repository initialized", delay: 400 },
12 { prompt: false, text: "◇ Remote configured", delay: 300 },
13 { prompt: false, text: "└ Initialized letterpress-labs/grove-cli", delay: 200 },
14 { prompt: false, text: "", delay: 400 },
15 { prompt: true, text: 'sl commit -m "add CLI help system and read-only ISL mode"', delay: 35 },
16 { prompt: false, text: "", delay: 600 },
17 { prompt: true, text: "sl push --to main", delay: 40 },
18 { prompt: false, text: "pushing rev c21911da to bookmark main", delay: 400 },
19 { prompt: false, text: "edenapi: uploaded 9 files", delay: 300 },
20 { prompt: false, text: "edenapi: uploaded 1 commit", delay: 200 },
21 { prompt: false, text: 'updated remote bookmark main to c21911da', delay: 300 },
22];
23
24function TerminalDemo() {
25 const [lines, setLines] = useState<{ text: string; isPrompt: boolean }[]>([]);
26 const [currentTyping, setCurrentTyping] = useState("");
27 const [isTyping, setIsTyping] = useState(false);
28 const [showCursor, setShowCursor] = useState(true);
29 const termRef = useRef<HTMLDivElement>(null);
30
31 useEffect(() => {
32 const blink = setInterval(() => setShowCursor((c) => !c), 530);
33 return () => clearInterval(blink);
34 }, []);
35
36 useEffect(() => {
37 let cancelled = false;
38
39 async function run() {
40 await sleep(800);
41 for (const line of TERMINAL_LINES) {
42 if (cancelled) return;
43 if (line.prompt) {
44 setIsTyping(true);
45 for (let i = 0; i <= line.text.length; i++) {
46 if (cancelled) return;
47 setCurrentTyping(line.text.slice(0, i));
48 await sleep(line.delay + Math.random() * 20);
49 }
50 setIsTyping(false);
51 setLines((prev) => [...prev, { text: line.text, isPrompt: true }]);
52 setCurrentTyping("");
53 await sleep(200);
54 } else {
55 await sleep(line.delay);
56 if (line.text) {
57 setLines((prev) => [...prev, { text: line.text, isPrompt: false }]);
58 } else {
59 setLines((prev) => [...prev, { text: "", isPrompt: false }]);
60 }
61 }
62 }
63 }
64 run();
65 return () => { cancelled = true; };
66 }, []);
67
68 useEffect(() => {
69 if (termRef.current) {
70 termRef.current.scrollTop = termRef.current.scrollHeight;
71 }
72 }, [lines, currentTyping]);
73
74 const cursor = showCursor ? "█" : " ";
75
76 return (
77 <div
78 ref={termRef}
79 style={{
80 backgroundColor: "#1a1918",
81 border: "1px solid #302e2b",
82 padding: "20px",
83 fontFamily: "'JetBrains Mono', Menlo, monospace",
84 fontSize: "13px",
85 lineHeight: 1.7,
86 color: "#c4bfb8",
87 overflow: "hidden",
88 maxHeight: "380px",
89 }}
90 >
91 {lines.map((line, i) => (
92 <div key={i} style={{ minHeight: "1.7em" }}>
93 {line.isPrompt && <span style={{ color: "#7aab9c" }}>❯ </span>}
94 {line.isPrompt ? (
95 <span style={{ color: "#e8e4df" }}>{line.text}</span>
96 ) : (
97 <span style={{ color: "#9a948c" }}>{line.text}</span>
98 )}
99 </div>
100 ))}
101 {(isTyping || lines.length === 0) && (
102 <div>
103 <span style={{ color: "#7aab9c" }}>❯ </span>
104 <span style={{ color: "#e8e4df" }}>{currentTyping}</span>
105 <span style={{ color: "#7aab9c" }}>{cursor}</span>
106 </div>
107 )}
108 </div>
109 );
110}
111
112function sleep(ms: number) {
113 return new Promise((r) => setTimeout(r, ms));
114}
115
1167export function LandingPage() {
1178 const [islDomain, setIslDomain] = useState("");
1189 const { theme } = useTheme();
@@ -133,7 +24,7 @@
13324 }, [theme]);
13425
13526 return (
136 <div style={{ display: "flex", flexDirection: "column", minHeight: "calc(100vh - 3.5rem)" }}>
27 <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
13728 {/* Hero + Terminal + Features */}
13829 <div style={{ maxWidth: "800px", margin: "0 auto", padding: "60px 24px 0", width: "100%" }}>
13930
@@ -179,126 +70,29 @@
17970 </a>
18071 </div>
18172
182 {/* Terminal */}
183 <section style={{ marginBottom: "64px" }}>
184 <div style={{ marginBottom: "16px" }}>
185 <h2
186 style={{
187 fontSize: "0.75rem",
188 fontWeight: 600,
189 textTransform: "uppercase",
190 letterSpacing: "0.1em",
191 color: "var(--text-faint)",
192 marginBottom: "4px",
193 }}
194 >
195 Init, commit, push
196 </h2>
197 <p style={{ fontSize: "0.875rem", color: "var(--text-muted)" }}>
198 From zero to hosted repository in seconds. Sapling&apos;s clean CLI, backed by Mononoke.
199 </p>
200 </div>
201 <TerminalDemo />
202 </section>
203
204 {/* Features */}
205 <section style={{ marginBottom: "48px" }}>
206 <div
207 style={{
208 display: "grid",
209 gridTemplateColumns: "1fr 1fr",
210 gap: "1px",
211 backgroundColor: "var(--border-subtle)",
212 border: "1px solid var(--border-subtle)",
213 }}
214 >
215 {[
216 {
217 title: "Sapling SCM",
218 desc: "Stacked diffs, amend-based workflow, interactive rebase — built for how engineers actually work.",
219 },
220 {
221 title: "Mononoke",
222 desc: "Meta's scalable source control server. Handles monorepos with millions of files.",
223 },
224 {
225 title: "Canopy CI",
226 desc: "Pipelines defined in YAML, triggered on push. Build, test, and deploy from your own runners.",
227 },
228 {
229 title: "Self-hosted",
230 desc: "Your code, your servers. Deploy on any Linux machine with a single command.",
231 },
232 ].map((f) => (
233 <div
234 key={f.title}
235 style={{
236 backgroundColor: "var(--bg-card)",
237 padding: "24px",
238 }}
239 >
240 <h3
241 style={{
242 fontSize: "0.875rem",
243 color: "var(--text-primary)",
244 marginBottom: "6px",
245 }}
246 >
247 {f.title}
248 </h3>
249 <p style={{ fontSize: "0.8rem", color: "var(--text-muted)", lineHeight: 1.6 }}>
250 {f.desc}
251 </p>
252 </div>
253 ))}
254 </div>
255 </section>
256
25773 </div>{/* end centered wrapper */}
25874
25975 {/* ISL — fills remaining viewport */}
26076 {islDomain && (
261 <section style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: "500px" }}>
262 <div style={{ padding: "0 24px 16px", maxWidth: "800px", margin: "0 auto", width: "100%" }}>
263 <h2
264 style={{
265 fontSize: "0.75rem",
266 fontWeight: 600,
267 textTransform: "uppercase",
268 letterSpacing: "0.1em",
269 color: "var(--text-faint)",
270 marginBottom: "4px",
271 }}
272 >
273 Interactive Smartlog
274 </h2>
275 <p style={{ fontSize: "0.875rem", color: "var(--text-muted)" }}>
276 Visualize your commit graph, browse diffs, and manage stacked changes — all in the browser.
277 This is a live, read-only view of Grove&apos;s own repository.
278 </p>
279 </div>
280 <div
77 <section
78 style={{
79 padding: "24px",
80 }}
81 >
82 <iframe
83 ref={islRef}
84 src={`${islDomain}?theme=${theme}`}
28185 style={{
282 flex: 1,
283 borderTop: "1px solid var(--border-subtle)",
86 width: "100%",
87 height: "calc(100vh - 3.5rem)",
88 border: "1px solid var(--border-subtle)",
89 display: "block",
90 }}
91 title="Interactive Smartlog"
92 onLoad={() => {
93 islRef.current?.contentWindow?.postMessage({ type: "theme", value: theme }, "*");
28494 }}
285 >
286 <iframe
287 ref={islRef}
288 src={`${islDomain}?theme=${theme}`}
289 style={{
290 width: "100%",
291 height: "100%",
292 border: "none",
293 display: "block",
294 }}
295 title="Interactive Smartlog"
296 onLoad={() => {
297 // Send theme on initial load
298 islRef.current?.contentWindow?.postMessage({ type: "theme", value: theme }, "*");
299 }}
300 />
301 </div>
95 />
30296 </section>
30397 )}
30498 </div>
30599