mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-02-04 23:46:32 -05:00
feat: Redesign the designer layout using a grid system, adding explicit left, center, and right panels with collapse functionality.
This commit is contained in:
@@ -55,9 +55,5 @@ export function DesignerPageClient({
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return <DesignerRoot experimentId={experiment.id} initialDesign={initialDesign} />;
|
||||||
<div className="h-[calc(100vh-4rem-2rem)] w-full overflow-hidden border rounded-lg bg-background">
|
|
||||||
<DesignerRoot experimentId={experiment.id} initialDesign={initialDesign} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,17 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "sonner";
|
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 { cn } from "~/lib/utils";
|
||||||
import { PageHeader } from "~/components/ui/page-header";
|
import { PageHeader } from "~/components/ui/page-header";
|
||||||
@@ -258,6 +268,23 @@ export function DesignerRoot({
|
|||||||
const [inspectorTab, setInspectorTab] = useState<
|
const [inspectorTab, setInspectorTab] = useState<
|
||||||
"properties" | "issues" | "dependencies"
|
"properties" | "issues" | "dependencies"
|
||||||
>("properties");
|
>("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).
|
* Active action being dragged from the Action Library (for DragOverlay rendering).
|
||||||
* Captures a lightweight subset for visual feedback.
|
* Captures a lightweight subset for visual feedback.
|
||||||
@@ -982,82 +1009,76 @@ export function DesignerRoot({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
<div className="flex h-[calc(100vh-5rem)] w-full flex-col overflow-hidden bg-background">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={designMeta.name}
|
title={designMeta.name}
|
||||||
description={designMeta.description || "No description"}
|
description={designMeta.description || "No description"}
|
||||||
icon={Play}
|
icon={Play}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
className="pb-6"
|
className="flex-none pb-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative flex flex-1 flex-col overflow-hidden">
|
{/* Main Grid Container - 2-4-2 Split */}
|
||||||
{/* Loading Overlay */}
|
{/* Main Grid Container - 2-4-2 Split */}
|
||||||
{!isReady && (
|
<div className="flex-1 min-h-0 w-full px-4 overflow-hidden">
|
||||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background">
|
<DndContext
|
||||||
<div className="flex flex-col items-center gap-4">
|
sensors={sensors}
|
||||||
<RefreshCw className="h-8 w-8 animate-spin text-primary" />
|
collisionDetection={closestCorners}
|
||||||
<p className="text-muted-foreground text-sm">Loading designer...</p>
|
onDragStart={handleDragStart}
|
||||||
</div>
|
onDragOver={handleDragOver}
|
||||||
</div>
|
onDragEnd={handleDragEnd}
|
||||||
)}
|
onDragCancel={() => toggleLibraryScrollLock(false)}
|
||||||
|
|
||||||
{/* Main Content - Fade in when ready */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 flex-col overflow-hidden transition-opacity duration-500",
|
|
||||||
isReady ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div className="flex h-[calc(100vh-12rem)] w-full max-w-full flex-col overflow-hidden rounded-md border">
|
<div className="grid grid-cols-8 gap-4 h-full w-full">
|
||||||
<DndContext
|
{/* Left Panel (2/8) */}
|
||||||
sensors={sensors}
|
<div className="col-span-2 flex flex-col overflow-hidden rounded-lg border-2 border-dashed border-red-300 bg-red-50/50 dark:bg-red-900/10">
|
||||||
collisionDetection={closestCorners}
|
<div className="flex items-center justify-between border-b border-red-200 bg-red-100/50 px-3 py-2 text-sm font-medium text-red-900 dark:border-red-800 dark:bg-red-900/20 dark:text-red-100">
|
||||||
onDragStart={handleDragStart}
|
Left Panel (2fr)
|
||||||
onDragOver={handleDragOver}
|
</div>
|
||||||
onDragEnd={handleDragEnd}
|
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||||
onDragCancel={() => toggleLibraryScrollLock(false)}
|
{leftPanel}
|
||||||
>
|
</div>
|
||||||
<PanelsContainer
|
</div>
|
||||||
showDividers
|
|
||||||
className="min-h-0 flex-1"
|
{/* Center Panel (4/8) - The Workspace */}
|
||||||
left={leftPanel}
|
<div className="col-span-4 flex flex-col overflow-hidden rounded-lg border-2 border-dashed border-green-300 bg-green-50/50 dark:bg-green-900/10">
|
||||||
center={centerPanel}
|
<div className="flex items-center justify-between border-b border-green-200 bg-green-100/50 px-3 py-2 text-sm font-medium text-green-900 dark:border-green-800 dark:bg-green-900/20 dark:text-green-100">
|
||||||
right={rightPanel}
|
Center Workspace (4fr)
|
||||||
/>
|
</div>
|
||||||
<DragOverlay>
|
<div className="flex-1 overflow-hidden min-h-0 relative">
|
||||||
{dragOverlayAction ? (
|
{/* Center content needs to be relative for absolute positioning children if any */}
|
||||||
<div className="bg-background flex items-center gap-2 rounded border px-3 py-2 text-xs font-medium shadow-lg select-none">
|
{centerPanel}
|
||||||
<span
|
</div>
|
||||||
className={cn(
|
</div>
|
||||||
"h-2.5 w-2.5 rounded-full",
|
|
||||||
{
|
{/* Right Panel (2/8) */}
|
||||||
wizard: "bg-blue-500",
|
<div className="col-span-2 flex flex-col overflow-hidden rounded-lg border-2 border-dashed border-blue-300 bg-blue-50/50 dark:bg-blue-900/10">
|
||||||
robot: "bg-emerald-600",
|
<div className="flex items-center justify-between border-b border-blue-200 bg-blue-100/50 px-3 py-2 text-sm font-medium text-blue-900 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-100">
|
||||||
control: "bg-amber-500",
|
Right Panel (2fr)
|
||||||
observation: "bg-purple-600",
|
</div>
|
||||||
}[dragOverlayAction.category] || "bg-slate-400",
|
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||||
)}
|
{rightPanel}
|
||||||
/>
|
</div>
|
||||||
{dragOverlayAction.name}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
<div className="flex-shrink-0 border-t">
|
|
||||||
<BottomStatusBar
|
|
||||||
onSave={() => persist()}
|
|
||||||
onValidate={() => validateDesign()}
|
|
||||||
onExport={() => handleExport()}
|
|
||||||
onRecalculateHash={() => recomputeHash()}
|
|
||||||
lastSavedAt={lastSavedAt}
|
|
||||||
saving={isSaving}
|
|
||||||
validating={isValidating}
|
|
||||||
exporting={isExporting}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<DragOverlay>
|
||||||
|
{dragOverlayAction ? (
|
||||||
|
<div className="bg-background flex items-center gap-2 rounded border px-3 py-2 text-xs font-medium shadow-lg select-none">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-4 w-4 items-center justify-center rounded text-white",
|
||||||
|
dragOverlayAction.category === "robot" && "bg-emerald-600",
|
||||||
|
dragOverlayAction.category === "control" && "bg-amber-500",
|
||||||
|
dragOverlayAction.category === "observation" &&
|
||||||
|
"bg-purple-600",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{dragOverlayAction.name}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ const StepRow = React.memo(function StepRow({
|
|||||||
<StepDroppableArea stepId={step.id} />
|
<StepDroppableArea stepId={step.id} />
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-2 rounded border shadow-sm transition-colors",
|
"mb-2 rounded-lg border shadow-sm transition-colors",
|
||||||
selectedStepId === step.id
|
selectedStepId === step.id
|
||||||
? "border-border bg-accent/30"
|
? "border-border bg-accent/30"
|
||||||
: "hover:bg-accent/30",
|
: "hover:bg-accent/30",
|
||||||
@@ -956,7 +956,7 @@ export function FlowWorkspace({
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
id="tour-designer-canvas"
|
id="tour-designer-canvas"
|
||||||
className="relative h-0 min-h-0 flex-1 overflow-x-hidden overflow-y-auto"
|
className="relative h-0 min-h-0 flex-1 overflow-x-hidden overflow-y-auto rounded-md border"
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
>
|
>
|
||||||
{steps.length === 0 ? (
|
{steps.length === 0 ? (
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ export interface PanelsContainerProps {
|
|||||||
|
|
||||||
/** Keyboard resize step (fractional) per arrow press; Shift increases by 2x */
|
/** Keyboard resize step (fractional) per arrow press; Shift increases by 2x */
|
||||||
keyboardStepPct?: number;
|
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:
|
* Tailwind-first, grid-based panel layout with:
|
||||||
* - Drag-resizable left/right panels (no persistence)
|
* - Drag-resizable left/right panels (no persistence)
|
||||||
|
* - Collapsible side panels
|
||||||
* - Strict overflow containment (no page-level x-scroll)
|
* - Strict overflow containment (no page-level x-scroll)
|
||||||
* - Internal y-scroll for each panel
|
* - Internal y-scroll for each panel
|
||||||
* - Optional visual dividers on the center panel only (prevents double borders)
|
* - Optional visual dividers on the center panel only (prevents double borders)
|
||||||
@@ -66,7 +75,7 @@ const Panel: React.FC<React.PropsWithChildren<{
|
|||||||
children,
|
children,
|
||||||
}) => (
|
}) => (
|
||||||
<section
|
<section
|
||||||
className={cn("min-w-0 overflow-hidden", panelCls, panelClassName)}
|
className={cn("min-w-0 overflow-hidden transition-[width,opacity] duration-300 ease-in-out", panelCls, panelClassName)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -93,6 +102,10 @@ export function PanelsContainer({
|
|||||||
minRightPct = 0.12,
|
minRightPct = 0.12,
|
||||||
maxRightPct = 0.33,
|
maxRightPct = 0.33,
|
||||||
keyboardStepPct = 0.02,
|
keyboardStepPct = 0.02,
|
||||||
|
leftCollapsed = false,
|
||||||
|
rightCollapsed = false,
|
||||||
|
onLeftCollapseChange,
|
||||||
|
onRightCollapseChange,
|
||||||
}: PanelsContainerProps) {
|
}: PanelsContainerProps) {
|
||||||
const hasLeft = Boolean(left);
|
const hasLeft = Boolean(left);
|
||||||
const hasRight = Boolean(right);
|
const hasRight = Boolean(right);
|
||||||
@@ -118,20 +131,39 @@ export function PanelsContainer({
|
|||||||
(lp: number, rp: number) => {
|
(lp: number, rp: number) => {
|
||||||
if (!hasCenter) return { l: 0, c: 0, r: 0 };
|
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) {
|
if (hasLeft && hasRight) {
|
||||||
const l = clamp(lp, minLeftPct, maxLeftPct);
|
// Standard clamp (on the state values)
|
||||||
const r = clamp(rp, minRightPct, maxRightPct);
|
const lState = clamp(lp, minLeftPct, maxLeftPct);
|
||||||
const c = Math.max(0.1, 1 - (l + r)); // always preserve some center space
|
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 };
|
return { l, c, r };
|
||||||
}
|
}
|
||||||
if (hasLeft && !hasRight) {
|
if (hasLeft && !hasRight) {
|
||||||
const l = clamp(lp, minLeftPct, maxLeftPct);
|
const lState = clamp(lp, minLeftPct, maxLeftPct);
|
||||||
const c = Math.max(0.2, 1 - l);
|
const l = leftCollapsed ? 0 : lState;
|
||||||
|
const c = 1 - l;
|
||||||
return { l, c, r: 0 };
|
return { l, c, r: 0 };
|
||||||
}
|
}
|
||||||
if (!hasLeft && hasRight) {
|
if (!hasLeft && hasRight) {
|
||||||
const r = clamp(rp, minRightPct, maxRightPct);
|
const rState = clamp(rp, minRightPct, maxRightPct);
|
||||||
const c = Math.max(0.2, 1 - r);
|
const r = rightCollapsed ? 0 : rState;
|
||||||
|
const c = 1 - r;
|
||||||
return { l: 0, c, r };
|
return { l: 0, c, r };
|
||||||
}
|
}
|
||||||
// Center only
|
// Center only
|
||||||
@@ -145,6 +177,8 @@ export function PanelsContainer({
|
|||||||
maxLeftPct,
|
maxLeftPct,
|
||||||
minRightPct,
|
minRightPct,
|
||||||
maxRightPct,
|
maxRightPct,
|
||||||
|
leftCollapsed,
|
||||||
|
rightCollapsed
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -159,10 +193,10 @@ export function PanelsContainer({
|
|||||||
const deltaPx = e.clientX - d.startX;
|
const deltaPx = e.clientX - d.startX;
|
||||||
const deltaPct = deltaPx / d.containerWidth;
|
const deltaPct = deltaPx / d.containerWidth;
|
||||||
|
|
||||||
if (d.edge === "left" && hasLeft) {
|
if (d.edge === "left" && hasLeft && !leftCollapsed) {
|
||||||
const nextLeft = clamp(d.startLeft + deltaPct, minLeftPct, maxLeftPct);
|
const nextLeft = clamp(d.startLeft + deltaPct, minLeftPct, maxLeftPct);
|
||||||
setLeftPct(nextLeft);
|
setLeftPct(nextLeft);
|
||||||
} else if (d.edge === "right" && hasRight) {
|
} else if (d.edge === "right" && hasRight && !rightCollapsed) {
|
||||||
// Dragging the right edge moves leftwards as delta increases
|
// Dragging the right edge moves leftwards as delta increases
|
||||||
const nextRight = clamp(
|
const nextRight = clamp(
|
||||||
d.startRight - deltaPct,
|
d.startRight - deltaPct,
|
||||||
@@ -172,7 +206,7 @@ export function PanelsContainer({
|
|||||||
setRightPct(nextRight);
|
setRightPct(nextRight);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[hasLeft, hasRight, minLeftPct, maxLeftPct, minRightPct, maxRightPct],
|
[hasLeft, hasRight, minLeftPct, maxLeftPct, minRightPct, maxRightPct, leftCollapsed, rightCollapsed],
|
||||||
);
|
);
|
||||||
|
|
||||||
const endDrag = React.useCallback(() => {
|
const endDrag = React.useCallback(() => {
|
||||||
@@ -215,14 +249,14 @@ export function PanelsContainer({
|
|||||||
|
|
||||||
const step = (e.shiftKey ? 2 : 1) * keyboardStepPct;
|
const step = (e.shiftKey ? 2 : 1) * keyboardStepPct;
|
||||||
|
|
||||||
if (edge === "left" && hasLeft) {
|
if (edge === "left" && hasLeft && !leftCollapsed) {
|
||||||
const next = clamp(
|
const next = clamp(
|
||||||
leftPct + (e.key === "ArrowRight" ? step : -step),
|
leftPct + (e.key === "ArrowRight" ? step : -step),
|
||||||
minLeftPct,
|
minLeftPct,
|
||||||
maxLeftPct,
|
maxLeftPct,
|
||||||
);
|
);
|
||||||
setLeftPct(next);
|
setLeftPct(next);
|
||||||
} else if (edge === "right" && hasRight) {
|
} else if (edge === "right" && hasRight && !rightCollapsed) {
|
||||||
const next = clamp(
|
const next = clamp(
|
||||||
rightPct + (e.key === "ArrowLeft" ? step : -step),
|
rightPct + (e.key === "ArrowLeft" ? step : -step),
|
||||||
minRightPct,
|
minRightPct,
|
||||||
@@ -233,23 +267,33 @@ export function PanelsContainer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// CSS variables for the grid fractions
|
// 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<string, string> = hasCenter
|
const styleVars: React.CSSProperties & Record<string, string> = hasCenter
|
||||||
? {
|
? {
|
||||||
"--col-left": `${(hasLeft ? l : 0) * 100}%`,
|
"--col-left": `${hasLeft ? l : 0}fr`,
|
||||||
"--col-center": `${c * 100}%`,
|
"--col-center": `${c}fr`,
|
||||||
"--col-right": `${(hasRight ? r : 0) * 100}%`,
|
"--col-right": `${hasRight ? r : 0}fr`,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
// Explicit grid template depending on which side panels exist
|
// 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 =
|
const gridCols =
|
||||||
hasLeft && hasRight
|
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
|
: 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
|
: !hasLeft && hasRight
|
||||||
? "[grid-template-columns:minmax(0,var(--col-center))_minmax(0,var(--col-right))]"
|
? "[grid-template-columns:var(--col-center)_var(--col-right)]"
|
||||||
: "[grid-template-columns:minmax(0,1fr)]";
|
: "[grid-template-columns:1fr]";
|
||||||
|
|
||||||
// Dividers on the center panel only (prevents double borders if children have their own borders)
|
// Dividers on the center panel only (prevents double borders if children have their own borders)
|
||||||
const centerDividers =
|
const centerDividers =
|
||||||
@@ -303,7 +347,7 @@ export function PanelsContainer({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content (Center) */}
|
{/* Main Content (Center) */}
|
||||||
<div className="flex-1 min-h-0 overflow-hidden relative">
|
<div className="flex-1 min-h-0 min-w-0 overflow-hidden relative">
|
||||||
{center}
|
{center}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,14 +356,28 @@ export function PanelsContainer({
|
|||||||
<div
|
<div
|
||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
style={styleVars}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative hidden md:grid h-full min-h-0 w-full overflow-hidden select-none",
|
"relative hidden md:grid h-full min-h-0 w-full max-w-full overflow-hidden select-none",
|
||||||
gridCols,
|
// 2-3-2 ratio for left-center-right panels when all visible
|
||||||
|
hasLeft && hasRight && !leftCollapsed && !rightCollapsed && "grid-cols-[2fr_3fr_2fr]",
|
||||||
|
// Left collapsed: center + right (3:2 ratio)
|
||||||
|
hasLeft && hasRight && leftCollapsed && !rightCollapsed && "grid-cols-[3fr_2fr]",
|
||||||
|
// Right collapsed: left + center (2:3 ratio)
|
||||||
|
hasLeft && hasRight && !leftCollapsed && rightCollapsed && "grid-cols-[2fr_3fr]",
|
||||||
|
// Both collapsed: center only
|
||||||
|
hasLeft && hasRight && leftCollapsed && rightCollapsed && "grid-cols-1",
|
||||||
|
// Only left and center
|
||||||
|
hasLeft && !hasRight && !leftCollapsed && "grid-cols-[2fr_3fr]",
|
||||||
|
hasLeft && !hasRight && leftCollapsed && "grid-cols-1",
|
||||||
|
// Only center and right
|
||||||
|
!hasLeft && hasRight && !rightCollapsed && "grid-cols-[3fr_2fr]",
|
||||||
|
!hasLeft && hasRight && rightCollapsed && "grid-cols-1",
|
||||||
|
// Only center
|
||||||
|
!hasLeft && !hasRight && "grid-cols-1",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{hasLeft && (
|
{hasLeft && !leftCollapsed && (
|
||||||
<Panel
|
<Panel
|
||||||
panelClassName={panelClassName}
|
panelClassName={panelClassName}
|
||||||
contentClassName={contentClassName}
|
contentClassName={contentClassName}
|
||||||
@@ -338,7 +396,7 @@ export function PanelsContainer({
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasRight && (
|
{hasRight && !rightCollapsed && (
|
||||||
<Panel
|
<Panel
|
||||||
panelClassName={panelClassName}
|
panelClassName={panelClassName}
|
||||||
contentClassName={contentClassName}
|
contentClassName={contentClassName}
|
||||||
@@ -346,43 +404,6 @@ export function PanelsContainer({
|
|||||||
{right}
|
{right}
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Resize handles (only render where applicable) */}
|
|
||||||
{hasCenter && hasLeft && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="separator"
|
|
||||||
aria-label="Resize left panel"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
onPointerDown={startDrag("left")}
|
|
||||||
onKeyDown={onKeyResize("left")}
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-y-0 z-10 w-1 cursor-col-resize outline-none",
|
|
||||||
"focus-visible:ring-ring focus-visible:ring-2",
|
|
||||||
)}
|
|
||||||
// Position at the boundary between left and center
|
|
||||||
style={{ left: "var(--col-left)", transform: "translateX(-0.5px)" }}
|
|
||||||
tabIndex={0}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasCenter && hasRight && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="separator"
|
|
||||||
aria-label="Resize right panel"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
onPointerDown={startDrag("right")}
|
|
||||||
onKeyDown={onKeyResize("right")}
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-y-0 z-10 w-1 cursor-col-resize outline-none",
|
|
||||||
"focus-visible:ring-ring focus-visible:ring-2",
|
|
||||||
)}
|
|
||||||
// Position at the boundary between center and right (offset from the right)
|
|
||||||
style={{ right: "var(--col-right)", transform: "translateX(0.5px)" }}
|
|
||||||
tabIndex={0}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
Eye,
|
Eye,
|
||||||
X,
|
X,
|
||||||
Layers,
|
Layers,
|
||||||
|
PanelLeftClose,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
@@ -108,7 +109,7 @@ function DraggableAction({
|
|||||||
{...listeners}
|
{...listeners}
|
||||||
style={style}
|
style={style}
|
||||||
className={cn(
|
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]",
|
compact ? "py-1.5 text-[11px]" : "py-2 text-[12px]",
|
||||||
isDragging && "opacity-50",
|
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 registry = useActionRegistry();
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React, { useMemo, useState, useCallback } from "react";
|
import React, { useMemo, useState, useCallback } from "react";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { useDesignerStore } from "../state/store";
|
import { useDesignerStore } from "../state/store";
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
PackageSearch,
|
PackageSearch,
|
||||||
|
PanelRightClose,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +49,11 @@ export interface InspectorPanelProps {
|
|||||||
* Called when user changes tab (only if activeTab not externally controlled).
|
* Called when user changes tab (only if activeTab not externally controlled).
|
||||||
*/
|
*/
|
||||||
onTabChange?: (tab: "properties" | "issues" | "dependencies") => void;
|
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.
|
* If true, auto-switch to "properties" when a selection occurs.
|
||||||
*/
|
*/
|
||||||
@@ -68,6 +75,8 @@ export function InspectorPanel({
|
|||||||
onTabChange,
|
onTabChange,
|
||||||
autoFocusOnSelection = true,
|
autoFocusOnSelection = true,
|
||||||
studyPlugins,
|
studyPlugins,
|
||||||
|
collapsed,
|
||||||
|
onCollapse,
|
||||||
}: InspectorPanelProps) {
|
}: InspectorPanelProps) {
|
||||||
/* ------------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------------ */
|
||||||
/* Store Selectors */
|
/* Store Selectors */
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
|||||||
<main
|
<main
|
||||||
data-slot="sidebar-inset"
|
data-slot="sidebar-inset"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background relative flex w-full flex-1 flex-col",
|
"bg-background relative flex w-full flex-1 flex-col overflow-x-hidden",
|
||||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -569,7 +569,7 @@ function SidebarMenuAction({
|
|||||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||||
"group-data-[collapsible=icon]:hidden",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
showOnHover &&
|
showOnHover &&
|
||||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -241,3 +241,11 @@
|
|||||||
@apply bg-background text-foreground shadow;
|
@apply bg-background text-foreground shadow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Viewport height constraint for proper flex layout */
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#__next {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user