| @@ -4,115 +4,6 @@ |
| 4 | 4 | import { GroveLogo } from "@/app/components/grove-logo"; |
| 5 | 5 | import { useTheme } from "@/lib/theme"; |
| 6 | 6 | |
| 7 | | const 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 | | |
| 24 | | function 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 | | |
| 112 | | function sleep(ms: number) { |
| 113 | | return new Promise((r) => setTimeout(r, ms)); |
| 114 | | } |
| 115 | | |
| 116 | 7 | export function LandingPage() { |
| 117 | 8 | const [islDomain, setIslDomain] = useState(""); |
| 118 | 9 | const { theme } = useTheme(); |
| @@ -133,7 +24,7 @@ |
| 133 | 24 | }, [theme]); |
| 134 | 25 | |
| 135 | 26 | return ( |
| 136 | | <div style={{ display: "flex", flexDirection: "column", minHeight: "calc(100vh - 3.5rem)" }}> |
| 27 | <div style={{ display: "flex", flexDirection: "column", height: "100%" }}> |
| 137 | 28 | {/* Hero + Terminal + Features */} |
| 138 | 29 | <div style={{ maxWidth: "800px", margin: "0 auto", padding: "60px 24px 0", width: "100%" }}> |
| 139 | 30 | |
| @@ -179,126 +70,29 @@ |
| 179 | 70 | </a> |
| 180 | 71 | </div> |
| 181 | 72 | |
| 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'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 | | |
| 257 | 73 | </div>{/* end centered wrapper */} |
| 258 | 74 | |
| 259 | 75 | {/* ISL — fills remaining viewport */} |
| 260 | 76 | {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'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}`} |
| 281 | 85 | 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 }, "*"); |
| 284 | 94 | }} |
| 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 | /> |
| 302 | 96 | </section> |
| 303 | 97 | )} |
| 304 | 98 | </div> |
| 305 | 99 | |