@motiadev/workbench 0.14.0-beta.165-285707 → 0.15.0-beta.165
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/dist/index.d.ts +189 -10
- package/dist/index.html +1 -1
- package/dist/index.js +1065 -7
- package/dist/middleware.d.ts +66 -8
- package/dist/middleware.js +694 -86
- package/dist/motia-plugin/__tests__/generator.test.ts +129 -0
- package/dist/motia-plugin/__tests__/resolver.test.ts +82 -0
- package/dist/motia-plugin/__tests__/validator.test.ts +71 -0
- package/dist/motia-plugin/{generator.js → generator.ts} +37 -35
- package/dist/motia-plugin/hmr.ts +123 -0
- package/dist/motia-plugin/index.ts +183 -0
- package/dist/motia-plugin/{resolver.d.ts → resolver.ts} +38 -5
- package/dist/motia-plugin/types.ts +198 -0
- package/dist/motia-plugin/{utils.d.ts → utils.ts} +17 -4
- package/dist/motia-plugin/validator.ts +197 -0
- package/dist/src/App.tsx +41 -0
- package/dist/src/components/NotFoundPage.tsx +11 -0
- package/dist/src/components/bottom-panel.tsx +39 -0
- package/dist/src/components/flow/base-edge.tsx +61 -0
- package/dist/src/components/flow/flow-loader.tsx +3 -0
- package/dist/src/components/flow/flow-page.tsx +75 -0
- package/dist/src/components/flow/flow-tab-menu-item.tsx +52 -0
- package/dist/src/components/flow/flow-view.tsx +66 -0
- package/dist/src/components/flow/hooks/use-get-flow-state.tsx +171 -0
- package/dist/src/components/flow/hooks/use-save-workflow-config.ts +25 -0
- package/dist/src/components/flow/node-organizer.tsx +103 -0
- package/dist/src/components/flow/nodes/api-flow-node.tsx +6 -0
- package/dist/src/components/flow/nodes/cron-flow-node.tsx +6 -0
- package/dist/src/components/flow/nodes/event-flow-node.tsx +6 -0
- package/dist/src/components/flow/nodes/noop-flow-node.tsx +6 -0
- package/dist/src/components/header/deploy-button.tsx +110 -0
- package/dist/src/components/header/header.tsx +39 -0
- package/dist/src/components/root-motia.tsx +10 -0
- package/dist/src/components/top-panel.tsx +40 -0
- package/dist/src/components/tutorial/engine/tutorial-engine.ts +26 -0
- package/dist/src/components/tutorial/engine/tutorial-types.ts +26 -0
- package/dist/src/components/tutorial/engine/workbench-xpath.ts +53 -0
- package/dist/src/components/tutorial/hooks/tutorial-utils.ts +26 -0
- package/dist/src/components/tutorial/hooks/use-tutorial-engine.ts +213 -0
- package/dist/src/components/tutorial/hooks/use-tutorial.ts +14 -0
- package/dist/src/components/tutorial/tutorial-button.tsx +46 -0
- package/dist/src/components/tutorial/tutorial-step.tsx +82 -0
- package/dist/src/components/tutorial/tutorial.tsx +59 -0
- package/dist/src/components/ui/json-editor.tsx +68 -0
- package/dist/src/components/ui/table.tsx +75 -0
- package/dist/src/components/ui/theme-toggle.tsx +54 -0
- package/dist/src/components/ui/tooltip.tsx +26 -0
- package/dist/src/hooks/use-debounced.ts +22 -0
- package/dist/src/hooks/use-fetch-flows.ts +33 -0
- package/dist/src/hooks/use-mobile.ts +19 -0
- package/dist/src/hooks/use-update-handle-positions.ts +42 -0
- package/dist/src/index.css +5 -5
- package/dist/src/lib/__tests__/utils.test.ts +110 -0
- package/dist/src/lib/motia-analytics.ts +140 -0
- package/dist/src/lib/plugins.tsx +132 -0
- package/dist/src/lib/utils.ts +37 -0
- package/dist/src/main.tsx +30 -0
- package/dist/src/project-view-mode.tsx +32 -0
- package/dist/src/publicComponents/api-node.tsx +26 -0
- package/dist/src/publicComponents/base-node/base-handle.tsx +50 -0
- package/dist/src/publicComponents/base-node/base-node.tsx +114 -0
- package/dist/src/publicComponents/base-node/code-display.tsx +119 -0
- package/dist/src/publicComponents/base-node/emits.tsx +17 -0
- package/dist/src/publicComponents/base-node/feature-card.tsx +32 -0
- package/dist/src/publicComponents/base-node/language-indicator.tsx +131 -0
- package/dist/src/publicComponents/base-node/node-header.tsx +49 -0
- package/dist/src/publicComponents/base-node/node-sidebar.tsx +41 -0
- package/dist/src/publicComponents/base-node/subscribe.tsx +13 -0
- package/dist/src/publicComponents/cron-node.tsx +24 -0
- package/dist/src/publicComponents/event-node.tsx +20 -0
- package/dist/src/publicComponents/node-props.tsx +15 -0
- package/dist/src/publicComponents/noop-node.tsx +19 -0
- package/dist/src/setupTests.ts +1 -0
- package/dist/src/stores/use-app-tabs-store.ts +49 -0
- package/dist/src/stores/use-flow-store.ts +31 -0
- package/dist/src/stores/use-global-store.ts +24 -0
- package/dist/src/stores/use-motia-config-store.ts +36 -0
- package/dist/src/stores/use-tabs-store.ts +34 -0
- package/dist/src/system-view-mode.tsx +28 -0
- package/dist/src/types/endpoint.ts +12 -0
- package/dist/src/types/file.ts +7 -0
- package/dist/src/types/flow.ts +103 -0
- package/eslint.config.cjs +22 -0
- package/jest.config.cjs +68 -0
- package/package.json +53 -51
- package/dist/motia-plugin/__tests__/generator.test.d.ts +0 -1
- package/dist/motia-plugin/__tests__/generator.test.js +0 -97
- package/dist/motia-plugin/__tests__/resolver.test.d.ts +0 -1
- package/dist/motia-plugin/__tests__/resolver.test.js +0 -64
- package/dist/motia-plugin/__tests__/validator.test.d.ts +0 -1
- package/dist/motia-plugin/__tests__/validator.test.js +0 -59
- package/dist/motia-plugin/generator.d.ts +0 -78
- package/dist/motia-plugin/hmr.d.ts +0 -22
- package/dist/motia-plugin/hmr.js +0 -100
- package/dist/motia-plugin/index.d.ts +0 -3
- package/dist/motia-plugin/index.js +0 -153
- package/dist/motia-plugin/resolver.js +0 -92
- package/dist/motia-plugin/types.d.ts +0 -169
- package/dist/motia-plugin/types.js +0 -36
- package/dist/motia-plugin/utils.js +0 -75
- package/dist/motia-plugin/validator.d.ts +0 -19
- package/dist/motia-plugin/validator.js +0 -163
- package/dist/src/App.d.ts +0 -2
- package/dist/src/App.js +0 -35
- package/dist/src/components/NotFoundPage.d.ts +0 -1
- package/dist/src/components/NotFoundPage.js +0 -3
- package/dist/src/components/bottom-panel.d.ts +0 -1
- package/dist/src/components/bottom-panel.js +0 -15
- package/dist/src/components/flow/base-edge.d.ts +0 -3
- package/dist/src/components/flow/base-edge.js +0 -39
- package/dist/src/components/flow/flow-loader.d.ts +0 -1
- package/dist/src/components/flow/flow-loader.js +0 -4
- package/dist/src/components/flow/flow-page.d.ts +0 -1
- package/dist/src/components/flow/flow-page.js +0 -25
- package/dist/src/components/flow/flow-tab-menu-item.d.ts +0 -1
- package/dist/src/components/flow/flow-tab-menu-item.js +0 -18
- package/dist/src/components/flow/flow-view.d.ts +0 -12
- package/dist/src/components/flow/flow-view.js +0 -22
- package/dist/src/components/flow/hooks/use-get-flow-state.d.ts +0 -10
- package/dist/src/components/flow/hooks/use-get-flow-state.js +0 -133
- package/dist/src/components/flow/hooks/use-save-workflow-config.d.ts +0 -2
- package/dist/src/components/flow/hooks/use-save-workflow-config.js +0 -22
- package/dist/src/components/flow/node-organizer.d.ts +0 -10
- package/dist/src/components/flow/node-organizer.js +0 -82
- package/dist/src/components/flow/nodes/api-flow-node.d.ts +0 -2
- package/dist/src/components/flow/nodes/api-flow-node.js +0 -5
- package/dist/src/components/flow/nodes/cron-flow-node.d.ts +0 -2
- package/dist/src/components/flow/nodes/cron-flow-node.js +0 -5
- package/dist/src/components/flow/nodes/event-flow-node.d.ts +0 -2
- package/dist/src/components/flow/nodes/event-flow-node.js +0 -5
- package/dist/src/components/flow/nodes/noop-flow-node.d.ts +0 -2
- package/dist/src/components/flow/nodes/noop-flow-node.js +0 -5
- package/dist/src/components/header/deploy-button.d.ts +0 -1
- package/dist/src/components/header/deploy-button.js +0 -28
- package/dist/src/components/header/header.d.ts +0 -2
- package/dist/src/components/header/header.js +0 -23
- package/dist/src/components/root-motia.d.ts +0 -2
- package/dist/src/components/root-motia.js +0 -7
- package/dist/src/components/top-panel.d.ts +0 -1
- package/dist/src/components/top-panel.js +0 -15
- package/dist/src/components/tutorial/engine/tutorial-engine.d.ts +0 -12
- package/dist/src/components/tutorial/engine/tutorial-engine.js +0 -36
- package/dist/src/components/tutorial/engine/tutorial-types.d.ts +0 -22
- package/dist/src/components/tutorial/engine/tutorial-types.js +0 -1
- package/dist/src/components/tutorial/engine/workbench-xpath.d.ts +0 -45
- package/dist/src/components/tutorial/engine/workbench-xpath.js +0 -45
- package/dist/src/components/tutorial/hooks/tutorial-utils.d.ts +0 -1
- package/dist/src/components/tutorial/hooks/tutorial-utils.js +0 -17
- package/dist/src/components/tutorial/hooks/use-tutorial-engine.d.ts +0 -15
- package/dist/src/components/tutorial/hooks/use-tutorial-engine.js +0 -183
- package/dist/src/components/tutorial/hooks/use-tutorial.d.ts +0 -5
- package/dist/src/components/tutorial/hooks/use-tutorial.js +0 -10
- package/dist/src/components/tutorial/tutorial-button.d.ts +0 -2
- package/dist/src/components/tutorial/tutorial-button.js +0 -21
- package/dist/src/components/tutorial/tutorial-step.d.ts +0 -14
- package/dist/src/components/tutorial/tutorial-step.js +0 -19
- package/dist/src/components/tutorial/tutorial.d.ts +0 -2
- package/dist/src/components/tutorial/tutorial.js +0 -32
- package/dist/src/components/ui/json-editor.d.ts +0 -12
- package/dist/src/components/ui/json-editor.js +0 -35
- package/dist/src/components/ui/table.d.ts +0 -10
- package/dist/src/components/ui/table.js +0 -20
- package/dist/src/components/ui/theme-toggle.d.ts +0 -2
- package/dist/src/components/ui/theme-toggle.js +0 -19
- package/dist/src/components/ui/tooltip.d.ts +0 -6
- package/dist/src/components/ui/tooltip.js +0 -3
- package/dist/src/hooks/use-debounced.d.ts +0 -1
- package/dist/src/hooks/use-debounced.js +0 -18
- package/dist/src/hooks/use-fetch-flows.d.ts +0 -1
- package/dist/src/hooks/use-fetch-flows.js +0 -26
- package/dist/src/hooks/use-mobile.d.ts +0 -1
- package/dist/src/hooks/use-mobile.js +0 -15
- package/dist/src/hooks/use-update-handle-positions.d.ts +0 -10
- package/dist/src/hooks/use-update-handle-positions.js +0 -35
- package/dist/src/lib/__tests__/utils.test.d.ts +0 -1
- package/dist/src/lib/__tests__/utils.test.js +0 -94
- package/dist/src/lib/motia-analytics.d.ts +0 -38
- package/dist/src/lib/motia-analytics.js +0 -132
- package/dist/src/lib/plugins.d.ts +0 -2
- package/dist/src/lib/plugins.js +0 -105
- package/dist/src/lib/utils.d.ts +0 -7
- package/dist/src/lib/utils.js +0 -34
- package/dist/src/main.d.ts +0 -2
- package/dist/src/main.js +0 -17
- package/dist/src/project-view-mode.d.ts +0 -1
- package/dist/src/project-view-mode.js +0 -20
- package/dist/src/publicComponents/api-node.d.ts +0 -5
- package/dist/src/publicComponents/api-node.js +0 -5
- package/dist/src/publicComponents/base-node/base-handle.d.ts +0 -9
- package/dist/src/publicComponents/base-node/base-handle.js +0 -8
- package/dist/src/publicComponents/base-node/base-node.d.ts +0 -15
- package/dist/src/publicComponents/base-node/base-node.js +0 -30
- package/dist/src/publicComponents/base-node/code-display.d.ts +0 -9
- package/dist/src/publicComponents/base-node/code-display.js +0 -64
- package/dist/src/publicComponents/base-node/emits.d.ts +0 -5
- package/dist/src/publicComponents/base-node/emits.js +0 -5
- package/dist/src/publicComponents/base-node/feature-card.d.ts +0 -10
- package/dist/src/publicComponents/base-node/feature-card.js +0 -5
- package/dist/src/publicComponents/base-node/language-indicator.d.ts +0 -10
- package/dist/src/publicComponents/base-node/language-indicator.js +0 -29
- package/dist/src/publicComponents/base-node/node-header.d.ts +0 -13
- package/dist/src/publicComponents/base-node/node-header.js +0 -30
- package/dist/src/publicComponents/base-node/node-sidebar.d.ts +0 -14
- package/dist/src/publicComponents/base-node/node-sidebar.js +0 -9
- package/dist/src/publicComponents/base-node/subscribe.d.ts +0 -4
- package/dist/src/publicComponents/base-node/subscribe.js +0 -4
- package/dist/src/publicComponents/cron-node.d.ts +0 -4
- package/dist/src/publicComponents/cron-node.js +0 -6
- package/dist/src/publicComponents/event-node.d.ts +0 -4
- package/dist/src/publicComponents/event-node.js +0 -5
- package/dist/src/publicComponents/node-props.d.ts +0 -21
- package/dist/src/publicComponents/node-props.js +0 -1
- package/dist/src/publicComponents/noop-node.d.ts +0 -4
- package/dist/src/publicComponents/noop-node.js +0 -5
- package/dist/src/setupTests.d.ts +0 -1
- package/dist/src/setupTests.js +0 -1
- package/dist/src/stores/use-app-tabs-store.d.ts +0 -16
- package/dist/src/stores/use-app-tabs-store.js +0 -31
- package/dist/src/stores/use-flow-store.d.ts +0 -21
- package/dist/src/stores/use-flow-store.js +0 -16
- package/dist/src/stores/use-global-store.d.ts +0 -18
- package/dist/src/stores/use-global-store.js +0 -12
- package/dist/src/stores/use-motia-config-store.d.ts +0 -12
- package/dist/src/stores/use-motia-config-store.js +0 -24
- package/dist/src/stores/use-tabs-store.d.ts +0 -19
- package/dist/src/stores/use-tabs-store.js +0 -22
- package/dist/src/system-view-mode.d.ts +0 -1
- package/dist/src/system-view-mode.js +0 -10
- package/dist/src/types/endpoint.d.ts +0 -14
- package/dist/src/types/endpoint.js +0 -1
- package/dist/src/types/file.d.ts +0 -7
- package/dist/src/types/file.js +0 -1
- package/dist/src/types/flow.d.ts +0 -115
- package/dist/src/types/flow.js +0 -1
- package/dist/tsconfig.app.tsbuildinfo +0 -1
- package/dist/tsconfig.node.tsbuildinfo +0 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
interface AmplitudeInstance {
|
|
4
|
+
setOptOut(optOut: boolean): void
|
|
5
|
+
track(eventName: string, eventProperties?: Record<string, any>): void
|
|
6
|
+
identify(userId: string, userProperties?: Record<string, any>): void
|
|
7
|
+
setUserId(userId: string): void
|
|
8
|
+
getUserId(): string | undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
amplitude: AmplitudeInstance
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AnalyticsUserData {
|
|
18
|
+
userId: string
|
|
19
|
+
projectId: string
|
|
20
|
+
motiaVersion: string
|
|
21
|
+
analyticsEnabled: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class WorkbenchAnalytics {
|
|
25
|
+
private isInitialized = false
|
|
26
|
+
private userIdCache: string | null = null
|
|
27
|
+
private projectIdCache: string | null = null
|
|
28
|
+
private motiaVersion: string | null = null
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.initialize()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async initialize() {
|
|
35
|
+
if (typeof window !== 'undefined' && window.amplitude) {
|
|
36
|
+
await this.fetchUserData()
|
|
37
|
+
this.isInitialized = true
|
|
38
|
+
this.identifyUser()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async fetchUserData(): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch('/motia/analytics/user')
|
|
45
|
+
if (response.ok) {
|
|
46
|
+
const data: AnalyticsUserData = await response.json()
|
|
47
|
+
this.userIdCache = data.userId
|
|
48
|
+
this.projectIdCache = data.projectId
|
|
49
|
+
this.motiaVersion = data.motiaVersion
|
|
50
|
+
|
|
51
|
+
window.amplitude.setOptOut(!data.analyticsEnabled)
|
|
52
|
+
// Set the user ID in Amplitude to match backend
|
|
53
|
+
if (window.amplitude && data.userId) {
|
|
54
|
+
window.amplitude.setUserId(data.userId)
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
console.warn('Failed to fetch user data from backend, using fallback')
|
|
58
|
+
this.userIdCache = this.generateFallbackUserId()
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn('Error fetching user data:', error)
|
|
62
|
+
this.userIdCache = this.generateFallbackUserId()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private generateFallbackUserId(): string {
|
|
67
|
+
let userId = localStorage.getItem('motia-user-id')
|
|
68
|
+
if (!userId) {
|
|
69
|
+
userId = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
70
|
+
localStorage.setItem('motia-user-id', userId)
|
|
71
|
+
}
|
|
72
|
+
return userId
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private identifyUser() {
|
|
76
|
+
if (!this.isInitialized || !this.userIdCache) return
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
window.amplitude.identify(this.userIdCache, {
|
|
80
|
+
project_id: this.projectIdCache,
|
|
81
|
+
browser: this.getBrowserInfo(),
|
|
82
|
+
screen_resolution: `${window.screen.width}x${window.screen.height}`,
|
|
83
|
+
workbench_version: this.motiaVersion,
|
|
84
|
+
})
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn('Analytics user identification failed:', error)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private getBrowserInfo(): string {
|
|
91
|
+
const ua = navigator.userAgent
|
|
92
|
+
if (ua.includes('Chrome')) return 'Chrome'
|
|
93
|
+
if (ua.includes('Firefox')) return 'Firefox'
|
|
94
|
+
if (ua.includes('Safari')) return 'Safari'
|
|
95
|
+
if (ua.includes('Edge')) return 'Edge'
|
|
96
|
+
return 'Unknown'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Method to get current user and project IDs for external use
|
|
100
|
+
getAnalyticsIds() {
|
|
101
|
+
return {
|
|
102
|
+
userId: this.userIdCache,
|
|
103
|
+
projectId: this.projectIdCache,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Simple track method that preserves the user binding
|
|
108
|
+
track(eventName: string, properties?: Record<string, any>) {
|
|
109
|
+
if (!this.isInitialized) return
|
|
110
|
+
|
|
111
|
+
const eventProperties = {
|
|
112
|
+
project_id: this.projectIdCache,
|
|
113
|
+
source: 'frontend',
|
|
114
|
+
...properties,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
window.amplitude.track(eventName, eventProperties)
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn('Analytics tracking failed:', error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const motiaAnalytics = new WorkbenchAnalytics()
|
|
126
|
+
|
|
127
|
+
export const useAnalytics = () => {
|
|
128
|
+
const track = useCallback((eventName: string, properties?: Record<string, any>) => {
|
|
129
|
+
motiaAnalytics.track(eventName, properties)
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
const getAnalyticsIds = useCallback(() => {
|
|
133
|
+
return motiaAnalytics.getAnalyticsIds()
|
|
134
|
+
}, [])
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
track,
|
|
138
|
+
getAnalyticsIds,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { plugins } from 'virtual:motia-plugins'
|
|
2
|
+
import { DynamicIcon, dynamicIconImports, type IconName } from 'lucide-react/dynamic'
|
|
3
|
+
import { memo } from 'react'
|
|
4
|
+
import { type AppTab, TabLocation, useAppTabsStore } from '../stores/use-app-tabs-store'
|
|
5
|
+
import { isValidTabLocation } from './utils'
|
|
6
|
+
|
|
7
|
+
export const registerPluginTabs = (addTab: (position: TabLocation, tab: AppTab) => void): void => {
|
|
8
|
+
if (!Array.isArray(plugins)) {
|
|
9
|
+
console.warn('[Motia] Invalid plugins configuration: expected array')
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
plugins.forEach((plugin, index) => {
|
|
14
|
+
try {
|
|
15
|
+
if (!plugin.label) {
|
|
16
|
+
console.warn(`[Motia] Plugin at index ${index} missing label, skipping`)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!plugin.component) {
|
|
21
|
+
console.warn(`[Motia] Plugin "${plugin.label}" missing component, skipping`)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const position = plugin.position || 'top'
|
|
26
|
+
if (!isValidTabLocation(position)) {
|
|
27
|
+
console.warn(`[Motia] Plugin "${plugin.label}" has invalid position "${position}", defaulting to "top"`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tabLocation = isValidTabLocation(position) ? position : TabLocation.TOP
|
|
31
|
+
|
|
32
|
+
const PluginTabLabel = memo(() => {
|
|
33
|
+
const hasIcon = Object.keys(dynamicIconImports).includes(plugin.labelIcon as IconName)
|
|
34
|
+
const iconName = hasIcon ? (plugin.labelIcon as IconName) : 'toy-brick'
|
|
35
|
+
|
|
36
|
+
if (!hasIcon) {
|
|
37
|
+
console.warn(
|
|
38
|
+
`[Motia] Plugin "${plugin.label}" has invalid icon "${plugin.labelIcon}", defaulting to "toy-brick"`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<DynamicIcon name={iconName} />
|
|
45
|
+
<span>{plugin.label}</span>
|
|
46
|
+
</>
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
PluginTabLabel.displayName = `${plugin.label}TabLabel`
|
|
50
|
+
|
|
51
|
+
const PluginContent = memo(() => {
|
|
52
|
+
const Component = plugin.component
|
|
53
|
+
const props = plugin.props || {}
|
|
54
|
+
|
|
55
|
+
if (!Component) {
|
|
56
|
+
return <div>Error: Plugin component not found</div>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return <Component {...props} />
|
|
60
|
+
})
|
|
61
|
+
PluginContent.displayName = `${plugin.label}Content`
|
|
62
|
+
|
|
63
|
+
addTab(tabLocation, {
|
|
64
|
+
id: plugin.label.toLowerCase(),
|
|
65
|
+
tabLabel: PluginTabLabel,
|
|
66
|
+
content: PluginContent,
|
|
67
|
+
})
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`[Motia] Error registering plugin "${plugin.label}":`, error)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const refreshPluginTabs = (nextPlugins: typeof plugins): void => {
|
|
75
|
+
try {
|
|
76
|
+
const state = useAppTabsStore.getState()
|
|
77
|
+
const { removeTab, addTab } = state
|
|
78
|
+
|
|
79
|
+
const idsToRefresh = new Set(nextPlugins.map((p) => (p.label || '').toLowerCase()))
|
|
80
|
+
|
|
81
|
+
idsToRefresh.forEach((id) => {
|
|
82
|
+
removeTab(TabLocation.TOP, id)
|
|
83
|
+
removeTab(TabLocation.BOTTOM, id)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
nextPlugins.forEach((plugin, index) => {
|
|
87
|
+
try {
|
|
88
|
+
if (!plugin.label || !plugin.component) return
|
|
89
|
+
|
|
90
|
+
const position = plugin.position || 'top'
|
|
91
|
+
const tabLocation = isValidTabLocation(position) ? position : TabLocation.TOP
|
|
92
|
+
|
|
93
|
+
const PluginTabLabel = memo(() => {
|
|
94
|
+
const hasIcon = Object.keys(dynamicIconImports).includes(plugin.labelIcon as IconName)
|
|
95
|
+
const iconName: IconName = hasIcon ? (plugin.labelIcon as IconName) : 'toy-brick'
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
<DynamicIcon name={iconName} />
|
|
99
|
+
<span>{plugin.label}</span>
|
|
100
|
+
</>
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
PluginTabLabel.displayName = `${plugin.label}TabLabel_HMR_${index}`
|
|
104
|
+
|
|
105
|
+
const PluginContent = memo(() => {
|
|
106
|
+
const Component = plugin.component as React.ElementType
|
|
107
|
+
const props = plugin.props || {}
|
|
108
|
+
if (!Component) return <div>Error: Plugin component not found</div>
|
|
109
|
+
return <Component {...props} />
|
|
110
|
+
})
|
|
111
|
+
PluginContent.displayName = `${plugin.label}Content_HMR_${index}`
|
|
112
|
+
|
|
113
|
+
addTab(tabLocation, {
|
|
114
|
+
id: plugin.label.toLowerCase(),
|
|
115
|
+
tabLabel: PluginTabLabel,
|
|
116
|
+
content: PluginContent,
|
|
117
|
+
})
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`[Motia] Error refreshing plugin "${plugin.label}":`, error)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error('[Motia] Failed to refresh plugin tabs via HMR:', err)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (import.meta.hot) {
|
|
128
|
+
import.meta.hot.accept('virtual:motia-plugins', (mod) => {
|
|
129
|
+
const next = (mod as unknown as { plugins?: typeof plugins })?.plugins || []
|
|
130
|
+
refreshPluginTabs(next)
|
|
131
|
+
})
|
|
132
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { TabLocation } from '../stores/use-app-tabs-store'
|
|
2
|
+
|
|
3
|
+
export const formatDuration = (duration?: number) => {
|
|
4
|
+
if (duration === undefined || duration === null) return 'N/A'
|
|
5
|
+
if (duration < 1000) return `${duration}ms`
|
|
6
|
+
if (duration < 60000) return `${(duration / 1000).toFixed(1)}s`
|
|
7
|
+
if (duration < 3600000) return `${(duration / 60000).toFixed(1)}min`
|
|
8
|
+
return `${(duration / 3600000).toFixed(1)}h`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const formatTimestamp = (time: number) => {
|
|
12
|
+
const date = new Date(Number(time))
|
|
13
|
+
return `${date.toLocaleDateString('en-US', { year: undefined, month: 'short', day: '2-digit' })}, ${date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h24' })}.${date.getMilliseconds().toString().padStart(3, '0')}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ViewMode = 'project' | 'system'
|
|
17
|
+
export const DEFAULT_VIEW_MODE: ViewMode = 'system'
|
|
18
|
+
|
|
19
|
+
export const getViewModeFromURL = (): ViewMode => {
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(window.location.href)
|
|
22
|
+
const viewMode = url.searchParams.get('view-mode')
|
|
23
|
+
|
|
24
|
+
if (viewMode === 'project' || viewMode === 'system') {
|
|
25
|
+
return viewMode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return DEFAULT_VIEW_MODE
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('[Motia] Error parsing URL:', error)
|
|
31
|
+
return DEFAULT_VIEW_MODE
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const isValidTabLocation = (position: string): position is TabLocation => {
|
|
36
|
+
return Object.values(TabLocation).includes(position as TabLocation)
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MotiaStreamProvider } from '@motiadev/stream-client-react'
|
|
2
|
+
import { StrictMode } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
|
5
|
+
import { App } from './App'
|
|
6
|
+
import { NotFoundPage } from './components/NotFoundPage'
|
|
7
|
+
import { RootMotia } from './components/root-motia'
|
|
8
|
+
import './index.css'
|
|
9
|
+
|
|
10
|
+
const rootElement = document.getElementById('root')!
|
|
11
|
+
if (!rootElement.innerHTML) {
|
|
12
|
+
const basePath = workbenchBase
|
|
13
|
+
const root = createRoot(rootElement)
|
|
14
|
+
const address = window.location.origin.replace('http', 'ws')
|
|
15
|
+
|
|
16
|
+
root.render(
|
|
17
|
+
<StrictMode>
|
|
18
|
+
<MotiaStreamProvider address={address}>
|
|
19
|
+
<RootMotia>
|
|
20
|
+
<BrowserRouter>
|
|
21
|
+
<Routes>
|
|
22
|
+
<Route path={basePath} element={<App />} />
|
|
23
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
24
|
+
</Routes>
|
|
25
|
+
</BrowserRouter>
|
|
26
|
+
</RootMotia>
|
|
27
|
+
</MotiaStreamProvider>
|
|
28
|
+
</StrictMode>,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { APP_SIDEBAR_CONTAINER_ID, Panel } from '@motiadev/ui'
|
|
2
|
+
import { memo } from 'react'
|
|
3
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
4
|
+
import { type AppTabsState, TabLocation, useAppTabsStore } from './stores/use-app-tabs-store'
|
|
5
|
+
|
|
6
|
+
const topTabs = (state: AppTabsState) => state.tabs[TabLocation.TOP]
|
|
7
|
+
|
|
8
|
+
export const ProjectViewMode = memo(() => {
|
|
9
|
+
const tabs = useAppTabsStore(useShallow(topTabs))
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="grid grid-rows-1 grid-cols-[1fr_auto] bg-background text-foreground h-screen ">
|
|
13
|
+
<main className="m-2 overflow-hidden">
|
|
14
|
+
<Panel
|
|
15
|
+
contentClassName={'p-0'}
|
|
16
|
+
tabs={tabs.map((tab) => {
|
|
17
|
+
const Element = tab.content
|
|
18
|
+
const LabelComponent = tab.tabLabel
|
|
19
|
+
return {
|
|
20
|
+
label: tab.id,
|
|
21
|
+
labelComponent: <LabelComponent />,
|
|
22
|
+
content: <Element />,
|
|
23
|
+
'data-testid': tab.id,
|
|
24
|
+
}
|
|
25
|
+
})}
|
|
26
|
+
/>
|
|
27
|
+
</main>
|
|
28
|
+
<div id={APP_SIDEBAR_CONTAINER_ID} />
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
ProjectViewMode.displayName = 'ProjectViewMode'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { JSX, PropsWithChildren } from 'react'
|
|
2
|
+
import { BaseNode } from './base-node/base-node'
|
|
3
|
+
import type { ApiNodeProps } from './node-props'
|
|
4
|
+
|
|
5
|
+
type Props = PropsWithChildren<ApiNodeProps>
|
|
6
|
+
|
|
7
|
+
export const ApiNode = ({ data, children }: Props): JSX.Element => {
|
|
8
|
+
return (
|
|
9
|
+
<BaseNode
|
|
10
|
+
data={data}
|
|
11
|
+
variant="api"
|
|
12
|
+
title={data.name}
|
|
13
|
+
language={data.language}
|
|
14
|
+
subtitle={data.description}
|
|
15
|
+
disableSourceHandle={!data.emits?.length && !data.virtualEmits?.length}
|
|
16
|
+
disableTargetHandle={!data.subscribes?.length && !data.virtualSubscribes?.length}
|
|
17
|
+
>
|
|
18
|
+
{data.webhookUrl && (
|
|
19
|
+
<div className="flex gap-1 items-center text-muted-foreground">
|
|
20
|
+
<div className="font-mono">{data.webhookUrl}</div>
|
|
21
|
+
</div>
|
|
22
|
+
)}
|
|
23
|
+
{children}
|
|
24
|
+
</BaseNode>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type HandleProps, Position, Handle as RFHandle } from '@xyflow/react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import type React from 'react'
|
|
4
|
+
import type { HTMLAttributes } from 'react'
|
|
5
|
+
|
|
6
|
+
type Props = HandleProps &
|
|
7
|
+
Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
|
|
8
|
+
isHidden?: boolean
|
|
9
|
+
onTogglePosition?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const BaseHandle: React.FC<Props> = (props) => {
|
|
13
|
+
const { isHidden, position, onTogglePosition, ...rest } = props
|
|
14
|
+
const isHorizontal = position === Position.Left || position === Position.Right
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={clsx(
|
|
19
|
+
'absolute w-[6px] h-[6px]',
|
|
20
|
+
position === Position.Top && '-top-[4px]',
|
|
21
|
+
position === Position.Bottom && '-bottom-[4px]',
|
|
22
|
+
position === Position.Left && '-left-[4px]',
|
|
23
|
+
position === Position.Right && '-right-[4px]',
|
|
24
|
+
isHorizontal ? 'top-1/2 -mt-[2px]' : 'left-1/2 -ml-[2px]',
|
|
25
|
+
isHidden && 'hidden',
|
|
26
|
+
)}
|
|
27
|
+
onClick={onTogglePosition}
|
|
28
|
+
>
|
|
29
|
+
<RFHandle
|
|
30
|
+
{...rest}
|
|
31
|
+
position={position}
|
|
32
|
+
style={{ background: 'rgb(30,118,231)' }}
|
|
33
|
+
className="
|
|
34
|
+
bg-white/50
|
|
35
|
+
!static
|
|
36
|
+
!w-[6px]
|
|
37
|
+
!h-[6px]
|
|
38
|
+
!min-w-[6px]
|
|
39
|
+
!min-h-[6px]
|
|
40
|
+
!p-0
|
|
41
|
+
!border-none
|
|
42
|
+
!transform-none
|
|
43
|
+
!rounded-full
|
|
44
|
+
!outline-none
|
|
45
|
+
!shadow-none
|
|
46
|
+
"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Button, cn } from '@motiadev/ui'
|
|
2
|
+
import { ScanSearch } from 'lucide-react'
|
|
3
|
+
import type React from 'react'
|
|
4
|
+
import { type PropsWithChildren, useCallback, useEffect, useState } from 'react'
|
|
5
|
+
import { useHandlePositions } from '../../hooks/use-update-handle-positions'
|
|
6
|
+
import type { Feature } from '../../types/file'
|
|
7
|
+
import type { BaseNodeProps } from '../node-props'
|
|
8
|
+
import { BaseHandle } from './base-handle'
|
|
9
|
+
import { NodeHeader } from './node-header'
|
|
10
|
+
import { NodeSidebar } from './node-sidebar'
|
|
11
|
+
|
|
12
|
+
type Props = PropsWithChildren<{
|
|
13
|
+
title: string
|
|
14
|
+
subtitle?: string
|
|
15
|
+
variant: 'event' | 'api' | 'noop' | 'cron'
|
|
16
|
+
language?: string
|
|
17
|
+
className?: string
|
|
18
|
+
disableSourceHandle?: boolean
|
|
19
|
+
disableTargetHandle?: boolean
|
|
20
|
+
data: BaseNodeProps
|
|
21
|
+
}>
|
|
22
|
+
|
|
23
|
+
export const BaseNode: React.FC<Props> = ({
|
|
24
|
+
title,
|
|
25
|
+
variant,
|
|
26
|
+
children,
|
|
27
|
+
disableSourceHandle,
|
|
28
|
+
disableTargetHandle,
|
|
29
|
+
language,
|
|
30
|
+
subtitle,
|
|
31
|
+
data,
|
|
32
|
+
}) => {
|
|
33
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
34
|
+
const { sourcePosition, targetPosition, toggleTargetPosition, toggleSourcePosition } = useHandlePositions(data)
|
|
35
|
+
|
|
36
|
+
const [content, setContent] = useState<string | null>(null)
|
|
37
|
+
const [features, setFeatures] = useState<Feature[]>([])
|
|
38
|
+
|
|
39
|
+
const fetchContent = useCallback(async () => {
|
|
40
|
+
const response = await fetch(`/__motia/step/${data.id}`)
|
|
41
|
+
const responseData = await response.json()
|
|
42
|
+
setContent(responseData.content)
|
|
43
|
+
setFeatures(responseData.features)
|
|
44
|
+
}, [data.id])
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (data.id && isOpen) {
|
|
48
|
+
fetchContent()
|
|
49
|
+
}
|
|
50
|
+
}, [data.id, isOpen, fetchContent])
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
className={cn('p-1 rounded-lg max-w-[350px]', {
|
|
55
|
+
'bg-muted-foreground/20': isOpen,
|
|
56
|
+
})}
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
className="rounded-lg dark:bg-[#101010] bg-background border-1 border-muted-foreground/30 border-solid"
|
|
60
|
+
data-testid={`node-${title?.toLowerCase().replace(/ /g, '-')}`}
|
|
61
|
+
>
|
|
62
|
+
<div className="group relative">
|
|
63
|
+
{/* Main node content */}
|
|
64
|
+
<NodeHeader text={title} variant={variant} className="border-b-2 border-muted-foreground/10">
|
|
65
|
+
<div className="flex justify-end">
|
|
66
|
+
<Button
|
|
67
|
+
data-testid={`open-code-preview-button-${title?.toLowerCase()}`}
|
|
68
|
+
variant="ghost"
|
|
69
|
+
className="h-5 p-0.5"
|
|
70
|
+
onClick={() => setIsOpen(true)}
|
|
71
|
+
>
|
|
72
|
+
<ScanSearch className="w-4 h-4" />
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</NodeHeader>
|
|
76
|
+
|
|
77
|
+
{subtitle && <div className="py-4 px-6 text-sm text-muted-foreground">{subtitle}</div>}
|
|
78
|
+
{children && (
|
|
79
|
+
<div className="p-2">
|
|
80
|
+
<div
|
|
81
|
+
className={cn('space-y-3 p-4 text-sm text-muted-foreground', {
|
|
82
|
+
'bg-card': variant !== 'noop',
|
|
83
|
+
})}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Connection points */}
|
|
91
|
+
{!disableTargetHandle && (
|
|
92
|
+
<BaseHandle type="target" position={targetPosition} onTogglePosition={toggleTargetPosition} />
|
|
93
|
+
)}
|
|
94
|
+
{!disableSourceHandle && (
|
|
95
|
+
<BaseHandle type="source" position={sourcePosition} onTogglePosition={toggleSourcePosition} />
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{content && (
|
|
101
|
+
<NodeSidebar
|
|
102
|
+
features={features}
|
|
103
|
+
content={content}
|
|
104
|
+
title={title}
|
|
105
|
+
subtitle={subtitle}
|
|
106
|
+
variant={variant}
|
|
107
|
+
language={language}
|
|
108
|
+
isOpen={isOpen}
|
|
109
|
+
onClose={() => setIsOpen(false)}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useThemeStore } from '@motiadev/ui'
|
|
2
|
+
import type React from 'react'
|
|
3
|
+
import { useRef, useState } from 'react'
|
|
4
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
|
5
|
+
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
|
6
|
+
import type { Feature } from '../../types/file'
|
|
7
|
+
import { FeatureCard } from './feature-card'
|
|
8
|
+
import { LanguageIndicator } from './language-indicator'
|
|
9
|
+
|
|
10
|
+
type CodeDisplayProps = {
|
|
11
|
+
code: string
|
|
12
|
+
language?: string
|
|
13
|
+
features?: Feature[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const codeTagProps = {
|
|
17
|
+
style: {
|
|
18
|
+
fontFamily: 'DM Mono, monospace',
|
|
19
|
+
fontSize: '16px',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const customStyle = {
|
|
24
|
+
margin: 0,
|
|
25
|
+
borderRadius: 0,
|
|
26
|
+
padding: 0,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isHighlighted = (lines: string[], lineNumber: number) => {
|
|
30
|
+
return lines.some((line) => {
|
|
31
|
+
const [start, end] = line.split('-').map((num) => parseInt(num, 10))
|
|
32
|
+
|
|
33
|
+
if (end !== undefined) {
|
|
34
|
+
return lineNumber >= start && lineNumber <= end
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return lineNumber == start
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const getFirstLineNumber = (line: string) => {
|
|
42
|
+
const [start] = line.split('-').map((num) => parseInt(num, 10))
|
|
43
|
+
return start
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const CodeDisplay: React.FC<CodeDisplayProps> = ({ code, language, features }) => {
|
|
47
|
+
const theme = useThemeStore((state) => state.theme)
|
|
48
|
+
const themeStyle = theme === 'dark' ? oneDark : oneLight
|
|
49
|
+
const [highlightedLines, setHighlightedLines] = useState<string[]>([])
|
|
50
|
+
const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
|
|
51
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
52
|
+
|
|
53
|
+
const handleFeatureClick = (feature: Feature) => {
|
|
54
|
+
setSelectedFeature(feature)
|
|
55
|
+
setHighlightedLines(feature.lines)
|
|
56
|
+
const lineNumber = getFirstLineNumber(feature.lines[0])
|
|
57
|
+
const line = ref.current?.querySelector(`[data-line-number="${lineNumber}"]`)
|
|
58
|
+
|
|
59
|
+
if (line) {
|
|
60
|
+
line.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex flex-col h-full overflow-hidden">
|
|
66
|
+
<div className="flex items-center py-2 px-5 dark:bg-[#1e1e1e] gap-2 justify-center">
|
|
67
|
+
<div className="text-sm text-muted-foreground">Read only</div>
|
|
68
|
+
<div className="flex-1" />
|
|
69
|
+
<LanguageIndicator language={language} className="w-4 h-4" size={16} showLabel />
|
|
70
|
+
</div>
|
|
71
|
+
<div className="flex flex-row h-[calc(100%-36px)]">
|
|
72
|
+
{features && features.length > 0 && (
|
|
73
|
+
<div className="flex flex-col gap-2 p-2 bg-card overflow-y-auto min-w-[200px] w-[300px]">
|
|
74
|
+
{features.map((feature, index) => (
|
|
75
|
+
<FeatureCard
|
|
76
|
+
key={index}
|
|
77
|
+
feature={feature}
|
|
78
|
+
highlighted={selectedFeature === feature}
|
|
79
|
+
onClick={() => handleFeatureClick(feature)}
|
|
80
|
+
onHover={() => handleFeatureClick(feature)}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
<div className="overflow-y-auto" ref={ref}>
|
|
87
|
+
<SyntaxHighlighter
|
|
88
|
+
showLineNumbers
|
|
89
|
+
language={language}
|
|
90
|
+
style={themeStyle}
|
|
91
|
+
codeTagProps={codeTagProps}
|
|
92
|
+
customStyle={customStyle}
|
|
93
|
+
wrapLines
|
|
94
|
+
lineProps={(lineNumber) => {
|
|
95
|
+
if (isHighlighted(highlightedLines, lineNumber)) {
|
|
96
|
+
return {
|
|
97
|
+
'data-line-number': lineNumber,
|
|
98
|
+
style: {
|
|
99
|
+
borderLeft: '2px solid var(--accent-1000)',
|
|
100
|
+
backgroundColor: 'rgb(from var(--accent-1000) r g b / 0.2)',
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
'data-line-number': lineNumber,
|
|
107
|
+
style: {
|
|
108
|
+
borderLeft: '2px solid transparent',
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{code}
|
|
114
|
+
</SyntaxHighlighter>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|