@decido/shell 1.0.0 → 4.0.1
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.
- package/README.md +31 -0
- package/package.json +17 -14
- package/.turbo/turbo-build.log +0 -13
- package/src/AgentPlayer.tsx +0 -105
- package/src/DecidoPlayer.tsx +0 -117
- package/src/bridge/BridgeAgent.ts +0 -443
- package/src/components/DecidoIcon.tsx +0 -56
- package/src/components/JsonTreeEditor.tsx +0 -117
- package/src/components/PanelSplitter.tsx +0 -71
- package/src/components/PluginErrorBoundary.tsx +0 -69
- package/src/components/SafeLiquidUI.tsx +0 -114
- package/src/components/TransientLayer.tsx +0 -92
- package/src/components/agent/AgentChat.tsx +0 -134
- package/src/components/chat-extensions/IntentCatalogPanel.tsx +0 -81
- package/src/components/chat-extensions/chatSlashCommands.ts +0 -101
- package/src/components/controls/CreatorInputBar.tsx +0 -144
- package/src/components/controls/OSToolbar.tsx +0 -90
- package/src/components/controls/TimelineTape.tsx +0 -43
- package/src/components/debug/ActionTimelineTab.tsx +0 -111
- package/src/components/debug/CSSInspectorTab.tsx +0 -436
- package/src/components/debug/ExportTab.tsx +0 -192
- package/src/components/debug/FlowHealthTab.tsx +0 -86
- package/src/components/debug/LogsTab.tsx +0 -110
- package/src/components/debug/MorphStackTab.tsx +0 -241
- package/src/components/debug/NetworkTab.tsx +0 -173
- package/src/components/debug/PerformanceTab.tsx +0 -171
- package/src/components/debug/ProfilesTab.tsx +0 -238
- package/src/components/debug/ReplayTab.tsx +0 -70
- package/src/components/debug/StoresTab.tsx +0 -255
- package/src/components/debug/TopologyTab.tsx +0 -59
- package/src/components/debug/debugConfig.tsx +0 -66
- package/src/components/playground/DebugPanel.tsx +0 -112
- package/src/components/playground/HeaderCenterControls.tsx +0 -92
- package/src/components/playground/KeyframeListItem.tsx +0 -70
- package/src/components/playground/PlaygroundAppSidebar.tsx +0 -171
- package/src/components/playground/PlaygroundBottomControls.tsx +0 -132
- package/src/components/playground/PlaygroundCanvas.tsx +0 -87
- package/src/components/playground/PlaygroundChat.tsx +0 -236
- package/src/components/playground/PlaygroundErrorBoundary.tsx +0 -63
- package/src/components/playground/PlaygroundFloatingInput.tsx +0 -352
- package/src/components/playground/PlaygroundHeader.tsx +0 -222
- package/src/components/playground/PlaygroundSidebar.tsx +0 -136
- package/src/components/playground/PlaygroundTerminal.tsx +0 -44
- package/src/components/playground/SuggestionCards.tsx +0 -29
- package/src/components/playground/demos/ClinicaAINode.tsx +0 -221
- package/src/components/playground/demos/FinanceAINode.tsx +0 -226
- package/src/components/playground/demos/KiaAcademyNode.tsx +0 -250
- package/src/components/playground/demos/KiaBotNode.tsx +0 -207
- package/src/components/playground/demos/KiaCampaignNode.tsx +0 -191
- package/src/components/playground/demos/KiaComplianceNode.tsx +0 -140
- package/src/components/playground/demos/KiaCustomerJourneyNode.tsx +0 -220
- package/src/components/playground/demos/KiaCyberNode.tsx +0 -203
- package/src/components/playground/demos/KiaDashboardNode.tsx +0 -399
- package/src/components/playground/demos/KiaEmbudoOverviewNode.tsx +0 -168
- package/src/components/playground/demos/KiaExecutiveNode.tsx +0 -169
- package/src/components/playground/demos/KiaGamificationNode.tsx +0 -229
- package/src/components/playground/demos/KiaIntelligenceHubNode.tsx +0 -165
- package/src/components/playground/demos/KiaInventoryNode.tsx +0 -183
- package/src/components/playground/demos/KiaLeadScoringNode.tsx +0 -226
- package/src/components/playground/demos/KiaLiveSimulationNode.tsx +0 -177
- package/src/components/playground/demos/KiaMultiDealerNode.tsx +0 -223
- package/src/components/playground/demos/KiaNPSVoiceNode.tsx +0 -214
- package/src/components/playground/demos/KiaOmnichannelNode.tsx +0 -162
- package/src/components/playground/demos/KiaPBIBudgetNode.tsx +0 -152
- package/src/components/playground/demos/KiaPBIConversionNode.tsx +0 -206
- package/src/components/playground/demos/KiaPBIFunnelNode.tsx +0 -184
- package/src/components/playground/demos/KiaPBIOwnershipNode.tsx +0 -113
- package/src/components/playground/demos/KiaPBIPartnerNode.tsx +0 -143
- package/src/components/playground/demos/KiaPBIPreciosNode.tsx +0 -120
- package/src/components/playground/demos/KiaPBIRuntNode.tsx +0 -205
- package/src/components/playground/demos/KiaPartnerScoreNode.tsx +0 -206
- package/src/components/playground/demos/KiaPredictiveNode.tsx +0 -226
- package/src/components/playground/demos/KiaShowroomNode.tsx +0 -194
- package/src/components/playground/demos/KiaStoreNode.tsx +0 -215
- package/src/components/playground/demos/KiaSustainabilityNode.tsx +0 -173
- package/src/components/playground/demos/KiaUsedVehiclesNode.tsx +0 -163
- package/src/components/playground/demos/KiaWorkshopNode.tsx +0 -221
- package/src/components/playground/demos/SmartCityNode.tsx +0 -205
- package/src/components/playground/demos/kia_campaign_manifest.json +0 -112
- package/src/components/playground/input-parts/AIModelSelector.tsx +0 -156
- package/src/components/playground/input-parts/InputActions.tsx +0 -80
- package/src/components/playground/input-parts/InputToolbar.tsx +0 -245
- package/src/components/playground/input-parts/ResourceLibraryPanel.tsx +0 -287
- package/src/components/playground/sidebarDsdIO.ts +0 -82
- package/src/components/settings/SettingsPanel.tsx +0 -267
- package/src/components/shell/AppHeader.tsx +0 -9
- package/src/components/shell/AppShell.tsx +0 -139
- package/src/components/shell/ArtifactBar.tsx +0 -97
- package/src/components/shell/BootScreen.tsx +0 -19
- package/src/components/shell/CenterComposite.tsx +0 -87
- package/src/components/shell/CodeEditorPanel.tsx +0 -88
- package/src/components/shell/GlobalOverlays.tsx +0 -228
- package/src/components/shell/LayoutConfigurator.tsx +0 -209
- package/src/components/shell/LayoutGrid.tsx +0 -178
- package/src/components/shell/MorphShell.tsx +0 -368
- package/src/components/shell/PluginViewer.tsx +0 -147
- package/src/components/shell/ShellNexusPreview.tsx +0 -458
- package/src/components/shell/SlotRenderer.tsx +0 -115
- package/src/components/shell/TabBar.tsx +0 -94
- package/src/components/shell/TemplateLibrary.tsx +0 -195
- package/src/components/shell/layoutConstants.ts +0 -35
- package/src/components/shell/morphStageMeta.ts +0 -15
- package/src/components/shell/shells/BuiltInShells.tsx +0 -443
- package/src/components/shell/shells/DatawayChatShell.tsx +0 -42
- package/src/components/shell/shells/TokenPreview.tsx +0 -339
- package/src/components/shell/shells/bootShells.ts +0 -31
- package/src/components/shells/CreatorShell.tsx +0 -37
- package/src/components/shells/DecidoShell.tsx +0 -447
- package/src/components/shells/ExperimentalChatShell.tsx +0 -245
- package/src/components/shells/UserCanvas.tsx +0 -44
- package/src/components/studio/BlueprintManagerPanel.tsx +0 -137
- package/src/components/studio/DependencyTreePanel.tsx +0 -192
- package/src/components/studio/NodePalette.tsx +0 -92
- package/src/components/studio/NodePropertiesPanel.tsx +0 -81
- package/src/components/studio/ReactFlowEditor.tsx +0 -242
- package/src/components/studio/TimelineEditor.tsx +0 -122
- package/src/components/studio/TimelineKeyframeCard.tsx +0 -99
- package/src/components/studio/VariablePanel.tsx +0 -181
- package/src/components/studio/blueprint/BlueprintCard.tsx +0 -82
- package/src/components/studio/editor/CanvasContextMenu.tsx +0 -107
- package/src/components/studio/editor/EditorToolbar.tsx +0 -80
- package/src/components/studio/editor/StageContentRenderer.tsx +0 -134
- package/src/components/studio/editor/TrackPropertyEditors.tsx +0 -133
- package/src/components/studio/editor/TreeNodeItem.tsx +0 -91
- package/src/components/studio/editor/edgeStyles.ts +0 -43
- package/src/components/studio/editor/editorKeyHandler.ts +0 -95
- package/src/components/studio/editor/nodeTypeRegistry.ts +0 -137
- package/src/components/studio/editor/paletteCatalog.tsx +0 -84
- package/src/components/studio/nodes/shell/InteractionNodes.tsx +0 -82
- package/src/components/studio/nodes/shell/LayoutControlNodes.tsx +0 -69
- package/src/components/studio/nodes/shell/RegisterActionNode.tsx +0 -20
- package/src/components/studio/nodes/shell/RegisterButtonNode.tsx +0 -22
- package/src/components/studio/nodes/shell/RegisterPanelNode.tsx +0 -19
- package/src/components/studio/nodes/shell/RegisterSidebarNode.tsx +0 -19
- package/src/components/studio/nodes/shell/RegisterStatusBarNode.tsx +0 -22
- package/src/components/studio/nodes/shell/RegisterTabNode.tsx +0 -21
- package/src/components/studio/nodes/shell/RegisterTopBarNode.tsx +0 -22
- package/src/components/studio/nodes/shell/ShellConfigNode.tsx +0 -51
- package/src/components/studio/nodes/shell/ShellNodeBase.tsx +0 -100
- package/src/components/studio/nodes/shell/ThemeNodes.tsx +0 -51
- package/src/components/studio/nodes/shell/index.ts +0 -12
- package/src/components/widgets/BroadcastWidget.tsx +0 -93
- package/src/components/widgets/MarketplaceWidget.tsx +0 -298
- package/src/components/widgets/McpToolsWidget.tsx +0 -231
- package/src/components/widgets/OpsDashboard.tsx +0 -59
- package/src/components/widgets/QuickActionsWidget.tsx +0 -60
- package/src/components/widgets/UsageWidget.tsx +0 -112
- package/src/components/widgets/WidgetRenderer.tsx +0 -892
- package/src/components/widgets/WidgetSlotPanel.tsx +0 -213
- package/src/config/IconRegistry.ts +0 -126
- package/src/contexts/NetworkProvider.tsx +0 -162
- package/src/core/AIDirector.ts +0 -71
- package/src/core/EventBus.ts +0 -37
- package/src/core/PluginContext.tsx +0 -141
- package/src/hooks/listeners/useUIStateListener.ts +0 -59
- package/src/hooks/listeners/useWhatsAppListener.ts +0 -110
- package/src/hooks/morphBridge.ts +0 -82
- package/src/hooks/useAIModelSelector.ts +0 -144
- package/src/hooks/useAgentStream.ts +0 -220
- package/src/hooks/useAutoUpdater.ts +0 -89
- package/src/hooks/useBootSequence.ts +0 -20
- package/src/hooks/useExportDSD.ts +0 -53
- package/src/hooks/useFullscreen.ts +0 -35
- package/src/hooks/useGeminiStream.ts +0 -282
- package/src/hooks/useIntentLens.ts +0 -224
- package/src/hooks/useKeyboardShortcuts.ts +0 -69
- package/src/hooks/useLoggerBridge.ts +0 -32
- package/src/hooks/useMcpClient.ts +0 -112
- package/src/hooks/useNexusaiDeploy.ts +0 -118
- package/src/hooks/usePlaybackEngine.ts +0 -21
- package/src/hooks/usePlaygroundCommander.ts +0 -475
- package/src/hooks/usePluginEngine.ts +0 -165
- package/src/hooks/useScreenRecorder.ts +0 -73
- package/src/hooks/useShellKeyboard.ts +0 -40
- package/src/hooks/useShellShortcuts.ts +0 -118
- package/src/hooks/useSoundEffects.ts +0 -35
- package/src/hooks/useStudioConfig.ts +0 -72
- package/src/hooks/useSystemBoot.ts +0 -84
- package/src/hooks/useSystemTelemetry.ts +0 -62
- package/src/lib/debugLogger.ts +0 -80
- package/src/lib/networkInterceptor.ts +0 -100
- package/src/mocks/decido.tsx +0 -41
- package/src/plugins/pluginAPI.ts +0 -190
- package/src/store/McpStore.ts +0 -69
- package/src/store/UpdaterStore.ts +0 -60
- package/src/store/engine.ts +0 -392
- package/src/store/index.ts +0 -4
- package/src/store/layoutPresets.ts +0 -66
- package/src/store/playgroundTypes.ts +0 -98
- package/src/store/useActionTimelineStore.ts +0 -48
- package/src/store/useDebugPanelStore.ts +0 -98
- package/src/store/useDebugProfileStore.ts +0 -130
- package/src/store/useLayoutStore.ts +0 -205
- package/src/store/useMorphInstanceStore.ts +0 -289
- package/src/store/useMorphologyStore.ts +0 -103
- package/src/store/usePlaygroundStore.ts +0 -236
- package/src/store/useShellRegistry.ts +0 -123
- package/src/store/useSuggestionsStore.ts +0 -57
- package/src/store/useThemeStore.ts +0 -399
- package/src/store/useUIComponentStore.ts +0 -179
- package/src/types/DecidoStoryDefinition.ts +0 -43
- package/src/utils/ai/ai-architect.ts +0 -92
- package/src/utils/ai/ai-code.ts +0 -187
- package/src/utils/ai/ai-core.ts +0 -50
- package/src/utils/ai/ai-media.ts +0 -292
- package/src/utils/layoutGraph.ts +0 -67
- package/tsconfig.json +0 -17
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,458 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,115 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
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
|
-
}
|