/** * `useLayouts` — view-model hook for the named-layout tab bar (L4 / #4). * * Manages the list of named layouts for a project (list / create / rename / * delete / set-active). Speaks exclusively to the {@link LayoutGateway} port. */ import { useCallback, useEffect, useState } from "react"; import type { GatewayError, LayoutInfo, LayoutKind } from "@/domain"; import { useGateways } from "@/app/di"; export interface LayoutsViewModel { /** Ordered list of named layouts. */ layouts: LayoutInfo[]; /** Currently active layout id. */ activeId: string | null; /** Whether a request is in flight. */ busy: boolean; /** Last error message, or `null`. */ error: string | null; /** Switches the active layout (does NOT force a re-fetch here; the caller uses the returned activeId). */ setActive: (layoutId: string) => Promise; /** Creates a new layout with the given name and kind; resolves with the new layoutId. */ create: (name: string, kind?: LayoutKind) => Promise; /** Renames a layout. */ rename: (layoutId: string, name: string) => Promise; /** Deletes a layout; refuses if it is the last one. */ deleteLayout: (layoutId: string) => Promise; } function describe(e: unknown): string { if (e && typeof e === "object" && "message" in e) { return String((e as GatewayError).message); } return String(e); } export function useLayouts(projectId: string | null): LayoutsViewModel { const { layout: gateway } = useGateways(); const [layouts, setLayouts] = useState([]); const [activeId, setActiveId] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); // Load the list on mount and whenever projectId changes. useEffect(() => { if (!projectId || !gateway) { setLayouts([]); setActiveId(null); return; } let cancelled = false; setBusy(true); gateway .listLayouts(projectId) .then(({ layouts: list, activeId: aid }) => { if (!cancelled) { setLayouts(list); setActiveId(aid); } }) .catch((e: unknown) => { if (!cancelled) setError(describe(e)); }) .finally(() => { if (!cancelled) setBusy(false); }); return () => { cancelled = true; }; }, [gateway, projectId]); const setActive = useCallback( async (layoutId: string) => { if (!projectId || !gateway) return; setBusy(true); setError(null); try { await gateway.setActiveLayout(projectId, layoutId); setActiveId(layoutId); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [gateway, projectId], ); const create = useCallback( async (name: string, kind?: LayoutKind): Promise => { if (!projectId || !gateway) return null; setBusy(true); setError(null); try { const { layoutId } = await gateway.createLayout(projectId, name, kind); const updated = await gateway.listLayouts(projectId); setLayouts(updated.layouts); return layoutId; } catch (e) { setError(describe(e)); return null; } finally { setBusy(false); } }, [gateway, projectId], ); const rename = useCallback( async (layoutId: string, name: string) => { if (!projectId || !gateway) return; setBusy(true); setError(null); try { await gateway.renameLayout(projectId, layoutId, name); setLayouts((prev) => prev.map((l) => (l.id === layoutId ? { ...l, name } : l)), ); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [gateway, projectId], ); const deleteLayout = useCallback( async (layoutId: string) => { if (!projectId || !gateway) return; if (layouts.length <= 1) { setError("Cannot delete the last layout."); return; } setBusy(true); setError(null); try { const { activeId: newActiveId } = await gateway.deleteLayout(projectId, layoutId); setLayouts((prev) => prev.filter((l) => l.id !== layoutId)); setActiveId(newActiveId); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [gateway, projectId, layouts.length], ); return { layouts, activeId, busy, error, setActive, create, rename, deleteLayout }; }