From 0ec63b3c97632869bb68d3ad2735792d650d8ff0 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Mon, 2 Feb 2026 15:48:17 -0500 Subject: [PATCH] feat: Redesign the designer layout using a grid system, adding explicit left, center, and right panels with collapse functionality. --- .../designer/DesignerPageClient.tsx | 6 +- .../experiments/designer/DesignerRoot.tsx | 155 ++++++++++-------- .../designer/flow/FlowWorkspace.tsx | 4 +- .../designer/layout/PanelsContainer.tsx | 147 ++++++++++------- .../designer/panels/ActionLibraryPanel.tsx | 10 +- .../designer/panels/InspectorPanel.tsx | 9 + src/components/ui/sidebar.tsx | 4 +- src/styles/globals.css | 10 +- 8 files changed, 203 insertions(+), 142 deletions(-) diff --git a/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx index fa108b8..9ecf7d5 100755 --- a/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx +++ b/src/app/(dashboard)/studies/[id]/experiments/[experimentId]/designer/DesignerPageClient.tsx @@ -55,9 +55,5 @@ export function DesignerPageClient({ }, ]); - return ( -
- -
- ); + return ; } diff --git a/src/components/experiments/designer/DesignerRoot.tsx b/src/components/experiments/designer/DesignerRoot.tsx index 30a30b6..67bb398 100755 --- a/src/components/experiments/designer/DesignerRoot.tsx +++ b/src/components/experiments/designer/DesignerRoot.tsx @@ -8,7 +8,17 @@ import React, { useState, } from "react"; import { toast } from "sonner"; -import { Play, RefreshCw, HelpCircle } from "lucide-react"; +import { + Play, + RefreshCw, + HelpCircle, + PanelLeftClose, + PanelLeftOpen, + PanelRightClose, + PanelRightOpen, + Maximize2, + Minimize2 +} from "lucide-react"; import { cn } from "~/lib/utils"; import { PageHeader } from "~/components/ui/page-header"; @@ -258,6 +268,23 @@ export function DesignerRoot({ const [inspectorTab, setInspectorTab] = useState< "properties" | "issues" | "dependencies" >("properties"); + + const [leftCollapsed, setLeftCollapsed] = useState(false); + const [rightCollapsed, setRightCollapsed] = useState(false); + + // Responsive initialization: Collapse left sidebar on smaller screens (<1280px) + useEffect(() => { + const checkWidth = () => { + if (window.innerWidth < 1280) { + setLeftCollapsed(true); + } + }; + // Check once on mount + checkWidth(); + // Optional: Add resize listener if we want live responsiveness + // window.addEventListener('resize', checkWidth); + // return () => window.removeEventListener('resize', checkWidth); + }, []); /** * Active action being dragged from the Action Library (for DragOverlay rendering). * Captures a lightweight subset for visual feedback. @@ -982,82 +1009,76 @@ export function DesignerRoot({ ); return ( -
+
-
- {/* Loading Overlay */} - {!isReady && ( -
-
- -

Loading designer...

-
-
- )} - - {/* Main Content - Fade in when ready */} -
+ toggleLibraryScrollLock(false)} > -
- toggleLibraryScrollLock(false)} - > - - - {dragOverlayAction ? ( -
- - {dragOverlayAction.name} -
- ) : null} -
-
-
- persist()} - onValidate={() => validateDesign()} - onExport={() => handleExport()} - onRecalculateHash={() => recomputeHash()} - lastSavedAt={lastSavedAt} - saving={isSaving} - validating={isValidating} - exporting={isExporting} - /> +
+ {/* Left Panel (2/8) */} +
+
+ Left Panel (2fr) +
+
+ {leftPanel} +
+
+ + {/* Center Panel (4/8) - The Workspace */} +
+
+ Center Workspace (4fr) +
+
+ {/* Center content needs to be relative for absolute positioning children if any */} + {centerPanel} +
+
+ + {/* Right Panel (2/8) */} +
+
+ Right Panel (2fr) +
+
+ {rightPanel} +
-
+ + + {dragOverlayAction ? ( +
+
+ {dragOverlayAction.name} +
+ ) : null} + +
); diff --git a/src/components/experiments/designer/flow/FlowWorkspace.tsx b/src/components/experiments/designer/flow/FlowWorkspace.tsx index 78e43b5..91aefa8 100755 --- a/src/components/experiments/designer/flow/FlowWorkspace.tsx +++ b/src/components/experiments/designer/flow/FlowWorkspace.tsx @@ -161,7 +161,7 @@ const StepRow = React.memo(function StepRow({
{steps.length === 0 ? ( diff --git a/src/components/experiments/designer/layout/PanelsContainer.tsx b/src/components/experiments/designer/layout/PanelsContainer.tsx index 327e603..acb32b7 100755 --- a/src/components/experiments/designer/layout/PanelsContainer.tsx +++ b/src/components/experiments/designer/layout/PanelsContainer.tsx @@ -38,6 +38,14 @@ export interface PanelsContainerProps { /** Keyboard resize step (fractional) per arrow press; Shift increases by 2x */ keyboardStepPct?: number; + + /** + * Controlled collapse state + */ + leftCollapsed?: boolean; + rightCollapsed?: boolean; + onLeftCollapseChange?: (collapsed: boolean) => void; + onRightCollapseChange?: (collapsed: boolean) => void; } /** @@ -45,6 +53,7 @@ export interface PanelsContainerProps { * * Tailwind-first, grid-based panel layout with: * - Drag-resizable left/right panels (no persistence) + * - Collapsible side panels * - Strict overflow containment (no page-level x-scroll) * - Internal y-scroll for each panel * - Optional visual dividers on the center panel only (prevents double borders) @@ -66,7 +75,7 @@ const Panel: React.FC (
{ if (!hasCenter) return { l: 0, c: 0, r: 0 }; + // Effective widths (0 if collapsed) + const effectiveL = leftCollapsed ? 0 : lp; + const effectiveR = rightCollapsed ? 0 : rp; + + // When logic runs, we must clamp the *underlying* percentages (lp, rp) + // but return 0 for the CSS vars if collapsed. + + // Actually, if collapsed, we just want the CSS var to be 0. + // But we maintain the state `leftPct` so it restores correctly. + if (hasLeft && hasRight) { - const l = clamp(lp, minLeftPct, maxLeftPct); - const r = clamp(rp, minRightPct, maxRightPct); - const c = Math.max(0.1, 1 - (l + r)); // always preserve some center space + // Standard clamp (on the state values) + const lState = clamp(lp, minLeftPct, maxLeftPct); + const rState = clamp(rp, minRightPct, maxRightPct); + + // Effective output + const l = leftCollapsed ? 0 : lState; + const r = rightCollapsed ? 0 : rState; + + // Center takes remainder + const c = 1 - (l + r); return { l, c, r }; } if (hasLeft && !hasRight) { - const l = clamp(lp, minLeftPct, maxLeftPct); - const c = Math.max(0.2, 1 - l); + const lState = clamp(lp, minLeftPct, maxLeftPct); + const l = leftCollapsed ? 0 : lState; + const c = 1 - l; return { l, c, r: 0 }; } if (!hasLeft && hasRight) { - const r = clamp(rp, minRightPct, maxRightPct); - const c = Math.max(0.2, 1 - r); + const rState = clamp(rp, minRightPct, maxRightPct); + const r = rightCollapsed ? 0 : rState; + const c = 1 - r; return { l: 0, c, r }; } // Center only @@ -145,6 +177,8 @@ export function PanelsContainer({ maxLeftPct, minRightPct, maxRightPct, + leftCollapsed, + rightCollapsed ], ); @@ -159,10 +193,10 @@ export function PanelsContainer({ const deltaPx = e.clientX - d.startX; const deltaPct = deltaPx / d.containerWidth; - if (d.edge === "left" && hasLeft) { + if (d.edge === "left" && hasLeft && !leftCollapsed) { const nextLeft = clamp(d.startLeft + deltaPct, minLeftPct, maxLeftPct); setLeftPct(nextLeft); - } else if (d.edge === "right" && hasRight) { + } else if (d.edge === "right" && hasRight && !rightCollapsed) { // Dragging the right edge moves leftwards as delta increases const nextRight = clamp( d.startRight - deltaPct, @@ -172,7 +206,7 @@ export function PanelsContainer({ setRightPct(nextRight); } }, - [hasLeft, hasRight, minLeftPct, maxLeftPct, minRightPct, maxRightPct], + [hasLeft, hasRight, minLeftPct, maxLeftPct, minRightPct, maxRightPct, leftCollapsed, rightCollapsed], ); const endDrag = React.useCallback(() => { @@ -215,14 +249,14 @@ export function PanelsContainer({ const step = (e.shiftKey ? 2 : 1) * keyboardStepPct; - if (edge === "left" && hasLeft) { + if (edge === "left" && hasLeft && !leftCollapsed) { const next = clamp( leftPct + (e.key === "ArrowRight" ? step : -step), minLeftPct, maxLeftPct, ); setLeftPct(next); - } else if (edge === "right" && hasRight) { + } else if (edge === "right" && hasRight && !rightCollapsed) { const next = clamp( rightPct + (e.key === "ArrowLeft" ? step : -step), minRightPct, @@ -233,23 +267,33 @@ export function PanelsContainer({ }; // CSS variables for the grid fractions + // We use FR units instead of % to let the browser handle exact pixel fitting without rounding errors causing overflow const styleVars: React.CSSProperties & Record = hasCenter ? { - "--col-left": `${(hasLeft ? l : 0) * 100}%`, - "--col-center": `${c * 100}%`, - "--col-right": `${(hasRight ? r : 0) * 100}%`, + "--col-left": `${hasLeft ? l : 0}fr`, + "--col-center": `${c}fr`, + "--col-right": `${hasRight ? r : 0}fr`, } : {}; // Explicit grid template depending on which side panels exist + const gridAreas = + hasLeft && hasRight + ? '"left center right"' + : hasLeft && !hasRight + ? '"left center"' + : !hasLeft && hasRight + ? '"center right"' + : '"center"'; + const gridCols = hasLeft && hasRight - ? "[grid-template-columns:minmax(0,var(--col-left))_minmax(0,var(--col-center))_minmax(0,var(--col-right))]" + ? "[grid-template-columns:var(--col-left)_var(--col-center)_var(--col-right)]" : hasLeft && !hasRight - ? "[grid-template-columns:minmax(0,var(--col-left))_minmax(0,var(--col-center))]" + ? "[grid-template-columns:var(--col-left)_var(--col-center)]" : !hasLeft && hasRight - ? "[grid-template-columns:minmax(0,var(--col-center))_minmax(0,var(--col-right))]" - : "[grid-template-columns:minmax(0,1fr)]"; + ? "[grid-template-columns:var(--col-center)_var(--col-right)]" + : "[grid-template-columns:1fr]"; // Dividers on the center panel only (prevents double borders if children have their own borders) const centerDividers = @@ -303,7 +347,7 @@ export function PanelsContainer({
{/* Main Content (Center) */} -
+
{center}
@@ -312,14 +356,28 @@ export function PanelsContainer({ ); diff --git a/src/components/experiments/designer/panels/ActionLibraryPanel.tsx b/src/components/experiments/designer/panels/ActionLibraryPanel.tsx index 0619a8b..b2ccf8f 100755 --- a/src/components/experiments/designer/panels/ActionLibraryPanel.tsx +++ b/src/components/experiments/designer/panels/ActionLibraryPanel.tsx @@ -22,6 +22,7 @@ import { Eye, X, Layers, + PanelLeftClose, } from "lucide-react"; import { Input } from "~/components/ui/input"; import { Button } from "~/components/ui/button"; @@ -108,7 +109,7 @@ function DraggableAction({ {...listeners} style={style} className={cn( - "group bg-background/60 hover:bg-accent/50 relative flex w-full cursor-grab touch-none flex-col gap-1 rounded border px-2 text-left transition-colors select-none", + "group bg-background/60 hover:bg-accent/50 relative flex w-full cursor-grab touch-none flex-col gap-1 rounded-lg border px-2 text-left transition-colors select-none", compact ? "py-1.5 text-[11px]" : "py-2 text-[12px]", isDragging && "opacity-50", )} @@ -168,7 +169,12 @@ function DraggableAction({ ); } -export function ActionLibraryPanel() { +export interface ActionLibraryPanelProps { + collapsed?: boolean; + onCollapse?: (collapsed: boolean) => void; +} + +export function ActionLibraryPanel({ collapsed, onCollapse }: ActionLibraryPanelProps = {}) { const registry = useActionRegistry(); const [search, setSearch] = useState(""); diff --git a/src/components/experiments/designer/panels/InspectorPanel.tsx b/src/components/experiments/designer/panels/InspectorPanel.tsx index 0e5d2ac..2a29862 100755 --- a/src/components/experiments/designer/panels/InspectorPanel.tsx +++ b/src/components/experiments/designer/panels/InspectorPanel.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useState, useCallback } from "react"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs"; +import { Button } from "~/components/ui/button"; import { cn } from "~/lib/utils"; import { useDesignerStore } from "../state/store"; @@ -18,6 +19,7 @@ import { AlertTriangle, GitBranch, PackageSearch, + PanelRightClose, } from "lucide-react"; /** @@ -47,6 +49,11 @@ export interface InspectorPanelProps { * Called when user changes tab (only if activeTab not externally controlled). */ onTabChange?: (tab: "properties" | "issues" | "dependencies") => void; + /** + * Collapse state and handler + */ + collapsed?: boolean; + onCollapse?: (collapsed: boolean) => void; /** * If true, auto-switch to "properties" when a selection occurs. */ @@ -68,6 +75,8 @@ export function InspectorPanel({ onTabChange, autoFocusOnSelection = true, studyPlugins, + collapsed, + onCollapse, }: InspectorPanelProps) { /* ------------------------------------------------------------------------ */ /* Store Selectors */ diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index db1d8ce..7f3e588 100755 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -309,7 +309,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {