fix(app): settings review — startup theme default, slider/shell input UX, dedupe accents, memoize palette
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -138,8 +138,8 @@ export function App() {
|
|||||||
const effectiveFocus = focusedId && leaves.includes(focusedId) ? focusedId : leaves[0] ?? null;
|
const effectiveFocus = focusedId && leaves.includes(focusedId) ? focusedId : leaves[0] ?? null;
|
||||||
effectiveFocusRef.current = effectiveFocus;
|
effectiveFocusRef.current = effectiveFocus;
|
||||||
|
|
||||||
const termFont = config ? { family: config.font_family, size: config.font_size } : null;
|
const termPalette = useMemo(() => (config ? resolvePalette(config.theme, config.accent) : null), [config?.theme, config?.accent]);
|
||||||
const termPalette = config ? resolvePalette(config.theme, config.accent) : null;
|
const termFont = useMemo(() => (config ? { family: config.font_family, size: config.font_size } : null), [config?.font_family, config?.font_size]);
|
||||||
|
|
||||||
function selectWorkspace(id: string) {
|
function selectWorkspace(id: string) {
|
||||||
setActiveId(id);
|
setActiveId(id);
|
||||||
@@ -149,7 +149,7 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", height: "100vh", background: COLORS.bgApp }}>
|
<div style={{ display: "flex", flexDirection: "column", height: "100vh", background: COLORS.bgApp }}>
|
||||||
<TopBar active={active} eventsOpen={eventsOpen} onToggleEvents={() => setEventsOpen((v) => !v)} onShowEvents={() => setEventsOpen(true)} sidebarOpen={sidebarOpen} onToggleSidebar={() => setSidebarOpen((v) => !v)} unread={unread} onOpenSettings={() => setSettingsOpen(true)} />
|
<TopBar active={active} eventsOpen={eventsOpen} onToggleEvents={() => setEventsOpen((v) => !v)} onShowEvents={() => setEventsOpen(true)} sidebarOpen={sidebarOpen} onToggleSidebar={() => setSidebarOpen((v) => !v)} unread={unread} onOpenSettings={() => { if (config) setSettingsOpen(true); }} />
|
||||||
<div style={{ flex: 1, display: "flex", minHeight: 0 }}>
|
<div style={{ flex: 1, display: "flex", minHeight: 0 }}>
|
||||||
{sidebarOpen && <Sidebar groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />}
|
{sidebarOpen && <Sidebar groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />}
|
||||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||||
|
|||||||
+19
-12
@@ -1,17 +1,21 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { COLORS, FONT } from "./theme";
|
import { COLORS, FONT, ACCENTS } from "./theme";
|
||||||
import { setConfig, shutdownDaemon, restartDaemon } from "./socketBridge";
|
import { setConfig, shutdownDaemon, restartDaemon } from "./socketBridge";
|
||||||
import type { ConfigView, DaemonHealth } from "./socketBridge";
|
import type { ConfigView, DaemonHealth } from "./socketBridge";
|
||||||
|
|
||||||
const FONTS = ["JetBrains Mono", "Menlo", "Monaco", "SF Mono", "Fira Code", "Cascadia Code"];
|
const FONTS = ["JetBrains Mono", "Menlo", "Monaco", "SF Mono", "Fira Code", "Cascadia Code"];
|
||||||
const ACCENTS: { id: string; hex: string }[] = [
|
|
||||||
{ id: "blue", hex: "#4C8DFF" }, { id: "teal", hex: "#34D3C2" }, { id: "purple", hex: "#9B7BFF" },
|
|
||||||
{ id: "green", hex: "#3FB950" }, { id: "orange", hex: "#F2934B" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Settings({ config, health, onClose }: { config: ConfigView; health: DaemonHealth | null; onClose: () => void }) {
|
export function Settings({ config, health, onClose }: { config: ConfigView; health: DaemonHealth | null; onClose: () => void }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
useEffect(() => { ref.current?.focus(); }, []);
|
useEffect(() => { ref.current?.focus(); }, []);
|
||||||
|
|
||||||
|
// Fix 2: local state for font-size slider — committed only on pointer release.
|
||||||
|
const [sizeLocal, setSizeLocal] = useState(config.font_size);
|
||||||
|
useEffect(() => { setSizeLocal(config.font_size); }, [config.font_size]);
|
||||||
|
|
||||||
|
// Fix 3: controlled shell input — synced from config, committed on blur.
|
||||||
|
const [shellLocal, setShellLocal] = useState(config.default_shell);
|
||||||
|
useEffect(() => { setShellLocal(config.default_shell); }, [config.default_shell]);
|
||||||
return (
|
return (
|
||||||
<div onMouseDown={onClose} style={{ position: "fixed", inset: 0, zIndex: 2000, background: "#000A", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
<div onMouseDown={onClose} style={{ position: "fixed", inset: 0, zIndex: 2000, background: "#000A", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
<div ref={ref} tabIndex={-1} onMouseDown={(e) => e.stopPropagation()} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Escape") onClose(); }}
|
<div ref={ref} tabIndex={-1} onMouseDown={(e) => e.stopPropagation()} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Escape") onClose(); }}
|
||||||
@@ -24,8 +28,11 @@ export function Settings({ config, health, onClose }: { config: ConfigView; heal
|
|||||||
{FONTS.map((f) => <option key={f} value={f}>{f}</option>)}
|
{FONTS.map((f) => <option key={f} value={f}>{f}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 18 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 18 }}>
|
||||||
<span style={{ fontSize: 12, color: COLORS.textSecondary }}>Size {config.font_size}</span>
|
<span style={{ fontSize: 12, color: COLORS.textSecondary }}>Size {sizeLocal}</span>
|
||||||
<input type="range" min={10} max={20} value={config.font_size} onChange={(e) => void setConfig({ font_size: Number(e.target.value) })} style={{ flex: 1 }} />
|
<input type="range" min={10} max={20} value={sizeLocal}
|
||||||
|
onChange={(e) => setSizeLocal(Number(e.target.value))}
|
||||||
|
onPointerUp={() => void setConfig({ font_size: sizeLocal })}
|
||||||
|
style={{ flex: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 6 }}>Theme</div>
|
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 6 }}>Theme</div>
|
||||||
@@ -39,15 +46,15 @@ export function Settings({ config, health, onClose }: { config: ConfigView; heal
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 6 }}>Accent</div>
|
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 6 }}>Accent</div>
|
||||||
<div style={{ display: "flex", gap: 10, marginBottom: 18 }}>
|
<div style={{ display: "flex", gap: 10, marginBottom: 18 }}>
|
||||||
{ACCENTS.map((a) => (
|
{Object.entries(ACCENTS).map(([id, hex]) => (
|
||||||
<button key={a.id} onClick={() => void setConfig({ accent: a.id })} aria-label={a.id}
|
<button key={id} onClick={() => void setConfig({ accent: id })} aria-label={id}
|
||||||
style={{ width: 26, height: 26, borderRadius: "50%", background: a.hex, cursor: "pointer",
|
style={{ width: 26, height: 26, borderRadius: "50%", background: hex, cursor: "pointer",
|
||||||
border: config.accent === a.id ? `2px solid ${COLORS.textPrimary}` : "2px solid transparent" }} />
|
border: config.accent === id ? `2px solid ${COLORS.textPrimary}` : "2px solid transparent" }} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 6 }}>Default shell (empty = auto)</div>
|
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 6 }}>Default shell (empty = auto)</div>
|
||||||
<input defaultValue={config.default_shell} onBlur={(e) => void setConfig({ default_shell: e.target.value })}
|
<input value={shellLocal} onChange={(e) => setShellLocal(e.target.value)} onBlur={() => void setConfig({ default_shell: shellLocal })}
|
||||||
style={{ width: "100%", padding: 8, marginBottom: 18, background: COLORS.bgPanel, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 8 }} />
|
style={{ width: "100%", padding: 8, marginBottom: 18, background: COLORS.bgPanel, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 8 }} />
|
||||||
|
|
||||||
<DaemonSection health={health} />
|
<DaemonSection health={health} />
|
||||||
|
|||||||
@@ -103,10 +103,7 @@ export function TerminalView({ surfaceId, font, palette }: { surfaceId: string;
|
|||||||
}, [surfaceId]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [surfaceId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// Live re-apply font and theme when config changes without remounting.
|
// Live re-apply font and theme when config changes without remounting.
|
||||||
// palette is a new object each render so we depend on a stable key instead.
|
// font and palette are memoized in App.tsx so stable identity = no spurious re-applies.
|
||||||
const paletteKey = palette
|
|
||||||
? `${palette["bg-panel"]}|${palette["text-primary"]}|${palette["search-match"]}`
|
|
||||||
: null;
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = termRef.current;
|
const t = termRef.current;
|
||||||
if (!t) return;
|
if (!t) return;
|
||||||
@@ -116,7 +113,7 @@ export function TerminalView({ surfaceId, font, palette }: { surfaceId: string;
|
|||||||
}
|
}
|
||||||
if (palette) t.options.theme = xtermTheme(palette);
|
if (palette) t.options.theme = xtermTheme(palette);
|
||||||
requestAnimationFrame(() => { try { fitRef.current?.fit(); } catch { /* ignore */ } });
|
requestAnimationFrame(() => { try { fitRef.current?.fit(); } catch { /* ignore */ } });
|
||||||
}, [font?.family, font?.size, paletteKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [font, palette]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return <div ref={ref} style={{ width: "100%", height: "100%" }} />;
|
return <div ref={ref} style={{ width: "100%", height: "100%" }} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
|
import { applyTheme } from "./theme";
|
||||||
import "@fontsource/inter/400.css";
|
import "@fontsource/inter/400.css";
|
||||||
import "@fontsource/inter/500.css";
|
import "@fontsource/inter/500.css";
|
||||||
import "@fontsource/inter/600.css";
|
import "@fontsource/inter/600.css";
|
||||||
@@ -9,6 +10,10 @@ import "@fontsource-variable/jetbrains-mono";
|
|||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
|
// Apply default theme before React renders so CSS vars are never unset,
|
||||||
|
// even if the daemon is slow or offline. getConfig() overrides this later.
|
||||||
|
applyTheme("dark", "blue");
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
Reference in New Issue
Block a user