feat: agregar panel lateral derecho y mejorar la gestión de activos y chat
This commit is contained in:
parent
198fbe8ef6
commit
9a62cd634d
68
packages/studio-panel/src/components/ui/RightSidePanel.tsx
Normal file
68
packages/studio-panel/src/components/ui/RightSidePanel.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import React from 'react'
|
||||
|
||||
type Tab = { id: string; label: string }
|
||||
|
||||
const TABS: Tab[] = [
|
||||
{ id: 'assets', label: 'Activos multimedia' },
|
||||
{ id: 'style', label: 'Estilo' },
|
||||
{ id: 'people', label: 'Personas' },
|
||||
{ id: 'notes', label: 'Notas' },
|
||||
]
|
||||
|
||||
type Props = {
|
||||
activeTab: string | null
|
||||
onSelectTab: (id: string | null) => void
|
||||
}
|
||||
|
||||
const RightSidePanel: React.FC<Props> = ({ activeTab, onSelectTab }) => {
|
||||
return (
|
||||
<>
|
||||
{/* Tabs column */}
|
||||
<div className="fixed right-0 top-16 z-50 flex h-[calc(100vh-4rem)] flex-col items-center gap-2 p-2">
|
||||
{TABS.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => onSelectTab(activeTab === t.id ? null : t.id)}
|
||||
className={`w-12 h-12 flex items-center justify-center rounded-l-md bg-neutral-800 text-white text-xs overflow-hidden shadow ${
|
||||
activeTab === t.id ? 'bg-blue-600' : 'hover:bg-neutral-700'
|
||||
}`}
|
||||
title={t.label}
|
||||
>
|
||||
{t.label.split(' ')[0]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Panel area */}
|
||||
<div
|
||||
className={`fixed top-12 right-0 z-40 h-[calc(100vh-4rem)] w-72 transform transition-transform duration-300 ease-in-out ${
|
||||
activeTab ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
aria-hidden={!activeTab}
|
||||
>
|
||||
<aside className="h-full bg-neutral-900 border-l border-gray-800 text-white p-4 overflow-y-auto">
|
||||
<h3 className="text-sm font-semibold mb-3">{activeTab ? TABS.find((x) => x.id === activeTab)?.label : ''}</h3>
|
||||
<div className="space-y-3">
|
||||
{activeTab === 'assets' && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-300 mb-2">Activos multimedia</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<div className="h-16 bg-neutral-800 rounded-md" />
|
||||
<div className="h-16 bg-neutral-800 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'style' && <div className="text-sm text-gray-300">Opciones de estilo</div>}
|
||||
|
||||
{activeTab === 'people' && <div className="text-sm text-gray-300">Lista de personas</div>}
|
||||
|
||||
{activeTab === 'notes' && <div className="text-sm text-gray-300">Notas y recordatorios</div>}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RightSidePanel
|
||||
200
packages/studio-panel/src/components/ui/RightSidebar.tsx
Normal file
200
packages/studio-panel/src/components/ui/RightSidebar.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
import React from 'react'
|
||||
// Inline SVG icons (kept minimal for clarity)
|
||||
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" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const IconInfo = ({ size = 20 }: { size?: number }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth={1.5} />
|
||||
<path d="M12 8v.01M11 12h1v4h1" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const IconClock = ({ size = 20 }: { size?: number }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth={1.5} />
|
||||
<path d="M12 7v6l4 2" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const IconUsers = ({ size = 20 }: { size?: number }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<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'
|
||||
import { useChat } from '../../hooks/useChat'
|
||||
|
||||
type SidebarItem = {
|
||||
id: string
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
}
|
||||
|
||||
type Props = {
|
||||
activeTab: string | null
|
||||
onSelectTab: (id: string | null) => void
|
||||
}
|
||||
|
||||
const RightSidebar: React.FC<Props> = ({ activeTab, onSelectTab }) => {
|
||||
const { assets } = useAssets()
|
||||
const { people } = usePeople()
|
||||
const { style, update } = useStyle()
|
||||
const { messages, send } = useChat()
|
||||
|
||||
const items: SidebarItem[] = [
|
||||
{ id: 'comments', label: 'Comentarios', icon: <IconChat /> },
|
||||
{ id: 'style', label: 'Estilo', icon: <IconInfo /> },
|
||||
{ id: 'banners', label: 'Banners', icon: <IconInfo /> },
|
||||
{ id: 'media', label: 'Archivos multimedia', icon: <IconClock /> },
|
||||
{ id: 'people', label: 'Personas', icon: <IconUsers /> },
|
||||
{ id: 'private-chat', label: 'Chat privado', icon: <IconChat /> },
|
||||
{ id: 'notes', label: 'Notas', icon: <IconUsers /> },
|
||||
]
|
||||
|
||||
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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</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 = ''
|
||||
}
|
||||
}
|
||||
}} />
|
||||
</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 === '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>
|
||||
</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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RightSidebar
|
||||
20
packages/studio-panel/src/hooks/useAssets.ts
Normal file
20
packages/studio-panel/src/hooks/useAssets.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export type Asset = { id: string; name: string; type: 'image' | 'video' }
|
||||
|
||||
export function useAssets() {
|
||||
const [assets, setAssets] = useState<Asset[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
// load sample assets (could be replaced by API)
|
||||
setAssets([
|
||||
{ id: 'a1', name: 'Intro.mp4', type: 'video' },
|
||||
{ id: 'a2', name: 'Logo.png', type: 'image' },
|
||||
])
|
||||
}, [])
|
||||
|
||||
const addAsset = (a: Asset) => setAssets((s) => [a, ...s])
|
||||
const removeAsset = (id: string) => setAssets((s) => s.filter((x) => x.id !== id))
|
||||
|
||||
return { assets, addAsset, removeAsset }
|
||||
}
|
||||
15
packages/studio-panel/src/hooks/useChat.ts
Normal file
15
packages/studio-panel/src/hooks/useChat.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export type ChatMessage = { id: string; user: string; text: string }
|
||||
|
||||
export function useChat() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setMessages([{ id: 'm1', user: 'Alice', text: 'Hola a todos' }])
|
||||
}, [])
|
||||
|
||||
const send = (user: string, text: string) => setMessages((s) => [...s, { id: `${Date.now()}`, user, text }])
|
||||
|
||||
return { messages, send }
|
||||
}
|
||||
19
packages/studio-panel/src/hooks/usePeople.ts
Normal file
19
packages/studio-panel/src/hooks/usePeople.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export type Person = { id: string; name: string; role?: string }
|
||||
|
||||
export function usePeople() {
|
||||
const [people, setPeople] = useState<Person[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setPeople([
|
||||
{ id: 'p1', name: 'Alice' },
|
||||
{ id: 'p2', name: 'Bob' },
|
||||
])
|
||||
}, [])
|
||||
|
||||
const addPerson = (p: Person) => setPeople((s) => [p, ...s])
|
||||
const removePerson = (id: string) => setPeople((s) => s.filter((x) => x.id !== id))
|
||||
|
||||
return { people, addPerson, removePerson }
|
||||
}
|
||||
11
packages/studio-panel/src/hooks/useStyle.ts
Normal file
11
packages/studio-panel/src/hooks/useStyle.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
export type StyleConfig = { themeColor: string; showLogo: boolean }
|
||||
|
||||
export function useStyle() {
|
||||
const [style, setStyle] = useState<StyleConfig>({ themeColor: '#2563eb', showLogo: true })
|
||||
|
||||
const update = (patch: Partial<StyleConfig>) => setStyle((s) => ({ ...s, ...patch }))
|
||||
|
||||
return { style, update }
|
||||
}
|
||||
@ -1,15 +1,23 @@
|
||||
import React, { useState } from 'react'
|
||||
import Header from '../components/ui/Header'
|
||||
import LeftSidePanel from '../components/ui/LeftSidePanel'
|
||||
import RightSidebar from '../components/ui/RightSidebar'
|
||||
|
||||
const StudioLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const [leftOpen, setLeftOpen] = useState(true)
|
||||
const [rightActiveTab, setRightActiveTab] = useState<string | null>(null)
|
||||
|
||||
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'}`}>{children}</main>
|
||||
<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} />
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user