@tleblancureta/proto 0.1.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 (231) hide show
  1. package/core-web/src/ProtoApp.tsx +163 -0
  2. package/core-web/src/components/Shell.tsx +276 -0
  3. package/core-web/src/components/shell/EmptyState.tsx +33 -0
  4. package/core-web/src/components/shell/FocusView.tsx +55 -0
  5. package/core-web/src/components/shell/Toolbar.tsx +233 -0
  6. package/core-web/src/components/shell/persistence.ts +20 -0
  7. package/core-web/src/components/shell/types.ts +14 -0
  8. package/core-web/src/components/ui/avatar.tsx +18 -0
  9. package/core-web/src/components/ui/badge.tsx +28 -0
  10. package/core-web/src/components/ui/button.tsx +40 -0
  11. package/core-web/src/components/ui/card.tsx +32 -0
  12. package/core-web/src/components/ui/inline-edit.tsx +120 -0
  13. package/core-web/src/components/ui/input.tsx +18 -0
  14. package/core-web/src/components/ui/scroll-area.tsx +12 -0
  15. package/core-web/src/components/ui/separator.tsx +23 -0
  16. package/core-web/src/components/ui/shell-dialog.tsx +79 -0
  17. package/core-web/src/components/ui/skeleton.tsx +9 -0
  18. package/core-web/src/components/ui/textarea.tsx +17 -0
  19. package/core-web/src/components/widgets/agent/Generative.tsx +74 -0
  20. package/core-web/src/components/widgets/agent/Primitives.tsx +225 -0
  21. package/core-web/src/components/widgets/agent/actions.ts +52 -0
  22. package/core-web/src/hooks/useAuth.ts +80 -0
  23. package/core-web/src/hooks/useData.ts +44 -0
  24. package/core-web/src/hooks/useMountEffect.ts +10 -0
  25. package/core-web/src/hooks/useTheme.ts +37 -0
  26. package/core-web/src/index.ts +52 -0
  27. package/core-web/src/lib/api.ts +231 -0
  28. package/core-web/src/lib/config.ts +14 -0
  29. package/core-web/src/lib/define-widget.ts +71 -0
  30. package/core-web/src/lib/drag.ts +45 -0
  31. package/core-web/src/lib/supabase.ts +6 -0
  32. package/core-web/src/lib/utils.ts +6 -0
  33. package/core-web/src/lib/widgetCache.ts +29 -0
  34. package/core-web/src/vite-env.d.ts +1 -0
  35. package/dist/core-mcp/src/app.d.ts +40 -0
  36. package/dist/core-mcp/src/app.d.ts.map +1 -0
  37. package/dist/core-mcp/src/app.js +141 -0
  38. package/dist/core-mcp/src/app.js.map +1 -0
  39. package/dist/core-mcp/src/define-tool.d.ts +70 -0
  40. package/dist/core-mcp/src/define-tool.d.ts.map +1 -0
  41. package/dist/core-mcp/src/define-tool.js +38 -0
  42. package/dist/core-mcp/src/define-tool.js.map +1 -0
  43. package/dist/core-mcp/src/entity-tools.d.ts +27 -0
  44. package/dist/core-mcp/src/entity-tools.d.ts.map +1 -0
  45. package/dist/core-mcp/src/entity-tools.js +99 -0
  46. package/dist/core-mcp/src/entity-tools.js.map +1 -0
  47. package/dist/core-mcp/src/index.d.ts +36 -0
  48. package/dist/core-mcp/src/index.d.ts.map +1 -0
  49. package/dist/core-mcp/src/index.js +116 -0
  50. package/dist/core-mcp/src/index.js.map +1 -0
  51. package/dist/core-mcp/src/supabase.d.ts +7 -0
  52. package/dist/core-mcp/src/supabase.d.ts.map +1 -0
  53. package/dist/core-mcp/src/supabase.js +18 -0
  54. package/dist/core-mcp/src/supabase.js.map +1 -0
  55. package/dist/core-mcp/src/tools/_helpers.d.ts +44 -0
  56. package/dist/core-mcp/src/tools/_helpers.d.ts.map +1 -0
  57. package/dist/core-mcp/src/tools/_helpers.js +23 -0
  58. package/dist/core-mcp/src/tools/_helpers.js.map +1 -0
  59. package/dist/core-mcp/src/tools/ui.d.ts +9 -0
  60. package/dist/core-mcp/src/tools/ui.d.ts.map +1 -0
  61. package/dist/core-mcp/src/tools/ui.js +100 -0
  62. package/dist/core-mcp/src/tools/ui.js.map +1 -0
  63. package/dist/core-mcp/src/workflow-tools.d.ts +41 -0
  64. package/dist/core-mcp/src/workflow-tools.d.ts.map +1 -0
  65. package/dist/core-mcp/src/workflow-tools.js +382 -0
  66. package/dist/core-mcp/src/workflow-tools.js.map +1 -0
  67. package/dist/core-shared/src/define-entity.d.ts +73 -0
  68. package/dist/core-shared/src/define-entity.d.ts.map +1 -0
  69. package/dist/core-shared/src/define-entity.js +47 -0
  70. package/dist/core-shared/src/define-entity.js.map +1 -0
  71. package/dist/core-shared/src/define-workflow.d.ts +111 -0
  72. package/dist/core-shared/src/define-workflow.d.ts.map +1 -0
  73. package/dist/core-shared/src/define-workflow.js +92 -0
  74. package/dist/core-shared/src/define-workflow.js.map +1 -0
  75. package/dist/core-shared/src/index.d.ts +5 -0
  76. package/dist/core-shared/src/index.d.ts.map +1 -0
  77. package/dist/core-shared/src/index.js +7 -0
  78. package/dist/core-shared/src/index.js.map +1 -0
  79. package/dist/core-shared/src/scheduling.d.ts +69 -0
  80. package/dist/core-shared/src/scheduling.d.ts.map +1 -0
  81. package/dist/core-shared/src/scheduling.js +39 -0
  82. package/dist/core-shared/src/scheduling.js.map +1 -0
  83. package/dist/core-shared/src/schemas.d.ts +51 -0
  84. package/dist/core-shared/src/schemas.d.ts.map +1 -0
  85. package/dist/core-shared/src/schemas.js +18 -0
  86. package/dist/core-shared/src/schemas.js.map +1 -0
  87. package/dist/core-web/src/ProtoApp.d.ts +19 -0
  88. package/dist/core-web/src/ProtoApp.d.ts.map +1 -0
  89. package/dist/core-web/src/ProtoApp.js +92 -0
  90. package/dist/core-web/src/ProtoApp.js.map +1 -0
  91. package/dist/core-web/src/components/Shell.d.ts +46 -0
  92. package/dist/core-web/src/components/Shell.d.ts.map +1 -0
  93. package/dist/core-web/src/components/Shell.js +104 -0
  94. package/dist/core-web/src/components/Shell.js.map +1 -0
  95. package/dist/core-web/src/components/shell/EmptyState.d.ts +13 -0
  96. package/dist/core-web/src/components/shell/EmptyState.d.ts.map +1 -0
  97. package/dist/core-web/src/components/shell/EmptyState.js +7 -0
  98. package/dist/core-web/src/components/shell/EmptyState.js.map +1 -0
  99. package/dist/core-web/src/components/shell/FocusView.d.ts +16 -0
  100. package/dist/core-web/src/components/shell/FocusView.d.ts.map +1 -0
  101. package/dist/core-web/src/components/shell/FocusView.js +12 -0
  102. package/dist/core-web/src/components/shell/FocusView.js.map +1 -0
  103. package/dist/core-web/src/components/shell/Toolbar.d.ts +35 -0
  104. package/dist/core-web/src/components/shell/Toolbar.d.ts.map +1 -0
  105. package/dist/core-web/src/components/shell/Toolbar.js +42 -0
  106. package/dist/core-web/src/components/shell/Toolbar.js.map +1 -0
  107. package/dist/core-web/src/components/shell/persistence.d.ts +8 -0
  108. package/dist/core-web/src/components/shell/persistence.d.ts.map +1 -0
  109. package/dist/core-web/src/components/shell/persistence.js +20 -0
  110. package/dist/core-web/src/components/shell/persistence.js.map +1 -0
  111. package/dist/core-web/src/components/shell/types.d.ts +13 -0
  112. package/dist/core-web/src/components/shell/types.d.ts.map +1 -0
  113. package/dist/core-web/src/components/shell/types.js +2 -0
  114. package/dist/core-web/src/components/shell/types.js.map +1 -0
  115. package/dist/core-web/src/components/ui/avatar.d.ts +5 -0
  116. package/dist/core-web/src/components/ui/avatar.d.ts.map +1 -0
  117. package/dist/core-web/src/components/ui/avatar.js +9 -0
  118. package/dist/core-web/src/components/ui/avatar.js.map +1 -0
  119. package/dist/core-web/src/components/ui/badge.d.ts +13 -0
  120. package/dist/core-web/src/components/ui/badge.d.ts.map +1 -0
  121. package/dist/core-web/src/components/ui/badge.js +13 -0
  122. package/dist/core-web/src/components/ui/badge.js.map +1 -0
  123. package/dist/core-web/src/components/ui/button.d.ts +22 -0
  124. package/dist/core-web/src/components/ui/button.d.ts.map +1 -0
  125. package/dist/core-web/src/components/ui/button.js +21 -0
  126. package/dist/core-web/src/components/ui/button.js.map +1 -0
  127. package/dist/core-web/src/components/ui/card.d.ts +7 -0
  128. package/dist/core-web/src/components/ui/card.d.ts.map +1 -0
  129. package/dist/core-web/src/components/ui/card.js +13 -0
  130. package/dist/core-web/src/components/ui/card.js.map +1 -0
  131. package/dist/core-web/src/components/ui/inline-edit.d.ts +20 -0
  132. package/dist/core-web/src/components/ui/inline-edit.d.ts.map +1 -0
  133. package/dist/core-web/src/components/ui/inline-edit.js +63 -0
  134. package/dist/core-web/src/components/ui/inline-edit.js.map +1 -0
  135. package/dist/core-web/src/components/ui/input.d.ts +4 -0
  136. package/dist/core-web/src/components/ui/input.d.ts.map +1 -0
  137. package/dist/core-web/src/components/ui/input.js +7 -0
  138. package/dist/core-web/src/components/ui/input.js.map +1 -0
  139. package/dist/core-web/src/components/ui/scroll-area.d.ts +4 -0
  140. package/dist/core-web/src/components/ui/scroll-area.d.ts.map +1 -0
  141. package/dist/core-web/src/components/ui/scroll-area.js +7 -0
  142. package/dist/core-web/src/components/ui/scroll-area.js.map +1 -0
  143. package/dist/core-web/src/components/ui/separator.d.ts +7 -0
  144. package/dist/core-web/src/components/ui/separator.d.ts.map +1 -0
  145. package/dist/core-web/src/components/ui/separator.js +7 -0
  146. package/dist/core-web/src/components/ui/separator.js.map +1 -0
  147. package/dist/core-web/src/components/ui/shell-dialog.d.ts +16 -0
  148. package/dist/core-web/src/components/ui/shell-dialog.d.ts.map +1 -0
  149. package/dist/core-web/src/components/ui/shell-dialog.js +36 -0
  150. package/dist/core-web/src/components/ui/shell-dialog.js.map +1 -0
  151. package/dist/core-web/src/components/ui/skeleton.d.ts +3 -0
  152. package/dist/core-web/src/components/ui/skeleton.d.ts.map +1 -0
  153. package/dist/core-web/src/components/ui/skeleton.js +7 -0
  154. package/dist/core-web/src/components/ui/skeleton.js.map +1 -0
  155. package/dist/core-web/src/components/ui/textarea.d.ts +4 -0
  156. package/dist/core-web/src/components/ui/textarea.d.ts.map +1 -0
  157. package/dist/core-web/src/components/ui/textarea.js +7 -0
  158. package/dist/core-web/src/components/ui/textarea.js.map +1 -0
  159. package/dist/core-web/src/components/widgets/agent/Generative.d.ts +13 -0
  160. package/dist/core-web/src/components/widgets/agent/Generative.d.ts.map +1 -0
  161. package/dist/core-web/src/components/widgets/agent/Generative.js +42 -0
  162. package/dist/core-web/src/components/widgets/agent/Generative.js.map +1 -0
  163. package/dist/core-web/src/components/widgets/agent/Primitives.d.ts +79 -0
  164. package/dist/core-web/src/components/widgets/agent/Primitives.d.ts.map +1 -0
  165. package/dist/core-web/src/components/widgets/agent/Primitives.js +116 -0
  166. package/dist/core-web/src/components/widgets/agent/Primitives.js.map +1 -0
  167. package/dist/core-web/src/components/widgets/agent/actions.d.ts +3 -0
  168. package/dist/core-web/src/components/widgets/agent/actions.d.ts.map +1 -0
  169. package/dist/core-web/src/components/widgets/agent/actions.js +33 -0
  170. package/dist/core-web/src/components/widgets/agent/actions.js.map +1 -0
  171. package/dist/core-web/src/hooks/useAuth.d.ts +25 -0
  172. package/dist/core-web/src/hooks/useAuth.d.ts.map +1 -0
  173. package/dist/core-web/src/hooks/useAuth.js +53 -0
  174. package/dist/core-web/src/hooks/useAuth.js.map +1 -0
  175. package/dist/core-web/src/hooks/useData.d.ts +10 -0
  176. package/dist/core-web/src/hooks/useData.d.ts.map +1 -0
  177. package/dist/core-web/src/hooks/useData.js +37 -0
  178. package/dist/core-web/src/hooks/useData.js.map +1 -0
  179. package/dist/core-web/src/hooks/useMountEffect.d.ts +6 -0
  180. package/dist/core-web/src/hooks/useMountEffect.d.ts.map +1 -0
  181. package/dist/core-web/src/hooks/useMountEffect.js +10 -0
  182. package/dist/core-web/src/hooks/useMountEffect.js.map +1 -0
  183. package/dist/core-web/src/hooks/useTheme.d.ts +6 -0
  184. package/dist/core-web/src/hooks/useTheme.d.ts.map +1 -0
  185. package/dist/core-web/src/hooks/useTheme.js +31 -0
  186. package/dist/core-web/src/hooks/useTheme.js.map +1 -0
  187. package/dist/core-web/src/index.d.ts +33 -0
  188. package/dist/core-web/src/index.d.ts.map +1 -0
  189. package/dist/core-web/src/index.js +38 -0
  190. package/dist/core-web/src/index.js.map +1 -0
  191. package/dist/core-web/src/lib/api.d.ts +60 -0
  192. package/dist/core-web/src/lib/api.d.ts.map +1 -0
  193. package/dist/core-web/src/lib/api.js +204 -0
  194. package/dist/core-web/src/lib/api.js.map +1 -0
  195. package/dist/core-web/src/lib/config.d.ts +10 -0
  196. package/dist/core-web/src/lib/config.d.ts.map +1 -0
  197. package/dist/core-web/src/lib/config.js +10 -0
  198. package/dist/core-web/src/lib/config.js.map +1 -0
  199. package/dist/core-web/src/lib/define-widget.d.ts +52 -0
  200. package/dist/core-web/src/lib/define-widget.d.ts.map +1 -0
  201. package/dist/core-web/src/lib/define-widget.js +14 -0
  202. package/dist/core-web/src/lib/define-widget.js.map +1 -0
  203. package/dist/core-web/src/lib/drag.d.ts +20 -0
  204. package/dist/core-web/src/lib/drag.d.ts.map +1 -0
  205. package/dist/core-web/src/lib/drag.js +33 -0
  206. package/dist/core-web/src/lib/drag.js.map +1 -0
  207. package/dist/core-web/src/lib/supabase.d.ts +2 -0
  208. package/dist/core-web/src/lib/supabase.d.ts.map +1 -0
  209. package/dist/core-web/src/lib/supabase.js +5 -0
  210. package/dist/core-web/src/lib/supabase.js.map +1 -0
  211. package/dist/core-web/src/lib/utils.d.ts +3 -0
  212. package/dist/core-web/src/lib/utils.d.ts.map +1 -0
  213. package/dist/core-web/src/lib/utils.js +6 -0
  214. package/dist/core-web/src/lib/utils.js.map +1 -0
  215. package/dist/core-web/src/lib/widgetCache.d.ts +18 -0
  216. package/dist/core-web/src/lib/widgetCache.d.ts.map +1 -0
  217. package/dist/core-web/src/lib/widgetCache.js +28 -0
  218. package/dist/core-web/src/lib/widgetCache.js.map +1 -0
  219. package/dist/mcp.d.ts +2 -0
  220. package/dist/mcp.d.ts.map +1 -0
  221. package/dist/mcp.js +2 -0
  222. package/dist/mcp.js.map +1 -0
  223. package/dist/shared.d.ts +2 -0
  224. package/dist/shared.d.ts.map +1 -0
  225. package/dist/shared.js +2 -0
  226. package/dist/shared.js.map +1 -0
  227. package/dist/web.d.ts +2 -0
  228. package/dist/web.d.ts.map +1 -0
  229. package/dist/web.js +2 -0
  230. package/dist/web.js.map +1 -0
  231. package/package.json +62 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * ProtoApp — zero-config React app component.
3
+ *
4
+ * Wraps Shell with auth, entity management, and standard routing.
5
+ * The developer only passes their widgets array — everything else
6
+ * is handled by the framework.
7
+ *
8
+ * Usage:
9
+ *
10
+ * import { ProtoApp, defineWidget } from 'proto/web'
11
+ *
12
+ * const widgets = [
13
+ * defineWidget({ type: 'items', title: 'Items', ... }),
14
+ * ]
15
+ *
16
+ * export default function App() {
17
+ * return <ProtoApp widgets={widgets} />
18
+ * }
19
+ */
20
+ import { useState, useCallback, useRef, useMemo } from 'react'
21
+ import Shell, { type CockpitDefinition } from './components/Shell'
22
+ import { useAuth } from './hooks/useAuth'
23
+ import { useTheme } from './hooks/useTheme'
24
+ import { buildWidgetRegistry, type WidgetDefinition } from './lib/define-widget'
25
+ import { protoSocket } from './lib/api'
26
+ import type { EntityDefinition } from '../../core-shared/src/index.js'
27
+ import type { ActiveEntity, WidgetInstance } from './components/shell/types'
28
+
29
+ export interface ProtoAppProps {
30
+ /** Widget definitions — the core of your app's UI. */
31
+ widgets: WidgetDefinition[]
32
+
33
+ /** Entity definitions — for cockpit mode (optional). */
34
+ entities?: EntityDefinition[]
35
+
36
+ /** Default widgets shown on first load. If omitted, shows all general widgets. */
37
+ defaultWidgets?: WidgetInstance[]
38
+
39
+ /** Default grid layouts. If omitted, auto-generates a simple grid. */
40
+ defaultLayouts?: Record<string, unknown[]>
41
+
42
+ /** App display name (shown in header). */
43
+ appName?: string
44
+
45
+ /** Login component override. */
46
+ loginComponent?: React.ComponentType
47
+ }
48
+
49
+ function DefaultLogin() {
50
+ return (
51
+ <div className="flex h-screen items-center justify-center text-muted-foreground">
52
+ Please sign in.
53
+ </div>
54
+ )
55
+ }
56
+
57
+ export function ProtoApp({
58
+ widgets: widgetDefs,
59
+ entities = [],
60
+ defaultWidgets: defaultWidgetsProp,
61
+ defaultLayouts: defaultLayoutsProp,
62
+ appName,
63
+ loginComponent: LoginComponent = DefaultLogin,
64
+ }: ProtoAppProps) {
65
+ useTheme()
66
+
67
+ const { user, companyId, companies, profile, loading, signOut, setCompanyId } = useAuth()
68
+ const [refreshKey, setRefreshKey] = useState(0)
69
+ const chatSendRef = useRef<((msg: string) => void) | null>(null)
70
+
71
+ type Entity = { type: string; id: string; label: string }
72
+ const [activeEntity, setActiveEntity] = useState<Entity | null>(null)
73
+ const [openEntities, setOpenEntities] = useState<Entity[]>([])
74
+
75
+ const widgetRegistry = useMemo(() => buildWidgetRegistry(widgetDefs), [widgetDefs])
76
+
77
+ const cockpits = useMemo<Record<string, CockpitDefinition>>(() =>
78
+ Object.fromEntries(
79
+ entities
80
+ .filter(e => !!e.cockpit)
81
+ .map(e => [e.name, { widgets: e.cockpit!.widgets, layouts: e.cockpit!.layouts }])
82
+ ), [entities])
83
+
84
+ // Auto-generate defaults from general widgets if not provided
85
+ const defaultWidgets = useMemo(() => {
86
+ if (defaultWidgetsProp) return defaultWidgetsProp
87
+ return widgetDefs
88
+ .filter(w => w.category === 'general')
89
+ .map((w, i) => ({ id: `${w.type}-${i}`, type: w.type, title: w.title }))
90
+ }, [widgetDefs, defaultWidgetsProp])
91
+
92
+ const defaultLayouts = useMemo(() => {
93
+ if (defaultLayoutsProp) return defaultLayoutsProp
94
+ const lg = defaultWidgets.map((w, i) => ({
95
+ i: w.id, x: (i * 4) % 10, y: Math.floor(i / 2) * 5, w: 4, h: 5, minW: 2, minH: 3,
96
+ }))
97
+ return { lg, md: lg, sm: lg }
98
+ }, [defaultWidgets, defaultLayoutsProp])
99
+
100
+ const onSendToChat = useCallback((message: string) => {
101
+ chatSendRef.current?.(message)
102
+ }, [])
103
+
104
+ const activateEntity = useCallback((e: ActiveEntity) => {
105
+ setActiveEntity(e as Entity)
106
+ setOpenEntities(prev => {
107
+ const exists = prev.find(p => p.type === e.type && p.id === e.id)
108
+ return exists ? prev : [...prev, e as Entity]
109
+ })
110
+ }, [])
111
+
112
+ const closeEntityTab = useCallback((e: ActiveEntity) => {
113
+ setOpenEntities(prev => {
114
+ const next = prev.filter(p => !(p.type === e.type && p.id === e.id))
115
+ setActiveEntity(curr =>
116
+ (curr && curr.type === e.type && curr.id === e.id)
117
+ ? (next[next.length - 1] || null)
118
+ : curr
119
+ )
120
+ return next
121
+ })
122
+ }, [])
123
+
124
+ // WebSocket setup
125
+ const wsSetup = useRef(false)
126
+ if (!wsSetup.current && user) {
127
+ wsSetup.current = true
128
+ protoSocket.connect().catch(() => {})
129
+ protoSocket.onShellRefresh(() => setRefreshKey(k => k + 1))
130
+ }
131
+
132
+ if (loading) {
133
+ return <div className="flex h-screen items-center justify-center text-muted-foreground">Loading...</div>
134
+ }
135
+
136
+ if (!user) {
137
+ return <LoginComponent />
138
+ }
139
+
140
+ const effectiveCompanyId = companyId || user.id
141
+
142
+ return (
143
+ <Shell
144
+ widgets={widgetRegistry}
145
+ defaultWidgets={defaultWidgets}
146
+ defaultLayouts={defaultLayouts}
147
+ cockpits={cockpits}
148
+ companyId={effectiveCompanyId}
149
+ refreshKey={refreshKey}
150
+ onSendToChat={onSendToChat}
151
+ activeEntity={activeEntity}
152
+ onActivateEntity={activateEntity}
153
+ onDeactivateEntity={() => setActiveEntity(null)}
154
+ openEntities={openEntities}
155
+ onCloseTab={closeEntityTab}
156
+ companies={companies}
157
+ effectiveCompanyId={effectiveCompanyId}
158
+ setCompanyId={setCompanyId}
159
+ onSignOut={signOut}
160
+ userEmail={profile?.full_name || user.email || ''}
161
+ />
162
+ )
163
+ }
@@ -0,0 +1,276 @@
1
+ import { useState, useCallback, useRef, useMemo, type ReactNode } from 'react'
2
+ import { useMountEffect } from '../hooks/useMountEffect'
3
+ import { ResponsiveGridLayout, type Layout } from 'react-grid-layout'
4
+ import 'react-grid-layout/css/styles.css'
5
+ import { XIcon } from 'lucide-react'
6
+ import { loadShellState, saveShellState, clearShellState } from './shell/persistence'
7
+ import { Toolbar } from './shell/Toolbar'
8
+ import { FocusView } from './shell/FocusView'
9
+ import { EmptyState } from './shell/EmptyState'
10
+ import type { ActiveEntity, WidgetInstance, WidgetType } from './shell/types'
11
+ import type { ShellContext, WidgetRegistry } from '../lib/define-widget'
12
+
13
+ export type { WidgetType, ActiveEntity } from './shell/types'
14
+
15
+ function useContainerWidth(ref: React.RefObject<HTMLDivElement | null>) {
16
+ const [width, setWidth] = useState(800)
17
+ useMountEffect(() => {
18
+ if (!ref.current) return
19
+ const observer = new ResizeObserver(entries => {
20
+ for (const entry of entries) setWidth(entry.contentRect.width)
21
+ })
22
+ observer.observe(ref.current)
23
+ setWidth(ref.current.clientWidth)
24
+ return () => observer.disconnect()
25
+ })
26
+ return width
27
+ }
28
+
29
+ export interface CockpitDefinition {
30
+ widgets: WidgetInstance[]
31
+ layouts: any
32
+ }
33
+
34
+ interface Props {
35
+ // Widget registry + layouts (app-provided)
36
+ widgets: WidgetRegistry
37
+ defaultWidgets: WidgetInstance[]
38
+ defaultLayouts: any
39
+ /** Cockpit definitions keyed by activeEntity.type. */
40
+ cockpits?: Record<string, CockpitDefinition>
41
+
42
+ // Framework context fields
43
+ companyId: string
44
+ refreshKey: number
45
+ onSendToChat: (message: string) => void
46
+ agentView?: { spec: any; title?: string } | null
47
+ onAgentDismiss?: () => void
48
+ activeEntity?: ActiveEntity | null
49
+ onActivateEntity?: (e: ActiveEntity) => void
50
+ onDeactivateEntity?: () => void
51
+ openEntities?: ActiveEntity[]
52
+ onCloseTab?: (e: ActiveEntity) => void
53
+
54
+ // Toolbar
55
+ role?: string | null
56
+ companies?: Array<{ id: string; name: string }>
57
+ effectiveCompanyId?: string
58
+ setCompanyId?: (id: string) => void
59
+ onSignOut?: () => void
60
+ userEmail?: string
61
+ onOpenSettings?: () => void
62
+ toolbarExtras?: ReactNode
63
+
64
+ /** App-specific ShellContext fields — merged into the ctx passed to widget.render. */
65
+ contextExtras?: Record<string, unknown>
66
+ /** App-owned overlays (modals, floating buttons) rendered as Shell children. */
67
+ overlays?: ReactNode
68
+ }
69
+
70
+ export default function Shell({
71
+ widgets: widgetRegistry,
72
+ defaultWidgets,
73
+ defaultLayouts,
74
+ cockpits,
75
+ companyId, refreshKey, onSendToChat,
76
+ agentView, onAgentDismiss,
77
+ activeEntity, onActivateEntity, onDeactivateEntity,
78
+ openEntities, onCloseTab,
79
+ role, companies, effectiveCompanyId, setCompanyId, onSignOut, userEmail,
80
+ onOpenSettings, toolbarExtras,
81
+ contextExtras, overlays,
82
+ }: Props) {
83
+ const focusMode = !!agentView
84
+ const activeCockpit = activeEntity ? cockpits?.[activeEntity.type] : undefined
85
+ const cockpitMode = !!activeCockpit && !focusMode
86
+
87
+ const containerRef = useRef<HTMLDivElement>(null)
88
+ const containerWidth = useContainerWidth(containerRef)
89
+
90
+ const [localRefresh, setLocalRefresh] = useState(0)
91
+ const effectiveRefreshKey = refreshKey + localRefresh
92
+
93
+ const saved = loadShellState()
94
+ const [widgets, setWidgets] = useState<WidgetInstance[]>(saved?.widgets || defaultWidgets)
95
+ const [layouts, setLayouts] = useState<any>(saved?.layouts || { ...defaultLayouts })
96
+
97
+ const addWidget = useCallback((type: WidgetType) => {
98
+ const id = `${type}-${Date.now()}`
99
+ const def = widgetRegistry.get(type)
100
+ const size = def?.defaultSize || { w: 3, h: 4, minW: 2, minH: 3 }
101
+ const widget: WidgetInstance = { id, type, title: def?.title || type }
102
+
103
+ setWidgets(prev => [...prev, widget])
104
+ setLayouts((prev: any) => {
105
+ const next = { ...prev, lg: [...(prev.lg || []), { i: id, x: 0, y: Infinity, ...size }] }
106
+ return next
107
+ })
108
+ }, [widgetRegistry])
109
+
110
+ const removeWidget = useCallback((id: string) => {
111
+ setWidgets(prev => prev.filter(w => w.id !== id))
112
+ setLayouts((prev: any) => ({
113
+ ...prev,
114
+ lg: (prev.lg || []).filter((l: any) => l.i !== id),
115
+ }))
116
+ }, [])
117
+
118
+ const resetShell = useCallback(() => {
119
+ clearShellState()
120
+ setWidgets([...defaultWidgets])
121
+ setLayouts({ ...defaultLayouts })
122
+ }, [defaultWidgets, defaultLayouts])
123
+
124
+ const widgetsRef = useRef(widgets)
125
+ widgetsRef.current = widgets
126
+ const layoutsRef = useRef(layouts)
127
+ layoutsRef.current = layouts
128
+
129
+ const persistState = useCallback(() => {
130
+ saveShellState(widgetsRef.current, layoutsRef.current)
131
+ }, [])
132
+
133
+ const prevWidgetCount = useRef(widgets.length)
134
+ if (widgets.length !== prevWidgetCount.current) {
135
+ prevWidgetCount.current = widgets.length
136
+ persistState()
137
+ }
138
+
139
+ function onLayoutChange(_layout: any, allLayouts: any) {
140
+ setLayouts(allLayouts)
141
+ saveShellState(widgetsRef.current, allLayouts)
142
+ }
143
+
144
+ const triggerLocalRefresh = useCallback(() => setLocalRefresh(k => k + 1), [])
145
+
146
+ const shellCtx = useMemo<ShellContext>(() => ({
147
+ companyId,
148
+ refreshKey: effectiveRefreshKey,
149
+ activeEntity: activeEntity || null,
150
+ onSendToChat,
151
+ onActivateEntity,
152
+ onDeactivateEntity,
153
+ onCloseTab,
154
+ triggerLocalRefresh,
155
+ ...(contextExtras || {}),
156
+ } as ShellContext), [
157
+ companyId, effectiveRefreshKey, activeEntity,
158
+ onSendToChat, onActivateEntity, onDeactivateEntity, onCloseTab,
159
+ triggerLocalRefresh, contextExtras,
160
+ ])
161
+
162
+ function renderWidget(widget: WidgetInstance) {
163
+ const def = widgetRegistry.get(widget.type)
164
+ if (!def) {
165
+ return <p className="text-xs text-muted-foreground p-2">Widget "{widget.type}" not found.</p>
166
+ }
167
+ return def.render(widget, shellCtx)
168
+ }
169
+
170
+ const widgetCatalog = useMemo(
171
+ () => Array.from(widgetRegistry.values())
172
+ .filter(w => w.category === 'general')
173
+ .map(w => ({ type: w.type, title: w.title, icon: w.icon || '▦' })),
174
+ [widgetRegistry]
175
+ )
176
+
177
+ return (
178
+ <div ref={containerRef} id="shell-root" className="h-full overflow-y-auto scrollbar-thin bg-background dotted-bg relative">
179
+ <Toolbar
180
+ widgetCount={widgets.length}
181
+ cockpitMode={cockpitMode}
182
+ activeEntity={activeEntity}
183
+ onDeactivateEntity={onDeactivateEntity}
184
+ onReset={resetShell}
185
+ onAddWidget={addWidget}
186
+ widgetCatalog={widgetCatalog}
187
+ onOpenSettings={onOpenSettings}
188
+ openEntities={openEntities}
189
+ onSelectEntity={(e) => onActivateEntity?.(e)}
190
+ onCloseTab={onCloseTab}
191
+ role={role}
192
+ companies={companies}
193
+ effectiveCompanyId={effectiveCompanyId}
194
+ setCompanyId={setCompanyId}
195
+ onSignOut={onSignOut}
196
+ userEmail={userEmail}
197
+ rightActions={toolbarExtras}
198
+ />
199
+
200
+ {focusMode && agentView && (
201
+ <FocusView
202
+ spec={agentView.spec}
203
+ title={agentView.title}
204
+ widgets={widgets}
205
+ onDismiss={onAgentDismiss}
206
+ onSendToChat={onSendToChat}
207
+ />
208
+ )}
209
+
210
+ {cockpitMode && activeCockpit && (
211
+ <ResponsiveGridLayout
212
+ className="p-2"
213
+ width={containerWidth - 16}
214
+ breakpoints={{ lg: 800, md: 600, sm: 0 }}
215
+ cols={{ lg: 10, md: 6, sm: 4 }}
216
+ rowHeight={60}
217
+ layouts={activeCockpit.layouts}
218
+ dragConfig={{ enabled: false, bounded: false }}
219
+ resizeConfig={{ enabled: false }}
220
+ margin={[8, 8]}
221
+ >
222
+ {activeCockpit.widgets.map(widget => (
223
+ <div key={widget.id} className="bg-card border border-primary/20 rounded-lg overflow-hidden flex flex-col shadow-sm shadow-primary/5">
224
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-border/50 bg-gradient-to-r from-primary/5 to-transparent">
225
+ <span className="text-sm font-medium text-muted-foreground">{widget.title}</span>
226
+ </div>
227
+ <div className="flex-1 overflow-y-auto scrollbar-thin p-2 shell-content">
228
+ {renderWidget(widget)}
229
+ </div>
230
+ </div>
231
+ ))}
232
+ </ResponsiveGridLayout>
233
+ )}
234
+
235
+ {!focusMode && widgets.length === 0 && !cockpitMode && (
236
+ <EmptyState onAddWidget={addWidget} widgetCatalog={widgetCatalog} />
237
+ )}
238
+
239
+ {!focusMode && (
240
+ <div className={cockpitMode ? 'hidden' : ''}>
241
+ <ResponsiveGridLayout
242
+ className="p-2"
243
+ width={containerWidth - 16}
244
+ breakpoints={{ lg: 800, md: 600, sm: 0 }}
245
+ cols={{ lg: 10, md: 6, sm: 4 }}
246
+ rowHeight={60}
247
+ layouts={layouts}
248
+ onLayoutChange={onLayoutChange}
249
+ dragConfig={{ enabled: true, handle: '.widget-drag-handle', bounded: false }}
250
+ margin={[8, 8]}
251
+ >
252
+ {widgets.map(widget => (
253
+ <div key={widget.id} className="bg-card border border-border rounded-lg overflow-hidden flex flex-col">
254
+ <div className="widget-drag-handle flex items-center justify-between px-3 py-1.5 border-b border-border/50 bg-card cursor-grab active:cursor-grabbing">
255
+ <span className="text-sm font-medium text-muted-foreground select-none">{widget.title}</span>
256
+ <button
257
+ onClick={() => removeWidget(widget.id)}
258
+ className="p-1 -m-1 text-muted-foreground/40 hover:text-foreground transition-colors"
259
+ aria-label="Cerrar widget"
260
+ >
261
+ <XIcon className="w-4 h-4" />
262
+ </button>
263
+ </div>
264
+ <div className="flex-1 overflow-y-auto scrollbar-thin p-2 shell-content">
265
+ {renderWidget(widget)}
266
+ </div>
267
+ </div>
268
+ ))}
269
+ </ResponsiveGridLayout>
270
+ </div>
271
+ )}
272
+
273
+ {overlays}
274
+ </div>
275
+ )
276
+ }
@@ -0,0 +1,33 @@
1
+ import { PlusIcon } from 'lucide-react'
2
+ import { Button } from '../ui/button'
3
+ import type { WidgetType } from './types'
4
+
5
+ interface CatalogEntry {
6
+ type: WidgetType
7
+ title: string
8
+ icon: string
9
+ }
10
+
11
+ interface Props {
12
+ onAddWidget: (type: WidgetType) => void
13
+ widgetCatalog: CatalogEntry[]
14
+ }
15
+
16
+ export function EmptyState({ onAddWidget, widgetCatalog }: Props) {
17
+ return (
18
+ <div className="flex flex-col items-center justify-center h-[calc(100%-40px)] text-center px-4">
19
+ <div className="w-12 h-12 rounded-xl border-2 border-dashed border-border flex items-center justify-center mb-3">
20
+ <PlusIcon className="w-5 h-5 text-muted-foreground/30" />
21
+ </div>
22
+ <p className="text-sm text-muted-foreground/60 mb-1">Shell vacio</p>
23
+ <p className="text-xs text-muted-foreground/30 mb-4">Agrega widgets para ver tus datos.</p>
24
+ <div className="flex flex-wrap gap-1.5 justify-center">
25
+ {widgetCatalog.map(w => (
26
+ <Button key={w.type} variant="outline" size="sm" className="h-7 text-xs gap-1.5" onClick={() => onAddWidget(w.type)}>
27
+ <span>{w.icon}</span> {w.title}
28
+ </Button>
29
+ ))}
30
+ </div>
31
+ </div>
32
+ )
33
+ }
@@ -0,0 +1,55 @@
1
+ import { XIcon, SparklesIcon } from 'lucide-react'
2
+ import { Generative } from '../widgets/agent/Generative'
3
+ import type { WidgetInstance } from './types'
4
+
5
+ interface Props {
6
+ spec: any
7
+ title?: string
8
+ widgets: WidgetInstance[]
9
+ onDismiss?: () => void
10
+ onSendToChat: (msg: string) => void
11
+ }
12
+
13
+ /**
14
+ * Agent-generated focused view — replaces the normal widget grid while active.
15
+ * Shows a minimized strip of the original widgets for context so the user can
16
+ * see they're in a temporary view and not lost their layout.
17
+ */
18
+ export function FocusView({ spec, title, widgets, onDismiss, onSendToChat }: Props) {
19
+ return (
20
+ <div className="p-2">
21
+ {/* Minimized strip of original widgets */}
22
+ <div className="flex items-center gap-1.5 mb-2 overflow-x-auto scrollbar-thin pb-1">
23
+ <span className="text-[10px] text-muted-foreground/60 flex-shrink-0">Widgets:</span>
24
+ {widgets.map(w => (
25
+ <span
26
+ key={w.id}
27
+ className="flex-shrink-0 text-[10px] px-2 py-0.5 rounded-full border border-border/60 bg-card text-muted-foreground"
28
+ >
29
+ {w.title}
30
+ </span>
31
+ ))}
32
+ </div>
33
+
34
+ {/* Focused agent widget */}
35
+ <div className="bg-card border border-primary/30 rounded-lg overflow-hidden flex flex-col shadow-lg shadow-primary/5">
36
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-border/50 bg-gradient-to-r from-primary/5 to-transparent">
37
+ <div className="flex items-center gap-1.5">
38
+ <SparklesIcon className="w-3 h-3 text-primary" />
39
+ <span className="text-sm font-medium">{title || 'Vista generada'}</span>
40
+ </div>
41
+ <button
42
+ onClick={onDismiss}
43
+ className="p-1 -m-1 text-muted-foreground/40 hover:text-foreground transition-colors"
44
+ aria-label="Cerrar vista"
45
+ >
46
+ <XIcon className="w-4 h-4" />
47
+ </button>
48
+ </div>
49
+ <div className="flex-1 overflow-y-auto scrollbar-thin p-3">
50
+ <Generative spec={spec} onSendToChat={onSendToChat} />
51
+ </div>
52
+ </div>
53
+ </div>
54
+ )
55
+ }