feat: Redesign the designer layout using a grid system, adding explicit left, center, and right panels with collapse functionality.

This commit is contained in:
2026-02-02 15:48:17 -05:00
parent 89c44efcf7
commit 0ec63b3c97
8 changed files with 203 additions and 142 deletions

View File

@@ -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>
);
} }

View File

@@ -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>
); );

View File

@@ -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 ? (

View File

@@ -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>
</> </>
); );

View File

@@ -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("");

View File

@@ -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 */

View File

@@ -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}

View File

@@ -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;
}