Port floorplan item thumbnails, full mobile editor UI, and mobile polish from the private monorepo into the public editor. Monorepo PR #297 (feat/item-admin-advanced): - Add optional floorPlanUrl to item asset schema (core) - Render 2D floor-plan images inside item footprints on floorplan - Improve slider drag with modifier key re-anchoring Monorepo PR #302 (feat/mobile-ui): - Mobile editor layout with draggable bottom sheet - Mobile tab bar, selection bar, and panel sheet - Mobile-aware panel manager and panel wrapper - useIsMobile rewrite (useSyncExternalStore, SSR-safe) - Camera actions hideOrbit prop - GridSnapControl and SecondaryToggles exports - Action menu hides on mobile contextual tabs - SidebarTab extended with mobileDefaultSnap/mobileIcon Monorepo PR #306 (feat/mobile-ui-polish): - Touch gesture mapping for camera controls (one/two/three finger) - Snap ratio constants for bottom sheet - Thumbnail generator WebGL2 fallback (bottom-up row flip) - ErrorBoundary scope logging, viewer scene wrap - GPUDeviceWatcher enhanced logging and uncaptured error handler - WebGPURenderer init error handling and diagnostics - Post-processing log improvements - MergedOutlineNode WebGL2 FBO corruption fix Excluded: apps/community/*, apps/editor/*, packages/community-*, .cursor/*, .env.example (monorepo-only files) Co-authored-by: Pascal <open@pascal.app>
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
'use client'
|
|
|
|
import { ChevronLeft, RotateCcw, X } from 'lucide-react'
|
|
import Image from 'next/image'
|
|
import { useIsMobile } from '../../../hooks/use-mobile'
|
|
import { cn } from '../../../lib/utils'
|
|
|
|
interface PanelWrapperProps {
|
|
title: string
|
|
icon?: string
|
|
onClose?: () => void
|
|
onReset?: () => void
|
|
onBack?: () => void
|
|
children: React.ReactNode
|
|
className?: string
|
|
width?: number | string
|
|
}
|
|
|
|
export function PanelWrapper({
|
|
title,
|
|
icon,
|
|
onClose,
|
|
onReset,
|
|
onBack,
|
|
children,
|
|
className,
|
|
width = 320, // default width
|
|
}: PanelWrapperProps) {
|
|
const isMobile = useIsMobile()
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
isMobile
|
|
? 'flex h-full w-full flex-col overflow-hidden bg-transparent dark:text-foreground'
|
|
: 'pointer-events-auto fixed top-20 right-4 z-50 flex max-h-[calc(100dvh-100px)] flex-col overflow-hidden rounded-xl border border-border/50 bg-sidebar/95 shadow-2xl backdrop-blur-xl dark:text-foreground',
|
|
className,
|
|
)}
|
|
style={isMobile ? undefined : { width }}
|
|
>
|
|
{/* Header — desktop only; mobile sheet provides its own header */}
|
|
{!isMobile && (
|
|
<div className="flex items-center justify-between border-border/50 border-b px-3 py-3">
|
|
<div className="flex items-center gap-2">
|
|
{onBack && (
|
|
<button
|
|
className="mr-1 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
|
|
onClick={onBack}
|
|
type="button"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
{icon && (
|
|
<Image alt="" className="shrink-0 object-contain" height={16} src={icon} width={16} />
|
|
)}
|
|
<h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
|
|
{title}
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
{onReset && (
|
|
<button
|
|
className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
|
|
onClick={onReset}
|
|
type="button"
|
|
>
|
|
<RotateCcw className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
{onClose && (
|
|
<button
|
|
className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
|
|
onClick={onClose}
|
|
type="button"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Content */}
|
|
<div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto">{children}</div>
|
|
</div>
|
|
)
|
|
}
|