Skill Web · React

tldraw — Canvas Infinito para React

Sprint Board Visual colaborativo para CULTIVA IA: formas personalizadas, multijugador con Yjs y persistencia con snapshots.

Librería tldraw
Stack React 18 + TypeScript
Collab Yjs + y-websocket
Cliente CULTIVA IA · Sprint 12
Nivel Avanzado
Vista previa — Sprint Board Visual
☰ Select
✏ Draw
▭ Frame
☁ Note
CULTIVA IA — Sprint 12 Board
AL
MR
SR
⬇ Export
NutriPaw
EcoTienda
DataFlow SaaS
Integrar pasarela Stripe
critical
Conectar checkout de suscripciones Premium con webhooks de pago recurrente.
Ajuste SEO landing vet
medium
Optimizar meta tags y schema markup para keywords veterinarios.
Agente de recomendación IA
high
Prompt chain para sugerir productos eco según historial de compra.
Email onboarding secuencia
low
5 correos de bienvenida con storytelling sostenible (Mailchimp).
Dashboard de métricas v2
high
Rediseñar la vista de KPIs con charts interactivos (Recharts).
Documentar API pública
medium
Generar OpenAPI spec + ejemplos en TypeScript para desarrolladores.
AL
SR
Código generado — TaskCardShape.tsx
TaskCardShape.tsx
SprintBoardCanvas.tsx
useSprintBoard.ts
// src/shapes/TaskCardShape.tsx // Forma personalizada para el Sprint Board de CULTIVA IA import { BaseBoxShapeUtil, TLBaseShape, HTMLContainer } from "tldraw" type Priority = "low" | "medium" | "high" | "critical" type Status = "todo" | "in-progress" | "review" | "done" type TaskCard = TLBaseShape<"task-card", { title: string description: string assignee: string priority: Priority status: Status cliente: string // e.g. "NutriPaw" | "EcoTienda" | "DataFlow SaaS" w: number; h: number }> const PRIORITY_COLOR: Record<Priority, string> = { low: "#4ade80", medium: "#facc15", high: "#fb923c", critical: "#ef4444", } export class TaskCardShapeUtil extends BaseBoxShapeUtil<TaskCard> { static override type = "task-card" as const getDefaultProps(): TaskCard["props"] { return { title: "Nueva Tarea", description: "", assignee: "Sin asignar", priority: "medium", status: "todo", cliente: "", w: 280, h: 160, } } component(shape: TaskCard) { const color = PRIORITY_COLOR[shape.props.priority] return ( <HTMLContainer id={shape.id} style={{ padding: "12px", borderRadius: "10px", borderLeft: `4px solid ${color}`, background: "white", boxShadow: "0 4px 16px rgba(0,0,0,.15)", pointerEvents: "all", }}> <div style={{ display: "flex", justifyContent: "space-between" }}> <strong>{shape.props.title}</strong> <span style={{ background: color, color: "white", fontSize: "9px", padding: "2px 6px", borderRadius: "4px" }}> {shape.props.priority} </span> </div> <p style={{ fontSize: "11px", color: "#64748b", margin: "6px 0" }}> {shape.props.description} </p> <div style={{ fontSize: "10px", color: "#94a3b8", display: "flex", justifyContent: "space-between" }}> <span>{shape.props.status}</span> <span>👤 {shape.props.assignee}</span> </div> </HTMLContainer> ) } indicator(shape: TaskCard) { // Renderizado simplificado para zoom lejano (rendimiento) return <rect width={shape.props.w} height={shape.props.h} rx={10} /> } }
Código generado — SprintBoardCanvas.tsx
SprintBoardCanvas.tsx
// src/components/SprintBoardCanvas.tsx // Canvas colaborativo del Sprint Board — CULTIVA IA import { Tldraw } from "tldraw" import { useYjsStore } from "@tldraw/yjs" import * as Y from "yjs" import { WebsocketProvider } from "y-websocket" import { TaskCardShapeUtil } from "../shapes/TaskCardShape" import { useSprintBoard } from "../hooks/useSprintBoard" import "tldraw/tldraw.css" import { useMemo } from "react" const SHAPES = [TaskCardShapeUtil] const WS_URL = "wss://collab.cultivaia.com" export function SprintBoardCanvas({ sprintId }: { sprintId: string }) { const doc = useMemo(() => new Y.Doc(), []) const provider = useMemo(() => new WebsocketProvider(WS_URL, `cultiva-sprint-${sprintId}`, doc), [doc, sprintId] ) // Sincroniza el store de tldraw con Yjs → multijugador automático const store = useYjsStore({ yDoc: doc, provider, roomId: `cultiva-sprint-${sprintId}` }) return ( <div style={{ position: "fixed", inset: 0 }}> <Tldraw store={store} shapeUtils={SHAPES} persistenceKey={`sprint-${sprintId}`} /> </div> ) } // Uso en la plataforma CULTIVA IA: // <SprintBoardCanvas sprintId="12" />
Código generado — useSprintBoard.ts
useSprintBoard.ts
// src/hooks/useSprintBoard.ts // Hook para manipular el Sprint Board programáticamente import { useEditor } from "tldraw" import { useCallback, useEffect } from "react" export function useSprintBoard(sprintId: string) { const editor = useEditor() // Auto-save al backend cada 2 s (debounced) useEffect(() => { let timeout: ReturnType<typeof setTimeout> const unsub = editor.store.listen(() => { clearTimeout(timeout) timeout = setTimeout(async () => { const snapshot = editor.store.getSnapshot() await fetch(`/api/sprints/${sprintId}/snapshot`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(snapshot), }) }, 2000) }, { source: "user", scope: "document" }) return () => { unsub(); clearTimeout(timeout) } }, [editor, sprintId]) // Crear una TaskCard desde código (p.ej. importar desde Huly CRM) const addTaskCard = useCallback(( title: string, cliente: string, x = 100, y = 100 ) => { editor.createShape({ type: "task-card", x, y, props: { title, cliente, description: "", assignee: "Sin asignar", priority: "medium", status: "todo", w: 280, h: 160 }, }) }, [editor]) // Exportar el canvas como SVG (resolución independiente) const exportSVG = useCallback(async () => { const ids = [...editor.getCurrentPageShapeIds()] const svg = await editor.getSvg(ids, { scale: 2, background: true }) return svg }, [editor]) return { addTaskCard, exportSVG } }
Guias y mejores practicas
📐

Contenedor de ancho total

tldraw necesita un contenedor con dimensiones explícitas. Usa position:fixed; inset:0 para ocupar toda la ventana o un div con altura fija.

🧩

Formas de dominio

No uses texto/freehand para todo. Crea formas que representen tu dominio: TaskCard, ClientZone, MilestoneNode. Renders React completos en HTMLContainer.

Indicador para zoom

Implementa indicator() en cada shape — renderiza un SVG simple cuando hay zoom alejado, evitando re-renders costosos de HTML.

🔄

Auto-save debounced

Los cambios de canvas se disparan muy rápido durante el dibujado. Guarda con store.getSnapshot() debounceado cada 2000 ms, nunca en cada evento.

🤝

Multijugador con Yjs

useYjsStore sincroniza el store de tldraw sobre un WebSocket. Los cursores y selecciones de otros usuarios aparecen automáticamente sin código extra.

🖼

Exportar como SVG

Usa editor.getSvg() en lugar de PNG — independiente de resolución, mucho menor en tamaño y escalable a cualquier DPI para PDFs de cliente.

Instalacion
#Instalacion base
$npm install tldraw
#Soporte multijugador
$npm install @tldraw/yjs yjs y-websocket
#Configurar servidor Yjs (y-websocket)
$HOST=0.0.0.0 PORT=1234 npx y-websocket