feat(app): panel zoom — full-grid render + header toggle
Wire Cmd::SetZoom through Tauri bridge (set_zoom command), add zoomed field to WorkspaceView, short-circuit LayoutEngine to render only the zoomed panel full-grid, and toggle Maximize2/Minimize2 in panel header. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -299,6 +299,17 @@ pub async fn focus(state: BridgeState<'_>, surface_id: String) -> Result<Value,
|
|||||||
data_of(state.request(Cmd::Focus { surface_id: SurfaceId(surface_id) }).await.map_err(|e| e.to_string())?)
|
data_of(state.request(Cmd::Focus { surface_id: SurfaceId(surface_id) }).await.map_err(|e| e.to_string())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- SP4 zoom command ----
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn set_zoom(state: BridgeState<'_>, workspace_id: String, surface_id: Option<String>) -> Result<Value, String> {
|
||||||
|
let cmd = Cmd::SetZoom {
|
||||||
|
workspace_id: spacesh_proto::WorkspaceId(workspace_id),
|
||||||
|
surface_id: surface_id.map(spacesh_proto::SurfaceId),
|
||||||
|
};
|
||||||
|
data_of(state.request(cmd).await.map_err(|e| e.to_string())?)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- M3 event log commands ----
|
// ---- M3 event log commands ----
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ pub fn run() {
|
|||||||
bridge::set_group,
|
bridge::set_group,
|
||||||
bridge::delete_group,
|
bridge::delete_group,
|
||||||
bridge::focus,
|
bridge::focus,
|
||||||
|
bridge::set_zoom,
|
||||||
bridge::event_log,
|
bridge::event_log,
|
||||||
bridge::mark_read,
|
bridge::mark_read,
|
||||||
bridge::health,
|
bridge::health,
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@ export function App() {
|
|||||||
)}
|
)}
|
||||||
<div style={{ flex: 1, minHeight: 0 }}>
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
{active
|
{active
|
||||||
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} states={states} surfaces={active.surfaces} focusedId={effectiveFocus} onFocus={setFocusedId} />
|
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} states={states} surfaces={active.surfaces} focusedId={effectiveFocus} onFocus={setFocusedId} zoomed={active.zoomed} />
|
||||||
: <div style={{ color: COLORS.textMuted, padding: 24 }}>No workspace — create one to begin.</div>}
|
: <div style={{ color: COLORS.textMuted, padding: 24 }}>No workspace — create one to begin.</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+32
-11
@@ -1,10 +1,10 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { Maximize2, RotateCw } from "lucide-react";
|
import { Maximize2, Minimize2, RotateCw } from "lucide-react";
|
||||||
import { TerminalView } from "./TerminalView";
|
import { TerminalView } from "./TerminalView";
|
||||||
import { StatusRing } from "./StatusRing";
|
import { StatusRing } from "./StatusRing";
|
||||||
import { COLORS, FONT, STATE_COLOR } from "./theme";
|
import { COLORS, FONT, STATE_COLOR } from "./theme";
|
||||||
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
|
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
|
||||||
import { setRatios, restartSurface } from "./socketBridge";
|
import { setRatios, restartSurface, setZoom } from "./socketBridge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@@ -15,6 +15,7 @@ interface Props {
|
|||||||
surfaces: Record<string, SurfaceView>;
|
surfaces: Record<string, SurfaceView>;
|
||||||
focusedId: string | null;
|
focusedId: string | null;
|
||||||
onFocus: (id: string) => void;
|
onFocus: (id: string) => void;
|
||||||
|
zoomed: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Collapse an absolute cwd into a ~/<leaf> style label for the panel header. */
|
/** Collapse an absolute cwd into a ~/<leaf> style label for the panel header. */
|
||||||
@@ -23,21 +24,29 @@ function shortPath(cwd: string): string {
|
|||||||
return leaf ? `~/${leaf}` : cwd;
|
return leaf ? `~/${leaf}` : cwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LayoutEngine({ workspaceId, layout, running, states, surfaces, focusedId, onFocus }: Props) {
|
export function LayoutEngine({ workspaceId, layout, running, states, surfaces, focusedId, onFocus, zoomed }: Props) {
|
||||||
if (!layout) {
|
if (!layout) {
|
||||||
return <div style={{ color: COLORS.textMuted, padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
|
return <div style={{ color: COLORS.textMuted, padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
|
||||||
}
|
}
|
||||||
|
if (zoomed) {
|
||||||
|
return (
|
||||||
|
<div style={{ width: "100%", height: "100%", padding: 12, boxSizing: "border-box" }}>
|
||||||
|
<Node workspaceId={workspaceId} node={{ leaf: { surface_id: zoomed } }} path={[]} running={running} states={states} surfaces={surfaces} focusedId={focusedId} onFocus={onFocus} zoomed={zoomed} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", height: "100%", padding: 12, boxSizing: "border-box" }}>
|
<div style={{ width: "100%", height: "100%", padding: 12, boxSizing: "border-box" }}>
|
||||||
<Node workspaceId={workspaceId} node={layout} path={[]} running={running} states={states} surfaces={surfaces} focusedId={focusedId} onFocus={onFocus} />
|
<Node workspaceId={workspaceId} node={layout} path={[]} running={running} states={states} surfaces={surfaces} focusedId={focusedId} onFocus={onFocus} zoomed={zoomed} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Node({ workspaceId, node, path, running, states, surfaces, focusedId, onFocus }: {
|
function Node({ workspaceId, node, path, running, states, surfaces, focusedId, onFocus, zoomed }: {
|
||||||
workspaceId: string; node: LayoutNode; path: number[];
|
workspaceId: string; node: LayoutNode; path: number[];
|
||||||
running: Record<string, boolean>; states: Record<string, SurfaceState>;
|
running: Record<string, boolean>; states: Record<string, SurfaceState>;
|
||||||
surfaces: Record<string, SurfaceView>; focusedId: string | null; onFocus: (id: string) => void;
|
surfaces: Record<string, SurfaceView>; focusedId: string | null; onFocus: (id: string) => void;
|
||||||
|
zoomed: string | null;
|
||||||
}) {
|
}) {
|
||||||
if ("leaf" in node) {
|
if ("leaf" in node) {
|
||||||
const id = node.leaf.surface_id;
|
const id = node.leaf.surface_id;
|
||||||
@@ -60,10 +69,18 @@ function Node({ workspaceId, node, path, running, states, surfaces, focusedId, o
|
|||||||
return card(
|
return card(
|
||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", width: "100%", color: COLORS.textSecondary, flexDirection: "column", gap: 10 }}>
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", width: "100%", color: COLORS.textSecondary, flexDirection: "column", gap: 10 }}>
|
||||||
<div style={{ fontFamily: FONT.mono, fontSize: 13 }}>Process exited</div>
|
<div style={{ fontFamily: FONT.mono, fontSize: 13 }}>Process exited</div>
|
||||||
<button onClick={() => void restartSurface(id)}
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
|
<button onClick={() => void restartSurface(id)}
|
||||||
<RotateCw size={13} /> Restart
|
style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
|
||||||
</button>
|
<RotateCw size={13} /> Restart
|
||||||
|
</button>
|
||||||
|
{zoomed === id && (
|
||||||
|
<button onClick={() => void setZoom(workspaceId, null)}
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: "transparent", color: COLORS.textSecondary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 12 }}>
|
||||||
|
<Minimize2 size={13} /> Exit zoom
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,7 +98,11 @@ function Node({ workspaceId, node, path, running, states, surfaces, focusedId, o
|
|||||||
<span style={{ display: "flex", alignItems: "center", height: 16, padding: "0 7px", borderRadius: 8, background: "#000", fontFamily: FONT.mono, fontSize: 10, fontWeight: 600, color: STATE_COLOR[state] }}>
|
<span style={{ display: "flex", alignItems: "center", height: 16, padding: "0 7px", borderRadius: 8, background: "#000", fontFamily: FONT.mono, fontSize: 10, fontWeight: 600, color: STATE_COLOR[state] }}>
|
||||||
{state}
|
{state}
|
||||||
</span>
|
</span>
|
||||||
<Maximize2 size={13} color={COLORS.textMuted} style={{ cursor: "pointer" }} aria-label="Zoom (mock)" />
|
{zoomed === id
|
||||||
|
? <Minimize2 size={13} color={COLORS.textSecondary} style={{ cursor: "pointer" }} aria-label="Unzoom"
|
||||||
|
onMouseDown={(e) => { e.stopPropagation(); void setZoom(workspaceId, null); }} />
|
||||||
|
: <Maximize2 size={13} color={COLORS.textMuted} style={{ cursor: "pointer" }} aria-label="Zoom"
|
||||||
|
onMouseDown={(e) => { e.stopPropagation(); onFocus(id); void setZoom(workspaceId, id); }} />}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minHeight: 0 }}>
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
<TerminalView key={id} surfaceId={id} />
|
<TerminalView key={id} surfaceId={id} />
|
||||||
@@ -102,7 +123,7 @@ function Node({ workspaceId, node, path, running, states, surfaces, focusedId, o
|
|||||||
next[i + 1] = Math.max(0.05, (next[i + 1] ?? 1) - deltaFrac);
|
next[i + 1] = Math.max(0.05, (next[i + 1] ?? 1) - deltaFrac);
|
||||||
void setRatios(workspaceId, path, next);
|
void setRatios(workspaceId, path, next);
|
||||||
}}>
|
}}>
|
||||||
<Node workspaceId={workspaceId} node={child} path={[...path, i]} running={running} states={states} surfaces={surfaces} focusedId={focusedId} onFocus={onFocus} />
|
<Node workspaceId={workspaceId} node={child} path={[...path, i]} running={running} states={states} surfaces={surfaces} focusedId={focusedId} onFocus={onFocus} zoomed={zoomed} />
|
||||||
</Pane>
|
</Pane>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface WorkspaceView {
|
|||||||
order: number;
|
order: number;
|
||||||
unread: boolean;
|
unread: boolean;
|
||||||
layout: LayoutNode | null;
|
layout: LayoutNode | null;
|
||||||
|
zoomed: string | null;
|
||||||
surfaces: Record<string, SurfaceView>;
|
surfaces: Record<string, SurfaceView>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,3 +181,7 @@ export interface DaemonHealth { version: string; pid: number; started_at_ms: num
|
|||||||
export async function getHealth(): Promise<DaemonHealth> {
|
export async function getHealth(): Promise<DaemonHealth> {
|
||||||
return await invoke<DaemonHealth>("health");
|
return await invoke<DaemonHealth>("health");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setZoom(workspaceId: string, surfaceId: string | null): Promise<void> {
|
||||||
|
await invoke("set_zoom", { workspaceId, surfaceId });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user