@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,287 @@
1
+ /**
2
+ * ResourceLibraryPanel — Quick-access library for generated pages and templates.
3
+ *
4
+ * Two tabs:
5
+ * 📁 Mis Creaciones — pages generated via chat (from MorphInstanceStore)
6
+ * 🎨 Templates — starter templates for quick use
7
+ *
8
+ * Features: category/tag filters, thumbnail preview, one-click load.
9
+ */
10
+
11
+ import React, { useState, useMemo, useCallback } from 'react';
12
+ import { motion, AnimatePresence } from 'motion/react';
13
+ import { FolderOpen, Palette, Search, Tag, Clock, Trash2, ExternalLink, X } from 'lucide-react';
14
+ import { useShallow } from 'zustand/react/shallow';
15
+ import { useMorphInstanceStore, type MorphInstance } from '../../../store/useMorphInstanceStore';
16
+ import { useLayoutStore } from '../../../store/useLayoutStore';
17
+
18
+ // ─── Template data (lightweight copies from TemplateLibrary) ──
19
+
20
+ interface Template {
21
+ id: string; title: string; category: string; emoji: string;
22
+ color: string; description: string; html: string;
23
+ }
24
+
25
+ const TEMPLATES: Template[] = [
26
+ { id: 'landing-saas', title: 'Landing SaaS', category: 'Marketing', emoji: '🚀', color: '#7c3aed', description: 'Hero, CTA, features' ,
27
+ html: '<section class="min-h-screen bg-linear-to-br from-violet-950 via-indigo-950 to-black text-white flex items-center justify-center"><h1 class="text-5xl font-bold">Landing SaaS</h1></section>' },
28
+ { id: 'dashboard', title: 'Dashboard', category: 'App', emoji: '📊', color: '#06b6d4', description: 'Métricas, sidebar, cards',
29
+ html: '<div class="min-h-screen bg-gray-950 text-white flex items-center justify-center"><h1 class="text-5xl font-bold">Dashboard</h1></div>' },
30
+ { id: 'portfolio', title: 'Portfolio', category: 'Personal', emoji: '🎨', color: '#ec4899', description: 'Bio, proyectos, contacto',
31
+ html: '<div class="min-h-screen bg-gray-950 text-white flex items-center justify-center"><h1 class="text-5xl font-bold">Portfolio</h1></div>' },
32
+ { id: 'pricing', title: 'Pricing', category: 'Marketing', emoji: '💳', color: '#10b981', description: '3 planes, toggle anual',
33
+ html: '<section class="min-h-screen bg-gray-950 text-white flex items-center justify-center"><h1 class="text-5xl font-bold">Pricing</h1></section>' },
34
+ { id: 'login', title: 'Login', category: 'Auth', emoji: '🔐', color: '#f59e0b', description: 'Form, social, glassmorphism',
35
+ html: '<div class="min-h-screen bg-gray-950 text-white flex items-center justify-center"><h1 class="text-5xl font-bold">Login</h1></div>' },
36
+ { id: 'blog', title: 'Blog', category: 'Content', emoji: '📝', color: '#3b82f6', description: 'Lista de artículos',
37
+ html: '<div class="min-h-screen bg-gray-950 text-white flex items-center justify-center"><h1 class="text-5xl font-bold">Blog</h1></div>' },
38
+ ];
39
+
40
+ const ALL_CATEGORIES = ['All', 'Marketing', 'App', 'Personal', 'Auth', 'Content', 'Generated'];
41
+
42
+ // ─── Component ──────────────────────────────────────────────
43
+
44
+ interface ResourceLibraryPanelProps {
45
+ isOpen: boolean;
46
+ onClose: () => void;
47
+ onLoadTemplate: (html: string) => void;
48
+ onOpenInstance: (instanceId: string) => void;
49
+ }
50
+
51
+ export const ResourceLibraryPanel = React.memo(function ResourceLibraryPanel({
52
+ isOpen, onClose, onLoadTemplate, onOpenInstance,
53
+ }: ResourceLibraryPanelProps) {
54
+ const [tab, setTab] = useState<'creations' | 'templates'>('creations');
55
+ const [searchQuery, setSearchQuery] = useState('');
56
+ const [selectedCategory, setSelectedCategory] = useState('All');
57
+
58
+ // Get generated pages from MorphInstance store (useShallow prevents infinite re-renders)
59
+ const instances = useMorphInstanceStore(
60
+ useShallow(s => Array.from(s.instances.values()))
61
+ );
62
+
63
+ const setActiveInstance = useMorphInstanceStore(s => s.setActiveInstance);
64
+ const removeInstance = useMorphInstanceStore(s => s.removeInstance);
65
+
66
+ // Filter instances that have HTML data
67
+ const htmlInstances = useMemo(() =>
68
+ instances
69
+ .filter(inst => inst.data?.html || inst.shellType === 'iframe')
70
+ .sort((a, b) => b.createdAt - a.createdAt),
71
+ [instances]
72
+ );
73
+
74
+ // Filter templates by search/category
75
+ const filteredTemplates = useMemo(() => {
76
+ return TEMPLATES.filter(t => {
77
+ const matchSearch = !searchQuery ||
78
+ t.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
79
+ t.description.toLowerCase().includes(searchQuery.toLowerCase());
80
+ const matchCat = selectedCategory === 'All' || t.category === selectedCategory;
81
+ return matchSearch && matchCat;
82
+ });
83
+ }, [searchQuery, selectedCategory]);
84
+
85
+ // Filter creations by search
86
+ const filteredCreations = useMemo(() => {
87
+ if (!searchQuery) return htmlInstances;
88
+ const q = searchQuery.toLowerCase();
89
+ return htmlInstances.filter(inst =>
90
+ inst.label.toLowerCase().includes(q) ||
91
+ inst.shellType.toLowerCase().includes(q)
92
+ );
93
+ }, [htmlInstances, searchQuery]);
94
+
95
+ const handleLoadInstance = useCallback((inst: MorphInstance) => {
96
+ setActiveInstance(inst.id);
97
+ useLayoutStore.getState().setActiveTab(`morph-${inst.id}`);
98
+ onOpenInstance(inst.id);
99
+ onClose();
100
+ }, [setActiveInstance, onOpenInstance, onClose]);
101
+
102
+ const handleLoadTemplate = useCallback((t: Template) => {
103
+ onLoadTemplate(t.html);
104
+ onClose();
105
+ }, [onLoadTemplate, onClose]);
106
+
107
+ const timeAgo = (ts: number) => {
108
+ const diff = Date.now() - ts;
109
+ const mins = Math.floor(diff / 60000);
110
+ if (mins < 1) return 'ahora';
111
+ if (mins < 60) return `${mins}m`;
112
+ const hrs = Math.floor(mins / 60);
113
+ if (hrs < 24) return `${hrs}h`;
114
+ return `${Math.floor(hrs / 24)}d`;
115
+ };
116
+
117
+ return (
118
+ <AnimatePresence>
119
+ {isOpen && (
120
+ <motion.div
121
+ initial={{ opacity: 0, y: 8 }}
122
+ animate={{ opacity: 1, y: 0 }}
123
+ exit={{ opacity: 0, y: 8 }}
124
+ className="absolute bottom-full left-0 right-0 z-50 mb-2"
125
+ >
126
+ <div className="bg-white/95 dark:bg-[#1a1a2e] border border-black/[0.08] dark:border-border-default
127
+ rounded-2xl shadow-xl dark:shadow-2xl overflow-hidden max-h-[420px] flex flex-col backdrop-blur-xl">
128
+ {/* Header */}
129
+ <div className="flex items-center justify-between px-4 py-3 border-b border-black/[0.06] dark:border-border-subtle shrink-0">
130
+ <div className="flex items-center gap-3">
131
+ <h3 className="text-sm font-bold text-text-primary">Librería</h3>
132
+ <div className="flex bg-black/[0.04] dark:bg-surface-glass rounded-lg p-0.5">
133
+ <button
134
+ onClick={() => setTab('creations')}
135
+ className={`px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all flex items-center gap-1.5
136
+ ${tab === 'creations' ? 'bg-white dark:bg-surface-tertiary text-text-primary shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}
137
+ >
138
+ <FolderOpen size={12} /> Mis Creaciones
139
+ {htmlInstances.length > 0 && (
140
+ <span className="text-[9px] bg-cyan-500/20 text-cyan-600 dark:text-cyan-400 px-1.5 py-0.5 rounded-full font-bold">
141
+ {htmlInstances.length}
142
+ </span>
143
+ )}
144
+ </button>
145
+ <button
146
+ onClick={() => setTab('templates')}
147
+ className={`px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all flex items-center gap-1.5
148
+ ${tab === 'templates' ? 'bg-white dark:bg-surface-tertiary text-text-primary shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}
149
+ >
150
+ <Palette size={12} /> Templates
151
+ </button>
152
+ </div>
153
+ </div>
154
+ <button onClick={onClose} className="w-7 h-7 rounded-lg text-text-muted hover:text-text-primary hover:bg-black/[0.04] dark:hover:bg-surface-glass flex items-center justify-center transition-colors">
155
+ <X size={14} />
156
+ </button>
157
+ </div>
158
+
159
+ {/* Search + Category filter */}
160
+ <div className="px-4 py-2 border-b border-black/[0.04] dark:border-border-subtle shrink-0 flex items-center gap-2">
161
+ <div className="flex-1 relative">
162
+ <Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-muted" />
163
+ <input
164
+ type="text"
165
+ placeholder="Buscar..."
166
+ value={searchQuery}
167
+ onChange={e => setSearchQuery(e.target.value)}
168
+ className="w-full pl-8 pr-3 py-1.5 bg-black/[0.03] dark:bg-surface-glass border border-transparent focus:border-cyan-500/30
169
+ rounded-lg text-xs text-text-primary placeholder-text-muted outline-hidden transition"
170
+ />
171
+ </div>
172
+ {tab === 'templates' && (
173
+ <div className="flex gap-1 overflow-x-auto no-scrollbar">
174
+ {ALL_CATEGORIES.filter(c => c !== 'Generated').map(cat => (
175
+ <button
176
+ key={cat}
177
+ onClick={() => setSelectedCategory(cat)}
178
+ className={`px-2 py-1 text-[10px] font-semibold rounded-md whitespace-nowrap transition-all
179
+ ${selectedCategory === cat
180
+ ? 'bg-cyan-500/15 text-cyan-600 dark:text-cyan-400'
181
+ : 'text-text-muted hover:text-text-secondary hover:bg-black/[0.04] dark:hover:bg-surface-glass'}`}
182
+ >
183
+ {cat}
184
+ </button>
185
+ ))}
186
+ </div>
187
+ )}
188
+ </div>
189
+
190
+ {/* Content */}
191
+ <div className="flex-1 overflow-y-auto px-3 py-3 custom-scrollbar">
192
+ <AnimatePresence mode="wait">
193
+ {tab === 'creations' ? (
194
+ <motion.div key="creations" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
195
+ {filteredCreations.length === 0 ? (
196
+ <div className="flex flex-col items-center justify-center py-12 text-center">
197
+ <FolderOpen size={32} className="text-text-muted/30 mb-3" />
198
+ <p className="text-sm text-text-muted">Sin creaciones aún</p>
199
+ <p className="text-[10px] text-text-muted/60 mt-1">Genera una página desde el chat para verla aquí</p>
200
+ </div>
201
+ ) : (
202
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
203
+ {filteredCreations.map((inst, i) => (
204
+ <motion.button
205
+ key={inst.id}
206
+ initial={{ opacity: 0, y: 6 }}
207
+ animate={{ opacity: 1, y: 0 }}
208
+ transition={{ delay: i * 0.03 }}
209
+ onClick={() => handleLoadInstance(inst)}
210
+ className="group relative bg-black/[0.02] dark:bg-surface-glass border border-black/[0.06] dark:border-border-subtle
211
+ hover:border-cyan-500/30 rounded-xl p-3 text-left transition-all hover:shadow-md overflow-hidden"
212
+ >
213
+ {/* Mini thumbnail (HTML preview placeholder) */}
214
+ <div className="h-16 rounded-lg bg-gradient-to-br from-cyan-500/10 to-purple-500/10 dark:from-cyan-500/5 dark:to-purple-500/5
215
+ mb-2 flex items-center justify-center overflow-hidden border border-black/[0.04] dark:border-border-subtle">
216
+ <span className="text-lg opacity-50">🌐</span>
217
+ </div>
218
+
219
+ {/* Info */}
220
+ <h4 className="text-[11px] font-semibold text-text-primary truncate">{inst.label}</h4>
221
+ <div className="flex items-center gap-1.5 mt-1">
222
+ <span className="flex items-center gap-1 text-[9px] text-text-muted">
223
+ <Clock size={9} /> {timeAgo(inst.createdAt)}
224
+ </span>
225
+ <span className="flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 font-semibold">
226
+ <Tag size={8} /> {inst.shellType}
227
+ </span>
228
+ </div>
229
+
230
+ {/* Hover actions */}
231
+ <div className="absolute top-1.5 right-1.5 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
232
+ <button
233
+ onClick={e => { e.stopPropagation(); removeInstance(inst.id); }}
234
+ className="w-5 h-5 rounded bg-red-500/10 hover:bg-red-500/20 text-red-400 flex items-center justify-center"
235
+ title="Eliminar"
236
+ >
237
+ <Trash2 size={10} />
238
+ </button>
239
+ </div>
240
+ </motion.button>
241
+ ))}
242
+ </div>
243
+ )}
244
+ </motion.div>
245
+ ) : (
246
+ <motion.div key="templates" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
247
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
248
+ {filteredTemplates.map((t, i) => (
249
+ <motion.button
250
+ key={t.id}
251
+ initial={{ opacity: 0, y: 6 }}
252
+ animate={{ opacity: 1, y: 0 }}
253
+ transition={{ delay: i * 0.03 }}
254
+ onClick={() => handleLoadTemplate(t)}
255
+ className="group bg-black/[0.02] dark:bg-surface-glass border border-black/[0.06] dark:border-border-subtle
256
+ hover:border-opacity-50 rounded-xl p-3 text-left transition-all hover:shadow-md overflow-hidden"
257
+ style={{ '--hover-color': t.color } as React.CSSProperties}
258
+ >
259
+ {/* Color preview */}
260
+ <div
261
+ className="h-14 rounded-lg mb-2 flex items-center justify-center transition-all group-hover:scale-[1.02]"
262
+ style={{ background: `linear-gradient(135deg, ${t.color}20, ${t.color}08)` }}
263
+ >
264
+ <span className="text-2xl">{t.emoji}</span>
265
+ </div>
266
+
267
+ <h4 className="text-[11px] font-semibold text-text-primary">{t.title}</h4>
268
+ <p className="text-[9px] text-text-muted mt-0.5">{t.description}</p>
269
+ <span
270
+ className="inline-block mt-1.5 text-[9px] font-semibold px-1.5 py-0.5 rounded"
271
+ style={{ background: `${t.color}15`, color: t.color }}
272
+ >
273
+ {t.category}
274
+ </span>
275
+ </motion.button>
276
+ ))}
277
+ </div>
278
+ </motion.div>
279
+ )}
280
+ </AnimatePresence>
281
+ </div>
282
+ </div>
283
+ </motion.div>
284
+ )}
285
+ </AnimatePresence>
286
+ );
287
+ });
@@ -0,0 +1,82 @@
1
+ import { usePlaygroundStore } from '../../store/usePlaygroundStore';
2
+
3
+ /**
4
+ * Exports a sequence in DSD (Decido Standard Document) format.
5
+ */
6
+ export function downloadSequenceAsDSD(seq: any) {
7
+ const dsdFormat = {
8
+ id: seq.id,
9
+ meta: {
10
+ version: "1.0",
11
+ author: "Decido OS Playground",
12
+ createdAt: Date.now(),
13
+ updatedAt: Date.now(),
14
+ title: seq.prompt,
15
+ description: "Exported Decido Session",
16
+ tags: ["playground", seq.brand]
17
+ },
18
+ config: {
19
+ defaultVoice: "zephyr",
20
+ brandTheme: seq.brand,
21
+ environment: "dark"
22
+ },
23
+ actors: [
24
+ { id: "zephyr", name: "Zephyr", role: "AI Assistant" }
25
+ ],
26
+ timeline: seq.keyframes
27
+ };
28
+
29
+ const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(dsdFormat, null, 2));
30
+ const downloadAnchorNode = document.createElement('a');
31
+ downloadAnchorNode.setAttribute("href", dataStr);
32
+ downloadAnchorNode.setAttribute("download", `decido-dsd-${seq.id}.json`);
33
+ document.body.appendChild(downloadAnchorNode);
34
+ downloadAnchorNode.click();
35
+ downloadAnchorNode.remove();
36
+ }
37
+
38
+ /**
39
+ * Handles file upload — supports DSD format and legacy format.
40
+ */
41
+ export function handleSequenceUpload(e: React.ChangeEvent<HTMLInputElement>, prototypeBrand: string) {
42
+ const fileReader = new FileReader();
43
+ if (e.target.files && e.target.files[0]) {
44
+ fileReader.readAsText(e.target.files[0], "UTF-8");
45
+ fileReader.onload = (event) => {
46
+ if (event.target && event.target.result) {
47
+ try {
48
+ const parsed = JSON.parse(event.target.result as string);
49
+ let sequenceToSave = null;
50
+
51
+ // DSD Format
52
+ if (parsed.meta && parsed.timeline) {
53
+ const brand = parsed.config?.brandTheme || prototypeBrand;
54
+ const duration = parsed.timeline.length > 0
55
+ ? Math.max(...parsed.timeline.map((k: any) => k.t)) + 2
56
+ : 10;
57
+ sequenceToSave = {
58
+ id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
59
+ prompt: parsed.meta.title || "Imported DSD Story",
60
+ brand, duration,
61
+ keyframes: parsed.timeline
62
+ };
63
+ }
64
+ // Legacy Format
65
+ else if (parsed.id && parsed.prompt && parsed.keyframes) {
66
+ parsed.id = Date.now().toString() + Math.random().toString(36).substr(2, 5);
67
+ sequenceToSave = parsed;
68
+ }
69
+
70
+ if (sequenceToSave) {
71
+ usePlaygroundStore.getState().addSavedSequence(sequenceToSave);
72
+ } else {
73
+ console.error("Invalid sequence format");
74
+ }
75
+ } catch (error) {
76
+ console.error("Error parsing uploaded sequence", error);
77
+ }
78
+ }
79
+ };
80
+ e.target.value = ''; // Reset input
81
+ }
82
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * SettingsPanel — Sidebar settings panel with Suggestions CRUD + AI generation.
3
+ */
4
+ import React, { useState, useCallback } from 'react';
5
+ import { motion, AnimatePresence } from 'motion/react';
6
+ import { X, Plus, Trash2, Sparkles, Loader2, Key, Cpu, Box, BrainCircuit } from 'lucide-react';
7
+ import { useSuggestionsStore } from '../../store/useSuggestionsStore';
8
+
9
+ const AVAILABLE_MODELS = [
10
+ { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash', type: 'Ultra-Fast' },
11
+ { id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', type: 'Deep-Reasoning' },
12
+ { id: 'gemini-3.1-flash-lite-preview', name: 'Gemini 3.1 Flash Lite', type: 'Cutting-Edge' }
13
+ ];
14
+
15
+ interface SettingsPanelProps {
16
+ isOpen: boolean;
17
+ onClose: () => void;
18
+ }
19
+
20
+ export const SettingsPanel = React.memo(function SettingsPanel({ isOpen, onClose }: SettingsPanelProps) {
21
+ const { seeds, generated, addSeed, removeSeed, updateSeed, setGenerated } = useSuggestionsStore();
22
+ const [newSeed, setNewSeed] = useState('');
23
+ const [genCount, setGenCount] = useState(5);
24
+ const [isGenerating, setIsGenerating] = useState(false);
25
+ const [editingIndex, setEditingIndex] = useState<number | null>(null);
26
+
27
+ // API Key state
28
+ const [apiKey, setApiKey] = useState(() => localStorage.getItem('google_gemini_api_key') || '');
29
+ const [showKey, setShowKey] = useState(false);
30
+
31
+ // AI Model State
32
+ const [activeModel, setActiveModel] = useState(() => localStorage.getItem('decido_gemini_model') || 'gemini-3-flash-preview');
33
+
34
+ const handleModelChange = useCallback((modelId: string) => {
35
+ setActiveModel(modelId);
36
+ localStorage.setItem('decido_gemini_model', modelId);
37
+
38
+ // Sincronizar con el Orquestador Rust para A/B Testing live
39
+ fetch('http://localhost:3100/api/models/active', {
40
+ method: 'PUT',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ model_id: modelId })
43
+ }).catch(() => console.warn('Orquestador local no expone /api/models/active aún.'));
44
+ }, []);
45
+
46
+ const handleAddSeed = useCallback(() => {
47
+ if (newSeed.trim()) { addSeed(newSeed.trim()); setNewSeed(''); }
48
+ }, [newSeed, addSeed]);
49
+
50
+ const handleGenerateSuggestions = useCallback(async () => {
51
+ if (seeds.length === 0) return;
52
+ setIsGenerating(true);
53
+ try {
54
+ const key = localStorage.getItem('google_gemini_api_key')
55
+ || (window as any).__DECIDO__?._apiKey
56
+ || '';
57
+ const model = localStorage.getItem('decido_gemini_model') || 'gemini-3-flash-preview';
58
+ if (!key) { console.warn('❌ No API key for suggestions'); setIsGenerating(false); return; }
59
+
60
+ const prompt = `Basándote en estas frases semilla, genera exactamente ${genCount} sugerencias similares para un chat de IA de productividad. Solo devuelve las sugerencias, una por línea, sin numeración ni bullets:\n\n${seeds.join('\n')}`;
61
+
62
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`, {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
66
+ });
67
+ const data = await res.json();
68
+ const text = data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
69
+ const lines = text.split('\n').map((l: string) => l.trim()).filter((l: string) => l.length > 3);
70
+ setGenerated(lines.slice(0, genCount));
71
+ } catch (e) {
72
+ console.error('Error generating suggestions:', e);
73
+ } finally {
74
+ setIsGenerating(false);
75
+ }
76
+ }, [seeds, genCount, setGenerated]);
77
+
78
+ const saveApiKey = useCallback(() => {
79
+ if (apiKey.trim()) localStorage.setItem('google_gemini_api_key', apiKey.trim());
80
+ else localStorage.removeItem('google_gemini_api_key');
81
+ }, [apiKey]);
82
+
83
+ return (
84
+ <AnimatePresence>
85
+ {isOpen && (
86
+ <>
87
+ <motion.div
88
+ initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
89
+ className="fixed inset-0 bg-surface-overlay backdrop-blur-xs z-200"
90
+ style={{ position: 'fixed', inset: 0, zIndex: 200 }}
91
+ />
92
+ <motion.div
93
+ initial={{ x: '-100%' }} animate={{ x: 0 }} exit={{ x: '-100%' }}
94
+ transition={{ type: 'spring', damping: 30, stiffness: 300 }}
95
+ className="fixed inset-y-0 left-0 w-full sm:w-[360px] bg-surface-secondary border-r border-border-default z-201 flex flex-col shadow-2xl"
96
+ style={{ position: 'fixed', top: 0, bottom: 0, left: 0, zIndex: 201 }}
97
+ >
98
+ {/* Header */}
99
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border-subtle">
100
+ <h2 className="text-sm font-bold text-text-primary">⚙️ Configuración</h2>
101
+ <button onClick={onClose} className="w-7 h-7 rounded-full bg-surface-glass hover:bg-surface-glass flex items-center justify-center text-text-secondary hover:text-text-primary transition-colors">
102
+ <X size={14} />
103
+ </button>
104
+ </div>
105
+
106
+ {/* Scrollable Content */}
107
+ <div className="flex-1 overflow-y-auto px-4 py-4 space-y-6" style={{ scrollbarWidth: 'thin', scrollbarColor: 'rgba(255,255,255,0.15) transparent' }}>
108
+
109
+ {/* ── API Key ── */}
110
+ <section>
111
+ <div className="flex items-center gap-2 mb-3">
112
+ <Key size={14} className="text-amber-400" />
113
+ <h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider">API Key</h3>
114
+ </div>
115
+ <div className="flex gap-2">
116
+ <input
117
+ type={showKey ? 'text' : 'password'}
118
+ value={apiKey}
119
+ onChange={e => setApiKey(e.target.value)}
120
+ onBlur={saveApiKey}
121
+ placeholder="AIzaSy..."
122
+ className="flex-1 bg-surface-glass border border-border-default rounded-lg px-3 py-2 text-xs text-text-primary placeholder-zinc-600 outline-hidden focus:border-border-strong font-mono"
123
+ />
124
+ <button onClick={() => setShowKey(!showKey)} className="px-2 rounded-lg text-[10px] text-text-muted hover:text-text-primary bg-surface-glass hover:bg-surface-glass border border-border-default transition-colors">
125
+ {showKey ? '🙈' : '👁️'}
126
+ </button>
127
+ </div>
128
+ </section>
129
+
130
+ {/* ── BrainRouter (A/B Testing LLMs) ── */}
131
+ <section>
132
+ <div className="flex items-center gap-2 mb-3">
133
+ <BrainCircuit size={14} className="text-emerald-400" />
134
+ <h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider">Brain Router (Enjambre)</h3>
135
+ </div>
136
+ <div className="flex flex-col gap-2">
137
+ {AVAILABLE_MODELS.map(m => (
138
+ <button
139
+ key={m.id}
140
+ onClick={() => handleModelChange(m.id)}
141
+ className={`flex items-center justify-between px-3 py-2 rounded-xl transition-all border ${activeModel === m.id ? 'bg-emerald-500/10 border-emerald-500/30 shadow-[0_0_10px_rgba(16,185,129,0.2)]' : 'bg-surface-glass border-border-default hover:border-border-strong'}`}
142
+ >
143
+ <div className="flex flex-col items-start gap-0.5">
144
+ <span className={`text-xs font-bold ${activeModel === m.id ? 'text-emerald-400' : 'text-text-primary'}`}>{m.name}</span>
145
+ <span className="text-[9px] font-mono text-text-muted uppercase tracking-wider">{m.id}</span>
146
+ </div>
147
+ <span className={`text-[9px] px-2 py-0.5 rounded-full font-bold uppercase tracking-widest ${activeModel === m.id ? 'bg-emerald-500/20 text-emerald-300' : 'bg-surface-tertiary text-text-secondary'}`}>
148
+ {m.type}
149
+ </span>
150
+ </button>
151
+ ))}
152
+ </div>
153
+ </section>
154
+
155
+ {/* ── Suggestion Seeds CRUD ── */}
156
+ <section>
157
+ <div className="flex items-center gap-2 mb-3">
158
+ <Sparkles size={14} className="text-cyan-400" />
159
+ <h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider">Frases Semilla</h3>
160
+ <span className="text-[10px] text-text-muted font-mono ml-auto">{seeds.length}</span>
161
+ </div>
162
+
163
+ <div className="space-y-1.5 mb-3">
164
+ {seeds.map((seed, i) => (
165
+ <div key={i} className="flex items-center gap-1.5 group">
166
+ {editingIndex === i ? (
167
+ <input
168
+ autoFocus
169
+ value={seed}
170
+ onChange={e => updateSeed(i, e.target.value)}
171
+ onBlur={() => setEditingIndex(null)}
172
+ onKeyDown={e => e.key === 'Enter' && setEditingIndex(null)}
173
+ className="flex-1 bg-surface-glass border border-cyan-500/30 rounded-lg px-2.5 py-1.5 text-xs text-text-primary outline-hidden font-mono"
174
+ />
175
+ ) : (
176
+ <button
177
+ onClick={() => setEditingIndex(i)}
178
+ className="flex-1 text-left px-2.5 py-1.5 rounded-lg text-xs text-text-secondary hover:text-text-primary hover:bg-surface-glass transition-colors font-mono truncate"
179
+ >
180
+ {seed}
181
+ </button>
182
+ )}
183
+ <button
184
+ onClick={() => removeSeed(i)}
185
+ className="w-6 h-6 rounded-md text-text-muted hover:text-red-400 hover:bg-red-500/10 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all shrink-0"
186
+ >
187
+ <Trash2 size={12} />
188
+ </button>
189
+ </div>
190
+ ))}
191
+ </div>
192
+
193
+ {/* Add new seed */}
194
+ <div className="flex gap-1.5">
195
+ <input
196
+ value={newSeed}
197
+ onChange={e => setNewSeed(e.target.value)}
198
+ onKeyDown={e => e.key === 'Enter' && handleAddSeed()}
199
+ placeholder="Nueva frase semilla..."
200
+ className="flex-1 bg-surface-glass border border-border-default rounded-lg px-2.5 py-1.5 text-xs text-text-primary placeholder-zinc-600 outline-hidden focus:border-border-strong font-mono"
201
+ />
202
+ <button onClick={handleAddSeed} className="w-8 h-8 rounded-lg bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 hover:bg-cyan-500/20 flex items-center justify-center transition-colors">
203
+ <Plus size={14} />
204
+ </button>
205
+ </div>
206
+ </section>
207
+
208
+ {/* ── AI Generation ── */}
209
+ <section>
210
+ <div className="flex items-center gap-2 mb-3">
211
+ <Sparkles size={14} className="text-purple-400" />
212
+ <h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider">Generar con AI</h3>
213
+ </div>
214
+ <div className="flex items-center gap-2 mb-3">
215
+ <label className="text-[10px] text-text-muted font-mono">Cantidad:</label>
216
+ <input
217
+ type="number" min={1} max={20} value={genCount}
218
+ onChange={e => setGenCount(Math.max(1, Math.min(20, parseInt(e.target.value) || 5)))}
219
+ className="w-14 bg-surface-glass border border-border-default rounded-lg px-2 py-1 text-xs text-text-primary outline-hidden text-center font-mono"
220
+ />
221
+ <button
222
+ onClick={handleGenerateSuggestions}
223
+ disabled={isGenerating || seeds.length === 0}
224
+ className="flex-1 flex items-center justify-center gap-2 px-3 py-1.5 rounded-lg bg-purple-500/15 border border-purple-500/20 text-purple-300 hover:bg-purple-500/25 disabled:opacity-40 disabled:cursor-not-allowed transition-colors text-xs font-semibold"
225
+ >
226
+ {isGenerating ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
227
+ {isGenerating ? 'Generando...' : 'Generar Sugerencias'}
228
+ </button>
229
+ </div>
230
+
231
+ {/* Generated Results */}
232
+ {generated.length > 0 && (
233
+ <div className="space-y-1 bg-surface-glass rounded-lg p-2 border border-border-subtle">
234
+ <div className="text-[9px] text-text-muted font-mono uppercase mb-1">Generadas ({generated.length})</div>
235
+ {generated.map((g, i) => (
236
+ <div key={i} className="text-xs text-text-secondary font-mono px-2 py-1 rounded hover:bg-surface-glass transition-colors">
237
+ {g}
238
+ </div>
239
+ ))}
240
+ </div>
241
+ )}
242
+ </section>
243
+
244
+ {/* ── Plugin Suggestions ── */}
245
+ {Object.keys(useSuggestionsStore.getState().pluginSuggestions).length > 0 && (
246
+ <section>
247
+ <div className="flex items-center gap-2 mb-3">
248
+ <Box size={14} className="text-emerald-400" />
249
+ <h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider">Plugins</h3>
250
+ </div>
251
+ {Object.entries(useSuggestionsStore.getState().pluginSuggestions).map(([pluginId, items]) => (
252
+ <div key={pluginId} className="mb-2">
253
+ <div className="text-[10px] text-text-muted font-mono mb-1">{pluginId} ({items.length})</div>
254
+ {items.map((item, i) => (
255
+ <div key={i} className="text-xs text-text-secondary font-mono px-2 py-1">{item}</div>
256
+ ))}
257
+ </div>
258
+ ))}
259
+ </section>
260
+ )}
261
+ </div>
262
+ </motion.div>
263
+ </>
264
+ )}
265
+ </AnimatePresence>
266
+ );
267
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * AppHeader — Re-export of PlaygroundHeader with new name.
3
+ *
4
+ * During the transition period, this is a simple re-export.
5
+ * In future sprints, AppHeader will be simplified and absorb
6
+ * the T2 TabBar as part of the toolbar area.
7
+ */
8
+ export { PlaygroundHeader as AppHeader } from '../playground/PlaygroundHeader';
9
+ export type { PlaygroundHeaderProps as AppHeaderProps, HeaderAction } from '../playground/PlaygroundHeader';