@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.
- package/core-web/src/ProtoApp.tsx +163 -0
- package/core-web/src/components/Shell.tsx +276 -0
- package/core-web/src/components/shell/EmptyState.tsx +33 -0
- package/core-web/src/components/shell/FocusView.tsx +55 -0
- package/core-web/src/components/shell/Toolbar.tsx +233 -0
- package/core-web/src/components/shell/persistence.ts +20 -0
- package/core-web/src/components/shell/types.ts +14 -0
- package/core-web/src/components/ui/avatar.tsx +18 -0
- package/core-web/src/components/ui/badge.tsx +28 -0
- package/core-web/src/components/ui/button.tsx +40 -0
- package/core-web/src/components/ui/card.tsx +32 -0
- package/core-web/src/components/ui/inline-edit.tsx +120 -0
- package/core-web/src/components/ui/input.tsx +18 -0
- package/core-web/src/components/ui/scroll-area.tsx +12 -0
- package/core-web/src/components/ui/separator.tsx +23 -0
- package/core-web/src/components/ui/shell-dialog.tsx +79 -0
- package/core-web/src/components/ui/skeleton.tsx +9 -0
- package/core-web/src/components/ui/textarea.tsx +17 -0
- package/core-web/src/components/widgets/agent/Generative.tsx +74 -0
- package/core-web/src/components/widgets/agent/Primitives.tsx +225 -0
- package/core-web/src/components/widgets/agent/actions.ts +52 -0
- package/core-web/src/hooks/useAuth.ts +80 -0
- package/core-web/src/hooks/useData.ts +44 -0
- package/core-web/src/hooks/useMountEffect.ts +10 -0
- package/core-web/src/hooks/useTheme.ts +37 -0
- package/core-web/src/index.ts +52 -0
- package/core-web/src/lib/api.ts +231 -0
- package/core-web/src/lib/config.ts +14 -0
- package/core-web/src/lib/define-widget.ts +71 -0
- package/core-web/src/lib/drag.ts +45 -0
- package/core-web/src/lib/supabase.ts +6 -0
- package/core-web/src/lib/utils.ts +6 -0
- package/core-web/src/lib/widgetCache.ts +29 -0
- package/core-web/src/vite-env.d.ts +1 -0
- package/dist/core-mcp/src/app.d.ts +40 -0
- package/dist/core-mcp/src/app.d.ts.map +1 -0
- package/dist/core-mcp/src/app.js +141 -0
- package/dist/core-mcp/src/app.js.map +1 -0
- package/dist/core-mcp/src/define-tool.d.ts +70 -0
- package/dist/core-mcp/src/define-tool.d.ts.map +1 -0
- package/dist/core-mcp/src/define-tool.js +38 -0
- package/dist/core-mcp/src/define-tool.js.map +1 -0
- package/dist/core-mcp/src/entity-tools.d.ts +27 -0
- package/dist/core-mcp/src/entity-tools.d.ts.map +1 -0
- package/dist/core-mcp/src/entity-tools.js +99 -0
- package/dist/core-mcp/src/entity-tools.js.map +1 -0
- package/dist/core-mcp/src/index.d.ts +36 -0
- package/dist/core-mcp/src/index.d.ts.map +1 -0
- package/dist/core-mcp/src/index.js +116 -0
- package/dist/core-mcp/src/index.js.map +1 -0
- package/dist/core-mcp/src/supabase.d.ts +7 -0
- package/dist/core-mcp/src/supabase.d.ts.map +1 -0
- package/dist/core-mcp/src/supabase.js +18 -0
- package/dist/core-mcp/src/supabase.js.map +1 -0
- package/dist/core-mcp/src/tools/_helpers.d.ts +44 -0
- package/dist/core-mcp/src/tools/_helpers.d.ts.map +1 -0
- package/dist/core-mcp/src/tools/_helpers.js +23 -0
- package/dist/core-mcp/src/tools/_helpers.js.map +1 -0
- package/dist/core-mcp/src/tools/ui.d.ts +9 -0
- package/dist/core-mcp/src/tools/ui.d.ts.map +1 -0
- package/dist/core-mcp/src/tools/ui.js +100 -0
- package/dist/core-mcp/src/tools/ui.js.map +1 -0
- package/dist/core-mcp/src/workflow-tools.d.ts +41 -0
- package/dist/core-mcp/src/workflow-tools.d.ts.map +1 -0
- package/dist/core-mcp/src/workflow-tools.js +382 -0
- package/dist/core-mcp/src/workflow-tools.js.map +1 -0
- package/dist/core-shared/src/define-entity.d.ts +73 -0
- package/dist/core-shared/src/define-entity.d.ts.map +1 -0
- package/dist/core-shared/src/define-entity.js +47 -0
- package/dist/core-shared/src/define-entity.js.map +1 -0
- package/dist/core-shared/src/define-workflow.d.ts +111 -0
- package/dist/core-shared/src/define-workflow.d.ts.map +1 -0
- package/dist/core-shared/src/define-workflow.js +92 -0
- package/dist/core-shared/src/define-workflow.js.map +1 -0
- package/dist/core-shared/src/index.d.ts +5 -0
- package/dist/core-shared/src/index.d.ts.map +1 -0
- package/dist/core-shared/src/index.js +7 -0
- package/dist/core-shared/src/index.js.map +1 -0
- package/dist/core-shared/src/scheduling.d.ts +69 -0
- package/dist/core-shared/src/scheduling.d.ts.map +1 -0
- package/dist/core-shared/src/scheduling.js +39 -0
- package/dist/core-shared/src/scheduling.js.map +1 -0
- package/dist/core-shared/src/schemas.d.ts +51 -0
- package/dist/core-shared/src/schemas.d.ts.map +1 -0
- package/dist/core-shared/src/schemas.js +18 -0
- package/dist/core-shared/src/schemas.js.map +1 -0
- package/dist/core-web/src/ProtoApp.d.ts +19 -0
- package/dist/core-web/src/ProtoApp.d.ts.map +1 -0
- package/dist/core-web/src/ProtoApp.js +92 -0
- package/dist/core-web/src/ProtoApp.js.map +1 -0
- package/dist/core-web/src/components/Shell.d.ts +46 -0
- package/dist/core-web/src/components/Shell.d.ts.map +1 -0
- package/dist/core-web/src/components/Shell.js +104 -0
- package/dist/core-web/src/components/Shell.js.map +1 -0
- package/dist/core-web/src/components/shell/EmptyState.d.ts +13 -0
- package/dist/core-web/src/components/shell/EmptyState.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/EmptyState.js +7 -0
- package/dist/core-web/src/components/shell/EmptyState.js.map +1 -0
- package/dist/core-web/src/components/shell/FocusView.d.ts +16 -0
- package/dist/core-web/src/components/shell/FocusView.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/FocusView.js +12 -0
- package/dist/core-web/src/components/shell/FocusView.js.map +1 -0
- package/dist/core-web/src/components/shell/Toolbar.d.ts +35 -0
- package/dist/core-web/src/components/shell/Toolbar.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/Toolbar.js +42 -0
- package/dist/core-web/src/components/shell/Toolbar.js.map +1 -0
- package/dist/core-web/src/components/shell/persistence.d.ts +8 -0
- package/dist/core-web/src/components/shell/persistence.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/persistence.js +20 -0
- package/dist/core-web/src/components/shell/persistence.js.map +1 -0
- package/dist/core-web/src/components/shell/types.d.ts +13 -0
- package/dist/core-web/src/components/shell/types.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/types.js +2 -0
- package/dist/core-web/src/components/shell/types.js.map +1 -0
- package/dist/core-web/src/components/ui/avatar.d.ts +5 -0
- package/dist/core-web/src/components/ui/avatar.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/avatar.js +9 -0
- package/dist/core-web/src/components/ui/avatar.js.map +1 -0
- package/dist/core-web/src/components/ui/badge.d.ts +13 -0
- package/dist/core-web/src/components/ui/badge.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/badge.js +13 -0
- package/dist/core-web/src/components/ui/badge.js.map +1 -0
- package/dist/core-web/src/components/ui/button.d.ts +22 -0
- package/dist/core-web/src/components/ui/button.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/button.js +21 -0
- package/dist/core-web/src/components/ui/button.js.map +1 -0
- package/dist/core-web/src/components/ui/card.d.ts +7 -0
- package/dist/core-web/src/components/ui/card.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/card.js +13 -0
- package/dist/core-web/src/components/ui/card.js.map +1 -0
- package/dist/core-web/src/components/ui/inline-edit.d.ts +20 -0
- package/dist/core-web/src/components/ui/inline-edit.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/inline-edit.js +63 -0
- package/dist/core-web/src/components/ui/inline-edit.js.map +1 -0
- package/dist/core-web/src/components/ui/input.d.ts +4 -0
- package/dist/core-web/src/components/ui/input.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/input.js +7 -0
- package/dist/core-web/src/components/ui/input.js.map +1 -0
- package/dist/core-web/src/components/ui/scroll-area.d.ts +4 -0
- package/dist/core-web/src/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/scroll-area.js +7 -0
- package/dist/core-web/src/components/ui/scroll-area.js.map +1 -0
- package/dist/core-web/src/components/ui/separator.d.ts +7 -0
- package/dist/core-web/src/components/ui/separator.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/separator.js +7 -0
- package/dist/core-web/src/components/ui/separator.js.map +1 -0
- package/dist/core-web/src/components/ui/shell-dialog.d.ts +16 -0
- package/dist/core-web/src/components/ui/shell-dialog.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/shell-dialog.js +36 -0
- package/dist/core-web/src/components/ui/shell-dialog.js.map +1 -0
- package/dist/core-web/src/components/ui/skeleton.d.ts +3 -0
- package/dist/core-web/src/components/ui/skeleton.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/skeleton.js +7 -0
- package/dist/core-web/src/components/ui/skeleton.js.map +1 -0
- package/dist/core-web/src/components/ui/textarea.d.ts +4 -0
- package/dist/core-web/src/components/ui/textarea.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/textarea.js +7 -0
- package/dist/core-web/src/components/ui/textarea.js.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Generative.d.ts +13 -0
- package/dist/core-web/src/components/widgets/agent/Generative.d.ts.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Generative.js +42 -0
- package/dist/core-web/src/components/widgets/agent/Generative.js.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.d.ts +79 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.d.ts.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.js +116 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.js.map +1 -0
- package/dist/core-web/src/components/widgets/agent/actions.d.ts +3 -0
- package/dist/core-web/src/components/widgets/agent/actions.d.ts.map +1 -0
- package/dist/core-web/src/components/widgets/agent/actions.js +33 -0
- package/dist/core-web/src/components/widgets/agent/actions.js.map +1 -0
- package/dist/core-web/src/hooks/useAuth.d.ts +25 -0
- package/dist/core-web/src/hooks/useAuth.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useAuth.js +53 -0
- package/dist/core-web/src/hooks/useAuth.js.map +1 -0
- package/dist/core-web/src/hooks/useData.d.ts +10 -0
- package/dist/core-web/src/hooks/useData.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useData.js +37 -0
- package/dist/core-web/src/hooks/useData.js.map +1 -0
- package/dist/core-web/src/hooks/useMountEffect.d.ts +6 -0
- package/dist/core-web/src/hooks/useMountEffect.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useMountEffect.js +10 -0
- package/dist/core-web/src/hooks/useMountEffect.js.map +1 -0
- package/dist/core-web/src/hooks/useTheme.d.ts +6 -0
- package/dist/core-web/src/hooks/useTheme.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useTheme.js +31 -0
- package/dist/core-web/src/hooks/useTheme.js.map +1 -0
- package/dist/core-web/src/index.d.ts +33 -0
- package/dist/core-web/src/index.d.ts.map +1 -0
- package/dist/core-web/src/index.js +38 -0
- package/dist/core-web/src/index.js.map +1 -0
- package/dist/core-web/src/lib/api.d.ts +60 -0
- package/dist/core-web/src/lib/api.d.ts.map +1 -0
- package/dist/core-web/src/lib/api.js +204 -0
- package/dist/core-web/src/lib/api.js.map +1 -0
- package/dist/core-web/src/lib/config.d.ts +10 -0
- package/dist/core-web/src/lib/config.d.ts.map +1 -0
- package/dist/core-web/src/lib/config.js +10 -0
- package/dist/core-web/src/lib/config.js.map +1 -0
- package/dist/core-web/src/lib/define-widget.d.ts +52 -0
- package/dist/core-web/src/lib/define-widget.d.ts.map +1 -0
- package/dist/core-web/src/lib/define-widget.js +14 -0
- package/dist/core-web/src/lib/define-widget.js.map +1 -0
- package/dist/core-web/src/lib/drag.d.ts +20 -0
- package/dist/core-web/src/lib/drag.d.ts.map +1 -0
- package/dist/core-web/src/lib/drag.js +33 -0
- package/dist/core-web/src/lib/drag.js.map +1 -0
- package/dist/core-web/src/lib/supabase.d.ts +2 -0
- package/dist/core-web/src/lib/supabase.d.ts.map +1 -0
- package/dist/core-web/src/lib/supabase.js +5 -0
- package/dist/core-web/src/lib/supabase.js.map +1 -0
- package/dist/core-web/src/lib/utils.d.ts +3 -0
- package/dist/core-web/src/lib/utils.d.ts.map +1 -0
- package/dist/core-web/src/lib/utils.js +6 -0
- package/dist/core-web/src/lib/utils.js.map +1 -0
- package/dist/core-web/src/lib/widgetCache.d.ts +18 -0
- package/dist/core-web/src/lib/widgetCache.d.ts.map +1 -0
- package/dist/core-web/src/lib/widgetCache.js +28 -0
- package/dist/core-web/src/lib/widgetCache.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2 -0
- package/dist/mcp.js.map +1 -0
- package/dist/shared.d.ts +2 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +2 -0
- package/dist/shared.js.map +1 -0
- package/dist/web.d.ts +2 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +2 -0
- package/dist/web.js.map +1 -0
- 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
|
+
}
|