Aymeric Rabot aa43bb6a43
feat: backport monorepo PRs #297, #302, #306 — floorplan thumbnails + mobile editor UI
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>
2026-04-28 06:36:08 -07:00

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