@motiadev/workbench 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +50 -0
  2. package/components.json +21 -0
  3. package/dist/.empty +0 -0
  4. package/dist/assets/index-DGmArPOa.css +1 -0
  5. package/dist/assets/index-hQsWtfVb.js +182 -0
  6. package/dist/index.html +20 -0
  7. package/eslint.config.js +28 -0
  8. package/index.html +19 -0
  9. package/index.tsx +10 -0
  10. package/middleware.ts +46 -0
  11. package/package.json +56 -0
  12. package/postcss.config.js +6 -0
  13. package/public/.empty +0 -0
  14. package/src/assets/.empty +0 -0
  15. package/src/components/app-sidebar.tsx +55 -0
  16. package/src/components/log-console.tsx +76 -0
  17. package/src/components/log-level-badge.tsx +12 -0
  18. package/src/components/ui/badge.tsx +31 -0
  19. package/src/components/ui/button.tsx +47 -0
  20. package/src/components/ui/collapsible.tsx +9 -0
  21. package/src/components/ui/dialog.tsx +120 -0
  22. package/src/components/ui/input.tsx +21 -0
  23. package/src/components/ui/label.tsx +26 -0
  24. package/src/components/ui/select.tsx +157 -0
  25. package/src/components/ui/separator.tsx +22 -0
  26. package/src/components/ui/sheet.tsx +106 -0
  27. package/src/components/ui/sidebar.tsx +637 -0
  28. package/src/components/ui/skeleton.tsx +7 -0
  29. package/src/components/ui/switch.tsx +27 -0
  30. package/src/components/ui/table.tsx +76 -0
  31. package/src/components/ui/textarea.tsx +22 -0
  32. package/src/components/ui/tooltip.tsx +32 -0
  33. package/src/hooks/use-list-flows.tsx +20 -0
  34. package/src/hooks/use-log-listener.tsx +32 -0
  35. package/src/hooks/use-mobile.tsx +19 -0
  36. package/src/index.css +190 -0
  37. package/src/lib/utils.ts +6 -0
  38. package/src/main.tsx +28 -0
  39. package/src/publicComponents/api-node.tsx +28 -0
  40. package/src/publicComponents/base-handle.tsx +43 -0
  41. package/src/publicComponents/base-node.tsx +57 -0
  42. package/src/publicComponents/emits.tsx +22 -0
  43. package/src/publicComponents/event-node.tsx +36 -0
  44. package/src/publicComponents/node-props.tsx +15 -0
  45. package/src/publicComponents/noop-node.tsx +21 -0
  46. package/src/publicComponents/subscribe.tsx +19 -0
  47. package/src/route-wrapper.tsx +9 -0
  48. package/src/routeTree.gen.ts +109 -0
  49. package/src/routes/__root.tsx +26 -0
  50. package/src/routes/flow/$id.tsx +21 -0
  51. package/src/routes/index.tsx +13 -0
  52. package/src/stores/use-logs.ts +22 -0
  53. package/src/views/flow/arrow-head.tsx +13 -0
  54. package/src/views/flow/base-edge.tsx +44 -0
  55. package/src/views/flow/flow-loader.tsx +3 -0
  56. package/src/views/flow/flow-view.tsx +72 -0
  57. package/src/views/flow/hooks/use-get-flow-state.tsx +109 -0
  58. package/src/views/flow/hooks/use-organize-nodes.ts +60 -0
  59. package/src/views/flow/legend.tsx +59 -0
  60. package/src/views/flow/node-organizer.tsx +70 -0
  61. package/src/views/flow/nodes/api-flow-node.tsx +6 -0
  62. package/src/views/flow/nodes/event-flow-node.tsx +6 -0
  63. package/src/views/flow/nodes/json-schema-form.tsx +110 -0
  64. package/src/views/flow/nodes/language-indicator.tsx +74 -0
  65. package/src/views/flow/nodes/nodes.types.ts +36 -0
  66. package/src/views/flow/nodes/noop-flow-node.tsx +6 -0
  67. package/src/vite-env.d.ts +1 -0
  68. package/tailwind.config.ts +75 -0
  69. package/tsconfig.app.json +30 -0
  70. package/tsconfig.json +13 -0
  71. package/tsconfig.node.json +22 -0
  72. package/tsconfig.node.tsbuildinfo +1 -0
  73. package/vite.config.ts +14 -0
@@ -0,0 +1,76 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div className="relative w-full overflow-auto">
8
+ <table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
9
+ </div>
10
+ ),
11
+ )
12
+ Table.displayName = 'Table'
13
+
14
+ const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
15
+ ({ className, ...props }, ref) => <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />,
16
+ )
17
+ TableHeader.displayName = 'TableHeader'
18
+
19
+ const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
20
+ ({ className, ...props }, ref) => (
21
+ <tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
22
+ ),
23
+ )
24
+ TableBody.displayName = 'TableBody'
25
+
26
+ const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
27
+ ({ className, ...props }, ref) => (
28
+ <tfoot ref={ref} className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)} {...props} />
29
+ ),
30
+ )
31
+ TableFooter.displayName = 'TableFooter'
32
+
33
+ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
34
+ ({ className, ...props }, ref) => (
35
+ <tr
36
+ ref={ref}
37
+ className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)}
38
+ {...props}
39
+ />
40
+ ),
41
+ )
42
+ TableRow.displayName = 'TableRow'
43
+
44
+ const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
45
+ ({ className, ...props }, ref) => (
46
+ <th
47
+ ref={ref}
48
+ className={cn(
49
+ 'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
50
+ className,
51
+ )}
52
+ {...props}
53
+ />
54
+ ),
55
+ )
56
+ TableHead.displayName = 'TableHead'
57
+
58
+ const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
59
+ ({ className, ...props }, ref) => (
60
+ <td
61
+ ref={ref}
62
+ className={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className)}
63
+ {...props}
64
+ />
65
+ ),
66
+ )
67
+ TableCell.displayName = 'TableCell'
68
+
69
+ const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
70
+ ({ className, ...props }, ref) => (
71
+ <caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
72
+ ),
73
+ )
74
+ TableCaption.displayName = 'TableCaption'
75
+
76
+ export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
@@ -0,0 +1,22 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Textarea = React.forwardRef<
6
+ HTMLTextAreaElement,
7
+ React.ComponentProps<"textarea">
8
+ >(({ className, ...props }, ref) => {
9
+ return (
10
+ <textarea
11
+ className={cn(
12
+ "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
13
+ className
14
+ )}
15
+ ref={ref}
16
+ {...props}
17
+ />
18
+ )
19
+ })
20
+ Textarea.displayName = "Textarea"
21
+
22
+ export { Textarea }
@@ -0,0 +1,32 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5
+
6
+ import { cn } from '@/lib/utils'
7
+
8
+ const TooltipProvider = TooltipPrimitive.Provider
9
+
10
+ const Tooltip = TooltipPrimitive.Root
11
+
12
+ const TooltipTrigger = TooltipPrimitive.Trigger
13
+
14
+ const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Portal>
19
+ <TooltipPrimitive.Content
20
+ ref={ref}
21
+ sideOffset={sideOffset}
22
+ className={cn(
23
+ 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ </TooltipPrimitive.Portal>
29
+ ))
30
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
31
+
32
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,20 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ type Flow = {
4
+ id: string
5
+ name: string
6
+ }
7
+
8
+ export const useListFlows = () => {
9
+ const [isLoading, setIsLoading] = useState(true)
10
+ const [flows, setFlows] = useState<Flow[]>([])
11
+
12
+ useEffect(() => {
13
+ fetch('/flows')
14
+ .then((res) => res.json())
15
+ .then(setFlows)
16
+ .finally(() => setIsLoading(false))
17
+ }, [])
18
+
19
+ return { flows, isLoading }
20
+ }
@@ -0,0 +1,32 @@
1
+ import { Log, useLogs } from '@/stores/use-logs'
2
+ import { useState, useEffect } from 'react'
3
+ import { io } from 'socket.io-client'
4
+
5
+ type UseWebSocketReturn = {
6
+ isConnected: boolean
7
+ }
8
+
9
+ const socket = io('/')
10
+
11
+ export const useLogListener = (): UseWebSocketReturn => {
12
+ const [isConnected, setIsConnected] = useState(socket.connected)
13
+ const addLog = useLogs((state) => state.addLog)
14
+
15
+ useEffect(() => {
16
+ const onConnect = () => setIsConnected(true)
17
+ const onDisconnect = () => setIsConnected(false)
18
+ const onLog = (log: Log) => addLog(log)
19
+
20
+ socket.on('connect', onConnect)
21
+ socket.on('disconnect', onDisconnect)
22
+ socket.on('log', onLog)
23
+
24
+ return () => {
25
+ socket.off('connect', onConnect)
26
+ socket.off('disconnect', onDisconnect)
27
+ socket.off('log', onLog)
28
+ }
29
+ }, [addLog])
30
+
31
+ return { isConnected }
32
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from 'react'
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener('change', onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener('change', onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
package/src/index.css ADDED
@@ -0,0 +1,190 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ html, body, div, span, applet, object, iframe,
6
+ h1, h2, h3, h4, h5, h6, p, blockquote, pre,
7
+ a, abbr, acronym, address, big, cite, code,
8
+ del, dfn, em, img, ins, kbd, q, s, samp,
9
+ small, strike, strong, sub, sup, tt, var,
10
+ b, u, i, center,
11
+ dl, dt, dd, ol, ul, li,
12
+ fieldset, form, label, legend,
13
+ table, caption, tbody, tfoot, thead, tr, th, td,
14
+ article, aside, canvas, details, embed,
15
+ figure, figcaption, footer, header, hgroup,
16
+ menu, nav, output, ruby, section, summary,
17
+ time, mark, audio, video {
18
+ margin: 0;
19
+ padding: 0;
20
+ border: 0;
21
+ font: inherit;
22
+ vertical-align: baseline;
23
+ }
24
+
25
+ /* HTML5 display-role reset for older browsers */
26
+ article, aside, details, figcaption, figure,
27
+ footer, header, hgroup, menu, nav, section {
28
+ display: block;
29
+ }
30
+ body {
31
+ line-height: 1;
32
+ }
33
+ ol, ul {
34
+ list-style: none;
35
+ }
36
+ blockquote, q {
37
+ quotes: none;
38
+ }
39
+ blockquote:before, blockquote:after,
40
+ q:before, q:after {
41
+ content: '';
42
+ content: none;
43
+ }
44
+ table {
45
+ border-collapse: collapse;
46
+ border-spacing: 0;
47
+ }
48
+
49
+ :root {
50
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
51
+ line-height: 1.5;
52
+ font-size: 16px;
53
+
54
+ color-scheme: light dark;
55
+ color: rgba(255, 255, 255, 0.87);
56
+ background-color: #242424;
57
+
58
+ font-synthesis: none;
59
+ text-rendering: optimizeLegibility;
60
+ -webkit-font-smoothing: antialiased;
61
+ -moz-osx-font-smoothing: grayscale;
62
+ }
63
+
64
+ :root {
65
+ width: 100%;
66
+ color-scheme: light dark;
67
+ font-synthesis: none;
68
+ text-rendering: optimizeLegibility;
69
+ -webkit-font-smoothing: antialiased;
70
+ -moz-osx-font-smoothing: grayscale;
71
+ font-family: 'PT Sans', serif;
72
+ font-optical-sizing: auto;
73
+ }
74
+
75
+ body {
76
+ margin: 0;
77
+ place-items: center;
78
+ min-height: 100dvh;
79
+ width: 100%;
80
+ }
81
+
82
+
83
+ button, textarea {
84
+ font-family: 'PT Sans', serif;
85
+ }
86
+
87
+ strong {
88
+ font-weight: 800;
89
+ }
90
+
91
+ body, #root {
92
+ width: 100dvw;
93
+ height: 100dvh;
94
+ }
95
+
96
+ .react-flow__attribution {
97
+ display: none;
98
+ }
99
+
100
+ @layer base {
101
+ :root {
102
+ --background: 0 0% 100%;
103
+ --foreground: 224 71.4% 4.1%;
104
+ --card: 0 0% 100%;
105
+ --card-foreground: 224 71.4% 4.1%;
106
+ --popover: 0 0% 100%;
107
+ --popover-foreground: 224 71.4% 4.1%;
108
+ --primary: 220.9 39.3% 11%;
109
+ --primary-foreground: 210 20% 98%;
110
+ --secondary: 220 14.3% 95.9%;
111
+ --secondary-foreground: 220.9 39.3% 11%;
112
+ --muted: 220 14.3% 95.9%;
113
+ --muted-foreground: 220 8.9% 46.1%;
114
+ --accent: 220 14.3% 95.9%;
115
+ --accent-foreground: 220.9 39.3% 11%;
116
+ --destructive: 0 84.2% 60.2%;
117
+ --destructive-foreground: 210 20% 98%;
118
+ --border: 220 13% 91%;
119
+ --input: 220 13% 91%;
120
+ --ring: 224 71.4% 4.1%;
121
+ --chart-1: 12 76% 61%;
122
+ --chart-2: 173 58% 39%;
123
+ --chart-3: 197 37% 24%;
124
+ --chart-4: 43 74% 66%;
125
+ --chart-5: 27 87% 67%;
126
+ --radius: 0.5rem;
127
+ --sidebar-background: 0 0% 98%;
128
+ --sidebar-foreground: 240 5.3% 26.1%;
129
+ --sidebar-primary: 240 5.9% 10%;
130
+ --sidebar-primary-foreground: 0 0% 98%;
131
+ --sidebar-accent: 240 4.8% 95.9%;
132
+ --sidebar-accent-foreground: 240 5.9% 10%;
133
+ --sidebar-border: 220 13% 91%;
134
+ --sidebar-ring: 217.2 91.2% 59.8%;
135
+ }
136
+ .dark {
137
+ --background: 224 71.4% 4.1%;
138
+ --foreground: 210 20% 98%;
139
+ --card: 224 71.4% 4.1%;
140
+ --card-foreground: 210 20% 98%;
141
+ --popover: 224 71.4% 4.1%;
142
+ --popover-foreground: 210 20% 98%;
143
+ --primary: 210 20% 98%;
144
+ --primary-foreground: 220.9 39.3% 11%;
145
+ --secondary: 215 27.9% 16.9%;
146
+ --secondary-foreground: 210 20% 98%;
147
+ --muted: 215 27.9% 16.9%;
148
+ --muted-foreground: 217.9 10.6% 64.9%;
149
+ --accent: 215 27.9% 16.9%;
150
+ --accent-foreground: 210 20% 98%;
151
+ --destructive: 0 62.8% 30.6%;
152
+ --destructive-foreground: 210 20% 98%;
153
+ --border: 215 27.9% 16.9%;
154
+ --input: 215 27.9% 16.9%;
155
+ --ring: 216 12.2% 83.9%;
156
+ --chart-1: 220 70% 50%;
157
+ --chart-2: 160 60% 45%;
158
+ --chart-3: 30 80% 55%;
159
+ --chart-4: 280 65% 60%;
160
+ --chart-5: 340 75% 55%;
161
+ --sidebar-background: 240 5.9% 10%;
162
+ --sidebar-foreground: 240 4.8% 95.9%;
163
+ --sidebar-primary: 224.3 76.3% 48%;
164
+ --sidebar-primary-foreground: 0 0% 100%;
165
+ --sidebar-accent: 240 3.7% 15.9%;
166
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
167
+ --sidebar-border: 240 3.7% 15.9%;
168
+ --sidebar-ring: 217.2 91.2% 59.8%;
169
+ }
170
+ }
171
+
172
+ @layer base {
173
+ * {
174
+ @apply border-border;
175
+ }
176
+ body {
177
+ @apply bg-background text-foreground;
178
+ }
179
+ }
180
+
181
+ @keyframes flowDash {
182
+ 0% { stroke-dashoffset: 0; }
183
+ 100% { stroke-dashoffset: -20; }
184
+ }
185
+
186
+ .edge-animated {
187
+ stroke-dasharray: 5; /* length of dash pattern */
188
+ stroke-linecap: round; /* round the dash ends */
189
+ animation: flowDash 1s linear infinite;
190
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,28 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import { RouterProvider, createRouter } from '@tanstack/react-router'
5
+
6
+ // Import the generated route tree
7
+ import { routeTree } from './routeTree.gen'
8
+
9
+ // Create a new router instance
10
+ const router = createRouter({ routeTree })
11
+
12
+ // Register the router instance for type safety
13
+ declare module '@tanstack/react-router' {
14
+ interface Register {
15
+ router: typeof router
16
+ }
17
+ }
18
+
19
+ // Render the app
20
+ const rootElement = document.getElementById('root')!
21
+ if (!rootElement.innerHTML) {
22
+ const root = createRoot(rootElement)
23
+ root.render(
24
+ <StrictMode>
25
+ <RouterProvider router={router} />
26
+ </StrictMode>,
27
+ )
28
+ }
@@ -0,0 +1,28 @@
1
+ import { Webhook } from 'lucide-react'
2
+ import { PropsWithChildren } from 'react'
3
+ import { BaseNode } from './base-node'
4
+ import { Emits } from './emits'
5
+ import { ApiNodeProps } from './node-props'
6
+
7
+ type Props = PropsWithChildren<ApiNodeProps & { excludePubsub?: boolean }>
8
+
9
+ export const ApiNode = ({ data, children, excludePubsub }: Props) => {
10
+ return (
11
+ <BaseNode
12
+ variant="api"
13
+ title={data.name}
14
+ disableSourceHandle={!data.emits?.length}
15
+ disableTargetHandle={!data.subscribes?.length}
16
+ >
17
+ {data.description && <div className="text-sm max-w-[300px] text-white/60">{data.description}</div>}
18
+ {children}
19
+ {data.webhookUrl && (
20
+ <div className="flex gap-1 items-center text-xs text-white/60">
21
+ <Webhook className="w-3 h-3 text-white/40" />
22
+ <div className="font-mono">{data.webhookUrl}</div>
23
+ </div>
24
+ )}
25
+ {!excludePubsub && <Emits emits={data.emits} />}
26
+ </BaseNode>
27
+ )
28
+ }
@@ -0,0 +1,43 @@
1
+ import React, { HTMLAttributes } from 'react'
2
+ import { HandleProps, Position, Handle as RFHandle } from '@xyflow/react'
3
+ import clsx from 'clsx'
4
+
5
+ type Props = HandleProps &
6
+ Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
7
+ isHidden?: boolean
8
+ }
9
+
10
+ export const BaseHandle: React.FC<Props> = (props) => {
11
+ const { isHidden, position, ...rest } = props
12
+
13
+ return (
14
+ <div
15
+ className={clsx(
16
+ 'absolute w-1 h-1',
17
+ position === Position.Top && '-top-[10px]',
18
+ position === Position.Bottom && '-bottom-[10px]',
19
+ 'left-1/2 -ml-[2px]',
20
+ isHidden && 'hidden',
21
+ )}
22
+ >
23
+ <RFHandle
24
+ {...rest}
25
+ position={position}
26
+ className="
27
+ !static
28
+ !w-1
29
+ !h-1
30
+ !min-w-[6px]
31
+ !min-h-[6px]
32
+ !p-0
33
+ !border-none
34
+ !bg-[#666666]
35
+ !transform-none
36
+ !rounded-full
37
+ !outline-none
38
+ !shadow-none
39
+ "
40
+ />
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,57 @@
1
+ import { cn } from '@/lib/utils'
2
+ import { Position } from '@xyflow/react'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { PropsWithChildren } from 'react'
5
+ import { BaseHandle } from './base-handle'
6
+
7
+ const baseNodeVariants = cva('relative flex flex-col min-w-[300px] rounded-md overflow-hidden font-mono')
8
+
9
+ const baseBackgroundVariants = cva('absolute -inset-[1px] rounded-md bg-gradient-to-r', {
10
+ variants: {
11
+ variant: {
12
+ event: 'from-teal-500/20 to-teal-400/10',
13
+ api: 'from-blue-500/20 to-blue-400/10',
14
+ noop: 'from-white/20 to-white/10',
15
+ },
16
+ },
17
+ })
18
+
19
+ type Props = PropsWithChildren<{
20
+ variant?: VariantProps<typeof baseBackgroundVariants>['variant']
21
+ title: string
22
+ headerChildren?: React.ReactNode
23
+ className?: string
24
+ disableSourceHandle?: boolean
25
+ disableTargetHandle?: boolean
26
+ }>
27
+
28
+ const HeaderBar = ({ text, children }: { text: string; children?: React.ReactNode }) => (
29
+ <div className="px-3 py-1 border-b border-white/20 bg-black/30 text-xs text-white/70 flex justify-between items-center">
30
+ <span>{text}</span>
31
+ {children}
32
+ </div>
33
+ )
34
+
35
+ export const BaseNode = (props: Props) => {
36
+ const { title, variant, className, children, disableSourceHandle, disableTargetHandle, headerChildren } = props
37
+
38
+ return (
39
+ <div className="group relative">
40
+ {/* Border container */}
41
+ <div className={cn(baseBackgroundVariants({ variant }))} />
42
+
43
+ {/* Main node content */}
44
+ <div className={cn(baseNodeVariants(), className)}>
45
+ <HeaderBar text={title} children={headerChildren} />
46
+ <div className="p-4 space-y-3">{children}</div>
47
+ </div>
48
+
49
+ {/* Connection points */}
50
+ {!disableTargetHandle && <BaseHandle type="target" position={Position.Top} />}
51
+ {!disableSourceHandle && <BaseHandle type="source" position={Position.Bottom} />}
52
+
53
+ {/* Stacked card effect */}
54
+ <div className="absolute inset-0 -z-10 translate-y-1 translate-x-1 bg-black/20 rounded-md border border-white/5" />
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,22 @@
1
+ import { Send } from 'lucide-react'
2
+ import { EventNodeData } from '../views/flow/nodes/nodes.types'
3
+
4
+ const toType = (emit: string | { type: string; label?: string; conditional?: boolean }) =>
5
+ typeof emit === 'string' ? emit : emit.type
6
+
7
+ export const Emits: React.FC<{ emits: EventNodeData['emits'] }> = ({ emits }) => {
8
+ return (
9
+ <>
10
+ {emits.map((emit) => (
11
+ <div
12
+ key={toType(emit)}
13
+ className="flex gap-2 items-center text-xs text-white/60"
14
+ data-testid={`emits__${toType(emit)}`}
15
+ >
16
+ <Send className="w-3 h-3 text-white/40" />
17
+ <div className="font-mono tracking-wider">{toType(emit)}</div>
18
+ </div>
19
+ ))}
20
+ </>
21
+ )
22
+ }
@@ -0,0 +1,36 @@
1
+ import { PropsWithChildren } from 'react'
2
+ import { LanguageIndicator } from '../views/flow/nodes/language-indicator'
3
+ import { BaseNode } from './base-node'
4
+ import { Emits } from './emits'
5
+ import { EventNodeProps } from './node-props'
6
+ import { Subscribe } from './subscribe'
7
+
8
+ type Props = PropsWithChildren<
9
+ EventNodeProps & {
10
+ excludePubsub?: boolean
11
+ className?: string
12
+ }
13
+ >
14
+
15
+ export const EventNode = (props: Props) => {
16
+ const { data, excludePubsub, children } = props
17
+
18
+ return (
19
+ <BaseNode
20
+ variant="event"
21
+ title={data.name}
22
+ disableSourceHandle={!data.emits.length}
23
+ disableTargetHandle={!data.subscribes.length}
24
+ headerChildren={<LanguageIndicator language={data.language} />}
25
+ >
26
+ {data.description && <div className="text-sm max-w-[300px] text-white/60">{data.description}</div>}
27
+ {children}
28
+ {!excludePubsub && (
29
+ <div className="space-y-2 pt-2 border-t border-white/10">
30
+ <Subscribe data={data} />
31
+ </div>
32
+ )}
33
+ <Emits emits={data.emits} />
34
+ </BaseNode>
35
+ )
36
+ }
@@ -0,0 +1,15 @@
1
+ import { EventNodeData, ApiNodeData, NoopNodeData } from '../views/flow/nodes/nodes.types'
2
+
3
+ export type BaseNodeProps = EventNodeProps | NoopNodeProps | ApiNodeProps
4
+
5
+ export type EventNodeProps = {
6
+ data: EventNodeData
7
+ }
8
+
9
+ export type NoopNodeProps = {
10
+ data: NoopNodeData
11
+ }
12
+
13
+ export type ApiNodeProps = {
14
+ data: ApiNodeData
15
+ }
@@ -0,0 +1,21 @@
1
+ import { PropsWithChildren } from 'react'
2
+ import { NoopNodeData } from '../views/flow/nodes/nodes.types'
3
+ import { BaseNode } from './base-node'
4
+
5
+ type Props = PropsWithChildren<{
6
+ data: NoopNodeData
7
+ }>
8
+
9
+ export const NoopNode = ({ data, children }: Props) => {
10
+ return (
11
+ <BaseNode
12
+ variant="noop"
13
+ title={data.name}
14
+ disableSourceHandle={!data.emits?.length}
15
+ disableTargetHandle={!data.subscribes?.length}
16
+ >
17
+ {data.description && <div className="text-sm max-w-[300px] text-white/60">{data.description}</div>}
18
+ {children}
19
+ </BaseNode>
20
+ )
21
+ }
@@ -0,0 +1,19 @@
1
+ import { Eye } from 'lucide-react'
2
+ import { EventNodeData } from '../views/flow/nodes/nodes.types'
3
+
4
+ export const Subscribe: React.FC<{ data: EventNodeData }> = ({ data }) => {
5
+ return (
6
+ <>
7
+ {data.subscribes.map((subscribe) => (
8
+ <div
9
+ key={subscribe}
10
+ className="flex gap-2 items-center text-xs text-white/60"
11
+ data-testid={`subscribes__${subscribe}`}
12
+ >
13
+ <Eye className="w-3 h-3 text-white/40" />
14
+ <div className="font-mono tracking-wider">{subscribe}</div>
15
+ </div>
16
+ ))}
17
+ </>
18
+ )
19
+ }