@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,74 @@
1
+ /**
2
+ * Recursive renderer for agent-generated UI specs.
3
+ *
4
+ * The agent sends a JSON tree via the `render_ui` MCP tool. Each node has
5
+ * `{ type: 'PrimitiveName', ...props, children?: UINode[] }`. Unknown types
6
+ * render a muted fallback so the agent can iterate.
7
+ */
8
+ import type { ComponentType, ReactNode } from 'react'
9
+ import * as P from './Primitives'
10
+
11
+ export interface UINode {
12
+ type: string
13
+ children?: UINode[]
14
+ [prop: string]: any
15
+ }
16
+
17
+ const PRIMITIVES: Record<string, ComponentType<any>> = {
18
+ Stack: P.Stack,
19
+ Row: P.Row,
20
+ Grid: P.Grid,
21
+ Heading: P.Heading,
22
+ Text: P.Text,
23
+ Image: P.Image,
24
+ Badge: P.Badge,
25
+ Stat: P.Stat,
26
+ Rating: P.Rating,
27
+ GoldSupplier: P.GoldSupplier,
28
+ Card: P.Card,
29
+ CardBody: P.CardBody,
30
+ LinkOut: P.LinkOut,
31
+ Button: P.Button,
32
+ Table: P.Table,
33
+ }
34
+
35
+ export const KNOWN_PRIMITIVES = Object.keys(PRIMITIVES)
36
+
37
+ interface Props {
38
+ spec: UINode | UINode[] | null | undefined
39
+ onSendToChat?: (message: string) => void
40
+ }
41
+
42
+ export function Generative({ spec, onSendToChat }: Props) {
43
+ if (!spec) return null
44
+ const nodes = Array.isArray(spec) ? spec : [spec]
45
+ return <>{nodes.map((n, i) => renderNode(n, i, onSendToChat))}</>
46
+ }
47
+
48
+ function renderNode(node: UINode, key: number | string, onSendToChat?: (m: string) => void): ReactNode {
49
+ if (!node || typeof node !== 'object') return null
50
+ const { type, children, ...props } = node
51
+ const Cmp = PRIMITIVES[type]
52
+
53
+ if (!Cmp) {
54
+ return (
55
+ <div key={key} className="text-[10px] text-muted-foreground/60 italic">
56
+ [unknown primitive: {String(type)}]
57
+ </div>
58
+ )
59
+ }
60
+
61
+ // Inject onSendToChat into any primitive that accepts it (Button)
62
+ const injectedProps = type === 'Button' ? { ...props, onSendToChat } : props
63
+
64
+ const renderedChildren =
65
+ Array.isArray(children) && children.length > 0
66
+ ? children.map((c, i) => renderNode(c, `${key}-${i}`, onSendToChat))
67
+ : undefined
68
+
69
+ return (
70
+ <Cmp key={key} {...injectedProps}>
71
+ {renderedChildren}
72
+ </Cmp>
73
+ )
74
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Primitive building blocks for agent-generated UIs.
3
+ *
4
+ * Each primitive is a simple React component that accepts typed props and
5
+ * optional `children` (already rendered by Generative). Primitives have no
6
+ * side effects beyond `onSendToChat` for interactive ones.
7
+ */
8
+ import { useState, type ReactNode } from 'react'
9
+ import { ExternalLinkIcon, StarIcon, ShieldCheckIcon, Loader2Icon, CheckIcon } from 'lucide-react'
10
+ import { Badge as UIBadge } from '../../ui/badge'
11
+ import { ACTIONS } from './actions'
12
+
13
+ type OnChat = (message: string) => void
14
+
15
+ // ---------- Layout ----------
16
+
17
+ export function Stack({ gap = 2, children }: { gap?: number; children?: ReactNode }) {
18
+ return <div className={`flex flex-col gap-${Math.min(gap, 6)}`}>{children}</div>
19
+ }
20
+
21
+ export function Row({ gap = 2, align = 'center', children }: { gap?: number; align?: 'start' | 'center' | 'end' | 'baseline'; children?: ReactNode }) {
22
+ const a = { start: 'items-start', center: 'items-center', end: 'items-end', baseline: 'items-baseline' }[align]
23
+ return <div className={`flex flex-row gap-${Math.min(gap, 6)} ${a}`}>{children}</div>
24
+ }
25
+
26
+ export function Grid({ cols = 3, gap = 2, children }: { cols?: 1 | 2 | 3 | 4; gap?: number; children?: ReactNode }) {
27
+ // Default to 3 columns. Tight breakpoints so it actually shows 3 at normal shell widths.
28
+ const c = {
29
+ 1: 'grid-cols-1',
30
+ 2: 'grid-cols-2',
31
+ 3: 'grid-cols-3',
32
+ 4: 'grid-cols-4',
33
+ }[cols] || 'grid-cols-4'
34
+ return <div className={`grid ${c} gap-${Math.min(gap, 6)}`}>{children}</div>
35
+ }
36
+
37
+ // ---------- Typography ----------
38
+
39
+ export function Heading({ text, level = 2 }: { text: string; level?: 1 | 2 | 3 }) {
40
+ const size = level === 1 ? 'text-lg font-semibold' : level === 2 ? 'text-sm font-semibold' : 'text-xs font-medium text-muted-foreground'
41
+ return <p className={size}>{text}</p>
42
+ }
43
+
44
+ export function Text({ text, muted, size = 'sm' }: { text: string; muted?: boolean; size?: 'xs' | 'sm' }) {
45
+ return <p className={`${size === 'xs' ? 'text-[11px]' : 'text-xs'} ${muted ? 'text-muted-foreground' : ''}`}>{text}</p>
46
+ }
47
+
48
+ // ---------- Content ----------
49
+
50
+ export function Image({ src, alt, aspect = 'square', fit = 'contain' }: { src: string; alt?: string; aspect?: 'square' | 'video' | 'auto'; fit?: 'cover' | 'contain' }) {
51
+ const a = aspect === 'square' ? 'aspect-square max-h-32' : aspect === 'video' ? 'aspect-video' : ''
52
+ const f = fit === 'contain' ? 'object-contain' : 'object-cover'
53
+ return (
54
+ <div className={`${a} bg-muted/20 overflow-hidden rounded-md flex items-center justify-center`}>
55
+ <img src={src} alt={alt || ''} loading="lazy" className={`w-full h-full ${f}`} />
56
+ </div>
57
+ )
58
+ }
59
+
60
+ export function Badge({ text, variant = 'default' }: { text: string; variant?: 'default' | 'secondary' | 'outline' | 'success' | 'warning' }) {
61
+ const colors: Record<string, string> = {
62
+ success: 'bg-emerald-600/15 text-emerald-500 border-emerald-600/30',
63
+ warning: 'bg-amber-500/15 text-amber-500 border-amber-500/30',
64
+ }
65
+ if (colors[variant]) {
66
+ return <span className={`text-[10px] px-1.5 py-0.5 rounded border ${colors[variant]}`}>{text}</span>
67
+ }
68
+ return <UIBadge variant={variant as any} className="text-[10px]">{text}</UIBadge>
69
+ }
70
+
71
+ export function Stat({ label, value, hint, tone = 'default' }: { label: string; value: string; hint?: string; tone?: 'default' | 'success' | 'warning' | 'danger' }) {
72
+ const color = { default: '', success: 'text-emerald-500', warning: 'text-amber-500', danger: 'text-red-500' }[tone]
73
+ return (
74
+ <div className="bg-accent/40 border border-border/60 rounded-lg p-2.5">
75
+ <p className="text-[10px] text-muted-foreground uppercase tracking-wide">{label}</p>
76
+ <p className={`text-base font-semibold ${color}`}>{value}</p>
77
+ {hint && <p className="text-[10px] text-muted-foreground/60 mt-0.5">{hint}</p>}
78
+ </div>
79
+ )
80
+ }
81
+
82
+ export function Rating({ score, count }: { score: number; count?: number }) {
83
+ return (
84
+ <span className="inline-flex items-center gap-0.5 text-[11px]">
85
+ <StarIcon className="w-3 h-3 text-yellow-500 fill-yellow-500" />
86
+ {score.toFixed(1)}
87
+ {count ? <span className="text-muted-foreground/60"> ({count})</span> : null}
88
+ </span>
89
+ )
90
+ }
91
+
92
+ export function GoldSupplier({ years }: { years: number }) {
93
+ if (!years) return null
94
+ return (
95
+ <span className="inline-flex items-center gap-0.5 text-[11px] text-amber-500">
96
+ <ShieldCheckIcon className="w-3 h-3" /> {years}y Gold
97
+ </span>
98
+ )
99
+ }
100
+
101
+ // ---------- Containers ----------
102
+
103
+ export function Card({ children }: { children?: ReactNode }) {
104
+ return (
105
+ <div className="bg-accent/40 border border-border/60 rounded-lg overflow-hidden flex flex-col hover:border-primary/40 transition-colors">
106
+ {children}
107
+ </div>
108
+ )
109
+ }
110
+
111
+ export function CardBody({ children }: { children?: ReactNode }) {
112
+ return <div className="p-2.5 flex flex-col gap-1.5 flex-1">{children}</div>
113
+ }
114
+
115
+ // ---------- Interactive ----------
116
+
117
+ export function LinkOut({ href, label }: { href: string; label?: string }) {
118
+ return (
119
+ <a
120
+ href={href}
121
+ target="_blank"
122
+ rel="noreferrer"
123
+ className="inline-flex items-center gap-1 text-[11px] py-1 px-2 rounded border border-border hover:bg-accent transition-colors"
124
+ >
125
+ {label || 'Ver'} <ExternalLinkIcon className="w-2.5 h-2.5" />
126
+ </a>
127
+ )
128
+ }
129
+
130
+ export function Button({
131
+ label,
132
+ send,
133
+ action,
134
+ actionPayload,
135
+ variant = 'default',
136
+ onSendToChat,
137
+ }: {
138
+ label: string
139
+ send?: string
140
+ action?: string
141
+ actionPayload?: Record<string, any>
142
+ variant?: 'default' | 'primary' | 'ghost'
143
+ onSendToChat?: OnChat
144
+ }) {
145
+ const [state, setState] = useState<'idle' | 'running' | 'done' | 'error'>('idle')
146
+ const [result, setResult] = useState<string | null>(null)
147
+
148
+ const cls =
149
+ state === 'done'
150
+ ? 'bg-emerald-600/20 text-emerald-500 border border-emerald-600/30 cursor-default'
151
+ : variant === 'primary'
152
+ ? 'bg-emerald-600 hover:bg-emerald-500 text-white'
153
+ : variant === 'ghost'
154
+ ? 'hover:bg-accent'
155
+ : 'border border-border hover:bg-accent'
156
+
157
+ async function handle() {
158
+ if (state === 'running' || state === 'done') return
159
+ // Direct action takes priority
160
+ if (action && ACTIONS[action]) {
161
+ setState('running')
162
+ try {
163
+ const msg = await ACTIONS[action](actionPayload || {})
164
+ setResult(msg)
165
+ setState('done')
166
+ } catch (e: any) {
167
+ setResult(`Error: ${e?.message || String(e)}`)
168
+ setState('error')
169
+ setTimeout(() => setState('idle'), 2500)
170
+ }
171
+ return
172
+ }
173
+ // Fallback: send chat message
174
+ if (send) onSendToChat?.(send)
175
+ }
176
+
177
+ return (
178
+ <button
179
+ onClick={handle}
180
+ disabled={state === 'running' || state === 'done'}
181
+ className={`text-[11px] py-1 px-2 rounded transition-colors inline-flex items-center gap-1 ${cls}`}
182
+ >
183
+ {state === 'running' && <Loader2Icon className="w-2.5 h-2.5 animate-spin" />}
184
+ {state === 'done' && <CheckIcon className="w-2.5 h-2.5" />}
185
+ <span>{state === 'done' ? result || 'Guardado' : state === 'error' ? result || 'Error' : label}</span>
186
+ </button>
187
+ )
188
+ }
189
+
190
+ // ---------- Tabular ----------
191
+
192
+ export function Table({
193
+ columns,
194
+ rows,
195
+ }: {
196
+ columns: string[]
197
+ rows: (string | number)[][]
198
+ }) {
199
+ return (
200
+ <div className="overflow-x-auto">
201
+ <table className="w-full text-[11px]">
202
+ <thead>
203
+ <tr className="border-b border-border">
204
+ {columns.map((c, i) => (
205
+ <th key={i} className="text-left py-1 px-2 font-medium text-muted-foreground">
206
+ {c}
207
+ </th>
208
+ ))}
209
+ </tr>
210
+ </thead>
211
+ <tbody>
212
+ {rows.map((row, i) => (
213
+ <tr key={i} className="border-b border-border/40 hover:bg-accent/30">
214
+ {row.map((cell, j) => (
215
+ <td key={j} className="py-1 px-2">
216
+ {cell}
217
+ </td>
218
+ ))}
219
+ </tr>
220
+ ))}
221
+ </tbody>
222
+ </table>
223
+ </div>
224
+ )
225
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Registry of direct frontend actions that agent-rendered Buttons can trigger.
3
+ * Each action is an async handler that runs in the browser (usually a Supabase
4
+ * write) and returns a short confirmation label for the button's success state.
5
+ */
6
+ import { supabase } from '../../../lib/supabase'
7
+
8
+ export type ActionHandler = (payload: any) => Promise<string>
9
+
10
+ async function saveAlternative(payload: any): Promise<string> {
11
+ const {
12
+ product_id,
13
+ company_id,
14
+ supplier,
15
+ title,
16
+ url,
17
+ thumbnail,
18
+ price,
19
+ moq,
20
+ review_score,
21
+ review_count,
22
+ gold_supplier_years,
23
+ country,
24
+ } = payload || {}
25
+
26
+ if (!company_id || !supplier) throw new Error('company_id y supplier requeridos')
27
+
28
+ const { error } = await supabase.from('product_alternatives').upsert(
29
+ {
30
+ product_id: product_id || null,
31
+ company_id,
32
+ supplier,
33
+ title: title || null,
34
+ url: url || null,
35
+ thumbnail: thumbnail || null,
36
+ price: price || null,
37
+ moq: moq || null,
38
+ review_score: review_score ?? null,
39
+ review_count: review_count ?? null,
40
+ gold_supplier_years: gold_supplier_years ?? null,
41
+ country: country || null,
42
+ source: 'alibaba',
43
+ },
44
+ { onConflict: 'product_id,supplier,url' },
45
+ )
46
+ if (error) throw error
47
+ return `✓ ${supplier} guardado como alternativa`
48
+ }
49
+
50
+ export const ACTIONS: Record<string, ActionHandler> = {
51
+ save_alternative: saveAlternative,
52
+ }
@@ -0,0 +1,80 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { supabase } from '../lib/supabase'
3
+ import type { User } from '@supabase/supabase-js'
4
+
5
+ interface Company {
6
+ id: string
7
+ name: string
8
+ }
9
+
10
+ interface Profile {
11
+ full_name: string | null
12
+ role_title: string | null
13
+ onboarding_completed: boolean
14
+ }
15
+
16
+ interface AuthState {
17
+ user: User | null
18
+ role: 'admin' | 'client' | null
19
+ companyId: string | null
20
+ companies: Company[]
21
+ profile: Profile | null
22
+ loading: boolean
23
+ }
24
+
25
+ export function useAuth(): AuthState & { signOut: () => void; setCompanyId: (id: string) => void; reload: () => Promise<void> } {
26
+ const [state, setState] = useState<AuthState>({
27
+ user: null, role: null, companyId: null, companies: [], profile: null, loading: true,
28
+ })
29
+
30
+ const loadUser = useCallback(async (user: User | null) => {
31
+ if (!user) {
32
+ setState({ user: null, role: null, companyId: null, companies: [], profile: null, loading: false })
33
+ return
34
+ }
35
+
36
+ const [{ data: owned }, { data: profile }] = await Promise.all([
37
+ supabase.from('companies').select('id, name').eq('owner_id', user.id),
38
+ supabase.from('profiles').select('full_name, role_title, onboarding_completed').eq('id', user.id).maybeSingle(),
39
+ ])
40
+
41
+ if (owned && owned.length > 0) {
42
+ setState({ user, role: 'admin', companyId: owned[0].id, companies: owned, profile: profile || null, loading: false })
43
+ return
44
+ }
45
+
46
+ const { data: memberships } = await supabase
47
+ .from('company_users')
48
+ .select('company_id, companies(id, name)')
49
+ .eq('user_id', user.id)
50
+
51
+ const clientCompanies = (memberships || []).map((m: any) => m.companies).filter(Boolean)
52
+ setState({
53
+ user, role: clientCompanies.length > 0 ? 'client' : null,
54
+ companyId: clientCompanies[0]?.id || null, companies: clientCompanies,
55
+ profile: profile || null, loading: false,
56
+ })
57
+ }, [])
58
+
59
+ useEffect(() => {
60
+ let active = true
61
+ supabase.auth.getUser().then(({ data: { user } }) => {
62
+ if (active) loadUser(user)
63
+ }).catch(() => {})
64
+
65
+ const { data: { subscription } } = supabase.auth.onAuthStateChange((_ev, session) => {
66
+ if (active) loadUser(session?.user || null)
67
+ })
68
+ return () => { active = false; subscription.unsubscribe() }
69
+ }, [loadUser])
70
+
71
+ return {
72
+ ...state,
73
+ signOut: () => supabase.auth.signOut(),
74
+ setCompanyId: (id: string) => setState(s => ({ ...s, companyId: id })),
75
+ reload: async () => {
76
+ const { data: { user } } = await supabase.auth.getUser()
77
+ await loadUser(user)
78
+ },
79
+ }
80
+ }
@@ -0,0 +1,44 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ /**
4
+ * Minimal data-fetching hook. Replaces the `useEffect(() => fetch().then(setState), [deps])` pattern.
5
+ * Handles stale closures via an abort flag. Components should never useEffect for fetching.
6
+ */
7
+ export function useData<T>(
8
+ fetcher: (signal: AbortSignal) => Promise<T>,
9
+ deps: readonly unknown[],
10
+ initial: T,
11
+ ): { data: T; loading: boolean; error: Error | null } {
12
+ const [data, setData] = useState<T>(initial)
13
+ const [loading, setLoading] = useState(true)
14
+ const [error, setError] = useState<Error | null>(null)
15
+ const mountedRef = useRef(true)
16
+
17
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18
+ useEffect(() => {
19
+ const controller = new AbortController()
20
+ setLoading(true)
21
+ setError(null)
22
+ fetcher(controller.signal)
23
+ .then(result => {
24
+ if (!controller.signal.aborted) {
25
+ setData(result)
26
+ setLoading(false)
27
+ }
28
+ })
29
+ .catch(err => {
30
+ if (!controller.signal.aborted) {
31
+ setError(err)
32
+ setLoading(false)
33
+ }
34
+ })
35
+ return () => controller.abort()
36
+ }, deps)
37
+
38
+ useEffect(() => {
39
+ mountedRef.current = true
40
+ return () => { mountedRef.current = false }
41
+ }, [])
42
+
43
+ return { data, loading, error }
44
+ }
@@ -0,0 +1,10 @@
1
+ import { useEffect } from 'react'
2
+
3
+ /**
4
+ * Run an effect exactly once on mount. The only sanctioned direct useEffect wrapper.
5
+ * Components must never import useEffect directly — use this or another custom hook.
6
+ */
7
+ export function useMountEffect(effect: () => void | (() => void)) {
8
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9
+ useEffect(effect, [])
10
+ }
@@ -0,0 +1,37 @@
1
+ import { useState, useCallback } from 'react'
2
+
3
+ export type Theme = 'light' | 'dark' | 'system'
4
+
5
+ function getStoredTheme(): Theme {
6
+ return (localStorage.getItem('proto-theme') as Theme) || 'dark'
7
+ }
8
+
9
+ function applyTheme(theme: Theme) {
10
+ const root = document.documentElement
11
+ if (theme === 'system') {
12
+ const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
13
+ root.classList.toggle('dark', isDark)
14
+ } else {
15
+ root.classList.toggle('dark', theme === 'dark')
16
+ }
17
+ }
18
+
19
+ // Apply on load (before React renders)
20
+ applyTheme(getStoredTheme())
21
+
22
+ // Listen for system theme changes
23
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
24
+ if (getStoredTheme() === 'system') applyTheme('system')
25
+ })
26
+
27
+ export function useTheme() {
28
+ const [theme, setThemeState] = useState<Theme>(getStoredTheme)
29
+
30
+ const setTheme = useCallback((t: Theme) => {
31
+ localStorage.setItem('proto-theme', t)
32
+ setThemeState(t)
33
+ applyTheme(t)
34
+ }, [])
35
+
36
+ return { theme, setTheme }
37
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @proto/core-web — framework library for proto SPAs.
3
+ *
4
+ * Apps import Shell, defineWidget, hooks, lib helpers, and shadcn primitives
5
+ * from this barrel. Nothing in here is Hermes-specific.
6
+ */
7
+
8
+ // Framework components
9
+ export { default as Shell, type CockpitDefinition } from './components/Shell'
10
+ export { ProtoApp, type ProtoAppProps } from './ProtoApp'
11
+
12
+ // Extension API
13
+ export {
14
+ defineWidget,
15
+ buildWidgetRegistry,
16
+ type WidgetDefinition,
17
+ type WidgetRegistry,
18
+ type WidgetCategory,
19
+ type WidgetSize,
20
+ type ShellContext,
21
+ } from './lib/define-widget'
22
+ export type { ActiveEntity, WidgetInstance, WidgetType } from './components/shell/types'
23
+
24
+ // Hooks
25
+ export { useAuth } from './hooks/useAuth'
26
+ export { useData } from './hooks/useData'
27
+ export { useMountEffect } from './hooks/useMountEffect'
28
+ export { useTheme, type Theme } from './hooks/useTheme'
29
+
30
+ // Lib
31
+ export * from './lib/api'
32
+ export * from './lib/config'
33
+ export { supabase } from './lib/supabase'
34
+ export { cn } from './lib/utils'
35
+ export * from './lib/drag'
36
+ export * from './lib/widgetCache'
37
+
38
+ // Agent runtime (render_ui)
39
+ export { Generative } from './components/widgets/agent/Generative'
40
+
41
+ // UI primitives (shadcn)
42
+ export { Avatar, AvatarFallback } from './components/ui/avatar'
43
+ export { Badge } from './components/ui/badge'
44
+ export { Button } from './components/ui/button'
45
+ export { Card, CardContent, CardFooter, CardHeader } from './components/ui/card'
46
+ export { InlineEdit } from './components/ui/inline-edit'
47
+ export { Input } from './components/ui/input'
48
+ export { ScrollArea } from './components/ui/scroll-area'
49
+ export { Separator } from './components/ui/separator'
50
+ export { ShellDialog } from './components/ui/shell-dialog'
51
+ export { Skeleton } from './components/ui/skeleton'
52
+ export { Textarea } from './components/ui/textarea'