@reinaldor507/kaas-builder-ds
v1.6.0
Published
Kit de herramientas para crear presentaciones multimedia interactivas con React y API backend
Maintainers
Readme
KAAS Builder DS
Librería profesional de React para crear presentaciones multimedia interactivas. 100% controlada desde tu frontend - el paquete solo renderiza la UI.
🚀 Características
- ✅ Arquitectura Frontend-First: Todo estado y lógica en tu aplicación
- ✅ Canvas interactivo con drag & drop, snap-to-grid, guidelines
- ✅ Elementos multimedia: Sliders, imágenes, videos, texto, reloj
- ✅ Programación temporal: Horarios semanales y por fechas específicas
- ✅ Calendario interactivo para selección de fechas
- ✅ Sistema de resoluciones: Múltiples tamaños de pantalla
- ✅ Botones controlados desde frontend: Preview, Debug, Guardar, Resoluciones
- ✅ Exportación a JSON para guardar en tu backend
- ✅ API Client flexible para cualquier endpoint
- ✅ TypeScript nativo con tipos completos
📦 Instalación
pnpm add kaas-builder-ds🔧 Uso Principal - Arquitectura Controlada
La librería es completamente controlada - tú manejas el estado y ella solo renderiza. No tiene estado interno.
Setup Básico
import React, { useState } from 'react'
import { App, type Template } from 'kaas-builder-ds'
import 'kaas-builder-ds/dist/kaas-builder-ds.css'
function MyApp() {
// TÚ manejas el estado del template
const [template, setTemplate] = useState<Template>({
customer: 'Mi Cliente',
resourcePathImg: '/images/',
resourceDownload: '/downloads/',
destDownload: '/dest/',
useTracking: false,
screens: [{
id: 'screen-1',
width: 1920,
height: 1080,
backgroundColor: '#ffffff',
backgroundImage: '',
elements: [] // Inicia vacío - mostrará selector de plantillas
}]
})
// Callback obligatorio para cambios
const handleTemplateChange = (newTemplate: Template) => {
setTemplate(newTemplate)
// Opcional: Auto-guardar en tu backend
// await saveToDatabase(newTemplate)
}
// Callback para cuando el usuario haga clic en "Guardar"
const handleExportTemplate = async (template: Template) => {
try {
// Guardar en tu base de datos
await fetch('/api/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(template)
})
alert('Template guardado exitosamente')
} catch (error) {
alert('Error al guardar: ' + error.message)
}
}
return (
<div className="App">
<App
template={template} // ✅ Requerido: Tu estado
onTemplateChange={handleTemplateChange} // ✅ Requerido: Tu callback
onExportTemplate={handleExportTemplate} // ⚡ Opcional: Tu lógica de guardado
/>
</div>
)
}
export default MyApp🎮 Botones Controlados desde Frontend
IMPORTANTE: El paquete NO renderiza botones en la toolbar. Tú controlas TODO desde tu aplicación.
Tabla de Responsabilidades
| Control | Responsabilidad |
|---------|-----------------|
| Botón Preview | ✅ Tu frontend - muestra el template en modal/tab/iframe |
| Botón Debug | ✅ Tu frontend - abre consola o panel con JSON |
| Botón Guardar | ✅ Tu frontend - envía JSON a tu backend |
| Selector Resolución | ✅ Tu frontend - cambia width/height del screen |
| JSON Actualizado | ✅ Accesible en onTemplateChange callback |
| Canvas & Edición | ✅ El paquete renderiza todo |
Implementación Completa con Botones
import React, { useState, useRef } from 'react'
import { App, type Template } from 'kaas-builder-ds'
import 'kaas-builder-ds/dist/kaas-builder-ds.css'
function CompleteEditor() {
const [template, setTemplate] = useState<Template>({
customer: 'Mi Cliente',
resourcePathImg: '/images/',
resourceDownload: '/downloads/',
destDownload: '/dest/',
useTracking: false,
screens: [{
id: 'screen-1',
width: 1920,
height: 1080,
backgroundColor: '#ffffff',
backgroundImage: '',
elements: []
}]
})
const [showDebug, setShowDebug] = useState(false)
const [isSaving, setIsSaving] = useState(false)
// 🎯 HANDLER: Cuando el usuario cambia algo en el canvas
const handleTemplateChange = (newTemplate: Template) => {
setTemplate(newTemplate)
console.log('Template actualizado:', newTemplate)
}
// 📊 BOTÓN PREVIEW
const handlePreview = () => {
const serialized = encodeURIComponent(JSON.stringify(template))
window.open(
`/preview?template=${serialized}`,
'preview_window',
'width=1920,height=1080'
)
}
// 📋 BOTÓN DEBUG
const handleDebug = () => {
console.table({
'Cliente': template.customer,
'Pantalla': `${template.screens[0].width}x${template.screens[0].height}`,
'Elementos': template.screens[0].elements.length,
'Fondo': template.screens[0].backgroundColor,
'Timestamp': new Date().toISOString()
})
setShowDebug(!showDebug)
}
// 💾 BOTÓN GUARDAR
const handleSave = async () => {
setIsSaving(true)
try {
const response = await fetch('/api/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(template)
})
if (response.ok) {
alert('✅ Template guardado exitosamente')
} else {
throw new Error('Error del servidor')
}
} catch (error) {
alert('❌ Error: ' + (error instanceof Error ? error.message : 'Desconocido'))
} finally {
setIsSaving(false)
}
}
// 📐 SELECTOR RESOLUCIÓN
const handleResolutionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const [width, height] = e.target.value.split('x').map(Number)
setTemplate({
...template,
screens: [{
...template.screens[0],
width,
height
}]
})
}
return (
<div className="editor-container">
{/* 🎛️ TOOLBAR PERSONALIZADA - TOTALMENTE CONTROLADA POR TI */}
<div className="custom-toolbar" style={{
display: 'flex',
gap: '10px',
padding: '12px',
borderBottom: '1px solid #ddd',
backgroundColor: '#f8f9fa',
alignItems: 'center'
}}>
{/* Botón Preview */}
<button
onClick={handlePreview}
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
▶️ Preview
</button>
{/* Botón Debug */}
<button
onClick={handleDebug}
style={{
padding: '8px 16px',
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
🐛 Debug
</button>
{/* Botón Guardar */}
<button
onClick={handleSave}
disabled={isSaving}
style={{
padding: '8px 16px',
backgroundColor: isSaving ? '#ccc' : '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isSaving ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{isSaving ? '⏳ Guardando...' : '💾 Guardar'}
</button>
{/* Selector Resolución */}
<select
onChange={handleResolutionChange}
value={`${template.screens[0].width}x${template.screens[0].height}`}
style={{
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd',
backgroundColor: 'white',
cursor: 'pointer'
}}
>
<option value="1920x1080">🖥️ Full HD (1920x1080)</option>
<option value="1280x720">📺 HD (1280x720)</option>
<option value="3840x2160">🎬 4K (3840x2160)</option>
<option value="1024x768">💻 XGA (1024x768)</option>
</select>
{/* Indicador de estado */}
<span style={{ marginLeft: 'auto', fontSize: '12px', color: '#666' }}>
{template.screens[0].width}x{template.screens[0].height} ·
{template.screens[0].elements.length} elementos
</span>
</div>
{/* Panel Debug (opcional) */}
{showDebug && (
<div style={{
position: 'fixed',
bottom: 20,
right: 20,
backgroundColor: '#1e1e1e',
color: '#00ff00',
padding: '15px',
borderRadius: '8px',
fontFamily: 'monospace',
fontSize: '12px',
maxWidth: '400px',
maxHeight: '300px',
overflow: 'auto',
zIndex: 1000,
boxShadow: '0 4px 12px rgba(0,0,0,0.3)'
}}>
<pre>{JSON.stringify(template, null, 2)}</pre>
</div>
)}
{/* El paquete solo renderiza aquí */}
<div style={{ flex: 1 }}>
<App
template={template}
onTemplateChange={handleTemplateChange}
/>
</div>
</div>
)
}
export default CompleteEditorProps del Componente App
interface AppProps {
// 🟥 REQUERIDO - Estado controlado
template: Template // Tu template actual
onTemplateChange: (template: Template) => void // Callback para cambios
// 🟦 OPCIONAL - Callbacks de acciones
onExportTemplate?: (template: Template) => void // Al hacer clic en "Guardar"
onPreviewTemplate?: (template: Template) => void // Al hacer clic en "Preview"
// 🟦 OPCIONAL - Control de UI
readOnly?: boolean // Solo lectura (default: false)
showToolbar?: boolean // Mostrar toolbar (default: true)
showPropertiesPanel?: boolean // Mostrar panel izquierdo (default: true)
showComponentsPanel?: boolean // Mostrar panel derecho (default: true)
}Selector Automático de Plantillas
Cuando el template está vacío (sin elementos y sin imagen de fondo), se muestra automáticamente un selector con 9 plantillas predefinidas:
// Si template.screens[0].elements.length === 0 && !backgroundImage
// Se abre automáticamente el modal TemplateSelector
const emptyTemplate: Template = {
customer: 'Mi Cliente',
resourcePathImg: '/images/',
resourceDownload: '/downloads/',
destDownload: '/dest/',
useTracking: false,
screens: [{
id: 'screen-1',
width: 1920,
height: 1080,
backgroundColor: '#ffffff',
backgroundImage: '',
elements: [] // ← Vacío = muestra selector
}]
}Plantillas disponibles:
- 📱 Zone A + Zone B: Layout de 2 zonas horizontales
- 📱 Zone A + Zone C: Layout de 2 zonas verticales
- 📱 Zone A + Zone B + Zone C: Layout de 3 zonas
- 📺 HDMI A: Zona única para contenido HDMI
- 📺 HDMI B: Zona única alternativa para HDMI
- 📺 HDMI A + Ticker: HDMI con cintillo inferior
- 📺 HDMI B + Ticker: HDMI alternativo con cintillo
- 🔄 Zone A + Zone B + Ticker: 2 zonas con cintillo
- 🔄 Zone A + Zone C + Ticker: 2 zonas verticales con cintillo
Integración con Base de Datos
1. Guardar Automático al Hacer Cambios
function MyAppWithAutoSave() {
const [template, setTemplate] = useState<Template>(/* tu template inicial */)
const [isSaving, setIsSaving] = useState(false)
const handleTemplateChange = async (newTemplate: Template) => {
setTemplate(newTemplate)
// Auto-guardar en background
setIsSaving(true)
try {
await fetch('/api/templates/auto-save', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTemplate)
})
} catch (error) {
console.error('Error en auto-save:', error)
} finally {
setIsSaving(false)
}
}
return (
<div>
{isSaving && <div style={{position: 'fixed', top: 10, right: 10}}>
Guardando...
</div>}
<App
template={template}
onTemplateChange={handleTemplateChange}
/>
</div>
)
}2. Guardar Manual con Botón "Guardar"
function MyAppWithManualSave() {
const [template, setTemplate] = useState<Template>(/* tu template inicial */)
const handleTemplateChange = (newTemplate: Template) => {
setTemplate(newTemplate)
// Solo actualizar estado local - NO guardar aún
}
const handleExportTemplate = async (template: Template) => {
try {
const response = await fetch('/api/templates', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`
},
body: JSON.stringify({
id: 'template-123',
template: template,
updatedAt: new Date().toISOString()
})
})
if (response.ok) {
alert('✅ Template guardado exitosamente')
} else {
throw new Error('Error del servidor')
}
} catch (error) {
alert('❌ Error al guardar: ' + error.message)
}
}
return (
<App
template={template}
onTemplateChange={handleTemplateChange}
onExportTemplate={handleExportTemplate} // Al hacer clic en "Guardar"
/>
)
}3. Cargar Template Existente desde BD
import { useEffect } from 'react'
function MyAppWithDatabaseLoad({ templateId }: { templateId: string }) {
const [template, setTemplate] = useState<Template | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Cargar template inicial
useEffect(() => {
const loadTemplate = async () => {
try {
setLoading(true)
const response = await fetch(`/api/templates/${templateId}`, {
headers: {
'Authorization': `Bearer ${userToken}`
}
})
if (!response.ok) {
throw new Error('Template no encontrado')
}
const templateData = await response.json()
setTemplate(templateData)
} catch (err) {
setError(err instanceof Error ? err.message : 'Error desconocido')
} finally {
setLoading(false)
}
}
loadTemplate()
}, [templateId])
// Estados de carga
if (loading) return <div>Cargando template...</div>
if (error) return <div>Error: {error}</div>
if (!template) return <div>Template no encontrado</div>
const handleTemplateChange = (newTemplate: Template) => {
setTemplate(newTemplate)
}
const handleSaveTemplate = async (template: Template) => {
try {
await fetch(`/api/templates/${templateId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`
},
body: JSON.stringify(template)
})
alert('Template actualizado correctamente')
} catch (error) {
alert('Error al actualizar template')
}
}
return (
<App
template={template}
onTemplateChange={handleTemplateChange}
onExportTemplate={handleSaveTemplate}
/>
)
}Configuración de UI Opcional
Puedes controlar qué partes de la UI mostrar:
function CustomUIApp() {
const [template, setTemplate] = useState<Template>(/* tu template */)
return (
<App
template={template}
onTemplateChange={setTemplate}
// 🎛️ Control granular de UI
readOnly={false} // Permite edición
showToolbar={true} // Mostrar barra superior
showPropertiesPanel={true} // Mostrar panel izquierdo
showComponentsPanel={true} // Mostrar panel derecho
/>
)
}Casos de Uso Específicos
Solo Lectura (Display Mode)
function ReadOnlyViewer({ template }: { template: Template }) {
return (
<App
template={template}
onTemplateChange={() => {}} // No-op, no se pueden hacer cambios
readOnly={true} // Solo lectura
showComponentsPanel={false} // Ocultar panel de componentes
/>
)
}Editor Mínimo
function MinimalEditor({ template, onTemplateChange }: {
template: Template,
onTemplateChange: (t: Template) => void
}) {
return (
<App
template={template}
onTemplateChange={onTemplateChange}
showToolbar={false} // Sin toolbar
showComponentsPanel={false} // Solo canvas y propiedades
/>
)
}Preview con Callback
function EditorWithPreview() {
const [template, setTemplate] = useState<Template>(/* template */)
const handlePreview = (template: Template) => {
// Abrir preview en nueva ventana/modal/iframe
window.open(
`/preview?data=${encodeURIComponent(JSON.stringify(template))}`,
'_blank',
'width=1920,height=1080'
)
}
return (
<App
template={template}
onTemplateChange={setTemplate}
onPreviewTemplate={handlePreview} // Custom preview
/>
)
}🎮 Uso del Player (Solo Visualización)
El componente Player es para mostrar templates ya creados (no para editarlos). Ideal para TVs, pantallas digitales, o vistas de cliente.
Player Básico - Template Estático
import React from 'react'
import { Player, type Template } from 'kaas-builder-ds'
import 'kaas-builder-ds/dist/kaas-builder-ds.css'
function MyPlayer() {
const template: Template = {
customer: 'Mi Cliente',
resourcePathImg: '/images/',
resourceDownload: '/downloads/',
destDownload: '/dest/',
useTracking: false,
screens: [{
id: 'screen-1',
width: 1920,
height: 1080,
backgroundColor: '#ffffff',
backgroundImage: '',
elements: [
{
id: 'slider-1',
type: 'slider',
x: 0,
y: 0,
width: 1920,
height: 980,
playlist: [
{
type: 'image',
src: '/images/slide1.jpg',
duration: 5,
priority: 1
},
{
type: 'video',
src: '/videos/video1.mp4',
duration: 10,
priority: 1,
hasAudio: false
}
],
zIndex: 1
},
{
id: 'ticker-1',
type: 'ticker',
x: 0,
y: 980,
width: 1920,
height: 100,
text: 'Noticias en tiempo real desde la base de datos',
speed: 50,
fontSize: 24,
fontColor: '#ffffff',
backgroundColor: '#dc2626',
transparentBackground: false,
zIndex: 2
}
]
}]
}
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Player template={template} />
</div>
)
}
export default MyPlayerPlayer Dinámico - Desde Base de Datos
import React, { useState, useEffect } from 'react'
import { Player, type Template } from 'kaas-builder-ds'
import 'kaas-builder-ds/dist/kaas-builder-ds.css'
function DatabasePlayer({ templateId }: { templateId: string }) {
const [template, setTemplate] = useState<Template | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const loadTemplate = async () => {
try {
setLoading(true)
// Cargar desde tu API
const response = await fetch(`/api/templates/${templateId}`)
if (!response.ok) {
throw new Error('Template no encontrado')
}
const templateData: Template = await response.json()
setTemplate(templateData)
} catch (err) {
setError(err instanceof Error ? err.message : 'Error desconocido')
} finally {
setLoading(false)
}
}
if (templateId) {
loadTemplate()
}
}, [templateId])
// Estados de carga
if (loading) {
return (
<div style={{
width: '100vw',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div>Cargando presentación...</div>
</div>
)
}
if (error) {
return (
<div style={{
width: '100vw',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div>Error: {error}</div>
</div>
)
}
if (!template) {
return (
<div style={{
width: '100vw',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div>Presentación no encontrada</div>
</div>
)
}
// Renderizar el player
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Player template={template} />
</div>
)
}
export default DatabasePlayerPlayer con Auto-actualización
function LivePlayer({ templateId }: { templateId: string }) {
const [template, setTemplate] = useState<Template | null>(null)
useEffect(() => {
const loadTemplate = async () => {
try {
const response = await fetch(`/api/templates/${templateId}`)
const templateData = await response.json()
setTemplate(templateData)
} catch (error) {
console.error('Error cargando template:', error)
}
}
// Cargar inicial
loadTemplate()
// Auto-actualizar cada 30 segundos
const interval = setInterval(loadTemplate, 30000)
return () => clearInterval(interval)
}, [templateId])
if (!template) {
return <div>Cargando...</div>
}
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Player template={template} />
</div>
)
}Rutas para Player
// En tu App.tsx principal
import React from 'react'
import { BrowserRouter as Router, Routes, Route, useParams } from 'react-router-dom'
import DatabasePlayer from './components/DatabasePlayer'
function PlayerRoute() {
const { templateId } = useParams<{ templateId: string }>()
return <DatabasePlayer templateId={templateId!} />
}
function App() {
return (
<Router>
<Routes>
{/* Tu aplicación principal */}
<Route path="/" element={<HomePage />} />
<Route path="/editor" element={<EditorPage />} />
{/* Player routes */}
<Route path="/display/:templateId" element={<PlayerRoute />} />
<Route path="/display" element={<DatabasePlayer templateId="default" />} />
</Routes>
</Router>
)
}
export default AppURLs de ejemplo:
https://tuapp.com/display/template-123- Mostrar template específicohttps://tuapp.com/display- Mostrar template por defecto
API Backend de Ejemplo
// Express.js endpoint
app.get('/api/templates/:id', async (req, res) => {
try {
const { id } = req.params
// Buscar en tu base de datos
const template = await db.collection('templates').findOne({ id })
if (!template) {
return res.status(404).json({ error: 'Template no encontrado' })
}
res.json(template)
} catch (error) {
res.status(500).json({ error: 'Error del servidor' })
}
})
// Endpoint para actualizar
app.put('/api/templates/:id', async (req, res) => {
try {
const { id } = req.params
const { template } = req.body
await db.collection('templates').updateOne(
{ id },
{
$set: {
template,
updatedAt: new Date()
}
}
)
res.json({ success: true })
} catch (error) {
res.status(500).json({ error: 'Error al actualizar' })
}
})📚 Referencia de Tipos
import type {
// 🏗️ Componentes principales
App, // Editor completo controlado externamente
Player, // Reproductor para mostrar templates
// 🎨 Componentes individuales (para uso avanzado)
Canvas, // Canvas de edición
ComponentsPanel, // Panel de componentes (derecha)
PropertiesPanel, // Panel de propiedades (izquierda)
Toolbar, // Barra de herramientas (superior)
JsonPreview, // Modal de vista previa JSON
TemplateSelector, // Modal selector de plantillas
// 📦 Tipos de datos
Template, // Template completo con metadata
Screen, // Pantalla individual
CanvasElement, // Elemento base (union type)
// 🎛️ Elementos específicos
SliderElement, // Slider con playlist
ClockElement, // Reloj digital
TickerElement, // Cintillo de noticias
PlaylistItem, // Item de imagen/video en slider
// ⚙️ Configuración
Resolution, // Resolución de pantalla (1920x1080, 1280x720)
AppProps // Props para componente App
} from 'kaas-builder-ds'Interface Principal - Template
interface Template {
customer: string // Cliente/proyecto
resourcePathImg: string // Ruta base para imágenes
resourceDownload: string // Ruta de descarga de recursos
destDownload: string // Destino de descarga
useTracking: boolean // Habilitar tracking/analytics
screens: Screen[] // Array de pantallas (usualmente 1)
}Interface de Pantalla
interface Screen {
id: string // ID único de pantalla
width: number // Ancho en píxeles (1920 o 1280)
height: number // Alto en píxeles (1080 o 720)
backgroundColor: string // Color de fondo (hex)
backgroundImage: string // URL de imagen de fondo
elements: CanvasElement[] // Array de elementos en la pantalla
}Tipos de Elementos
// Slider con playlist de medios
interface SliderElement {
id: string
type: 'slider'
x: number // Posición X
y: number // Posición Y
width: number // Ancho
height: number // Alto
playlist: PlaylistItem[] // Lista de imágenes/videos
zIndex: number // Orden de capas
}
// Reloj digital
interface ClockElement {
id: string
type: 'clock'
x: number
y: number
width: number
height: number
format: '12h' | '24h' // Formato de hora
showDate: boolean // Mostrar fecha
fontSize: number // Tamaño de fuente
fontColor: string // Color de texto
backgroundColor: string // Color de fondo
transparentBackground?: boolean // Fondo transparente
zIndex: number
}
// Cintillo de noticias
interface TickerElement {
id: string
type: 'ticker'
x: number
y: number
width: number
height: number
text: string // Texto del cintillo
speed: number // Velocidad de movimiento (px/s)
fontSize: number // Tamaño de fuente
fontColor: string // Color de texto
backgroundColor: string // Color de fondo
transparentBackground?: boolean // Fondo transparente
zIndex: number
}
// Item de playlist (imagen o video)
interface PlaylistItem {
type: 'image' | 'video'
src: string // URL del archivo
duration: number // Duración en segundos
priority: number // Prioridad de reproducción
hasAudio?: boolean // Solo para videos
}🎨 Personalización de Estilos
CSS Variables Disponibles
/* En tu CSS global o componente */
:root {
/* 🎨 Canvas */
--kaas-canvas-bg: #ffffff;
--kaas-canvas-grid: #f3f4f6;
--kaas-canvas-border: #e5e7eb;
/* 🖱️ Elementos seleccionados */
--kaas-selected-border: #3b82f6;
--kaas-selected-handles: #1d4ed8;
/* 📋 Paneles */
--kaas-panel-bg: #f9fafb;
--kaas-panel-border: #e5e7eb;
--kaas-panel-text: #1f2937;
/* 🔘 Botones */
--kaas-button-primary: #3b82f6;
--kaas-button-secondary: #6b7280;
--kaas-button-hover: #2563eb;
/* 🌙 Modo oscuro (opcional) */
--kaas-dark-bg: #1f2937;
--kaas-dark-text: #f9fafb;
--kaas-dark-border: #374151;
}Tema Personalizado
function MyAppWithCustomTheme() {
const [template, setTemplate] = useState<Template>(/* ... */)
return (
<div className="custom-kaas-theme">
<App
template={template}
onTemplateChange={setTemplate}
/>
</div>
)
}/* custom-theme.css */
.custom-kaas-theme {
--kaas-canvas-bg: #0f172a;
--kaas-panel-bg: #1e293b;
--kaas-panel-text: #e2e8f0;
--kaas-selected-border: #06b6d4;
}🚀 Ejemplo Completo de Integración
// MiAplicacion.tsx - Ejemplo completo
import React, { useState, useEffect } from 'react'
import { App, Player, type Template } from 'kaas-builder-ds'
import 'kaas-builder-ds/dist/kaas-builder-ds.css'
import './custom-theme.css'
interface TemplateResponse {
id: string
template: Template
createdAt: string
updatedAt: string
}
function MiAplicacion() {
const [template, setTemplate] = useState<Template>({
customer: 'Mi Empresa',
resourcePathImg: '/uploads/images/',
resourceDownload: '/downloads/',
destDownload: '/exports/',
useTracking: true,
screens: [{
id: 'main-screen',
width: 1920,
height: 1080,
backgroundColor: '#ffffff',
backgroundImage: '',
elements: []
}]
})
const [isSaving, setIsSaving] = useState(false)
const [isPreviewMode, setIsPreviewMode] = useState(false)
// Cargar template inicial (opcional)
useEffect(() => {
const loadInitialTemplate = async () => {
try {
const response = await fetch('/api/templates/current')
if (response.ok) {
const data: TemplateResponse = await response.json()
setTemplate(data.template)
}
} catch (error) {
console.log('No hay template inicial, usando vacío')
}
}
loadInitialTemplate()
}, [])
// Callback para cambios en tiempo real
const handleTemplateChange = (newTemplate: Template) => {
setTemplate(newTemplate)
// Opcional: Auto-guardar después de 2 segundos sin cambios
// implementarAutoSave(newTemplate)
}
// Guardar al hacer clic en botón "Guardar"
const handleSaveTemplate = async (template: Template) => {
setIsSaving(true)
try {
const response = await fetch('/api/templates', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
},
body: JSON.stringify({
template,
metadata: {
savedAt: new Date().toISOString(),
version: '1.0'
}
})
})
if (!response.ok) throw new Error('Error al guardar')
const result = await response.json()
alert(`✅ Template guardado con ID: ${result.id}`)
} catch (error) {
alert('❌ Error al guardar: ' + (error as Error).message)
} finally {
setIsSaving(false)
}
}
// Preview en nueva ventana
const handlePreview = (template: Template) => {
const previewData = encodeURIComponent(JSON.stringify(template))
const previewUrl = `/preview.html?data=${previewData}`
window.open(
previewUrl,
'preview',
'width=1920,height=1080,menubar=no,toolbar=no,status=no'
)
}
return (
<div className="mi-aplicacion">
<header style={{ padding: '1rem', borderBottom: '1px solid #e5e7eb' }}>
<h1>Mi Editor de Presentaciones</h1>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1rem' }}>
<button
onClick={() => setIsPreviewMode(!isPreviewMode)}
style={{ padding: '0.5rem 1rem' }}
>
{isPreviewMode ? 'Volver a Editor' : 'Modo Preview'}
</button>
{isSaving && <span>Guardando...</span>}
</div>
</header>
<main style={{ height: 'calc(100vh - 120px)' }}>
{isPreviewMode ? (
// Modo preview
<Player template={template} />
) : (
// Modo editor
<App
template={template}
onTemplateChange={handleTemplateChange}
onExportTemplate={handleSaveTemplate}
onPreviewTemplate={handlePreview}
/>
)}
</main>
</div>
)
}
export default MiAplicacion📋 Resumen de Arquitectura
✅ LO QUE HACE la librería:
- ✅ Renderiza la interfaz de edición
- ✅ Maneja interacciones del usuario (drag, resize, etc.)
- ✅ Proporciona callbacks cuando hay cambios
- ✅ Muestra selector automático cuando está vacío
- ✅ Reproduce templates en el Player
❌ LO QUE NO HACE la librería:
- ❌ NO maneja estado interno del template
- ❌ NO guarda automáticamente
- ❌ NO hace llamadas a APIs
- ❌ NO maneja autenticación
- ❌ NO persiste datos
🔄 FLUJO DE CONTROL:
- Tu app → define template inicial
- Librería → renderiza UI con el template
- Usuario → interactúa con la UI
- Librería → ejecuta callback
onTemplateChange - Tu app → recibe nuevo template y decide qué hacer
- Tu app → actualiza su estado con el nuevo template
- Librería → re-renderiza con el nuevo template
💡 Consejos de Implementación
Auto-guardado Inteligente
function useAutoSave(template: Template) {
const timeoutRef = useRef<NodeJS.Timeout>()
const triggerAutoSave = useCallback(async (template: Template) => {
try {
await fetch('/api/templates/autosave', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(template)
})
console.log('Auto-guardado exitoso')
} catch (error) {
console.error('Error en auto-guardado:', error)
}
}, [])
const scheduleAutoSave = useCallback((template: Template) => {
// Cancelar auto-guardado anterior
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// Programar nuevo auto-guardado en 3 segundos
timeoutRef.current = setTimeout(() => {
triggerAutoSave(template)
}, 3000)
}, [triggerAutoSave])
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
return scheduleAutoSave
}
// Uso:
function MyApp() {
const [template, setTemplate] = useState<Template>(/* ... */)
const autoSave = useAutoSave(template)
const handleTemplateChange = (newTemplate: Template) => {
setTemplate(newTemplate)
autoSave(newTemplate) // Auto-guardar después de cambios
}
// ... resto del componente
}Control de Versiones
interface TemplateVersion {
id: string
template: Template
version: number
createdAt: string
description: string
}
function MyAppWithVersions() {
const [currentTemplate, setCurrentTemplate] = useState<Template>()
const [versions, setVersions] = useState<TemplateVersion[]>([])
const saveVersion = async (template: Template, description: string) => {
const version: TemplateVersion = {
id: crypto.randomUUID(),
template,
version: versions.length + 1,
createdAt: new Date().toISOString(),
description
}
// Guardar en backend
await fetch('/api/templates/versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(version)
})
setVersions([...versions, version])
}
const loadVersion = (version: TemplateVersion) => {
setCurrentTemplate(version.template)
}
// ... resto del componente
}🎯 Esta librería está diseñada para ser completamente controlada desde tu frontend. Tú decides cuándo y cómo guardar, cargar y manejar los templates.
