From 164f7fba21bfe8e3c79f05fe6592b2e92cf8d769 Mon Sep 17 00:00:00 2001 From: Cesar Mendivil Date: Sun, 9 Nov 2025 12:14:19 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20agregar=20gesti=C3=B3n=20de=20cierre=20?= =?UTF-8?q?en=20el=20modal=20de=20destino=20y=20mejorar=20el=20dise=C3=B1o?= =?UTF-8?q?=20del=20panel=20lateral=20y=20la=20barra=20lateral=20derecha?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ui/DestinationModal.tsx | 24 +- .../studio-panel/src/components/ui/Header.tsx | 30 +-- .../src/components/ui/LeftSidePanel.tsx | 20 +- .../src/components/ui/RightSidebar.tsx | 240 +++++++++--------- .../src/components/ui/StudioFrame.tsx | 68 +++++ .../studio-panel/src/layouts/StudioLayout.tsx | 30 ++- 6 files changed, 239 insertions(+), 173 deletions(-) create mode 100644 packages/studio-panel/src/components/ui/StudioFrame.tsx diff --git a/packages/studio-panel/src/components/ui/DestinationModal.tsx b/packages/studio-panel/src/components/ui/DestinationModal.tsx index 5aa0f10..02cb47b 100644 --- a/packages/studio-panel/src/components/ui/DestinationModal.tsx +++ b/packages/studio-panel/src/components/ui/DestinationModal.tsx @@ -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 = ({ open, onClose, editing = null, onSaved }) => { const { addDestination, updateDestination } = useDestinations() + const backdropRef = useRef(null) const [platform, setPlatform] = useState(PLATFORMS[0].id) const [label, setLabel] = useState('') const [url, setUrl] = useState('') @@ -35,6 +36,16 @@ const DestinationModal: React.FC = ({ 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 = ({ open, onClose, editing = null, onSa } return ( -
-
+
{ + // 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" + > +
e.stopPropagation()} className="w-full max-w-md bg-white dark:bg-gray-900 rounded-md p-4">

{editing ? 'Editar destino' : 'Agregar destino'}

diff --git a/packages/studio-panel/src/components/ui/Header.tsx b/packages/studio-panel/src/components/ui/Header.tsx index 8ac6bea..0a4c281 100644 --- a/packages/studio-panel/src/components/ui/Header.tsx +++ b/packages/studio-panel/src/components/ui/Header.tsx @@ -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(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 = () => {
- -
-
- ))} -
-
+ {/* Destinations list removed from inline layout to avoid deforming the header/container. + Destinations are managed via the DestinationModal (overlay) which does not affect layout. */} = ({ open, onToggle }) => { return ( - <> - {/* Panel */} -
+ {/* Panel (in-flow) */} +
+ - {/* Toggle tab */} + {/* Toggle tab - fixed to viewport edge like original behaviour */} - +
) } diff --git a/packages/studio-panel/src/components/ui/RightSidebar.tsx b/packages/studio-panel/src/components/ui/RightSidebar.tsx index 25d4ca0..d78ad3c 100644 --- a/packages/studio-panel/src/components/ui/RightSidebar.tsx +++ b/packages/studio-panel/src/components/ui/RightSidebar.tsx @@ -1,5 +1,5 @@ import React from 'react' -// Inline SVG icons (kept minimal for clarity) +// Inline SVG icons const IconChat = ({ size = 20 }: { size?: number }) => ( @@ -26,6 +26,7 @@ const IconUsers = ({ size = 20 }: { size?: number }) => ( ) + import { useAssets } from '../../hooks/useAssets' import { usePeople } from '../../hooks/usePeople' import { useStyle } from '../../hooks/useStyle' @@ -59,140 +60,131 @@ const RightSidebar: React.FC = ({ activeTab, onSelectTab }) => { ] return ( -
-
- {/* Panel: positioned to the left of tabs and slides with transform */} -
-
- {activeTab ? ( -
- {activeTab === 'media' && ( -
-

Archivos multimedia

-
    - {assets.map((a) => ( -
  • {a.name}
  • - ))} -
-
- )} +
+ {/* Panel: in-flow, collapses width when closed */} +
+
+ {activeTab ? ( +
+ {activeTab === 'media' && ( +
+

Archivos multimedia

+
    + {assets.map((a) => ( +
  • {a.name}
  • + ))} +
+
+ )} - {activeTab === 'people' && ( -
-

Personas

-
    - {people.map((p) => ( -
  • {p.name} {p.role}
  • - ))} -
-
- )} + {activeTab === 'people' && ( +
+

Personas

+
    + {people.map((p) => ( +
  • {p.name} {p.role}
  • + ))} +
+
+ )} - {activeTab === 'style' && ( -
-

Estilo

-
Color primario: {style.themeColor}
-
- -
+ {activeTab === 'style' && ( +
+

Estilo

+
Color primario: {style.themeColor}
+
+
- )} +
+ )} - {activeTab === 'private-chat' && ( -
-

Chat privado

-
- {messages.map((m) => ( -
-
{m.user}
-
{m.text}
-
- ))} -
-
- { - 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' && ( +
+

Chat privado

+
+ {messages.map((m) => ( +
+
{m.user}
+
{m.text}
+
+ ))} +
+
+ { + if (e.key === 'Enter') { + const val = (e.currentTarget as HTMLInputElement).value.trim() + if (val) { + send('Host', val) + ;(e.currentTarget as HTMLInputElement).value = '' } - }} /> -
+ } + }} />
- )} +
+ )} - {activeTab === 'comments' && ( -
-

Comentarios

-

Panel de comentarios en tiempo real.

-
- )} + {activeTab === 'comments' && ( +
+

Comentarios

+

Panel de comentarios en tiempo real.

+
+ )} - {activeTab === 'banners' && ( -
-

Banners

-

Gestión de banners y overlays.

-
- )} + {activeTab === 'banners' && ( +
+

Banners

+

Gestión de banners y overlays.

+
+ )} - {activeTab === 'notes' && ( -
-

Notas

-

Notas y recordatorios del stream.

-
- )} -
- ) : ( -
Seleccione una pestaña para ver opciones
- )} -
+ {activeTab === 'notes' && ( +
+

Notas

+

Notas y recordatorios del stream.

+
+ )} +
+ ) : ( +
Seleccione una pestaña para ver opciones
+ )}
- - {/* Tabs aside: always visible on the right */} -
+ + {/* Tabs aside: always visible on the right */} +
) } diff --git a/packages/studio-panel/src/components/ui/StudioFrame.tsx b/packages/studio-panel/src/components/ui/StudioFrame.tsx new file mode 100644 index 0000000..af7080d --- /dev/null +++ b/packages/studio-panel/src/components/ui/StudioFrame.tsx @@ -0,0 +1,68 @@ +import React, { useRef, useLayoutEffect, useState } from 'react' + +type Props = { + children: React.ReactNode + padding?: number + minScale?: number +} + +const StudioFrame: React.FC = ({ children, padding = 24, minScale = 0.6 }) => { + const containerRef = useRef(null) + const contentRef = useRef(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 ( +
+
+ {children} +
+
+ ) +} + +export default StudioFrame diff --git a/packages/studio-panel/src/layouts/StudioLayout.tsx b/packages/studio-panel/src/layouts/StudioLayout.tsx index 198c132..f8ecb40 100644 --- a/packages/studio-panel/src/layouts/StudioLayout.tsx +++ b/packages/studio-panel/src/layouts/StudioLayout.tsx @@ -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 (
-
- setLeftOpen((v) => !v)} /> -
- {children} -
- +
+ + {/* Layout: left panel | center container | right sidebar (panel + tabs) */} +
+ {/* Left column: width toggles between 0 and 16rem */} +
+ setLeftOpen((v) => !v)} /> +
+ + {/* Center column: flex-1, will shrink when sidebars occupy space */} +
+ +
{children}
+
+
+ + {/* Right column: RightSidebar manages internal collapse (panel + tabs) */} +
+ +
+
)