@decido/shell 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/.turbo/turbo-build.log +13 -0
  2. package/package.json +65 -0
  3. package/src/AgentPlayer.tsx +105 -0
  4. package/src/DecidoPlayer.tsx +117 -0
  5. package/src/bridge/BridgeAgent.ts +443 -0
  6. package/src/components/DecidoIcon.tsx +56 -0
  7. package/src/components/JsonTreeEditor.tsx +117 -0
  8. package/src/components/PanelSplitter.tsx +71 -0
  9. package/src/components/PluginErrorBoundary.tsx +69 -0
  10. package/src/components/SafeLiquidUI.tsx +114 -0
  11. package/src/components/TransientLayer.tsx +92 -0
  12. package/src/components/agent/AgentChat.tsx +134 -0
  13. package/src/components/chat-extensions/IntentCatalogPanel.tsx +81 -0
  14. package/src/components/chat-extensions/chatSlashCommands.ts +101 -0
  15. package/src/components/controls/CreatorInputBar.tsx +144 -0
  16. package/src/components/controls/OSToolbar.tsx +90 -0
  17. package/src/components/controls/TimelineTape.tsx +43 -0
  18. package/src/components/debug/ActionTimelineTab.tsx +111 -0
  19. package/src/components/debug/CSSInspectorTab.tsx +436 -0
  20. package/src/components/debug/ExportTab.tsx +192 -0
  21. package/src/components/debug/FlowHealthTab.tsx +86 -0
  22. package/src/components/debug/LogsTab.tsx +110 -0
  23. package/src/components/debug/MorphStackTab.tsx +241 -0
  24. package/src/components/debug/NetworkTab.tsx +173 -0
  25. package/src/components/debug/PerformanceTab.tsx +171 -0
  26. package/src/components/debug/ProfilesTab.tsx +238 -0
  27. package/src/components/debug/ReplayTab.tsx +70 -0
  28. package/src/components/debug/StoresTab.tsx +255 -0
  29. package/src/components/debug/TopologyTab.tsx +59 -0
  30. package/src/components/debug/debugConfig.tsx +66 -0
  31. package/src/components/playground/DebugPanel.tsx +112 -0
  32. package/src/components/playground/HeaderCenterControls.tsx +92 -0
  33. package/src/components/playground/KeyframeListItem.tsx +70 -0
  34. package/src/components/playground/PlaygroundAppSidebar.tsx +171 -0
  35. package/src/components/playground/PlaygroundBottomControls.tsx +132 -0
  36. package/src/components/playground/PlaygroundCanvas.tsx +87 -0
  37. package/src/components/playground/PlaygroundChat.tsx +236 -0
  38. package/src/components/playground/PlaygroundErrorBoundary.tsx +63 -0
  39. package/src/components/playground/PlaygroundFloatingInput.tsx +352 -0
  40. package/src/components/playground/PlaygroundHeader.tsx +222 -0
  41. package/src/components/playground/PlaygroundSidebar.tsx +136 -0
  42. package/src/components/playground/PlaygroundTerminal.tsx +44 -0
  43. package/src/components/playground/SuggestionCards.tsx +29 -0
  44. package/src/components/playground/demos/ClinicaAINode.tsx +221 -0
  45. package/src/components/playground/demos/FinanceAINode.tsx +226 -0
  46. package/src/components/playground/demos/KiaAcademyNode.tsx +250 -0
  47. package/src/components/playground/demos/KiaBotNode.tsx +207 -0
  48. package/src/components/playground/demos/KiaCampaignNode.tsx +191 -0
  49. package/src/components/playground/demos/KiaComplianceNode.tsx +140 -0
  50. package/src/components/playground/demos/KiaCustomerJourneyNode.tsx +220 -0
  51. package/src/components/playground/demos/KiaCyberNode.tsx +203 -0
  52. package/src/components/playground/demos/KiaDashboardNode.tsx +399 -0
  53. package/src/components/playground/demos/KiaEmbudoOverviewNode.tsx +168 -0
  54. package/src/components/playground/demos/KiaExecutiveNode.tsx +169 -0
  55. package/src/components/playground/demos/KiaGamificationNode.tsx +229 -0
  56. package/src/components/playground/demos/KiaIntelligenceHubNode.tsx +165 -0
  57. package/src/components/playground/demos/KiaInventoryNode.tsx +183 -0
  58. package/src/components/playground/demos/KiaLeadScoringNode.tsx +226 -0
  59. package/src/components/playground/demos/KiaLiveSimulationNode.tsx +177 -0
  60. package/src/components/playground/demos/KiaMultiDealerNode.tsx +223 -0
  61. package/src/components/playground/demos/KiaNPSVoiceNode.tsx +214 -0
  62. package/src/components/playground/demos/KiaOmnichannelNode.tsx +162 -0
  63. package/src/components/playground/demos/KiaPBIBudgetNode.tsx +152 -0
  64. package/src/components/playground/demos/KiaPBIConversionNode.tsx +206 -0
  65. package/src/components/playground/demos/KiaPBIFunnelNode.tsx +184 -0
  66. package/src/components/playground/demos/KiaPBIOwnershipNode.tsx +113 -0
  67. package/src/components/playground/demos/KiaPBIPartnerNode.tsx +143 -0
  68. package/src/components/playground/demos/KiaPBIPreciosNode.tsx +120 -0
  69. package/src/components/playground/demos/KiaPBIRuntNode.tsx +205 -0
  70. package/src/components/playground/demos/KiaPartnerScoreNode.tsx +206 -0
  71. package/src/components/playground/demos/KiaPredictiveNode.tsx +226 -0
  72. package/src/components/playground/demos/KiaShowroomNode.tsx +194 -0
  73. package/src/components/playground/demos/KiaStoreNode.tsx +215 -0
  74. package/src/components/playground/demos/KiaSustainabilityNode.tsx +173 -0
  75. package/src/components/playground/demos/KiaUsedVehiclesNode.tsx +163 -0
  76. package/src/components/playground/demos/KiaWorkshopNode.tsx +221 -0
  77. package/src/components/playground/demos/SmartCityNode.tsx +205 -0
  78. package/src/components/playground/demos/kia_campaign_manifest.json +112 -0
  79. package/src/components/playground/input-parts/AIModelSelector.tsx +156 -0
  80. package/src/components/playground/input-parts/InputActions.tsx +80 -0
  81. package/src/components/playground/input-parts/InputToolbar.tsx +245 -0
  82. package/src/components/playground/input-parts/ResourceLibraryPanel.tsx +287 -0
  83. package/src/components/playground/sidebarDsdIO.ts +82 -0
  84. package/src/components/settings/SettingsPanel.tsx +267 -0
  85. package/src/components/shell/AppHeader.tsx +9 -0
  86. package/src/components/shell/AppShell.tsx +139 -0
  87. package/src/components/shell/ArtifactBar.tsx +97 -0
  88. package/src/components/shell/BootScreen.tsx +19 -0
  89. package/src/components/shell/CenterComposite.tsx +87 -0
  90. package/src/components/shell/CodeEditorPanel.tsx +88 -0
  91. package/src/components/shell/GlobalOverlays.tsx +228 -0
  92. package/src/components/shell/LayoutConfigurator.tsx +209 -0
  93. package/src/components/shell/LayoutGrid.tsx +178 -0
  94. package/src/components/shell/MorphShell.tsx +368 -0
  95. package/src/components/shell/PluginViewer.tsx +147 -0
  96. package/src/components/shell/ShellNexusPreview.tsx +458 -0
  97. package/src/components/shell/SlotRenderer.tsx +115 -0
  98. package/src/components/shell/TabBar.tsx +94 -0
  99. package/src/components/shell/TemplateLibrary.tsx +195 -0
  100. package/src/components/shell/layoutConstants.ts +35 -0
  101. package/src/components/shell/morphStageMeta.ts +15 -0
  102. package/src/components/shell/shells/BuiltInShells.tsx +443 -0
  103. package/src/components/shell/shells/DatawayChatShell.tsx +42 -0
  104. package/src/components/shell/shells/TokenPreview.tsx +339 -0
  105. package/src/components/shell/shells/bootShells.ts +31 -0
  106. package/src/components/shells/CreatorShell.tsx +37 -0
  107. package/src/components/shells/DecidoShell.tsx +447 -0
  108. package/src/components/shells/ExperimentalChatShell.tsx +245 -0
  109. package/src/components/shells/UserCanvas.tsx +44 -0
  110. package/src/components/studio/BlueprintManagerPanel.tsx +137 -0
  111. package/src/components/studio/DependencyTreePanel.tsx +192 -0
  112. package/src/components/studio/NodePalette.tsx +92 -0
  113. package/src/components/studio/NodePropertiesPanel.tsx +81 -0
  114. package/src/components/studio/ReactFlowEditor.tsx +242 -0
  115. package/src/components/studio/TimelineEditor.tsx +122 -0
  116. package/src/components/studio/TimelineKeyframeCard.tsx +99 -0
  117. package/src/components/studio/VariablePanel.tsx +181 -0
  118. package/src/components/studio/blueprint/BlueprintCard.tsx +82 -0
  119. package/src/components/studio/editor/CanvasContextMenu.tsx +107 -0
  120. package/src/components/studio/editor/EditorToolbar.tsx +80 -0
  121. package/src/components/studio/editor/StageContentRenderer.tsx +134 -0
  122. package/src/components/studio/editor/TrackPropertyEditors.tsx +133 -0
  123. package/src/components/studio/editor/TreeNodeItem.tsx +91 -0
  124. package/src/components/studio/editor/edgeStyles.ts +43 -0
  125. package/src/components/studio/editor/editorKeyHandler.ts +95 -0
  126. package/src/components/studio/editor/nodeTypeRegistry.ts +137 -0
  127. package/src/components/studio/editor/paletteCatalog.tsx +84 -0
  128. package/src/components/studio/nodes/shell/InteractionNodes.tsx +82 -0
  129. package/src/components/studio/nodes/shell/LayoutControlNodes.tsx +69 -0
  130. package/src/components/studio/nodes/shell/RegisterActionNode.tsx +20 -0
  131. package/src/components/studio/nodes/shell/RegisterButtonNode.tsx +22 -0
  132. package/src/components/studio/nodes/shell/RegisterPanelNode.tsx +19 -0
  133. package/src/components/studio/nodes/shell/RegisterSidebarNode.tsx +19 -0
  134. package/src/components/studio/nodes/shell/RegisterStatusBarNode.tsx +22 -0
  135. package/src/components/studio/nodes/shell/RegisterTabNode.tsx +21 -0
  136. package/src/components/studio/nodes/shell/RegisterTopBarNode.tsx +22 -0
  137. package/src/components/studio/nodes/shell/ShellConfigNode.tsx +51 -0
  138. package/src/components/studio/nodes/shell/ShellNodeBase.tsx +100 -0
  139. package/src/components/studio/nodes/shell/ThemeNodes.tsx +51 -0
  140. package/src/components/studio/nodes/shell/index.ts +12 -0
  141. package/src/components/widgets/BroadcastWidget.tsx +93 -0
  142. package/src/components/widgets/MarketplaceWidget.tsx +298 -0
  143. package/src/components/widgets/McpToolsWidget.tsx +231 -0
  144. package/src/components/widgets/OpsDashboard.tsx +59 -0
  145. package/src/components/widgets/QuickActionsWidget.tsx +60 -0
  146. package/src/components/widgets/UsageWidget.tsx +112 -0
  147. package/src/components/widgets/WidgetRenderer.tsx +892 -0
  148. package/src/components/widgets/WidgetSlotPanel.tsx +213 -0
  149. package/src/config/IconRegistry.ts +126 -0
  150. package/src/contexts/NetworkProvider.tsx +162 -0
  151. package/src/core/AIDirector.ts +71 -0
  152. package/src/core/EventBus.ts +37 -0
  153. package/src/core/PluginContext.tsx +141 -0
  154. package/src/hooks/listeners/useUIStateListener.ts +59 -0
  155. package/src/hooks/listeners/useWhatsAppListener.ts +110 -0
  156. package/src/hooks/morphBridge.ts +82 -0
  157. package/src/hooks/useAIModelSelector.ts +144 -0
  158. package/src/hooks/useAgentStream.ts +220 -0
  159. package/src/hooks/useAutoUpdater.ts +89 -0
  160. package/src/hooks/useBootSequence.ts +20 -0
  161. package/src/hooks/useExportDSD.ts +53 -0
  162. package/src/hooks/useFullscreen.ts +35 -0
  163. package/src/hooks/useGeminiStream.ts +282 -0
  164. package/src/hooks/useIntentLens.ts +224 -0
  165. package/src/hooks/useKeyboardShortcuts.ts +69 -0
  166. package/src/hooks/useLoggerBridge.ts +32 -0
  167. package/src/hooks/useMcpClient.ts +112 -0
  168. package/src/hooks/useNexusaiDeploy.ts +118 -0
  169. package/src/hooks/usePlaybackEngine.ts +21 -0
  170. package/src/hooks/usePlaygroundCommander.ts +475 -0
  171. package/src/hooks/usePluginEngine.ts +165 -0
  172. package/src/hooks/useScreenRecorder.ts +73 -0
  173. package/src/hooks/useShellKeyboard.ts +40 -0
  174. package/src/hooks/useShellShortcuts.ts +118 -0
  175. package/src/hooks/useSoundEffects.ts +35 -0
  176. package/src/hooks/useStudioConfig.ts +72 -0
  177. package/src/hooks/useSystemBoot.ts +84 -0
  178. package/src/hooks/useSystemTelemetry.ts +62 -0
  179. package/src/index.ts +97 -0
  180. package/src/lib/debugLogger.ts +80 -0
  181. package/src/lib/networkInterceptor.ts +100 -0
  182. package/src/mocks/decido.tsx +41 -0
  183. package/src/plugins/pluginAPI.ts +190 -0
  184. package/src/store/McpStore.ts +69 -0
  185. package/src/store/UpdaterStore.ts +60 -0
  186. package/src/store/engine.ts +392 -0
  187. package/src/store/index.ts +4 -0
  188. package/src/store/layoutPresets.ts +66 -0
  189. package/src/store/playgroundTypes.ts +98 -0
  190. package/src/store/useActionTimelineStore.ts +48 -0
  191. package/src/store/useDebugPanelStore.ts +98 -0
  192. package/src/store/useDebugProfileStore.ts +130 -0
  193. package/src/store/useLayoutStore.ts +205 -0
  194. package/src/store/useMorphInstanceStore.ts +289 -0
  195. package/src/store/useMorphologyStore.ts +103 -0
  196. package/src/store/usePlaygroundStore.ts +236 -0
  197. package/src/store/useShellRegistry.ts +123 -0
  198. package/src/store/useSuggestionsStore.ts +57 -0
  199. package/src/store/useThemeStore.ts +399 -0
  200. package/src/store/useUIComponentStore.ts +179 -0
  201. package/src/types/DecidoStoryDefinition.ts +43 -0
  202. package/src/utils/ai/ai-architect.ts +92 -0
  203. package/src/utils/ai/ai-code.ts +187 -0
  204. package/src/utils/ai/ai-core.ts +50 -0
  205. package/src/utils/ai/ai-media.ts +292 -0
  206. package/src/utils/layoutGraph.ts +67 -0
  207. package/tsconfig.json +17 -0
  208. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,92 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { motion, AnimatePresence } from 'motion/react';
3
+ import { ChevronDown, ChevronRight, GripVertical, PanelLeftClose, PanelLeft, Search, Layout, Image } from 'lucide-react';
4
+ import { useUIComponentStore } from '../../store/useUIComponentStore';
5
+ import { STATIC_CATEGORIES, DRAG_DATA_KEY, PaletteItem, PaletteCategory } from './editor/paletteCatalog';
6
+
7
+ interface NodePaletteProps { className?: string; }
8
+
9
+ export const NodePalette: React.FC<NodePaletteProps> = ({ className }) => {
10
+ const [isCollapsed, setIsCollapsed] = useState(false);
11
+ const [expandedCategories, setExpandedCategories] = useState<Record<string, boolean>>({
12
+ dialogue: true, logic: true, ui: true, render: true, hsm: false, triggers: false,
13
+ morphology: false, 'shell-register': true, 'shell-layout': true, 'shell-theme': true,
14
+ 'shell-interaction': true, 'shell-config': true
15
+ });
16
+ const [searchFilter, setSearchFilter] = useState('');
17
+ const uiComponents = useUIComponentStore(s => s.components);
18
+
19
+ const categories = useMemo<PaletteCategory[]>(() => {
20
+ const renderItems: PaletteItem[] = Object.values(uiComponents).map(comp => ({
21
+ type: 'render', label: comp.name, icon: <Layout size={16} />,
22
+ color: comp.source === 'ai-generated' ? 'border-pink-500/40' : 'border-cyan-500/40',
23
+ description: `${comp.category} · ${comp.source}`, meta: { uiSchema: comp.schema, componentId: comp.id }
24
+ }));
25
+ const all = [...STATIC_CATEGORIES];
26
+ all.splice(3, 0, { id: 'render', label: 'UI Render', icon: <Image size={14} />, items: renderItems });
27
+ return all;
28
+ }, [uiComponents]);
29
+
30
+ const filteredCategories = useMemo(() => {
31
+ if (!searchFilter.trim()) return categories;
32
+ const q = searchFilter.toLowerCase();
33
+ return categories.map(c => ({ ...c, items: c.items.filter(i => i.label.toLowerCase().includes(q) || i.type.toLowerCase().includes(q) || (i.description || '').toLowerCase().includes(q)) })).filter(c => c.items.length > 0);
34
+ }, [categories, searchFilter]);
35
+
36
+ const onDragStart = (e: React.DragEvent, item: PaletteItem) => {
37
+ e.dataTransfer.setData(DRAG_DATA_KEY, JSON.stringify({ type: item.type, label: item.label, meta: item.meta || {} }));
38
+ e.dataTransfer.effectAllowed = 'move';
39
+ };
40
+
41
+ if (isCollapsed) return (
42
+ <div className={`flex flex-col items-center py-3 px-1 bg-surface-primary/80 border-r border-border-subtle ${className || ''}`}>
43
+ <button onClick={() => setIsCollapsed(false)} className="p-2 rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-glass transition-colors" title="Abrir Palette"><PanelLeft size={18} /></button>
44
+ </div>
45
+ );
46
+
47
+ return (
48
+ <motion.div initial={{ width: 0, opacity: 0 }} animate={{ width: 220, opacity: 1 }} exit={{ width: 0, opacity: 0 }} transition={{ duration: 0.2 }}
49
+ className={`flex flex-col bg-surface-primary/90 border-r border-border-subtle overflow-hidden select-none ${className || ''}`} style={{ width: 220 }}>
50
+ <div className="flex items-center justify-between px-3 py-2.5 border-b border-border-subtle">
51
+ <span className="text-xs font-bold text-text-secondary uppercase tracking-wider">Nodos</span>
52
+ <button onClick={() => setIsCollapsed(true)} className="p-1 rounded text-text-muted hover:text-text-primary hover:bg-surface-glass transition-colors"><PanelLeftClose size={14} /></button>
53
+ </div>
54
+ <div className="px-2 py-2 border-b border-border-subtle">
55
+ <div className="flex items-center gap-2 bg-surface-glass rounded-lg px-2.5 py-1.5">
56
+ <Search size={12} className="text-text-muted shrink-0" />
57
+ <input type="text" placeholder="Buscar nodos..." value={searchFilter} onChange={e => setSearchFilter(e.target.value)} className="bg-transparent text-xs text-text-primary placeholder-zinc-600 outline-hidden w-full" />
58
+ </div>
59
+ </div>
60
+ <div className="flex-1 overflow-y-auto custom-scrollbar">
61
+ {filteredCategories.map(cat => (
62
+ <div key={cat.id}>
63
+ <button onClick={() => setExpandedCategories(p => ({ ...p, [cat.id]: !p[cat.id] }))} className="w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold text-text-secondary hover:text-text-primary hover:bg-surface-glass transition-colors">
64
+ {expandedCategories[cat.id] ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
65
+ <span className="text-text-muted">{cat.icon}</span><span>{cat.label}</span>
66
+ <span className="ml-auto text-[10px] text-text-muted bg-surface-glass px-1.5 py-0.5 rounded">{cat.items.length}</span>
67
+ </button>
68
+ <AnimatePresence>
69
+ {expandedCategories[cat.id] && (
70
+ <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.15 }} className="overflow-hidden">
71
+ {cat.items.map(item => (
72
+ <div key={`${cat.id}-${item.type}-${item.label}`} draggable onDragStart={e => onDragStart(e, item)}
73
+ className={`flex items-center gap-2 px-3 py-2 mx-2 mb-1 rounded-lg border bg-surface-secondary/60 hover:bg-surface-glass cursor-grab active:cursor-grabbing transition-all group ${item.color}`}>
74
+ <GripVertical size={12} className="text-text-muted group-hover:text-text-secondary shrink-0" />
75
+ <div className="text-text-secondary group-hover:text-text-primary shrink-0">{item.icon}</div>
76
+ <div className="flex-1 min-w-0">
77
+ <div className="text-[11px] font-medium text-text-primary group-hover:text-text-primary truncate">{item.label}</div>
78
+ {item.description && <div className="text-[9px] text-text-muted truncate">{item.description}</div>}
79
+ </div>
80
+ </div>
81
+ ))}
82
+ </motion.div>
83
+ )}
84
+ </AnimatePresence>
85
+ </div>
86
+ ))}
87
+ </div>
88
+ </motion.div>
89
+ );
90
+ };
91
+
92
+ export { DRAG_DATA_KEY };
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { X, Variable } from 'lucide-react';
3
+ import { motion, AnimatePresence } from 'motion/react';
4
+ import { usePlaygroundStore } from '../../store/usePlaygroundStore';
5
+ import { useTimelineStore, useEngineStore } from '@decido/engine';
6
+ import { getTrackConfig, DialogueEditor, UIStateEditor, RenderEditor, LogicEditor, TriggerEditor, SetVariableEditor } from './editor/TrackPropertyEditors';
7
+
8
+ interface NodePropertiesPanelProps { selectedNodeId: string | null; onClose: () => void; }
9
+
10
+ export const NodePropertiesPanel: React.FC<NodePropertiesPanelProps> = ({ selectedNodeId, onClose }) => {
11
+ const activeTimelineId = usePlaygroundStore(s => s.prototypeBrand);
12
+ const timelines = useTimelineStore(s => s.timelines);
13
+ const updateKeyframe = useTimelineStore(s => s.updateKeyframe);
14
+ const variables = useEngineStore(s => s.variables);
15
+
16
+ const kf = timelines[activeTimelineId]?.keyframes.find(k => k.id === selectedNodeId);
17
+ const track = (kf?.track as string) || '';
18
+
19
+ const referencedVars = React.useMemo(() => {
20
+ if (!kf) return [];
21
+ const text = (kf.speech || '') + (kf.state || '');
22
+ const m = text.match(/\{\{([^}]+)\}\}/g) || [];
23
+ return [...new Set(m.map(x => x.replace(/\{\{|\}\}/g, '').trim()))];
24
+ }, [kf?.speech, kf?.state, kf]);
25
+
26
+ if (!kf) return null;
27
+
28
+ const cfg = getTrackConfig(track);
29
+ const Icon = cfg.icon;
30
+ const onUpdate = (patch: Record<string, any>) => updateKeyframe(activeTimelineId, kf.id, patch);
31
+
32
+ return (
33
+ <AnimatePresence>
34
+ <motion.div initial={{ x: 300, opacity: 0 }} animate={{ x: 0, opacity: 1 }} exit={{ x: 300, opacity: 0 }}
35
+ transition={{ type: 'spring', bounce: 0, duration: 0.3 }}
36
+ className="absolute right-0 top-0 bottom-0 w-[300px] bg-surface-primary border-l border-border-default z-50 shadow-2xl flex flex-col">
37
+ {/* Header */}
38
+ <div className={`p-4 border-b border-border-default flex items-center justify-between ${cfg.bg}`}>
39
+ <div className="flex items-center gap-2"><Icon size={16} className={cfg.color} /><h3 className={`text-sm font-bold uppercase tracking-widest ${cfg.color}`}>{cfg.label}</h3></div>
40
+ <button onClick={onClose} className="p-1 rounded-md hover:bg-surface-glass text-text-primary/50 hover:text-text-primary transition-colors"><X size={16} /></button>
41
+ </div>
42
+ {/* Body */}
43
+ <div className="flex-1 overflow-y-auto p-4 custom-scrollbar flex flex-col gap-5">
44
+ {/* Metadata */}
45
+ <div className="flex flex-col gap-2">
46
+ <h4 className="text-[10px] font-bold text-text-primary/40 uppercase tracking-widest border-b border-border-default pb-1">Metadatos Base</h4>
47
+ <div className="grid grid-cols-2 gap-2 mt-1">
48
+ <div className="flex flex-col gap-1"><label className="text-[10px] text-text-muted">Node ID</label><input type="text" value={kf.id} readOnly className="bg-surface-overlay border border-border-subtle rounded px-2 py-1 text-xs text-text-secondary font-mono outline-hidden" /></div>
49
+ <div className="flex flex-col gap-1"><label className="text-[10px] text-text-muted">Tiempo (t)</label>
50
+ <div className="flex items-center border border-border-default rounded overflow-hidden"><input type="number" step="0.1" value={kf.t} onChange={e => onUpdate({ t: parseFloat(e.target.value) || 0 })} className="bg-surface-glass px-2 py-1 text-xs text-text-primary outline-hidden w-full font-mono" /><span className="bg-surface-glass px-2 py-1 text-xs text-text-muted border-l border-border-default">s</span></div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ {/* Track-specific editor */}
55
+ {track === 'dialogue' && <DialogueEditor keyframe={kf} onUpdate={onUpdate} />}
56
+ {track === 'ui' && <UIStateEditor keyframe={kf} onUpdate={onUpdate} />}
57
+ {track === 'render' && <RenderEditor keyframe={kf} onUpdate={onUpdate} />}
58
+ {track === 'logic' && <LogicEditor keyframe={kf} onUpdate={onUpdate} />}
59
+ {track === 'trigger' && <TriggerEditor keyframe={kf} onUpdate={onUpdate} />}
60
+ {track === 'set' && <SetVariableEditor keyframe={kf} onUpdate={onUpdate} />}
61
+ {/* Data Bindings */}
62
+ <div className="flex flex-col gap-3 mt-2">
63
+ <h4 className="text-[10px] font-bold text-emerald-400/40 uppercase tracking-widest border-b border-emerald-500/20 pb-1 flex items-center gap-1.5"><Variable size={10} />Data Bindings</h4>
64
+ {referencedVars.length > 0 && (
65
+ <div className="flex flex-col gap-1"><span className="text-[9px] text-text-muted uppercase">Variables Referenciadas</span>
66
+ <div className="flex flex-wrap gap-1">{referencedVars.map(v => (
67
+ <span key={v} className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${variables[v] !== undefined ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400' : 'bg-red-500/10 border-red-500/20 text-red-400'}`}>
68
+ {`{{${v}}}`} {variables[v] !== undefined ? `= ${variables[v]}` : '⚠ undefined'}
69
+ </span>
70
+ ))}</div>
71
+ </div>
72
+ )}
73
+ <div className="p-2 bg-emerald-500/5 border border-emerald-500/10 rounded text-[10px] text-text-muted">
74
+ Usa <code className="text-emerald-400 bg-surface-glass px-1 rounded">{`{{variable}}`}</code> en el texto para interpolar valores del Motor.
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </motion.div>
79
+ </AnimatePresence>
80
+ );
81
+ };
@@ -0,0 +1,242 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ ReactFlow, MiniMap, Controls, Background,
4
+ useNodesState, useEdgesState, addEdge,
5
+ Connection, Edge, Node, MarkerType, ReactFlowProvider, useReactFlow, SelectionMode
6
+ } from '@xyflow/react';
7
+ import '@xyflow/react/dist/style.css';
8
+ import { usePlaygroundStore } from '../../store/usePlaygroundStore';
9
+ import { useEngineStore, useTimelineStore } from '@decido/engine';
10
+ import { convertLegacyTimelineToBlueprint } from '@decido/core';
11
+ import { useBlueprintNavigation } from '@decido/engine';
12
+ import { getLayoutedElements } from '../../utils/layoutGraph';
13
+ import { Home, ChevronRight } from 'lucide-react';
14
+ import { getRegisteredMorphComponents } from '../../store/useMorphologyStore';
15
+
16
+ import { nodeTypes, TRACK_X_MAP, NODE_TYPE_TO_TRACK, mapNodePayload, buildKeyframeDefaults } from './editor/nodeTypeRegistry';
17
+ import { buildStyledEdges } from './editor/edgeStyles';
18
+ import { CanvasContextMenu } from './editor/CanvasContextMenu';
19
+ import { EditorToolbar } from './editor/EditorToolbar';
20
+ import { createEditorKeyHandler } from './editor/editorKeyHandler';
21
+ import { NodePropertiesPanel } from './NodePropertiesPanel';
22
+ import { NodePalette } from './NodePalette';
23
+ import { VariablePanel } from './VariablePanel';
24
+
25
+ interface ReactFlowEditorProps { className?: string; style?: React.CSSProperties; id?: string; }
26
+
27
+ const ReactFlowEditorInner: React.FC<ReactFlowEditorProps> = ({ className, style, id }) => {
28
+ const { fitView, getIntersectingNodes, screenToFlowPosition } = useReactFlow();
29
+ const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
30
+ const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
31
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
32
+ const [isVariablePanelOpen, setIsVariablePanelOpen] = useState(false);
33
+ const lastFittedBlueprintRef = useRef<string | null>(null);
34
+ const clipboardRef = useRef<{ nodes: Node[]; edges: Edge[] }>({ nodes: [], edges: [] });
35
+ const [contextMenu, setContextMenu] = useState<{ x: number; y: number; flowPos: { x: number; y: number } } | null>(null);
36
+
37
+ // ─── Store subscriptions ───
38
+ const activeTimelineId = usePlaygroundStore(s => s.prototypeBrand);
39
+ const activeNodeIds = useEngineStore(s => s.activeNodeIds);
40
+ const activeEdgeIds = useEngineStore(s => s.activeEdgeIds);
41
+ const timelines = useTimelineStore(s => s.timelines);
42
+ const blueprintLibrary = useTimelineStore(s => s.blueprintLibrary);
43
+ const updateKeyframePosition = useTimelineStore(s => s.updateKeyframePosition);
44
+ const updateKeyframe = useTimelineStore(s => s.updateKeyframe);
45
+ const addTimelineEdge = useTimelineStore(s => s.addEdge);
46
+ const removeTimelineEdge = useTimelineStore(s => s.removeEdge);
47
+ const addKeyframe = useTimelineStore(s => s.addKeyframe);
48
+ const removeKeyframe = useTimelineStore(s => s.removeKeyframe);
49
+ const undo = useTimelineStore(s => s.undo);
50
+ const redo = useTimelineStore(s => s.redo);
51
+ const setActiveTimeline = useTimelineStore(s => s.setActiveTimeline);
52
+ const pastTimelines = useTimelineStore(s => s.pastTimelines);
53
+ const futureTimelines = useTimelineStore(s => s.futureTimelines);
54
+
55
+ useEffect(() => { setActiveTimeline(activeTimelineId); }, [activeTimelineId, setActiveTimeline]);
56
+
57
+ // Keyboard: Undo/Redo
58
+ useEffect(() => {
59
+ const h = (e: KeyboardEvent) => {
60
+ if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
61
+ else if ((e.metaKey || e.ctrlKey) && (e.key === 'Z' || e.key === 'y')) { e.preventDefault(); redo(); }
62
+ };
63
+ window.addEventListener('keydown', h);
64
+ return () => window.removeEventListener('keydown', h);
65
+ }, [undo, redo]);
66
+
67
+ const canUndo = (pastTimelines[activeTimelineId]?.length || 0) > 0;
68
+ const canRedo = (futureTimelines[activeTimelineId]?.length || 0) > 0;
69
+
70
+ // HSM drill-down
71
+ const currentBlueprintId = useBlueprintNavigation(s => s.currentBlueprintId);
72
+ const pushNavigation = useBlueprintNavigation(s => s.pushNavigation);
73
+ const drillTarget = currentBlueprintId ? blueprintLibrary[currentBlueprintId] : null;
74
+ const activeTimeline = drillTarget || timelines[activeTimelineId];
75
+
76
+ // ─── Sync nodes/edges from timeline ───
77
+ useEffect(() => {
78
+ if (!activeTimeline) return;
79
+ const renderTimelineId = drillTarget ? currentBlueprintId : activeTimelineId;
80
+ const blueprint = convertLegacyTimelineToBlueprint(renderTimelineId, activeTimeline.keyframes || [], activeTimeline.edges || []);
81
+ const decidoNodes = Object.values(blueprint.nodes);
82
+
83
+ // Position resolution
84
+ const posMap: Record<string, { x: number; y: number }> = {};
85
+ decidoNodes.forEach(node => {
86
+ if (node.visual?.x !== undefined && node.visual?.y !== undefined && (node.visual.y !== 0 || node.visual.x !== 0))
87
+ posMap[node.id] = { x: node.visual.x, y: node.visual.y };
88
+ });
89
+
90
+ const gapY = 150;
91
+ let unresolved = decidoNodes.filter(n => !posMap[n.id]);
92
+ let changes = true;
93
+ while (unresolved.length > 0 && changes) {
94
+ changes = false;
95
+ unresolved = unresolved.filter(n => {
96
+ const inEdge = blueprint.edges.find(e => e.target === n.id && posMap[e.source]);
97
+ if (inEdge) {
98
+ const src = posMap[inEdge.source];
99
+ const pos = { x: src.x, y: src.y + gapY };
100
+ let col = true;
101
+ while (col) { col = Object.values(posMap).some(p => Math.abs(p.x - pos.x) < 50 && Math.abs(p.y - pos.y) < 50); if (col) pos.x += 250; }
102
+ posMap[n.id] = pos; changes = true; return false;
103
+ }
104
+ return true;
105
+ });
106
+ }
107
+ unresolved.forEach((n, i) => { posMap[n.id] = { x: TRACK_X_MAP[n.type] || 0, y: i * 100 }; });
108
+
109
+ const newNodes: Node[] = decidoNodes.map(dn => ({
110
+ id: dn.id, type: dn.type, position: posMap[dn.id],
111
+ parentId: dn.visual?.parentId, extent: dn.visual?.parentId ? 'parent' : undefined,
112
+ data: {
113
+ keyframeId: dn.id,
114
+ isInteractive: true,
115
+ isExecuting: activeNodeIds.includes(dn.id),
116
+ onUpdateNodeData: (nodeId: string, partialData: any) => updateKeyframe(renderTimelineId, nodeId, partialData),
117
+ variables: useEngineStore.getState().variables,
118
+ isSpeaking: useEngineStore.getState().isSpeaking,
119
+ registeredMorphComponents: getRegisteredMorphComponents(),
120
+ onDrillDown: (blueprintId: string, label: string) => pushNavigation({ blueprintId, label }),
121
+ ...mapNodePayload(dn)
122
+ },
123
+ }));
124
+
125
+ const newEdges = activeTimeline.edges ? buildStyledEdges(activeTimeline.edges, activeNodeIds, activeEdgeIds) : [];
126
+ setNodes(newNodes);
127
+ setEdges(newEdges);
128
+
129
+ if (lastFittedBlueprintRef.current !== renderTimelineId) {
130
+ lastFittedBlueprintRef.current = renderTimelineId;
131
+ const needsLayout = newNodes.every(n => n.position.x === 0 && n.position.y === 0) || newNodes.filter(n => n.position.x === 0 && n.position.y === 0).length > newNodes.length * 0.5;
132
+ if (needsLayout && newNodes.length > 1) {
133
+ const { nodes: ln } = getLayoutedElements(newNodes, newEdges, { direction: 'TB' });
134
+ setNodes(ln); ln.forEach(n => updateKeyframePosition(activeTimelineId, n.id, n.position));
135
+ }
136
+ setTimeout(() => fitView({ padding: 0.2, duration: 800 }), 50);
137
+ }
138
+ }, [activeTimeline, activeNodeIds, drillTarget, setNodes, setEdges, fitView]);
139
+
140
+ // ─── Handlers ───
141
+ const onConnect = useCallback((p: Connection | Edge) => {
142
+ setEdges(eds => addEdge(p, eds));
143
+ addTimelineEdge(activeTimelineId, { id: `edge-${p.source}-${p.target}-${Date.now()}`, source: p.source, target: p.target, sourceHandle: p.sourceHandle || null, targetHandle: p.targetHandle || null });
144
+ }, [activeTimelineId, addTimelineEdge, setEdges]);
145
+
146
+ const onEdgesDelete = useCallback((del: Edge[]) => { del.forEach(e => removeTimelineEdge(activeTimelineId, e.id)); }, [activeTimelineId, removeTimelineEdge]);
147
+
148
+ const onNodeDragStop = useCallback((_: React.MouseEvent, node: Node, dragged: Node[]) => {
149
+ (dragged.length > 1 ? dragged : [node]).forEach(n => {
150
+ updateKeyframePosition(activeTimelineId, n.id, n.position);
151
+ if (n.id === node.id) {
152
+ const groups = getIntersectingNodes(n).filter(g => g.type === 'group');
153
+ if (n.type !== 'group') updateKeyframe(activeTimelineId, n.id, { parentId: groups[0]?.id || undefined });
154
+ }
155
+ });
156
+ }, [activeTimelineId, updateKeyframePosition, updateKeyframe, getIntersectingNodes]);
157
+
158
+ const onNodeClick = useCallback((_: React.MouseEvent, n: Node) => setSelectedNodeId(n.id), []);
159
+ const onPaneClick = useCallback(() => { setSelectedNodeId(null); setContextMenu(null); }, []);
160
+ const onPaneContextMenu = useCallback((e: React.MouseEvent | MouseEvent) => {
161
+ e.preventDefault();
162
+ const flowPos = screenToFlowPosition({ x: (e as React.MouseEvent).clientX, y: (e as React.MouseEvent).clientY });
163
+ setContextMenu({ x: (e as React.MouseEvent).clientX, y: (e as React.MouseEvent).clientY, flowPos });
164
+ }, [screenToFlowPosition]);
165
+
166
+ const addNodeAt = useCallback((track: string, label: string, extra?: any) => {
167
+ if (!contextMenu) return;
168
+ const targetId = currentBlueprintId || activeTimelineId;
169
+ addKeyframe(targetId, buildKeyframeDefaults(track, label, contextMenu.flowPos, extra));
170
+ setContextMenu(null);
171
+ }, [contextMenu, activeTimelineId, currentBlueprintId, addKeyframe]);
172
+
173
+ // ─── Keyboard: Select All, Copy, Paste, Delete ───
174
+ useEffect(() => {
175
+ const h = createEditorKeyHandler({
176
+ getNodes: () => nodes, getEdges: () => edges, setNodes, setEdges,
177
+ clipboard: clipboardRef, activeTimelineId, currentBlueprintId,
178
+ addKeyframe, addTimelineEdge, removeKeyframe, removeTimelineEdge,
179
+ setSelectedNodeId, NODE_TYPE_TO_TRACK,
180
+ });
181
+ window.addEventListener('keydown', h);
182
+ return () => window.removeEventListener('keydown', h);
183
+ }, [nodes, edges, activeTimelineId, currentBlueprintId, setNodes, setEdges, addKeyframe, addTimelineEdge, removeKeyframe, removeTimelineEdge]);
184
+
185
+ // ─── Breadcrumbs ───
186
+ const breadcrumbs = useBlueprintNavigation(s => s.navigationStack);
187
+ const navigateToLevel = useBlueprintNavigation(s => s.navigateToLevel);
188
+ const isInsideSubflow = breadcrumbs.length > 1;
189
+
190
+ return (
191
+ <div style={style} className={`relative flex flex-col overflow-hidden w-full h-full ${className || ''}`} id={id}>
192
+ {isInsideSubflow && (
193
+ <div className="flex-none flex items-center gap-1 px-3 py-2 bg-surface-overlay border-b border-border-default backdrop-blur-xs z-10">
194
+ <button onClick={() => navigateToLevel(0)} className="flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary transition-colors px-1.5 py-0.5 rounded hover:bg-surface-glass"><Home size={12} /><span>Root</span></button>
195
+ {breadcrumbs.slice(1).map((frame, idx) => (
196
+ <React.Fragment key={idx + 1}>
197
+ <ChevronRight size={12} className="text-text-muted" />
198
+ <button onClick={() => navigateToLevel(idx + 1)} className={`text-xs px-1.5 py-0.5 rounded transition-colors ${idx + 1 === breadcrumbs.length - 1 ? 'text-purple-300 bg-purple-500/10 border border-purple-500/20' : 'text-text-secondary hover:text-text-primary hover:bg-surface-glass'}`}>📦 {frame.label}</button>
199
+ </React.Fragment>
200
+ ))}
201
+ </div>
202
+ )}
203
+
204
+ <div className="flex-1 relative flex overflow-hidden"
205
+ onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
206
+ onDrop={e => {
207
+ e.preventDefault();
208
+ const raw = e.dataTransfer.getData('application/decido-node'); if (!raw) return;
209
+ try {
210
+ const { type, label, meta } = JSON.parse(raw);
211
+ const flowPos = screenToFlowPosition({ x: e.clientX, y: e.clientY });
212
+ addKeyframe(currentBlueprintId || activeTimelineId, buildKeyframeDefaults(type, label, flowPos, meta));
213
+ } catch (err) { console.error('[ReactFlowEditor] Drop failed:', err); }
214
+ }}
215
+ >
216
+ <NodePalette />
217
+ <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onEdgesDelete={onEdgesDelete}
218
+ onConnect={onConnect} onNodeDragStop={onNodeDragStop} onNodeClick={onNodeClick} onPaneClick={onPaneClick} onPaneContextMenu={onPaneContextMenu}
219
+ colorMode="dark" fitView fitViewOptions={{ padding: 0.2 }} minZoom={0.1} selectionOnDrag panOnDrag={[1]} selectionMode={SelectionMode.Partial} multiSelectionKeyCode="Meta" deleteKeyCode={null}>
220
+ <Controls className="fill-white" />
221
+ <EditorToolbar canUndo={canUndo} canRedo={canRedo} undoCount={pastTimelines[activeTimelineId]?.length || 0} redoCount={futureTimelines[activeTimelineId]?.length || 0}
222
+ undo={undo} redo={redo} nodes={nodes} edges={edges} setNodes={setNodes} updateKeyframePosition={updateKeyframePosition} removeKeyframe={removeKeyframe}
223
+ activeTimelineId={activeTimelineId} currentBlueprintId={currentBlueprintId} fitView={fitView} setSelectedNodeId={setSelectedNodeId} />
224
+ <MiniMap nodeStrokeColor={() => '#ffffff'} nodeColor={() => '#333333'} maskColor="rgba(0, 0, 0, 0.7)" />
225
+ <Background color="#555" gap={20} />
226
+ </ReactFlow>
227
+ {contextMenu && (
228
+ <>
229
+ <div className="fixed inset-0 z-998" onClick={() => setContextMenu(null)} />
230
+ <CanvasContextMenu x={contextMenu.x} y={contextMenu.y} onAdd={addNodeAt} onClose={() => setContextMenu(null)} />
231
+ </>
232
+ )}
233
+ </div>
234
+ {selectedNodeId && <NodePropertiesPanel selectedNodeId={selectedNodeId} onClose={() => setSelectedNodeId(null)} />}
235
+ <VariablePanel isOpen={isVariablePanelOpen} onToggle={() => setIsVariablePanelOpen(o => !o)} />
236
+ </div>
237
+ );
238
+ };
239
+
240
+ export const ReactFlowEditor: React.FC<ReactFlowEditorProps> = (props) => (
241
+ <ReactFlowProvider><ReactFlowEditorInner {...props} /></ReactFlowProvider>
242
+ );
@@ -0,0 +1,122 @@
1
+ import React, { useState } from 'react';
2
+ import { useTimelineStore } from '@decido/engine';
3
+ import { usePlaygroundStore } from '../../store/usePlaygroundStore';
4
+ import { Plus, MonitorPlay } from 'lucide-react';
5
+ import { AnimatePresence } from 'motion/react';
6
+ import { TimelineKeyframeCard } from './TimelineKeyframeCard';
7
+
8
+ export const TimelineEditor = () => {
9
+ const activeTimelineId = usePlaygroundStore(state => state.prototypeBrand); // Sync with currently viewed brand
10
+ const timelines = useTimelineStore(state => state.timelines);
11
+ const addKeyframe = useTimelineStore(state => state.addKeyframe);
12
+ const updateKeyframe = useTimelineStore(state => state.updateKeyframe);
13
+ const removeKeyframe = useTimelineStore(state => state.removeKeyframe);
14
+ const updateDuration = useTimelineStore(state => state.updateDuration);
15
+
16
+ // For drag and drop reordering, we need to map over keyframes, but time 't' usually dictates order.
17
+ // For now we'll just allow direct editing of 't' which auto-sorts in the store.
18
+ const activeTimeline = timelines[activeTimelineId];
19
+
20
+ if (!activeTimeline) return null;
21
+
22
+ const handleAdd = (trackName: 'dialogue' | 'ui' | 'logic') => {
23
+ addKeyframe(activeTimelineId, {
24
+ t: activeTimeline.duration,
25
+ track: trackName,
26
+ ...(trackName === 'dialogue' ? { speech: '', actorId: 'system' } : {}),
27
+ ...(trackName === 'ui' ? { state: 'new_state', canvas: false } : {}),
28
+ ...(trackName === 'logic' ? { intent: '' } : {})
29
+ });
30
+ };
31
+
32
+ const tracks = ['dialogue', 'ui', 'logic'] as const;
33
+
34
+ return (
35
+ <div className="flex flex-col h-full bg-surface-secondary text-text-primary border-l border-border-default w-full shadow-2xl z-40 overflow-hidden relative">
36
+ <div className="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E')] opacity-[0.10] mix-blend-overlay pointer-events-none z-0"></div>
37
+
38
+ <div className="p-4 border-b border-border-default relative z-10 flex justify-between items-center bg-surface-glass">
39
+ <div>
40
+ <h2 className="text-lg font-bold text-text-primary flex items-center gap-2">
41
+ <MonitorPlay size={18} className="text-purple-400" />
42
+ Multi-Track Sequence
43
+ </h2>
44
+ <p className="text-xs text-text-secondary mt-1 font-mono uppercase tracking-wider">
45
+ Editando: {activeTimeline.name}
46
+ </p>
47
+ </div>
48
+ <div className="text-right flex flex-col items-end gap-1">
49
+ <span className="text-xs text-text-muted font-mono">Duración Global (s)</span>
50
+ <input
51
+ type="number"
52
+ className="bg-surface-glass border border-border-strong rounded px-2 py-1 w-20 text-text-primary text-right text-sm focus:outline-hidden focus:border-purple-400"
53
+ value={activeTimeline.duration}
54
+ onChange={(e) => updateDuration(activeTimelineId, parseFloat(e.target.value) || 0)}
55
+ />
56
+ </div>
57
+ </div>
58
+
59
+ <div className="flex-1 flex overflow-hidden w-full relative z-10">
60
+ {/* Left Sidebar: Tracks list */}
61
+ <div className="w-[220px] shrink-0 flex flex-col border-r border-border-default bg-surface-primary z-20">
62
+ <div className="h-8 border-b border-border-default shrink-0 bg-surface-secondary" />
63
+
64
+ {tracks.map(track => (
65
+ <div key={track} className={`h-[170px] flex flex-col justify-center p-4 border-b border-border-subtle relative
66
+ ${track === 'dialogue' ? 'bg-purple-500/5' :
67
+ track === 'ui' ? 'bg-cyan-500/5' :
68
+ 'bg-orange-500/5'}`}>
69
+ <div className={`text-[10px] font-bold uppercase tracking-widest
70
+ ${track === 'dialogue' ? 'text-purple-400' :
71
+ track === 'ui' ? 'text-cyan-400' :
72
+ 'text-orange-400'}`}>
73
+ {track === 'dialogue' ? 'Track 1: Diálogo & Audio' :
74
+ track === 'ui' ? 'Track 2: UI & Canvas' :
75
+ 'Track 3: Lógica & MCP'}
76
+ </div>
77
+ <button
78
+ onClick={() => handleAdd(track)}
79
+ className="mt-3 w-fit py-1.5 px-3 rounded text-xs transition-colors flex items-center gap-1 border border-border-default hover:bg-surface-glass text-text-primary/70 hover:text-text-primary"
80
+ >
81
+ <Plus size={12} /> Add Node
82
+ </button>
83
+ </div>
84
+ ))}
85
+ </div>
86
+
87
+ {/* Right Area: Scrolling Timeline */}
88
+ <div className="flex-1 overflow-x-auto overflow-y-hidden custom-scrollbar bg-surface-secondary relative">
89
+ <div className="h-full relative min-w-max pb-10" style={{ width: `${Math.max(activeTimeline.duration, 15) * 100 + 400}px` }}>
90
+
91
+ {/* Visual Time Ruler (Sticky top) */}
92
+ <div className="h-8 border-b border-border-default sticky top-0 bg-surface-secondary z-20 flex items-end">
93
+ {Array.from({ length: Math.ceil(Math.max(activeTimeline.duration, 15)) + 5 }).map((_, i) => (
94
+ <div key={i} className="absolute h-3 border-l border-border-strong" style={{ left: `${i * 100}px` }}>
95
+ <span className="absolute -top-5 -left-1.5 text-[10px] text-text-primary/30 font-mono tracking-tighter">{i}s</span>
96
+ {/* Minor ticks */}
97
+ <div className="absolute left-[50px] -bottom-3 h-1.5 border-l border-border-default" />
98
+ </div>
99
+ ))}
100
+ </div>
101
+
102
+ {/* Track Lanes */}
103
+ <div className="flex flex-col w-full relative">
104
+ {tracks.map(track => (
105
+ <div key={track} className="h-[170px] border-b border-border-subtle relative hover:bg-surface-glass transition-colors group/lane">
106
+ <AnimatePresence>
107
+ {activeTimeline.keyframes
108
+ .filter(kf => kf.track === track)
109
+ .map((kf) => (
110
+ <TimelineKeyframeCard key={kf.id} kf={kf} track={track}
111
+ activeTimelineId={activeTimelineId} updateKeyframe={updateKeyframe} removeKeyframe={removeKeyframe} />
112
+ ))}
113
+ </AnimatePresence>
114
+ </div>
115
+ ))}
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ );
122
+ };
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+ import { Trash2, Clock } from 'lucide-react';
3
+ import { motion } from 'motion/react';
4
+
5
+ interface TimelineKeyframeCardProps {
6
+ kf: any;
7
+ track: 'dialogue' | 'ui' | 'logic';
8
+ activeTimelineId: string;
9
+ updateKeyframe: (tid: string, id: string, patch: any) => void;
10
+ removeKeyframe: (tid: string, id: string) => void;
11
+ }
12
+
13
+ const trackColors = {
14
+ dialogue: { clock: 'text-purple-400/70', input: 'focus:border-purple-500/50', text: 'text-purple-300', label: 'text-purple-400/70' },
15
+ ui: { clock: 'text-cyan-400/70', input: 'focus:border-cyan-500/50', text: 'text-text-primary', label: 'text-cyan-400/70' },
16
+ logic: { clock: 'text-orange-400/70', input: 'focus:border-orange-500/50', text: 'text-text-primary', label: 'text-orange-400/70' },
17
+ };
18
+
19
+ export const TimelineKeyframeCard = React.memo(function TimelineKeyframeCard({
20
+ kf, track, activeTimelineId, updateKeyframe, removeKeyframe
21
+ }: TimelineKeyframeCardProps) {
22
+ const colors = trackColors[track];
23
+
24
+ return (
25
+ <motion.div
26
+ layout
27
+ initial={{ opacity: 0, scale: 0.9 }}
28
+ animate={{ opacity: 1, scale: 1 }}
29
+ exit={{ opacity: 0, scale: 0.9 }}
30
+ transition={{ duration: 0.2 }}
31
+ style={{ left: `${kf.t * 100}px`, width: '260px' }}
32
+ className="absolute top-2 bottom-2 bg-surface-secondary border border-border-default rounded-xl p-3 flex flex-col gap-2 group hover:border-border-subtle0 shadow-[0_8px_30px_rgb(0,0,0,0.5)] hover:shadow-[0_8px_30px_rgb(0,0,0,0.8)] transition-all z-10"
33
+ >
34
+ {/* Header */}
35
+ <div className="flex items-center justify-between gap-2 border-b border-border-subtle pb-1.5">
36
+ <div className="flex items-center gap-1 bg-surface-overlay px-1.5 py-0.5 rounded border border-border-default transition-colors">
37
+ <Clock size={10} className={colors.clock} />
38
+ <input type="number" step="0.1"
39
+ className="bg-transparent w-10 text-xs text-text-primary font-mono focus:outline-hidden text-center"
40
+ value={kf.t}
41
+ onChange={(e) => updateKeyframe(activeTimelineId, kf.id, { t: parseFloat(e.target.value) || 0 })} />
42
+ </div>
43
+ <button onClick={() => removeKeyframe(activeTimelineId, kf.id)}
44
+ className="p-1 text-text-muted hover:text-red-400 hover:bg-red-500/10 rounded transition-colors opacity-0 group-hover:opacity-100"
45
+ title="Eliminar Keyframe">
46
+ <Trash2 size={12} />
47
+ </button>
48
+ </div>
49
+
50
+ {/* Payload */}
51
+ {track === 'dialogue' && (
52
+ <div className="flex flex-col gap-2 flex-1">
53
+ <div className="flex items-center gap-2">
54
+ <span className={`text-[9px] uppercase tracking-wider ${colors.label} w-10`}>Actor</span>
55
+ <input type="text"
56
+ className={`flex-1 bg-surface-glass border border-border-subtle rounded px-2 py-1 text-xs ${colors.text} focus:outline-hidden ${colors.input}`}
57
+ value={kf.actorId || ''} placeholder="system"
58
+ onChange={(e) => updateKeyframe(activeTimelineId, kf.id, { actorId: e.target.value })} />
59
+ </div>
60
+ <textarea
61
+ className={`w-full bg-surface-glass border border-border-subtle rounded px-2 py-1.5 text-xs text-text-primary focus:outline-hidden ${colors.input} resize-none flex-1 min-h-0`}
62
+ placeholder="Texto a sintetizar..." value={kf.speech || ''}
63
+ onChange={(e) => updateKeyframe(activeTimelineId, kf.id, { speech: e.target.value })} />
64
+ </div>
65
+ )}
66
+
67
+ {track === 'ui' && (
68
+ <div className="flex flex-col gap-2 flex-1 justify-center">
69
+ <div className="flex flex-col gap-1">
70
+ <span className={`text-[9px] uppercase tracking-wider ${colors.label}`}>App State</span>
71
+ <input type="text"
72
+ className={`w-full bg-surface-glass border border-border-subtle rounded px-2 py-1.5 text-xs text-text-primary focus:outline-hidden ${colors.input}`}
73
+ value={kf.state || ''} placeholder="idle"
74
+ onChange={(e) => updateKeyframe(activeTimelineId, kf.id, { state: e.target.value })} />
75
+ </div>
76
+ <label className="flex items-center gap-2 p-1.5 mt-1 rounded hover:bg-surface-glass cursor-pointer border border-transparent hover:border-border-subtle transition-colors">
77
+ <input type="checkbox" checked={kf.canvas || false}
78
+ onChange={(e) => updateKeyframe(activeTimelineId, kf.id, { canvas: e.target.checked })}
79
+ className="rounded border-border-strong bg-surface-glass text-cyan-500 w-3.5 h-3.5" />
80
+ <span className="text-[10px] uppercase tracking-wider text-cyan-300">Mostrar Lienzo 3D</span>
81
+ </label>
82
+ </div>
83
+ )}
84
+
85
+ {track === 'logic' && (
86
+ <div className="flex flex-col gap-1 flex-1 justify-center">
87
+ <span className={`text-[9px] uppercase tracking-wider ${colors.label}`}>Intent / Action</span>
88
+ <input type="text"
89
+ className={`w-full bg-surface-glass border border-border-subtle rounded px-2 py-1.5 text-xs text-text-primary focus:outline-hidden ${colors.input} font-mono mt-1`}
90
+ value={kf.intent || ''} placeholder="action_name"
91
+ onChange={(e) => updateKeyframe(activeTimelineId, kf.id, { intent: e.target.value })} />
92
+ <span className="text-[9px] text-text-muted leading-tight mt-2 px-1">
93
+ Dispara un trigger local o llamada MCP.
94
+ </span>
95
+ </div>
96
+ )}
97
+ </motion.div>
98
+ );
99
+ });