feat: agregar gestión de cierre en el modal de destino y mejorar el diseño del panel lateral y la barra lateral derecha
This commit is contained in:
parent
9a62cd634d
commit
164f7fba21
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useDestinations, Destination } from '../../hooks/useDestinations'
|
||||
|
||||
const PLATFORMS = [
|
||||
@ -17,6 +17,7 @@ type Props = {
|
||||
|
||||
const DestinationModal: React.FC<Props> = ({ open, onClose, editing = null, onSaved }) => {
|
||||
const { addDestination, updateDestination } = useDestinations()
|
||||
const backdropRef = useRef<HTMLDivElement | null>(null)
|
||||
const [platform, setPlatform] = useState(PLATFORMS[0].id)
|
||||
const [label, setLabel] = useState('')
|
||||
const [url, setUrl] = useState('')
|
||||
@ -35,6 +36,16 @@ const DestinationModal: React.FC<Props> = ({ open, onClose, editing = null, onSa
|
||||
setError(null)
|
||||
}, [editing, open])
|
||||
|
||||
// Close on Escape
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [open, onClose])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
const validate = () => {
|
||||
@ -63,8 +74,15 @@ const DestinationModal: React.FC<Props> = ({ open, onClose, editing = null, onSa
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="w-full max-w-md bg-white dark:bg-gray-900 rounded-md p-4">
|
||||
<div
|
||||
ref={backdropRef}
|
||||
onMouseDown={(e) => {
|
||||
// close when clicking on the backdrop (but not when clicking inside the dialog)
|
||||
if (e.target === backdropRef.current) onClose()
|
||||
}}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
>
|
||||
<div onMouseDown={(e) => e.stopPropagation()} className="w-full max-w-md bg-white dark:bg-gray-900 rounded-md p-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">{editing ? 'Editar destino' : 'Agregar destino'}</h3>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm text-gray-600 dark:text-gray-300">Plataforma</label>
|
||||
|
||||
@ -9,17 +9,7 @@ const PlatformBadge: React.FC<{ color: string; children: React.ReactNode }> = ({
|
||||
const Header: React.FC = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<Destination | null>(null)
|
||||
const { destinations, removeDestination } = useDestinations()
|
||||
|
||||
const handleEdit = (d: Destination) => {
|
||||
setEditing(d)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (!confirm('¿Eliminar destino?')) return
|
||||
removeDestination(id)
|
||||
}
|
||||
const { destinations } = useDestinations()
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -37,9 +27,6 @@ const Header: React.FC = () => {
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={() => setOpen(true)} className="relative flex items-center gap-3 bg-[#2563eb] hover:bg-[#1e40af] text-white px-4 py-2 rounded-md text-sm font-medium shadow">
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 5v14M5 12h14" strokeWidth={2} stroke="white" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span>Agregar destino</span>
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<PlatformBadge color="bg-red-600">▶</PlatformBadge>
|
||||
@ -60,19 +47,8 @@ const Header: React.FC = () => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="w-full bg-white/5 text-white p-3">
|
||||
<div className="flex gap-2 overflow-x-auto">
|
||||
{destinations.map((d) => (
|
||||
<div key={d.id} className="flex items-center gap-2 bg-white/5 px-3 py-2 rounded-md">
|
||||
<div className="text-sm">{d.label}</div>
|
||||
<div className="flex gap-1">
|
||||
<button onClick={() => handleEdit(d)} className="text-xs px-2 py-1 bg-blue-600 rounded text-white">Editar</button>
|
||||
<button onClick={() => handleDelete(d.id)} className="text-xs px-2 py-1 bg-red-600 rounded text-white">Eliminar</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Destinations list removed from inline layout to avoid deforming the header/container.
|
||||
Destinations are managed via the DestinationModal (overlay) which does not affect layout. */}
|
||||
|
||||
<DestinationModal
|
||||
open={open}
|
||||
|
||||
@ -13,15 +13,14 @@ type Props = {
|
||||
|
||||
const LeftSidePanel: React.FC<Props> = ({ open, onToggle }) => {
|
||||
return (
|
||||
<>
|
||||
{/* Panel */}
|
||||
<div
|
||||
<div className="relative h-full">
|
||||
{/* Panel (in-flow) */}
|
||||
<aside
|
||||
aria-hidden={!open}
|
||||
className={`fixed top-12 inset-y-0 left-0 z-40 w-64 transform transition-transform duration-300 ease-in-out ${
|
||||
open ? 'translate-x-0' : '-translate-x-full'
|
||||
className={`h-full bg-neutral-900 border-r border-gray-800 text-white flex flex-col overflow-hidden transition-all duration-300 ${
|
||||
open ? 'w-64' : 'w-0'
|
||||
}`}
|
||||
>
|
||||
<aside className="h-full bg-neutral-900 border-r border-gray-800 text-white flex flex-col">
|
||||
<div className="px-4 py-3 border-b border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">Escenas</h3>
|
||||
@ -48,14 +47,13 @@ const LeftSidePanel: React.FC<Props> = ({ open, onToggle }) => {
|
||||
</div>
|
||||
|
||||
<div className="p-3 border-t border-gray-800 text-xs text-gray-400">Establecer video de cierre</div>
|
||||
</aside>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Toggle tab */}
|
||||
{/* Toggle tab - fixed to viewport edge like original behaviour */}
|
||||
<button
|
||||
aria-label={open ? 'Cerrar panel' : 'Abrir panel'}
|
||||
onClick={onToggle}
|
||||
className={`fixed z-50 h-12 w-8 flex items-center justify-center rounded-r-md bg-[#0b1220] text-white shadow top-22 transform transition-left duration-300 ${
|
||||
className={`fixed z-50 h-12 w-8 flex items-center justify-center rounded-r-md bg-[#0b1220] text-white shadow top-12 transition-all duration-300 ${
|
||||
open ? 'left-64' : 'left-0'
|
||||
}`}
|
||||
>
|
||||
@ -67,7 +65,7 @@ const LeftSidePanel: React.FC<Props> = ({ open, onToggle }) => {
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
// Inline SVG icons (kept minimal for clarity)
|
||||
// Inline SVG icons
|
||||
const IconChat = ({ size = 20 }: { size?: number }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M21 15a2 2 0 0 1-2 2H8l-5 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
@ -26,6 +26,7 @@ const IconUsers = ({ size = 20 }: { size?: number }) => (
|
||||
<circle cx="12" cy="7" r="4" stroke="currentColor" strokeWidth={1.5} />
|
||||
</svg>
|
||||
)
|
||||
|
||||
import { useAssets } from '../../hooks/useAssets'
|
||||
import { usePeople } from '../../hooks/usePeople'
|
||||
import { useStyle } from '../../hooks/useStyle'
|
||||
@ -59,140 +60,131 @@ const RightSidebar: React.FC<Props> = ({ activeTab, onSelectTab }) => {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="fixed top-12 right-0 z-50 h-[calc(100vh-3rem)] pointer-events-none">
|
||||
<div className="relative h-full flex items-stretch pointer-events-auto">
|
||||
{/* Panel: positioned to the left of tabs and slides with transform */}
|
||||
<div
|
||||
role="region"
|
||||
aria-hidden={!activeTab}
|
||||
className="h-full bg-[#131418] text-gray-200 overflow-y-auto shadow-lg"
|
||||
style={{
|
||||
width: 320,
|
||||
position: 'absolute',
|
||||
right: '84.44px',
|
||||
transform: activeTab ? 'translateX(0)' : 'translateX(120%)',
|
||||
transition: 'transform 260ms cubic-bezier(.2,.9,.2,1), opacity 200ms ease',
|
||||
opacity: activeTab ? 1 : 0,
|
||||
zIndex: 10, // below tabs (tabs will be z-20)
|
||||
pointerEvents: activeTab ? 'auto' : 'none',
|
||||
}}
|
||||
>
|
||||
<div className="p-4">
|
||||
{activeTab ? (
|
||||
<div>
|
||||
{activeTab === 'media' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Archivos multimedia</h4>
|
||||
<ul className="space-y-2">
|
||||
{assets.map((a) => (
|
||||
<li key={a.id} className="text-sm text-gray-300">{a.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full flex items-stretch">
|
||||
{/* Panel: in-flow, collapses width when closed */}
|
||||
<div
|
||||
role="region"
|
||||
aria-hidden={!activeTab}
|
||||
className={`h-full bg-[#131418] text-gray-200 overflow-y-auto shadow-lg transition-all duration-300 ${
|
||||
activeTab ? 'w-[320px]' : 'w-0'
|
||||
}`}
|
||||
style={{ pointerEvents: activeTab ? 'auto' : 'none' }}
|
||||
>
|
||||
<div className={`p-4 ${activeTab ? '' : 'hidden'}`}>
|
||||
{activeTab ? (
|
||||
<div>
|
||||
{activeTab === 'media' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Archivos multimedia</h4>
|
||||
<ul className="space-y-2">
|
||||
{assets.map((a) => (
|
||||
<li key={a.id} className="text-sm text-gray-300">{a.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'people' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Personas</h4>
|
||||
<ul className="space-y-2">
|
||||
{people.map((p) => (
|
||||
<li key={p.id} className="text-sm text-gray-300">{p.name} <span className="text-xs text-gray-500">{p.role}</span></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'people' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Personas</h4>
|
||||
<ul className="space-y-2">
|
||||
{people.map((p) => (
|
||||
<li key={p.id} className="text-sm text-gray-300">{p.name} <span className="text-xs text-gray-500">{p.role}</span></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'style' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Estilo</h4>
|
||||
<div className="text-sm text-gray-300">Color primario: <span className="font-medium">{style.themeColor}</span></div>
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className="px-3 py-1 bg-blue-600 text-white rounded"
|
||||
onClick={() => update({ showLogo: !style.showLogo })}
|
||||
>
|
||||
Alternar logo ({style.showLogo ? 'ON' : 'OFF'})
|
||||
</button>
|
||||
</div>
|
||||
{activeTab === 'style' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Estilo</h4>
|
||||
<div className="text-sm text-gray-300">Color primario: <span className="font-medium">{style.themeColor}</span></div>
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className="px-3 py-1 bg-blue-600 text-white rounded"
|
||||
onClick={() => update({ showLogo: !style.showLogo })}
|
||||
>
|
||||
Alternar logo ({style.showLogo ? 'ON' : 'OFF'})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'private-chat' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Chat privado</h4>
|
||||
<div className="h-64 overflow-y-auto bg-[#0b0d0f] p-2 rounded">
|
||||
{messages.map((m) => (
|
||||
<div key={m.id} className="text-sm text-gray-300 mb-2">
|
||||
<div className="font-medium text-white">{m.user}</div>
|
||||
<div className="text-gray-400 text-xs">{m.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<input type="text" placeholder="Escribe..." className="flex-1 p-2 rounded bg-[#071018] text-sm text-white" onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const val = (e.currentTarget as HTMLInputElement).value.trim()
|
||||
if (val) {
|
||||
send('Host', val)
|
||||
;(e.currentTarget as HTMLInputElement).value = ''
|
||||
}
|
||||
{activeTab === 'private-chat' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Chat privado</h4>
|
||||
<div className="h-64 overflow-y-auto bg-[#0b0d0f] p-2 rounded">
|
||||
{messages.map((m) => (
|
||||
<div key={m.id} className="text-sm text-gray-300 mb-2">
|
||||
<div className="font-medium text-white">{m.user}</div>
|
||||
<div className="text-gray-400 text-xs">{m.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<input type="text" placeholder="Escribe..." className="flex-1 p-2 rounded bg-[#071018] text-sm text-white" onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const val = (e.currentTarget as HTMLInputElement).value.trim()
|
||||
if (val) {
|
||||
send('Host', val)
|
||||
;(e.currentTarget as HTMLInputElement).value = ''
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'comments' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Comentarios</h4>
|
||||
<p className="text-sm text-gray-300">Panel de comentarios en tiempo real.</p>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'comments' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Comentarios</h4>
|
||||
<p className="text-sm text-gray-300">Panel de comentarios en tiempo real.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'banners' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Banners</h4>
|
||||
<p className="text-sm text-gray-300">Gestión de banners y overlays.</p>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'banners' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Banners</h4>
|
||||
<p className="text-sm text-gray-300">Gestión de banners y overlays.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'notes' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Notas</h4>
|
||||
<p className="text-sm text-gray-300">Notas y recordatorios del stream.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">Seleccione una pestaña para ver opciones</div>
|
||||
)}
|
||||
</div>
|
||||
{activeTab === 'notes' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Notas</h4>
|
||||
<p className="text-sm text-gray-300">Notas y recordatorios del stream.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">Seleccione una pestaña para ver opciones</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs aside: always visible on the right */}
|
||||
<aside className="flex-shrink-0 bg-[#0f1720] border-l border-gray-800 flex flex-col items-center py-3" style={{ width: '84.44px', zIndex: 20 }}>
|
||||
{items.map((item) => {
|
||||
const active = activeTab === item.id
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onSelectTab(active ? null : item.id)}
|
||||
aria-pressed={active}
|
||||
aria-label={item.label}
|
||||
className={`flex flex-col items-center justify-center text-[11px] transition-colors duration-150 ${
|
||||
active
|
||||
? 'bg-gray-700 text-white border-r-4 border-blue-500 shadow-inner'
|
||||
: 'bg-[#0f1720] text-gray-300 hover:bg-gray-800 hover:text-white'
|
||||
}`}
|
||||
style={{ width: '84.44px', height: '84.44px' }}
|
||||
>
|
||||
<div className="mb-1">{React.cloneElement(item.icon as any, { size: 20 })}</div>
|
||||
<div className="text-[11px] leading-tight text-center">{item.label}</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Tabs aside: always visible on the right */}
|
||||
<aside className="flex-shrink-0 bg-[#0f1720] border-l border-gray-800 flex flex-col items-center py-3" style={{ width: '84.44px', zIndex: 20 }}>
|
||||
{items.map((item) => {
|
||||
const active = activeTab === item.id
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onSelectTab(active ? null : item.id)}
|
||||
aria-pressed={active}
|
||||
aria-label={item.label}
|
||||
className={`flex flex-col items-center justify-center text-[11px] transition-colors duration-150 ${
|
||||
active
|
||||
? 'bg-gray-700 text-white border-r-4 border-blue-500 shadow-inner'
|
||||
: 'bg-[#0f1720] text-gray-300 hover:bg-gray-800 hover:text-white'
|
||||
}`}
|
||||
style={{ width: '84.44px', height: '84.44px' }}
|
||||
>
|
||||
<div className="mb-1">{item.icon}</div>
|
||||
<div className="text-[11px] leading-tight text-center">{item.label}</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
68
packages/studio-panel/src/components/ui/StudioFrame.tsx
Normal file
68
packages/studio-panel/src/components/ui/StudioFrame.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import React, { useRef, useLayoutEffect, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
padding?: number
|
||||
minScale?: number
|
||||
}
|
||||
|
||||
const StudioFrame: React.FC<Props> = ({ children, padding = 24, minScale = 0.6 }) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const contentRef = useRef<HTMLDivElement | null>(null)
|
||||
const [scale, setScale] = useState(1)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!containerRef.current || !contentRef.current) return
|
||||
|
||||
const container = containerRef.current
|
||||
const content = contentRef.current
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
const cw = container.clientWidth - padding
|
||||
const ch = container.clientHeight - padding
|
||||
|
||||
const iw = content.scrollWidth
|
||||
const ih = content.scrollHeight
|
||||
|
||||
const sx = cw / iw
|
||||
const sy = ch / ih
|
||||
const sRaw = Math.min(1, Math.min(sx, sy))
|
||||
const s = Math.max(minScale, sRaw)
|
||||
setScale(s)
|
||||
})
|
||||
|
||||
ro.observe(container)
|
||||
ro.observe(content)
|
||||
|
||||
// initial
|
||||
const init = () => {
|
||||
const cw = container.clientWidth - padding
|
||||
const ch = container.clientHeight - padding
|
||||
const iw = content.scrollWidth
|
||||
const ih = content.scrollHeight
|
||||
const sx = cw / iw
|
||||
const sy = ch / ih
|
||||
const sRaw = Math.min(1, Math.min(sx, sy))
|
||||
const s = Math.max(minScale, sRaw)
|
||||
setScale(s)
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
return () => ro.disconnect()
|
||||
}, [padding])
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full h-full flex items-start justify-center overflow-hidden">
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="transform-origin-top-left"
|
||||
style={{ transform: `scale(${scale})`, willChange: 'transform', transition: 'transform 180ms ease' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StudioFrame
|
||||
@ -2,6 +2,7 @@ import React, { useState } from 'react'
|
||||
import Header from '../components/ui/Header'
|
||||
import LeftSidePanel from '../components/ui/LeftSidePanel'
|
||||
import RightSidebar from '../components/ui/RightSidebar'
|
||||
import StudioFrame from '../components/ui/StudioFrame'
|
||||
|
||||
const StudioLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const [leftOpen, setLeftOpen] = useState(true)
|
||||
@ -9,14 +10,27 @@ const StudioLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) =>
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-neutral-900 text-white">
|
||||
<Header />
|
||||
<LeftSidePanel open={leftOpen} onToggle={() => setLeftOpen((v) => !v)} />
|
||||
<main
|
||||
className={`p-4 transition-all duration-300 ${leftOpen ? 'md:ml-64' : 'md:ml-0'} ${rightActiveTab ? 'md:mr-[404.44px]' : 'md:mr-0'}`}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
<RightSidebar activeTab={rightActiveTab} onSelectTab={setRightActiveTab} />
|
||||
<Header />
|
||||
|
||||
{/* Layout: left panel | center container | right sidebar (panel + tabs) */}
|
||||
<div className="flex h-[calc(100vh-3rem)]">
|
||||
{/* Left column: width toggles between 0 and 16rem */}
|
||||
<div className={`${leftOpen ? 'w-64' : 'w-0'} transition-all duration-300 overflow-hidden`}>
|
||||
<LeftSidePanel open={leftOpen} onToggle={() => setLeftOpen((v) => !v)} />
|
||||
</div>
|
||||
|
||||
{/* Center column: flex-1, will shrink when sidebars occupy space */}
|
||||
<div className="flex-1 p-4">
|
||||
<StudioFrame>
|
||||
<main>{children}</main>
|
||||
</StudioFrame>
|
||||
</div>
|
||||
|
||||
{/* Right column: RightSidebar manages internal collapse (panel + tabs) */}
|
||||
<div className="flex-shrink-0">
|
||||
<RightSidebar activeTab={rightActiveTab} onSelectTab={setRightActiveTab} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user