Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions components/studio/layout/studio-layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,16 @@ vi.mock("@/components/ui/sidebar", () => ({
{children}
</div>
),
SidebarRail: () => (
<button data-testid="sidebar-rail" aria-label="Toggle Sidebar" />
SidebarRail: ({ className, children }: { className?: string, children?: ReactNode }) => (
<button data-testid="sidebar-rail" className={className} aria-label="Toggle Sidebar">
{children}
</button>
),
useSidebar: () => ({
toggleSidebar: vi.fn(),
open: true,
isMobile: false
})
}))

describe("StudioLayout", () => {
Expand All @@ -64,7 +71,7 @@ describe("StudioLayout", () => {

expect(screen.getByTestId("sidebar-content")).toBeInTheDocument()
expect(screen.getByTestId("canvas-content")).toBeInTheDocument()

// Check if SidebarProvider for left sidebar receives correct open prop
const providers = screen.getAllByTestId("sidebar-provider")
expect(providers[0]).toHaveAttribute("data-open", "true")
Expand All @@ -81,12 +88,12 @@ describe("StudioLayout", () => {
)

expect(screen.getByTestId("gallery-content")).toBeInTheDocument()

// Check if nested SidebarProvider for gallery receives correct open prop
const providers = screen.getAllByTestId("sidebar-provider")
// Provider 0 is usually left, Provider 1 is inner/right
expect(providers[1]).toHaveAttribute("data-open", "true")

// Check if right sidebar is rendered
const sidebars = screen.getAllByTestId("studio-gallery-panel")
expect(sidebars.length).toBeGreaterThan(0)
Expand Down Expand Up @@ -124,7 +131,7 @@ describe("StudioLayout", () => {
<StudioLayout
sidebar={mockSidebar}
canvas={mockCanvas}
// No gallery prop
// No gallery prop
/>
)

Expand All @@ -144,4 +151,38 @@ describe("StudioLayout", () => {
const layout = screen.getByTestId("studio-layout")
expect(layout).toHaveClass("custom-class")
})

it("renders mobile toggle buttons", () => {
render(
<StudioLayout
sidebar={mockSidebar}
canvas={mockCanvas}
gallery={mockGallery}
showSidebar={true}
showGallery={true}
/>
)

// There should be two mobile triggers (left and right)
// We can find them by the Button (which renders as button) or the icon?
// Note: MobileMenuButton has sr-only text "Toggle Sidebar"
// But checking for 'md:hidden' class on the container or button is a good proxy for "mobile only" validity

// Use getAllByRole to find buttons with "Toggle Sidebar" name
// We expect at least 2 (Left, Right) + Rails might have aria-label "Toggle Sidebar" too?
// The mock SidebarRail has aria-label="Toggle Sidebar".
// The real SidebarRail does too.

// Let's look at the implementation. MobileMenuButton has <span className="sr-only">Toggle Sidebar</span>
// So they will be found.

// Filter for the ones that are NOT the rails.
// The rails in our mock have data-testid="sidebar-rail".
// The mobile buttons do not have that test id, but they have class 'md:hidden'

const buttons = screen.getAllByRole("button", { name: "Toggle Sidebar" })
const mobileButtons = buttons.filter(b => b.classList.contains("md:hidden"))

expect(mobileButtons.length).toBeGreaterThanOrEqual(2)
})
})
54 changes: 42 additions & 12 deletions components/studio/layout/studio-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {
SidebarInset,
SidebarProvider,
SidebarRail,
useSidebar,
} from "@/components/ui/sidebar"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { ChevronLeft, ChevronRight, PanelLeft, PanelRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import * as React from "react"

Expand All @@ -33,7 +35,7 @@ export interface StudioLayoutProps {
onGalleryOpenChange?: (open: boolean) => void
/** Additional class names */
className?: string

// Legacy props - kept for compatibility but unused
defaultSidebarSize?: number | string
defaultGallerySize?: number | string
Expand All @@ -42,6 +44,22 @@ export interface StudioLayoutProps {
defaultLayout?: Record<string, number>
}

function MobileMenuButton({ side, className }: { side: "left" | "right", className?: string }) {
const { toggleSidebar } = useSidebar()

return (
<Button
variant="ghost"
size="icon"
className={cn("md:hidden h-10 w-10 shrink-0", className)}
onClick={toggleSidebar}
>
{side === "left" ? <PanelLeft className="h-6 w-6" /> : <PanelRight className="h-6 w-6" />}
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
}

export function StudioLayout({
sidebar,
canvas,
Expand Down Expand Up @@ -77,16 +95,21 @@ export function StudioLayout({
<SidebarContent className="h-full min-h-0 overflow-hidden">
{sidebar}
</SidebarContent>
<SidebarRail className="group/rail">
<SidebarRail className="group/rail !flex">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-background shadow-sm border opacity-0 group-hover/rail:opacity-100 transition-all">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-background shadow-sm border opacity-100 md:opacity-0 md:group-hover/rail:opacity-100 transition-all">
<ChevronLeft className="h-3 w-3" />
</div>
</div>
</SidebarRail>
</Sidebar>

<SidebarInset className="h-full min-h-0 min-w-0 flex-1 overflow-hidden relative">
{/* Left Mobile Trigger - Top Left */}
<div className="absolute top-2 left-2 z-50 md:hidden">
<MobileMenuButton side="left" className="bg-background/80 backdrop-blur-sm border shadow-sm rounded-md" />
</div>

{/* Right Sidebar Provider (Nested) */}
<SidebarProvider
open={showGallery && !!gallery}
Expand All @@ -101,13 +124,20 @@ export function StudioLayout({
} as React.CSSProperties
}
>
<SidebarInset className="h-full min-h-0 min-w-0 flex-1 overflow-hidden relative">
<div className="h-full w-full" data-testid="studio-canvas-panel">
{canvas}
</div>
</SidebarInset>
<SidebarInset className="h-full min-h-0 min-w-0 flex-1 overflow-hidden relative">
{/* Right Mobile Trigger - Top Right (only if gallery enabled) */}
{gallery && (
<div className="absolute top-2 right-2 z-50 md:hidden">
<MobileMenuButton side="right" className="bg-background/80 backdrop-blur-sm border shadow-sm rounded-md" />
</div>
)}

<div className="h-full w-full" data-testid="studio-canvas-panel">
{canvas}
</div>
</SidebarInset>

{gallery && (
{gallery && (
<Sidebar
side="right"
collapsible="offcanvas"
Expand All @@ -117,9 +147,9 @@ export function StudioLayout({
<SidebarContent className="h-full min-h-0 overflow-hidden">
{gallery}
</SidebarContent>
<SidebarRail className="group/rail">
<SidebarRail className="group/rail !flex">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-background shadow-sm border opacity-0 group-hover/rail:opacity-100 transition-all">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-background shadow-sm border opacity-100 md:opacity-0 md:group-hover/rail:opacity-100 transition-all">
<ChevronRight className="h-3 w-3" />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/studio/layout/studio-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ export function StudioShell({ defaultLayout, initialGalleryPage }: StudioShellPr
</div>

{/* Generate / Pause / Resume Batch Button */}
<div className="p-1.5 border-t bg-background/60">
<div className="p-1.5 pb-[calc(0.375rem+env(safe-area-inset-bottom))] border-t bg-background/60">
{batchMode.isBatchActive ? (
<BatchActionButton
isPaused={batchMode.isBatchPaused}
Expand Down
20 changes: 20 additions & 0 deletions todo/studio-optimizations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Studio Optimization Todo List

## Mobile Experience Optimizations
The following are identified quick wins to improve the mobile user experience without affecting desktop layouts.

### 1. Disable Zoom on Inputs (Priority: High)
iOS devices automatically zoom in when focusing on input fields with a font size smaller than 16px. This creates a disjointed experience where the user has to zoom out manually.

**Implementation Plan:**
- Update all `Input`, `Textarea`, and `Select` components.
- Apply `md:text-sm text-base` classes.
- This ensures inputs are 16px on mobile (preventing zoom) while remaining compact (14px) on desktop.

### 2. Control Overscroll Behavior (Priority: Medium)
On mobile, scrolling to the edge of the sidebar or gallery often causes the entire page (body) to "bounce" or scroll, which feels unpolished for a web app interface.

**Implementation Plan:**
- Add `overscroll-behavior-y: none` to the main scroll containers or the root layout.
- Use the Tailwind utility `overscroll-y-none`.
- Target: `StudioLayout` container or specific scroll areas in `StudioShell`.