@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,458 @@
1
+ /**
2
+ * ShellNexusPreview — AI-powered HTML preview shell.
3
+ *
4
+ * Renders HTML/CSS/JS in a sandboxed iframe with:
5
+ * - Tailwind CDN auto-injection
6
+ * - Device preview (Desktop / Tablet / Mobile)
7
+ * - Toolbar with reload, open-in-new-tab, device switcher
8
+ * - Sprint AH: Element Inspector (hover highlight → click select → postMessage)
9
+ *
10
+ * Based on NexusAI Studio's PreviewPanel architecture, adapted
11
+ * for the Decido Studio shell registry.
12
+ */
13
+ import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react';
14
+ import {
15
+ Monitor, Tablet, Smartphone, RotateCcw, ExternalLink,
16
+ Maximize2, Minimize2, MousePointer2, Rocket, Copy, Check, Loader2, Code2,
17
+ Undo2, Redo2
18
+ } from 'lucide-react';
19
+ import type { ShellProps } from '../../store/useShellRegistry';
20
+ import { useNexusaiDeploy } from '../../hooks/useNexusaiDeploy';
21
+ import { useMorphInstanceStore } from '../../store/useMorphInstanceStore';
22
+ import { CodeEditorPanel } from './CodeEditorPanel';
23
+ import { TemplateLibrary } from './TemplateLibrary';
24
+
25
+ type DeviceView = 'desktop' | 'tablet' | 'mobile';
26
+
27
+ const DEVICE_DIMS: Record<DeviceView, { w: string; h: string; label: string }> = {
28
+ desktop: { w: '100%', h: '100%', label: 'Desktop' },
29
+ tablet: { w: '768px', h: '1024px', label: 'Tablet' },
30
+ mobile: { w: '375px', h: '667px', label: 'Mobile' },
31
+ };
32
+
33
+ const DEVICE_ICONS: Record<DeviceView, typeof Monitor> = {
34
+ desktop: Monitor,
35
+ tablet: Tablet,
36
+ mobile: Smartphone,
37
+ };
38
+
39
+ export const ShellNexusPreview: React.FC<ShellProps> = ({ artifacts, activeArtifactIndex, data }) => {
40
+ const iframeRef = useRef<HTMLIFrameElement>(null);
41
+ const [device, setDevice] = useState<DeviceView>('desktop');
42
+ const [iframeKey, setIframeKey] = useState(Date.now());
43
+ const [isFullscreen, setIsFullscreen] = useState(false);
44
+ const [inspectMode, setInspectMode] = useState(false);
45
+ const [selectedSelector, setSelectedSelector] = useState<string | null>(null);
46
+ const [copied, setCopied] = useState(false);
47
+ const [showCodeEditor, setShowCodeEditor] = useState(false);
48
+ const deployHook = useNexusaiDeploy();
49
+
50
+ // Extract code from data or active artifact
51
+ const code = useMemo(() => {
52
+ const src = data || artifacts?.[activeArtifactIndex ?? 0]?.data || {};
53
+ const html = src.html || src.content || '';
54
+ const css = src.css || src.styles || '';
55
+ const js = src.javascript || src.js || '';
56
+ return { html, css, js };
57
+ }, [data, artifacts, activeArtifactIndex]);
58
+
59
+ // Listen for postMessage from iframe (element inspector)
60
+ useEffect(() => {
61
+ const handler = (e: MessageEvent) => {
62
+ if (e.data?.type === 'DECIDO_ELEMENT_SELECTED') {
63
+ setSelectedSelector(e.data.selector || null);
64
+ // Expose to window.__DECIDO__ for usePlaygroundCommander
65
+ if ((window as any).__DECIDO__) {
66
+ (window as any).__DECIDO__.selectedElementSelector = e.data.selector || null;
67
+ }
68
+ }
69
+ };
70
+ window.addEventListener('message', handler);
71
+ return () => window.removeEventListener('message', handler);
72
+ }, []);
73
+
74
+ // Sprint AN: Local keyboard shortcuts for Web Preview
75
+ const [showShortcuts, setShowShortcuts] = useState(false);
76
+ useEffect(() => {
77
+ const handler = (e: KeyboardEvent) => {
78
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
79
+ const meta = e.metaKey || e.ctrlKey;
80
+ if (!meta) return;
81
+
82
+ // Cmd+I → Toggle inspector
83
+ if (e.code === 'KeyI' && !e.shiftKey) {
84
+ e.preventDefault();
85
+ setInspectMode(v => !v);
86
+ return;
87
+ }
88
+ // Cmd+E → Toggle code editor
89
+ if (e.code === 'KeyE' && !e.shiftKey) {
90
+ e.preventDefault();
91
+ setShowCodeEditor(v => !v);
92
+ return;
93
+ }
94
+ // Cmd+D → Deploy
95
+ if (e.code === 'KeyD' && !e.shiftKey) {
96
+ e.preventDefault();
97
+ deployHook.deploy();
98
+ return;
99
+ }
100
+ // Cmd+/ → Show shortcuts help
101
+ if (e.key === '/') {
102
+ e.preventDefault();
103
+ setShowShortcuts(v => !v);
104
+ return;
105
+ }
106
+ };
107
+ window.addEventListener('keydown', handler);
108
+ return () => window.removeEventListener('keydown', handler);
109
+ }, [deployHook]);
110
+
111
+ // Toggle inspect mode inside iframe
112
+ useEffect(() => {
113
+ if (iframeRef.current?.contentWindow) {
114
+ iframeRef.current.contentWindow.postMessage({ type: 'DECIDO_SET_INSPECT', enabled: inspectMode }, '*');
115
+ }
116
+ }, [inspectMode, iframeKey]);
117
+
118
+ // Build srcDoc with Tailwind CDN + link safety + element inspector
119
+ const srcDoc = useMemo(() => {
120
+ // Sanitize JS to prevent premature </script> closure
121
+ const safeJs = (code.js || '').replace(/<\/script/gi, '<\\/script');
122
+ return `
123
+ <!DOCTYPE html>
124
+ <html lang="es">
125
+ <head>
126
+ <meta charset="UTF-8">
127
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
128
+ <base target="_blank">
129
+ <script src="https://cdn.tailwindcss.com"><\/script>
130
+ <style>
131
+ *, *::before, *::after { box-sizing: border-box; }
132
+ body {
133
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
134
+ margin: 0;
135
+ background: #fff;
136
+ min-height: 100vh;
137
+ color: #1a1a1a;
138
+ }
139
+ html, body { width: 100%; height: 100%; }
140
+ .__decido-hover { outline: 2px dashed #06b6d4 !important; outline-offset: 2px; cursor: crosshair !important; }
141
+ .__decido-selected { outline: 2px solid #06b6d4 !important; outline-offset: 2px; box-shadow: 0 0 0 4px rgba(6,182,212,0.15) !important; }
142
+ ${code.css}
143
+ </style>
144
+ </head>
145
+ <body>
146
+ ${code.html}
147
+ <script>
148
+ try { ${safeJs} } catch(e) { console.error('[Preview]', e); }
149
+ <\/script>
150
+ <script>
151
+ // Link interceptor
152
+ document.addEventListener('click', function(e) {
153
+ var a = e.target.closest('a');
154
+ if (!a) return;
155
+ var href = a.getAttribute('href') || '';
156
+ if (href.startsWith('#')) return;
157
+ if (href.startsWith('javascript:')) { e.preventDefault(); return; }
158
+ e.preventDefault();
159
+ if (href && href !== '#') window.open(href, '_blank', 'noopener,noreferrer');
160
+ }, true);
161
+
162
+ // Element Inspector
163
+ var _inspectEnabled = false;
164
+ var _hoveredEl = null;
165
+ var _selectedEl = null;
166
+
167
+ function _getSelector(el) {
168
+ if (el.id) return '#' + el.id;
169
+ var path = [];
170
+ while (el && el !== document.body && el !== document.documentElement) {
171
+ var tag = el.tagName.toLowerCase();
172
+ if (el.className && typeof el.className === 'string') {
173
+ var cls = el.className.split(/\s+/).filter(function(c) { return !c.startsWith('__decido'); }).slice(0, 2).join('.');
174
+ if (cls) tag += '.' + cls;
175
+ }
176
+ var parent = el.parentElement;
177
+ if (parent) {
178
+ var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === el.tagName; });
179
+ if (siblings.length > 1) tag += ':nth-child(' + (Array.from(parent.children).indexOf(el) + 1) + ')';
180
+ }
181
+ path.unshift(tag);
182
+ el = parent;
183
+ if (path.length >= 3) break;
184
+ }
185
+ return path.join(' > ');
186
+ }
187
+
188
+ document.addEventListener('mouseover', function(e) {
189
+ if (!_inspectEnabled) return;
190
+ if (_hoveredEl) _hoveredEl.classList.remove('__decido-hover');
191
+ _hoveredEl = e.target;
192
+ if (_hoveredEl !== _selectedEl) _hoveredEl.classList.add('__decido-hover');
193
+ });
194
+ document.addEventListener('mouseout', function(e) {
195
+ if (_hoveredEl) _hoveredEl.classList.remove('__decido-hover');
196
+ });
197
+ document.addEventListener('click', function(e) {
198
+ if (!_inspectEnabled) return;
199
+ e.preventDefault();
200
+ e.stopPropagation();
201
+ if (_selectedEl) _selectedEl.classList.remove('__decido-selected');
202
+ _selectedEl = e.target;
203
+ _selectedEl.classList.add('__decido-selected');
204
+ _hoveredEl = null;
205
+ var sel = _getSelector(_selectedEl);
206
+ window.parent.postMessage({ type: 'DECIDO_ELEMENT_SELECTED', selector: sel, tagName: _selectedEl.tagName }, '*');
207
+ }, true);
208
+
209
+ window.addEventListener('message', function(e) {
210
+ if (e.data && e.data.type === 'DECIDO_SET_INSPECT') {
211
+ _inspectEnabled = e.data.enabled;
212
+ if (!_inspectEnabled) {
213
+ if (_hoveredEl) _hoveredEl.classList.remove('__decido-hover');
214
+ if (_selectedEl) _selectedEl.classList.remove('__decido-selected');
215
+ _hoveredEl = null; _selectedEl = null;
216
+ }
217
+ document.body.style.cursor = _inspectEnabled ? 'crosshair' : '';
218
+ }
219
+ });
220
+ <\/script>
221
+ </body>
222
+ </html>`;
223
+ }, [code]);
224
+
225
+ const handleReload = useCallback(() => setIframeKey(Date.now()), []);
226
+
227
+ const handleOpenInTab = useCallback(() => {
228
+ const win = window.open();
229
+ if (win) { win.document.write(srcDoc); win.document.close(); }
230
+ }, [srcDoc]);
231
+
232
+ const handleToggleInspect = useCallback(() => {
233
+ const next = !inspectMode;
234
+ setInspectMode(next);
235
+ if (!next) setSelectedSelector(null);
236
+ }, [inspectMode]);
237
+
238
+ const dims = DEVICE_DIMS[device];
239
+ const hasContent = !!(code.html?.trim());
240
+
241
+ return (
242
+ <div className={`w-full h-full flex flex-col bg-surface-primary overflow-hidden ${isFullscreen ? 'fixed inset-0 z-9999' : ''}`}>
243
+ {/* ─── Toolbar ─── */}
244
+ <div className="flex items-center justify-between px-2 py-1.5 bg-surface-secondary border-b border-border-subtle text-[10px] text-text-muted select-none shrink-0">
245
+ <div className="flex items-center gap-1.5">
246
+ {/* Traffic lights */}
247
+ <div className="flex gap-1 px-1">
248
+ <span className="w-2.5 h-2.5 rounded-full bg-red-500/80" />
249
+ <span className="w-2.5 h-2.5 rounded-full bg-yellow-500/80" />
250
+ <span className="w-2.5 h-2.5 rounded-full bg-green-500/80" />
251
+ </div>
252
+ <button onClick={handleReload} className="p-1 rounded hover:bg-surface-glass transition-colors" title="Recargar">
253
+ <RotateCcw size={12} />
254
+ </button>
255
+ <button
256
+ onClick={() => useMorphInstanceStore.getState().undo()}
257
+ disabled={!useMorphInstanceStore.getState().canUndo()}
258
+ className={`p-1 rounded transition-colors ${useMorphInstanceStore.getState().canUndo() ? 'hover:bg-surface-glass text-text-secondary' : 'text-text-muted cursor-not-allowed'}`}
259
+ title="Deshacer"
260
+ >
261
+ <Undo2 size={12} />
262
+ </button>
263
+ <button
264
+ onClick={() => useMorphInstanceStore.getState().redo()}
265
+ disabled={!useMorphInstanceStore.getState().canRedo()}
266
+ className={`p-1 rounded transition-colors ${useMorphInstanceStore.getState().canRedo() ? 'hover:bg-surface-glass text-text-secondary' : 'text-text-muted cursor-not-allowed'}`}
267
+ title="Rehacer"
268
+ >
269
+ <Redo2 size={12} />
270
+ </button>
271
+ <span className="px-2 py-0.5 bg-surface-overlay rounded text-[9px] font-mono">{dims.w === '100%' ? 'Adaptable' : dims.w} × {dims.h === '100%' ? '100%' : dims.h}</span>
272
+ </div>
273
+
274
+ {/* Device Switcher */}
275
+ <div className="flex items-center gap-0.5">
276
+ {(['desktop', 'tablet', 'mobile'] as DeviceView[]).map(d => {
277
+ const Icon = DEVICE_ICONS[d];
278
+ return (
279
+ <button
280
+ key={d}
281
+ onClick={() => setDevice(d)}
282
+ className={`p-1.5 rounded transition-colors ${device === d ? 'bg-surface-glass text-text-primary' : 'hover:bg-surface-glass text-text-muted hover:text-text-primary'}`}
283
+ title={DEVICE_DIMS[d].label}
284
+ >
285
+ <Icon size={13} />
286
+ </button>
287
+ );
288
+ })}
289
+ </div>
290
+
291
+ {/* Actions */}
292
+ <div className="flex items-center gap-1">
293
+ <button
294
+ onClick={handleToggleInspect}
295
+ className={`p-1 rounded transition-colors ${inspectMode ? 'bg-cyan-500/20 text-cyan-400' : 'hover:bg-surface-glass text-text-muted'}`}
296
+ title={inspectMode ? 'Desactivar inspector' : 'Seleccionar elemento'}
297
+ >
298
+ <MousePointer2 size={12} />
299
+ </button>
300
+ <button
301
+ onClick={() => setShowCodeEditor(!showCodeEditor)}
302
+ className={`p-1 rounded transition-colors ${showCodeEditor ? 'bg-violet-500/20 text-violet-400' : 'hover:bg-surface-glass text-text-muted'}`}
303
+ title={showCodeEditor ? 'Ocultar código' : 'Ver / editar código'}
304
+ >
305
+ <Code2 size={12} />
306
+ </button>
307
+ <button
308
+ onClick={() => { deployHook.deploy(); }}
309
+ disabled={!hasContent || deployHook.status === 'deploying'}
310
+ className={`p-1 rounded transition-colors ${deployHook.status === 'deploying' ? 'text-amber-400 animate-pulse' : hasContent ? 'hover:bg-emerald-500/10 text-text-muted hover:text-emerald-400' : 'text-text-muted cursor-not-allowed'}`}
311
+ title="Desplegar preview"
312
+ >
313
+ {deployHook.status === 'deploying' ? <Loader2 size={12} className="animate-spin" /> : <Rocket size={12} />}
314
+ </button>
315
+ <button onClick={() => setIsFullscreen(!isFullscreen)} className="p-1 rounded hover:bg-surface-glass transition-colors" title={isFullscreen ? 'Salir fullscreen' : 'Fullscreen'}>
316
+ {isFullscreen ? <Minimize2 size={12} /> : <Maximize2 size={12} />}
317
+ </button>
318
+ <button onClick={handleOpenInTab} className="p-1 rounded hover:bg-surface-glass transition-colors" title="Abrir en nueva pestaña">
319
+ <ExternalLink size={12} />
320
+ </button>
321
+ </div>
322
+ </div>
323
+
324
+ {/* ─── Deploy status bar ─── */}
325
+ {deployHook.status !== 'idle' && (
326
+ <div className="flex items-center gap-2 px-3 py-1.5 border-b border-border-subtle text-[10px] shrink-0" style={{ background: deployHook.status === 'success' ? 'rgba(16,185,129,0.08)' : deployHook.status === 'error' ? 'rgba(239,68,68,0.08)' : 'rgba(245,158,11,0.08)' }}>
327
+ {deployHook.status === 'deploying' && (
328
+ <>
329
+ <Loader2 size={10} className="text-amber-400 animate-spin" />
330
+ <span className="text-amber-300 font-mono">Desplegando... {deployHook.progress}%</span>
331
+ <div className="flex-1 h-1 bg-surface-glass rounded-full overflow-hidden">
332
+ <div className="h-full bg-amber-400 transition-all duration-300 rounded-full" style={{ width: `${deployHook.progress}%` }} />
333
+ </div>
334
+ </>
335
+ )}
336
+ {deployHook.status === 'success' && deployHook.url && (
337
+ <>
338
+ <Check size={10} className="text-emerald-400" />
339
+ <a href={deployHook.url} target="_blank" rel="noopener noreferrer" className="text-emerald-300 font-mono flex-1 truncate hover:underline">{deployHook.url.length > 60 ? deployHook.url.slice(0, 60) + '...' : deployHook.url}</a>
340
+ <button onClick={() => { deployHook.copyUrl(); setCopied(true); setTimeout(() => setCopied(false), 1500); }} className="text-text-secondary hover:text-text-primary transition-colors" title="Copiar URL">
341
+ {copied ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} />}
342
+ </button>
343
+ <button onClick={deployHook.reset} className="text-text-muted hover:text-text-primary">✕</button>
344
+ </>
345
+ )}
346
+ {deployHook.status === 'error' && (
347
+ <>
348
+ <span className="text-red-400">⚠</span>
349
+ <span className="text-red-300 font-mono flex-1">{deployHook.error}</span>
350
+ <button onClick={deployHook.reset} className="text-text-muted hover:text-text-primary">✕</button>
351
+ </>
352
+ )}
353
+ </div>
354
+ )}
355
+
356
+ {/* ─── Selected element badge ─── */}
357
+ {selectedSelector && (
358
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-cyan-500/10 border-b border-cyan-500/20 text-[10px] shrink-0">
359
+ <MousePointer2 size={10} className="text-cyan-400" />
360
+ <code className="text-cyan-300 font-mono flex-1 truncate">{selectedSelector}</code>
361
+ <span className="text-text-muted">Escribe en el chat para modificar este elemento</span>
362
+ <button onClick={() => { setSelectedSelector(null); setInspectMode(false); }} className="text-text-muted hover:text-text-primary">✕</button>
363
+ </div>
364
+ )}
365
+
366
+ {/* ─── Preview Area ─── */}
367
+ <div className="flex-1 flex overflow-hidden">
368
+ {/* Preview */}
369
+ <div className={`flex items-center justify-center overflow-auto p-2 ${showCodeEditor ? 'w-1/2' : 'flex-1'}`} style={{ background: 'repeating-conic-gradient(#1a1a1b 0% 25%, #141415 0% 50%) 0 0 / 20px 20px' }}>
370
+ {hasContent ? (
371
+ <div
372
+ className="bg-white shadow-2xl transition-all duration-300 overflow-hidden w-full h-full"
373
+ style={{
374
+ maxWidth: showCodeEditor ? '100%' : dims.w,
375
+ maxHeight: showCodeEditor ? '100%' : dims.h,
376
+ border: device !== 'desktop' ? '6px solid #333' : '1px solid #2a2a2a',
377
+ borderRadius: device !== 'desktop' ? '1.5rem' : '4px',
378
+ }}
379
+ >
380
+ <iframe
381
+ ref={iframeRef}
382
+ key={iframeKey}
383
+ srcDoc={srcDoc}
384
+ title="NexusAI Preview"
385
+ sandbox="allow-scripts allow-same-origin allow-forms allow-modals"
386
+ className="w-full h-full border-0"
387
+ />
388
+ </div>
389
+ ) : (
390
+ <TemplateLibrary onSelect={(tpl) => {
391
+ const inst = useMorphInstanceStore.getState().getActiveInstance();
392
+ if (inst) {
393
+ useMorphInstanceStore.getState().upsertInstance({
394
+ ...inst,
395
+ data: { ...inst.data, html: tpl.html, css: tpl.css, javascript: tpl.js },
396
+ });
397
+ } else {
398
+ const id = 'tpl-' + Date.now();
399
+ useMorphInstanceStore.getState().upsertInstance({
400
+ id, shellType: 'nexusai-preview', sourceChatId: '_default', label: 'Template',
401
+ data: { html: tpl.html, css: tpl.css, javascript: tpl.js },
402
+ });
403
+ useMorphInstanceStore.getState().setActiveInstance(id);
404
+ }
405
+ }} />
406
+ )}
407
+ </div>
408
+
409
+ {/* Code Editor */}
410
+ {showCodeEditor && (
411
+ <div className="w-1/2 shrink-0">
412
+ <CodeEditorPanel
413
+ code={code}
414
+ onCodeChange={(updated) => {
415
+ // Sync edits to active MorphInstance
416
+ const inst = useMorphInstanceStore.getState().getActiveInstance();
417
+ if (inst) {
418
+ useMorphInstanceStore.getState().upsertInstance({
419
+ ...inst,
420
+ data: { ...inst.data, html: updated.html, css: updated.css, javascript: updated.js },
421
+ });
422
+ }
423
+ }}
424
+ />
425
+ </div>
426
+ )}
427
+ </div>
428
+
429
+ {/* Sprint AN: Shortcuts Help Modal */}
430
+ {showShortcuts && (
431
+ <div className="absolute inset-0 z-50 flex items-center justify-center bg-surface-overlay backdrop-blur-xs" onClick={() => setShowShortcuts(false)}>
432
+ <div className="bg-surface-tertiary border border-border-default rounded-2xl p-6 max-w-xs w-full shadow-2xl" onClick={e => e.stopPropagation()}>
433
+ <h3 className="text-sm font-bold text-text-primary mb-4 flex items-center gap-2">⌨️ Atajos de Teclado</h3>
434
+ <div className="space-y-2 text-[11px]">
435
+ {[
436
+ ['⌘ I', 'Inspector de elementos'],
437
+ ['⌘ E', 'Editor de código'],
438
+ ['⌘ D', 'Desplegar preview'],
439
+ ['⌘ Z', 'Deshacer (topología)'],
440
+ ['⌘ ⇧ Z', 'Rehacer (topología)'],
441
+ ['⌘ /', 'Esta ayuda'],
442
+ ['⌘ B', 'Toggle sidebar'],
443
+ ['⌘ ⇧ C', 'Toggle modo Creator'],
444
+ ['⌘ ⇧ V', 'Toggle canvas'],
445
+ ].map(([keys, desc]) => (
446
+ <div key={keys} className="flex items-center justify-between">
447
+ <span className="text-text-secondary">{desc}</span>
448
+ <kbd className="px-2 py-0.5 bg-surface-glass border border-border-default rounded text-text-primary font-mono text-[10px]">{keys}</kbd>
449
+ </div>
450
+ ))}
451
+ </div>
452
+ <button onClick={() => setShowShortcuts(false)} className="w-full mt-4 py-2 bg-surface-glass hover:bg-surface-glass rounded-xl text-xs text-text-secondary transition">Cerrar</button>
453
+ </div>
454
+ </div>
455
+ )}
456
+ </div>
457
+ );
458
+ };
@@ -0,0 +1,115 @@
1
+ import React from 'react';
2
+ import type { SlotComponentId } from '../../store/useLayoutStore';
3
+
4
+ /**
5
+ * SlotRenderer — maps a SlotComponentId to its lazy-loaded React component.
6
+ *
7
+ * This is the registry that connects slot configuration in useLayoutStore
8
+ * to actual React components. Components are lazy-loaded to avoid
9
+ * pulling in heavy dependencies (Three.js, ReactFlow, etc.) upfront.
10
+ */
11
+
12
+ // ── Lazy Component Registry ──
13
+ const WorkspaceChatLazy = React.lazy(() =>
14
+ import('./CenterComposite').then(m => ({ default: m.CenterComposite }))
15
+ );
16
+ const PlaygroundChat = React.lazy(() =>
17
+ import('../playground/PlaygroundChat').then(m => ({ default: m.PlaygroundChat }))
18
+ );
19
+ const DebugPanel = React.lazy(() =>
20
+ import('../playground/DebugPanel').then(m => ({ default: m.DebugPanel }))
21
+ );
22
+ const PlaygroundTerminal = React.lazy(() =>
23
+ import('../playground/PlaygroundTerminal').then(m => ({ default: m.PlaygroundTerminal }))
24
+ );
25
+ const MorphShellLazy = React.lazy(() =>
26
+ import('./MorphShell').then(m => ({ default: m.MorphShell }))
27
+ );
28
+ const ReactFlowEditor = React.lazy(() =>
29
+ import('../studio/ReactFlowEditor').then(m => ({ default: m.ReactFlowEditor }))
30
+ );
31
+ const TimelineEditor = React.lazy(() =>
32
+ import('../studio/TimelineEditor').then(m => ({ default: m.TimelineEditor }))
33
+ );
34
+ const PlaygroundAppSidebar = React.lazy(() =>
35
+ import('../playground/PlaygroundAppSidebar').then(m => ({ default: m.PlaygroundAppSidebar }))
36
+ );
37
+ const WidgetSlotPanelLazy = React.lazy(() =>
38
+ import('../widgets/WidgetSlotPanel').then(m => ({ default: m.WidgetSlotPanel }))
39
+ );
40
+
41
+ // ── Fallback ──
42
+ const SlotFallback = () => (
43
+ <div className="w-full h-full flex items-center justify-center">
44
+ <div className="w-6 h-6 border-2 border-cyan-500 border-t-transparent rounded-full animate-spin" />
45
+ </div>
46
+ );
47
+
48
+ // ── Slot Component Map ──
49
+ // Each entry returns a React element. Props are passed down via context
50
+ // or store subscriptions, not directly (keeps SlotRenderer simple).
51
+ // 'widget-panel' renders plugin widgets via WidgetRegistry (replaces old 'plugin-viewer' hack)
52
+ const SLOT_COMPONENTS: Record<string, React.LazyExoticComponent<any>> = {
53
+ 'workspace-chat': WorkspaceChatLazy,
54
+ 'chat': PlaygroundChat,
55
+ 'debug': DebugPanel,
56
+ 'terminal': PlaygroundTerminal,
57
+ 'canvas': MorphShellLazy,
58
+ 'graph-editor': ReactFlowEditor,
59
+ 'timeline-editor': TimelineEditor,
60
+ 'app-sidebar': PlaygroundAppSidebar,
61
+ 'widget-panel': WidgetSlotPanelLazy,
62
+ };
63
+
64
+ interface SlotRendererProps {
65
+ /** Which component to render */
66
+ componentId: SlotComponentId;
67
+ /** Additional props to pass through */
68
+ slotProps?: Record<string, any>;
69
+ /** CSS class for the container */
70
+ className?: string;
71
+ }
72
+
73
+ /**
74
+ * Renders the component assigned to a layout slot.
75
+ * Returns null if componentId is null (empty slot).
76
+ */
77
+ export const SlotRenderer = React.memo(function SlotRenderer({
78
+ componentId,
79
+ slotProps = {},
80
+ className = '',
81
+ }: SlotRendererProps) {
82
+ if (!componentId) return null;
83
+
84
+ const Component = SLOT_COMPONENTS[componentId];
85
+ if (!Component) {
86
+ return (
87
+ <div className={`w-full h-full flex items-center justify-center text-text-muted text-xs font-mono ${className}`}>
88
+ Unknown slot: {componentId}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <React.Suspense fallback={<SlotFallback />}>
95
+ <div className={`w-full h-full overflow-hidden ${className}`}>
96
+ <Component {...slotProps} />
97
+ </div>
98
+ </React.Suspense>
99
+ );
100
+ });
101
+
102
+ /**
103
+ * Register a custom component for use in slots.
104
+ * Plugins can call this to make their components available in the layout.
105
+ */
106
+ export function registerSlotComponent(id: string, component: React.LazyExoticComponent<any> | React.ComponentType<any>) {
107
+ SLOT_COMPONENTS[id] = component as any;
108
+ }
109
+
110
+ /**
111
+ * Get available slot component IDs (for UI dropdowns / configurator).
112
+ */
113
+ export function getAvailableSlotComponents(): string[] {
114
+ return Object.keys(SLOT_COMPONENTS);
115
+ }
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'motion/react';
3
+ import { X, Plus } from 'lucide-react';
4
+ import { useLayoutStore, type LayoutTab } from '../../store/useLayoutStore';
5
+ import { useMorphInstanceStore } from '../../store/useMorphInstanceStore';
6
+ import { usePlaygroundStore } from '../../store/usePlaygroundStore';
7
+
8
+ /**
9
+ * TabBar — Horizontal tab strip for the T2 zone.
10
+ *
11
+ * Displays tabs from useLayoutStore.tabs
12
+ * Each tab can be activated, closed, and dragged.
13
+ * New tabs appear when MorphShell pushes a stage.
14
+ *
15
+ * Clicking a tab syncs: LayoutStore ↔ MorphInstanceStore ↔ PlaygroundStore(activeChatId)
16
+ */
17
+
18
+ interface TabBarProps {
19
+ className?: string;
20
+ }
21
+
22
+ export function TabBar({ className = '' }: TabBarProps) {
23
+ const tabs = useLayoutStore((s) => s.tabs);
24
+ const activeTabId = useLayoutStore((s) => s.activeTabId);
25
+ const setActiveTab = useLayoutStore((s) => s.setActiveTab);
26
+ const removeTab = useLayoutStore((s) => s.removeTab);
27
+
28
+ const handleTabClick = React.useCallback((tabId: string) => {
29
+ setActiveTab(tabId);
30
+
31
+ // Sync with MorphInstance: tab IDs follow 'morph-{instanceId}' convention
32
+ if (tabId.startsWith('morph-')) {
33
+ const instanceId = tabId.replace('morph-', '');
34
+ useMorphInstanceStore.getState().setActiveInstance(instanceId);
35
+
36
+ // Sync with Chat: find chat instance with matching sourceChatId
37
+ const instance = useMorphInstanceStore.getState().instances.get(instanceId);
38
+ if (instance?.sourceChatId) {
39
+ usePlaygroundStore.getState().setActiveChatId(instance.sourceChatId);
40
+ }
41
+ }
42
+ }, [setActiveTab]);
43
+
44
+ if (tabs.length === 0) return null;
45
+
46
+ return (
47
+ <div className={`flex items-center gap-0.5 px-2 py-1 bg-surface-primary border-b border-border-subtle overflow-x-auto scrollbar-hide ${className}`}>
48
+ <AnimatePresence mode="popLayout">
49
+ {tabs.map((tab) => (
50
+ <motion.button
51
+ key={tab.id}
52
+ layout
53
+ initial={{ opacity: 0, scale: 0.9, width: 0 }}
54
+ animate={{ opacity: 1, scale: 1, width: 'auto' }}
55
+ exit={{ opacity: 0, scale: 0.9, width: 0 }}
56
+ onClick={() => handleTabClick(tab.id)}
57
+ className={`
58
+ flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-medium
59
+ transition-colors whitespace-nowrap group relative shrink-0
60
+ ${activeTabId === tab.id
61
+ ? 'bg-surface-glass text-text-primary shadow-xs border border-border-default'
62
+ : 'text-text-muted hover:text-text-primary hover:bg-surface-glass border border-transparent'
63
+ }
64
+ `}
65
+ >
66
+ <span className="truncate max-w-[120px]">{tab.label}</span>
67
+
68
+ {(tab.closeable !== false) && (
69
+ <span
70
+ onClick={(e) => {
71
+ e.stopPropagation();
72
+ removeTab(tab.id);
73
+ }}
74
+ className="w-4 h-4 rounded-md flex items-center justify-center
75
+ opacity-0 group-hover:opacity-100 hover:bg-surface-glass hover:text-red-400
76
+ transition-all ml-0.5"
77
+ >
78
+ <X size={10} />
79
+ </span>
80
+ )}
81
+
82
+ {/* Active indicator */}
83
+ {activeTabId === tab.id && (
84
+ <motion.div
85
+ layoutId="tab-indicator"
86
+ className="absolute bottom-0 left-1 right-1 h-[2px] bg-cyan-500/60 rounded-full"
87
+ />
88
+ )}
89
+ </motion.button>
90
+ ))}
91
+ </AnimatePresence>
92
+ </div>
93
+ );
94
+ }