@motiadev/workbench 0.6.0-beta.120 → 0.6.0-beta.122

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 (50) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.html +21 -1
  3. package/dist/index.js +1 -0
  4. package/dist/middleware.js +15 -2
  5. package/dist/src/App.js +28 -5
  6. package/dist/src/components/endpoints/endpoint-body-panel.d.ts +1 -0
  7. package/dist/src/components/endpoints/endpoint-body-panel.js +2 -2
  8. package/dist/src/components/endpoints/endpoint-call.js +1 -1
  9. package/dist/src/components/endpoints/endpoint-description.js +1 -1
  10. package/dist/src/components/flow/hooks/use-get-flow-state.js +1 -0
  11. package/dist/src/components/flow/node-organizer.js +4 -38
  12. package/dist/src/components/header/header.js +1 -1
  13. package/dist/src/components/logs/logs-page.js +1 -1
  14. package/dist/src/components/observability/trace-item/trace-item.js +1 -1
  15. package/dist/src/components/observability/traces-groups.js +1 -1
  16. package/dist/src/components/sidebar/sidebar.js +1 -1
  17. package/dist/src/components/tutorial/engine/tutorial-engine.d.ts +12 -0
  18. package/dist/src/components/tutorial/engine/tutorial-engine.js +36 -0
  19. package/dist/src/components/tutorial/engine/tutorial-types.d.ts +22 -0
  20. package/dist/src/components/tutorial/engine/tutorial-types.js +1 -0
  21. package/dist/src/components/tutorial/engine/workbench-xpath.d.ts +40 -0
  22. package/dist/src/components/tutorial/engine/workbench-xpath.js +40 -0
  23. package/dist/src/components/tutorial/hooks/tutorial-utils.d.ts +1 -0
  24. package/dist/src/components/tutorial/hooks/tutorial-utils.js +17 -0
  25. package/dist/src/components/tutorial/hooks/use-tutorial-engine.d.ts +15 -0
  26. package/dist/src/components/tutorial/hooks/use-tutorial-engine.js +176 -0
  27. package/dist/src/components/tutorial/hooks/use-tutorial.d.ts +5 -0
  28. package/dist/src/components/tutorial/hooks/use-tutorial.js +10 -0
  29. package/dist/src/components/tutorial/tutorial-button.js +21 -0
  30. package/dist/src/components/tutorial/tutorial-step.d.ts +14 -0
  31. package/dist/src/components/tutorial/tutorial-step.js +18 -0
  32. package/dist/src/components/tutorial/tutorial.css +208 -0
  33. package/dist/src/components/tutorial/tutorial.d.ts +2 -0
  34. package/dist/src/components/tutorial/tutorial.js +32 -0
  35. package/dist/src/components/ui/theme-toggle.js +8 -0
  36. package/dist/src/publicComponents/base-node/base-node.js +3 -1
  37. package/dist/src/publicComponents/base-node/code-display.d.ts +9 -0
  38. package/dist/src/publicComponents/base-node/code-display.js +64 -0
  39. package/dist/src/publicComponents/base-node/feature-card.d.ts +10 -0
  40. package/dist/src/publicComponents/base-node/feature-card.js +5 -0
  41. package/dist/src/publicComponents/base-node/node-sidebar.d.ts +2 -0
  42. package/dist/src/publicComponents/base-node/node-sidebar.js +4 -5
  43. package/dist/src/stores/use-theme-store.d.ts +1 -2
  44. package/dist/src/types/file.d.ts +7 -0
  45. package/dist/src/types/file.js +1 -0
  46. package/dist/tsconfig.app.tsbuildinfo +1 -1
  47. package/dist/tsconfig.node.tsbuildinfo +1 -1
  48. package/package.json +13 -12
  49. package/dist/src/components/ui/tutorial-button.js +0 -69
  50. /package/dist/src/components/{ui → tutorial}/tutorial-button.d.ts +0 -0
@@ -0,0 +1,176 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { MotiaTutorial } from '../engine/tutorial-engine';
3
+ import { waitForElementByXPath } from './tutorial-utils';
4
+ export const useTutorialEngine = () => {
5
+ const ref = useRef(null);
6
+ const highlighterRef = useRef(null);
7
+ const [title, setTitle] = useState('');
8
+ const [description, setDescription] = useState(undefined);
9
+ const [image, setImage] = useState(undefined);
10
+ const [link, setLink] = useState(undefined);
11
+ const [currentStep, setCurrentStep] = useState(0);
12
+ const [totalSteps, setTotalSteps] = useState(MotiaTutorial.steps.length);
13
+ const manualOpenRef = useRef(false);
14
+ const loading = useRef(false);
15
+ const currentStepRef = useRef(0);
16
+ const moveComponent = (x, y) => {
17
+ if (ref.current) {
18
+ ref.current.style.position = 'absolute';
19
+ ref.current.style.left = `${x}px`;
20
+ ref.current.style.top = `${y}px`;
21
+ }
22
+ };
23
+ const moveStep = async (stepNumber) => {
24
+ const container = ref.current;
25
+ if (container && !loading.current) {
26
+ if (stepNumber >= MotiaTutorial.steps.length) {
27
+ onClose();
28
+ return;
29
+ }
30
+ if (container.parentElement) {
31
+ container.style.transition = 'all 0.3s ease-in-out';
32
+ container.parentElement.style.opacity = '1';
33
+ container.parentElement.style.display = 'block';
34
+ }
35
+ loading.current = true;
36
+ currentStepRef.current = stepNumber;
37
+ const step = MotiaTutorial.steps[stepNumber];
38
+ // Run any before actions
39
+ if (step.before) {
40
+ for (const action of step.before) {
41
+ if (action.type === 'click') {
42
+ const element = await waitForElementByXPath(action.selector, action.optional);
43
+ if (element) {
44
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
45
+ element.click();
46
+ element.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', keyCode: 13 }));
47
+ }
48
+ }
49
+ else if (action.type === 'fill-editor' && monaco) {
50
+ monaco.editor?.getEditors?.()?.[0]?.setValue(JSON.stringify(action.content, null, 2));
51
+ }
52
+ }
53
+ }
54
+ setCurrentStep(stepNumber + 1);
55
+ setTitle(step.title);
56
+ setDescription(await step.description());
57
+ setImage(step.image);
58
+ setLink(step.link);
59
+ setTimeout(async () => {
60
+ const { height, width } = container.getBoundingClientRect();
61
+ if (step.elementXpath) {
62
+ const targetElement = await waitForElementByXPath(step.elementXpath);
63
+ if (!targetElement) {
64
+ console.warn(`Element not found after maximum attempts: ${step.elementXpath}`);
65
+ loading.current = false;
66
+ return;
67
+ }
68
+ if (targetElement && highlighterRef.current) {
69
+ const { top, left, width, height } = targetElement.getBoundingClientRect();
70
+ highlighterRef.current.style.top = `${top - 0}px`;
71
+ highlighterRef.current.style.left = `${left - 0}px`;
72
+ highlighterRef.current.style.width = `${width + 0}px`;
73
+ highlighterRef.current.style.height = `${height + 0}px`;
74
+ }
75
+ // Position tutorial relative to target element
76
+ if (targetElement) {
77
+ const targetRect = targetElement.getBoundingClientRect();
78
+ const spaceBelow = window.innerHeight - targetRect.bottom;
79
+ const spaceAbove = targetRect.top;
80
+ const spaceRight = window.innerWidth - targetRect.right;
81
+ const spaceLeft = targetRect.left;
82
+ // Helper function to adjust horizontal position within viewport bounds
83
+ const adjustHorizontalPosition = (x) => {
84
+ // this is important to avoid the tutorial from overflowing at far left or right
85
+ return Math.max(20, Math.min(x, window.innerWidth - width - 20));
86
+ };
87
+ // Try to position below first
88
+ if (spaceBelow >= height + 20) {
89
+ const x = targetRect.left + targetRect.width / 2 - width / 2;
90
+ moveComponent(adjustHorizontalPosition(x), targetRect.bottom + 20);
91
+ }
92
+ // Try above if not enough space below
93
+ else if (spaceAbove >= height + 20) {
94
+ const x = targetRect.left + targetRect.width / 2 - width / 2;
95
+ moveComponent(adjustHorizontalPosition(x), targetRect.top - height - 20);
96
+ }
97
+ // Try right side
98
+ else if (spaceRight >= width + 20) {
99
+ moveComponent(targetRect.right + 20, targetRect.top + targetRect.height / 2 - height / 2);
100
+ }
101
+ // Try left side
102
+ else if (spaceLeft >= width + 20) {
103
+ moveComponent(targetRect.left - width - 20, targetRect.top + targetRect.height / 2 - height / 2);
104
+ }
105
+ }
106
+ }
107
+ else {
108
+ if (highlighterRef.current) {
109
+ highlighterRef.current.style.top = '50%';
110
+ highlighterRef.current.style.left = '50%';
111
+ highlighterRef.current.style.width = '1px';
112
+ highlighterRef.current.style.height = '1px';
113
+ }
114
+ // Fallback to center of screen
115
+ moveComponent(window.innerWidth / 2 - width / 2, window.innerHeight / 2 - height / 2);
116
+ }
117
+ }, 1);
118
+ loading.current = false;
119
+ }
120
+ };
121
+ const onClose = () => {
122
+ if (ref.current?.parentElement) {
123
+ ref.current.parentElement.style.transition = 'opacity 0.3s ease-out';
124
+ ref.current.parentElement.style.opacity = '0';
125
+ localStorage.setItem('motia-tutorial-closed', 'true');
126
+ setTimeout(() => {
127
+ if (ref.current?.parentElement) {
128
+ ref.current.parentElement.style.display = 'none';
129
+ }
130
+ }, 300);
131
+ }
132
+ };
133
+ useEffect(() => {
134
+ importFile('tutorial.tsx').then((module) => {
135
+ if (Array.isArray(module.steps) && module.steps.length > 0) {
136
+ MotiaTutorial.register(module.steps);
137
+ }
138
+ });
139
+ }, []);
140
+ useEffect(() => {
141
+ const container = ref.current;
142
+ if (container?.parentElement) {
143
+ container.parentElement.style.display = 'none';
144
+ }
145
+ const onOpen = () => {
146
+ if (container?.parentElement) {
147
+ setTotalSteps(MotiaTutorial.steps.length);
148
+ moveStep(0);
149
+ }
150
+ };
151
+ MotiaTutorial.onOpen(() => {
152
+ manualOpenRef.current = true;
153
+ onOpen();
154
+ });
155
+ MotiaTutorial.onStepsRegistered(() => {
156
+ if (localStorage.getItem('motia-tutorial-closed') !== 'true') {
157
+ manualOpenRef.current = false;
158
+ onOpen();
159
+ }
160
+ });
161
+ }, []);
162
+ return {
163
+ ref,
164
+ highlighterRef,
165
+ title,
166
+ description,
167
+ image,
168
+ link,
169
+ currentStep,
170
+ totalSteps,
171
+ onClose,
172
+ moveStep,
173
+ currentStepRef,
174
+ manualOpenRef,
175
+ };
176
+ };
@@ -0,0 +1,5 @@
1
+ import { TutorialStep } from '../engine/tutorial-types';
2
+ export declare const useTutorial: () => {
3
+ open: () => void;
4
+ steps: TutorialStep[];
5
+ };
@@ -0,0 +1,10 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { MotiaTutorial } from '../engine/tutorial-engine';
3
+ export const useTutorial = () => {
4
+ const open = () => MotiaTutorial.open();
5
+ const [steps, setSteps] = useState([]);
6
+ useEffect(() => {
7
+ MotiaTutorial.onStepsRegistered(() => setSteps(MotiaTutorial.steps));
8
+ }, []);
9
+ return { open, steps };
10
+ };
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { analytics } from '@/lib/analytics';
3
+ import { Button } from '@motiadev/ui';
4
+ import { Book } from 'lucide-react';
5
+ import { Tooltip } from '../ui/tooltip';
6
+ import { useTutorial } from './hooks/use-tutorial';
7
+ export const TutorialButton = () => {
8
+ const { open, steps } = useTutorial();
9
+ const isTutorialFlowMissing = steps.length === 0;
10
+ const onTutorialButtonClick = () => {
11
+ if (!isTutorialFlowMissing) {
12
+ open();
13
+ }
14
+ analytics.track('tutorial_button_clicked', { isTutorialFlowMissing });
15
+ };
16
+ const trigger = (_jsxs(Button, { "data-testid": "tutorial-trigger", variant: "default", onClick: () => onTutorialButtonClick(), children: [_jsx(Book, { className: "h-4 w-4" }), "Tutorial"] }));
17
+ if (isTutorialFlowMissing) {
18
+ return (_jsx(Tooltip, { content: _jsxs("div", { className: "flex flex-col gap-4 p-4 max-w-[320px]", children: [_jsx("p", { className: "text-sm wrap-break-word p-0 m-0", children: "In order to start the tutorial, you need to download the tutorial steps using the Motia CLI. In your terminal execute the following command to create a new project:" }), _jsx("pre", { className: "text-sm font-bold", children: "npx motia@latest create" })] }), children: trigger }));
19
+ }
20
+ return trigger;
21
+ };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { TutorialImage } from './engine/tutorial-types';
3
+ type TutorialStepProps = {
4
+ step: number;
5
+ totalSteps: number;
6
+ title: string;
7
+ description: React.ReactNode;
8
+ link?: string;
9
+ image?: TutorialImage;
10
+ onNext: () => void;
11
+ onClose: () => void;
12
+ };
13
+ export declare const TutorialStep: React.ForwardRefExoticComponent<TutorialStepProps & React.RefAttributes<HTMLDivElement>>;
14
+ export {};
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect } from 'react';
3
+ export const TutorialStep = forwardRef(({ step, totalSteps, title, description, link, image, onNext, onClose }, ref) => {
4
+ useEffect(() => {
5
+ const handleKeyDown = (e) => {
6
+ if (e.key === 'Escape') {
7
+ onClose();
8
+ }
9
+ else if (e.key === 'ArrowRight') {
10
+ onNext();
11
+ }
12
+ };
13
+ window.addEventListener('keydown', handleKeyDown);
14
+ return () => window.removeEventListener('keydown', handleKeyDown);
15
+ }, [onClose, onNext]);
16
+ return (_jsxs("div", { ref: ref, className: "driver-popover", children: [image && (_jsx("img", { src: image.src, alt: "Step visual", className: "driver-popover-image object-cover", style: { height: image.height, width: '100%' } })), _jsx("div", { className: "driver-popover-title", children: _jsx("h2", { className: "popover-title", children: title }) }), _jsx("div", { className: "driver-popover-description", children: description }), link && (_jsx("a", { href: link, target: "_blank", className: "text-foreground text-xs font-semibold px-4 hover:underline", children: "Learn more" })), _jsxs("div", { className: "driver-popover-footer flex items-center justify-between", children: [_jsxs("div", { className: "text-sm text-muted-foreground font-semibold", children: [step, " ", _jsx("span", { className: "text-foreground", children: "/" }), " ", totalSteps] }), _jsx("div", { className: "driver-popover-navigation-btns flex gap-2", children: _jsx("button", { className: "driver-popover-next-btn", onClick: onNext, children: step < totalSteps ? 'Continue' : 'Finish' }) })] }), step < totalSteps && (_jsx("div", { className: "tutorial-opt-out-container", children: _jsx("button", { className: "tutorial-opt-out-button", onClick: onClose, children: "Close" }) }))] }));
17
+ });
18
+ TutorialStep.displayName = 'TutorialStep';
@@ -0,0 +1,208 @@
1
+ @import '@motiadev/ui/styles.css';
2
+ @import '@motiadev/ui/globals.css';
3
+ @import 'tw-animate-css';
4
+
5
+ :root {
6
+ --tutorial-text-color: var(--dark-800);
7
+ --tutorial-border: var(--light-1000);
8
+ --tutorial-code-bg: rgb(24, 24, 24, 0.1);
9
+ --tutorial-code-text: var(--text-body);
10
+ }
11
+
12
+ .dark {
13
+ --tutorial-border: rgb(65, 65, 65);
14
+ --tutorial-text-color: var(--light-800);
15
+ --tutorial-code-bg: rgb(24, 24, 24, 0.4);
16
+ --tutorial-code-text: var(--light-1000);
17
+ }
18
+
19
+ .driver-popover {
20
+ z-index: 10000;
21
+ width: 400px;
22
+ max-width: 100%;
23
+ background-color: var(--background);
24
+ padding: 0;
25
+ --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, #0000001a), 0 4px 6px -4px var(--tw-shadow-color, #0000001a);
26
+ box-shadow:
27
+ var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
28
+ var(--tw-shadow);
29
+ display: flex;
30
+ flex-flow: column;
31
+ border: 1px solid var(--tutorial-border);
32
+ border-radius: 16px;
33
+ }
34
+
35
+ .driver-popover-image {
36
+ border-radius: 16px 16px 0 0;
37
+ }
38
+
39
+ .driver-popover-intro-step {
40
+ max-width: 400px;
41
+ }
42
+
43
+ .popover-title {
44
+ font-size: 18px;
45
+ font-weight: 700;
46
+ font-weight: var(--font-weight-600);
47
+ line-height: 1.2;
48
+ color: var(--text-header);
49
+ font-family: var(--default-font-family);
50
+ }
51
+
52
+ .driver-popover-description * {
53
+ line-height: 1.25;
54
+ font-size: 14px;
55
+ font-weight: 500;
56
+ font-family: var(--default-font-family);
57
+ }
58
+
59
+ .driver-popover-title {
60
+ padding: 16px 16px 0 16px;
61
+
62
+ }
63
+
64
+ .driver-popover-description {
65
+ padding: 16px;
66
+
67
+ :not(b) {
68
+ color: var(--tutorial-text-color);
69
+ }
70
+
71
+ b {
72
+ font-weight: 800;
73
+ }
74
+
75
+ ul {
76
+ list-style-type: disc;
77
+ margin-left: 24px;
78
+
79
+ &.no-decoration {
80
+ list-style-type: none;
81
+ }
82
+ &.square-decoration {
83
+ list-style-type: square;
84
+ padding-left: 4px;
85
+ }
86
+ &.double-indented {
87
+ margin-left: calc(var(--spacing) * 4);
88
+ }
89
+ }
90
+
91
+ a {
92
+ text-decoration: underline;
93
+ }
94
+ }
95
+
96
+ .driver-popover-progress-text {
97
+ color: var(--text-placeholder);
98
+ }
99
+
100
+ .tutorial-opt-out-container {
101
+ display: flex;
102
+ flex-flow: column;
103
+ align-items: center;
104
+ border-top: 1px solid var(--border);
105
+ padding: 16px;
106
+ }
107
+
108
+ .tutorial-opt-out-button {
109
+ font-size: 14px;
110
+ font-weight: 600;
111
+ font-family: var(--default-font-family);
112
+ background: transparent;
113
+ border: none;
114
+ color: var(--text-body);
115
+ text-shadow: none;
116
+ cursor: pointer;
117
+ }
118
+
119
+ .driver-popover-navigation-btns {
120
+ position: relative;
121
+ flex-grow: 0;
122
+ }
123
+
124
+ .driver-popover-navigation-btns:hover:before {
125
+ content: 'Use arrow keys to navigate';
126
+ position: absolute;
127
+ color: white;
128
+ top: -20px;
129
+ right: 0;
130
+ text-align: right;
131
+ width: 180%;
132
+ height: 100%;
133
+
134
+ font-size: 12px;
135
+ font-weight: 500;
136
+ font-family: var(--default-font-family);
137
+ color: var(--text-body);
138
+ }
139
+
140
+ .driver-popover-navigation-btns button {
141
+ cursor: pointer;
142
+ border-radius: 999px;
143
+ color: white;
144
+ font-family: var(--default-font-family);
145
+ border: 0;
146
+ font-size: 16px;
147
+ font-weight: 500;
148
+ line-height: 125%;
149
+ letter-spacing: -0.25px;
150
+ text-shadow: none;
151
+ position: relative;
152
+ }
153
+
154
+ .driver-popover-btn-disabled {
155
+ display: none;
156
+ }
157
+
158
+ .driver-popover-next-btn, .driver-popover-next-btn:hover {
159
+ background-color: var(--accent-1000);
160
+ color: white;
161
+ padding: 16px 32px;
162
+ }
163
+
164
+ .driver-popover-footer {
165
+ padding: 16px;
166
+ }
167
+
168
+ .driver-popover-arrow {
169
+ border-width: 8px;
170
+ }
171
+
172
+ .driver-popover-arrow-side-top {
173
+ margin-top: 1px;
174
+ border-top-color: var(--tutorial-border);
175
+ }
176
+
177
+ .driver-popover-arrow-side-bottom {
178
+ margin-bottom: 1px;
179
+ border-bottom-color: var(--tutorial-border);
180
+ }
181
+
182
+ .driver-popover-arrow-side-left {
183
+ margin-left: 1px;
184
+ border-left-color: var(--tutorial-border);
185
+ }
186
+
187
+ .driver-popover-arrow-side-right {
188
+ margin-right: 1px;
189
+ border-right-color: var(--tutorial-border);
190
+ }
191
+
192
+ .code-preview {
193
+ overflow-x: auto;
194
+ background: var(--tutorial-code-bg);
195
+ padding: 16px;
196
+ border-radius: 8px;
197
+
198
+ code {
199
+ font-size: 14px;
200
+ font-weight: 400;
201
+ color: var(--tutorial-code-text);
202
+ font-family: var(--font-dm-mono);
203
+ }
204
+ }
205
+
206
+ .driver-popover-close-btn {
207
+ display: none;
208
+ }
@@ -0,0 +1,2 @@
1
+ import './tutorial.css';
2
+ export declare const Tutorial: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { analytics } from '@/lib/analytics';
3
+ import { useTutorialEngine } from './hooks/use-tutorial-engine';
4
+ import { TutorialStep } from './tutorial-step';
5
+ import './tutorial.css';
6
+ export const Tutorial = () => {
7
+ const engine = useTutorialEngine();
8
+ const onNext = () => {
9
+ const currentStep = engine.currentStepRef.current;
10
+ const nextStep = currentStep + 1;
11
+ engine.moveStep(nextStep);
12
+ if (engine.currentStep === engine.totalSteps) {
13
+ analytics.track('tutorial_completed', {
14
+ manualOpen: engine.manualOpenRef.current,
15
+ });
16
+ }
17
+ else {
18
+ analytics.track('tutorial_next_step', {
19
+ step: nextStep,
20
+ manualOpen: engine.manualOpenRef.current,
21
+ });
22
+ }
23
+ };
24
+ const onClose = () => {
25
+ analytics.track('tutorial_closed', {
26
+ step: engine.currentStepRef.current,
27
+ manualOpen: engine.manualOpenRef.current,
28
+ });
29
+ engine.onClose();
30
+ };
31
+ return (_jsxs("div", { children: [_jsx("div", { className: "fixed inset-0 z-[9999]" }), _jsx("div", { className: "absolute top-5 left-5 w-full h-full rounded-lg shadow-[0_0_0_9999px_rgba(0,0,0,0.5)] z-[10000] pointer-events-none", ref: engine.highlighterRef }), _jsx(TutorialStep, { ref: engine.ref, step: engine.currentStep, totalSteps: engine.totalSteps, title: engine.title, description: engine.description, link: engine.link, image: engine.image, onNext: onNext, onClose: onClose })] }));
32
+ };
@@ -2,11 +2,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Moon, Sun } from 'lucide-react';
3
3
  import { useThemeStore } from '@/stores/use-theme-store';
4
4
  import { cn } from '@/lib/utils';
5
+ import { useEffect } from 'react';
5
6
  export const ThemeToggle = () => {
6
7
  const theme = useThemeStore((state) => state.theme);
7
8
  const setTheme = useThemeStore((state) => state.setTheme);
8
9
  const toggleTheme = () => {
9
10
  setTheme(theme === 'light' ? 'dark' : 'light');
10
11
  };
12
+ useEffect(() => {
13
+ const url = new URL(window.location.href);
14
+ const colorScheme = url.searchParams.get('color-scheme');
15
+ if (colorScheme) {
16
+ setTheme(colorScheme);
17
+ }
18
+ }, [setTheme]);
11
19
  return (_jsxs("button", { onClick: toggleTheme, className: "relative flex items-center cursor-pointer w-16 h-8 border bg-muted-foreground/10 rounded-full p-1 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "aria-label": `Switch to ${theme === 'light' ? 'dark' : 'light'} theme`, children: [_jsx("div", { className: cn('absolute w-6 h-6 bg-background border border-border rounded-full shadow-sm transition-transform duration-200 ease-in-out', theme === 'dark' ? 'translate-x-8' : 'translate-x-0') }), _jsx("div", { className: "flex items-center justify-center w-6 h-6 z-10", children: _jsx(Sun, { className: cn('h-3.5 w-3.5 transition-colors duration-200', theme === 'light' ? 'text-foreground' : 'text-muted-foreground') }) }), _jsx("div", { className: "flex items-center justify-center w-6 h-6 z-10 ml-2", children: _jsx(Moon, { className: cn('h-3.5 w-3.5 transition-colors duration-200', theme === 'dark' ? 'text-foreground' : 'text-muted-foreground') }) })] }));
12
20
  };
@@ -10,10 +10,12 @@ export const BaseNode = ({ title, variant, children, disableSourceHandle, disabl
10
10
  const [isOpen, setIsOpen] = useState(false);
11
11
  const { sourcePosition, targetPosition, toggleTargetPosition, toggleSourcePosition } = useHandlePositions(data);
12
12
  const [content, setContent] = useState(null);
13
+ const [features, setFeatures] = useState([]);
13
14
  const fetchContent = useCallback(async () => {
14
15
  const response = await fetch(`/step/${data.id}`);
15
16
  const responseData = await response.json();
16
17
  setContent(responseData.content);
18
+ setFeatures(responseData.features);
17
19
  }, [data.id]);
18
20
  useEffect(() => {
19
21
  if (data.id && isOpen) {
@@ -24,5 +26,5 @@ export const BaseNode = ({ title, variant, children, disableSourceHandle, disabl
24
26
  'bg-muted-foreground/20': isOpen,
25
27
  }), children: [_jsx("div", { className: "rounded-lg bg-background border-1 border-muted-foreground/30 border-solid", "data-testid": `node-${title?.toLowerCase().replace(/ /g, '-')}`, children: _jsxs("div", { className: "group relative", children: [_jsx(NodeHeader, { text: title, variant: variant, className: "border-b-2 border-muted-foreground/10", children: _jsx("div", { className: "flex justify-end", children: _jsx(Button, { "data-testid": `open-code-preview-button-${title?.toLowerCase()}`, variant: "ghost", className: "h-5 p-0.5", onClick: () => setIsOpen(true), children: _jsx(ScanSearch, { className: "w-4 h-4" }) }) }) }), subtitle && _jsx("div", { className: "py-4 px-6 text-sm text-muted-foreground", children: subtitle }), children && (_jsx("div", { className: "p-2", children: _jsx("div", { className: cn('space-y-3 p-4 text-sm text-muted-foreground', {
26
28
  'bg-card': variant !== 'noop',
27
- }), children: children }) })), !disableTargetHandle && (_jsx(BaseHandle, { type: "target", position: targetPosition, onTogglePosition: toggleTargetPosition })), !disableSourceHandle && (_jsx(BaseHandle, { type: "source", position: sourcePosition, onTogglePosition: toggleSourcePosition }))] }) }), content && (_jsx(NodeSidebar, { content: content, title: title, subtitle: subtitle, variant: variant, language: language, isOpen: isOpen, onClose: () => setIsOpen(false) }))] }));
29
+ }), children: children }) })), !disableTargetHandle && (_jsx(BaseHandle, { type: "target", position: targetPosition, onTogglePosition: toggleTargetPosition })), !disableSourceHandle && (_jsx(BaseHandle, { type: "source", position: sourcePosition, onTogglePosition: toggleSourcePosition }))] }) }), content && (_jsx(NodeSidebar, { features: features, content: content, title: title, subtitle: subtitle, variant: variant, language: language, isOpen: isOpen, onClose: () => setIsOpen(false) }))] }));
28
30
  };
@@ -0,0 +1,9 @@
1
+ import { Feature } from '@/types/file';
2
+ import React from 'react';
3
+ type CodeDisplayProps = {
4
+ code: string;
5
+ language?: string;
6
+ features?: Feature[];
7
+ };
8
+ export declare const CodeDisplay: React.FC<CodeDisplayProps>;
9
+ export {};
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useThemeStore } from '@/stores/use-theme-store';
3
+ import { FeatureCard } from './feature-card';
4
+ import { useRef, useState } from 'react';
5
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
6
+ import { dracula, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
7
+ import { LanguageIndicator } from './language-indicator';
8
+ const codeTagProps = {
9
+ style: {
10
+ fontFamily: 'DM Mono, monospace',
11
+ fontSize: '16px',
12
+ },
13
+ };
14
+ const customStyle = {
15
+ margin: 0,
16
+ borderRadius: 0,
17
+ padding: 0,
18
+ };
19
+ const isHighlighted = (lines, lineNumber) => {
20
+ return lines.some((line) => {
21
+ const [start, end] = line.split('-').map((num) => parseInt(num, 10));
22
+ if (end !== undefined) {
23
+ return lineNumber >= start && lineNumber <= end;
24
+ }
25
+ return lineNumber == start;
26
+ });
27
+ };
28
+ const getFirstLineNumber = (line) => {
29
+ const [start] = line.split('-').map((num) => parseInt(num, 10));
30
+ return start;
31
+ };
32
+ export const CodeDisplay = ({ code, language, features }) => {
33
+ const theme = useThemeStore((state) => state.theme);
34
+ const themeStyle = theme === 'dark' ? dracula : oneLight;
35
+ const [highlightedLines, setHighlightedLines] = useState([]);
36
+ const [selectedFeature, setSelectedFeature] = useState(null);
37
+ const ref = useRef(null);
38
+ const handleFeatureClick = (feature) => {
39
+ setSelectedFeature(feature);
40
+ setHighlightedLines(feature.lines);
41
+ const lineNumber = getFirstLineNumber(feature.lines[0]);
42
+ const line = ref.current?.querySelector(`[data-line-number="${lineNumber}"]`);
43
+ if (line) {
44
+ line.scrollIntoView({ behavior: 'smooth', block: 'center' });
45
+ }
46
+ };
47
+ return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [_jsxs("div", { className: "flex items-center py-2 px-5 dark:bg-[#1e1e1e] gap-2 justify-center", children: [_jsx("div", { className: "text-sm text-muted-foreground", children: "Read only" }), _jsx("div", { className: "flex-1" }), _jsx(LanguageIndicator, { language: language, className: "w-4 h-4", size: 16, showLabel: true })] }), _jsxs("div", { className: "flex flex-row h-[calc(100%-36px)]", children: [features && features.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 p-2 bg-card overflow-y-auto min-w-[200px] w-[300px]", children: features.map((feature, index) => (_jsx(FeatureCard, { feature: feature, highlighted: selectedFeature === feature, onClick: () => handleFeatureClick(feature), onHover: () => handleFeatureClick(feature) }, index))) })), _jsx("div", { className: "overflow-y-auto", ref: ref, children: _jsx(SyntaxHighlighter, { showLineNumbers: true, language: language, style: themeStyle, codeTagProps: codeTagProps, customStyle: customStyle, wrapLines: true, lineProps: (lineNumber) => {
48
+ if (isHighlighted(highlightedLines, lineNumber)) {
49
+ return {
50
+ 'data-line-number': lineNumber,
51
+ style: {
52
+ borderLeft: '2px solid var(--accent-1000)',
53
+ backgroundColor: 'rgb(from var(--accent-1000) r g b / 0.2)',
54
+ },
55
+ };
56
+ }
57
+ return {
58
+ 'data-line-number': lineNumber,
59
+ style: {
60
+ borderLeft: '2px solid transparent',
61
+ },
62
+ };
63
+ }, children: code }) })] })] }));
64
+ };
@@ -0,0 +1,10 @@
1
+ import { Feature } from '@/types/file';
2
+ import React from 'react';
3
+ type Props = {
4
+ feature: Feature;
5
+ highlighted: boolean;
6
+ onClick: () => void;
7
+ onHover: () => void;
8
+ };
9
+ export declare const FeatureCard: React.FC<Props>;
10
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from '@/lib/utils';
3
+ export const FeatureCard = ({ feature, highlighted, onClick, onHover }) => {
4
+ return (_jsxs("div", { "data-feature-id": feature.id, className: cn('p-4 rounded-lg bg-card shadow-sm cursor-pointer hover:bg-card/50 border-2 border-transparent', highlighted && 'border-2 border-accent-1000 bg-accent-100'), onClick: onClick, onMouseEnter: onHover, children: [_jsx("div", { className: "text-md font-semibold text-foreground leading-tight whitespace-nowrap mb-2", children: feature.title }), _jsx("div", { className: "text-sm font-medium text-muted-foreground leading-tight", children: feature.description }), feature.link && (_jsx("div", { className: "text-sm font-medium text-muted-foreground leading-tight", children: _jsx("a", { href: feature.link, children: "Learn more" }) }))] }));
5
+ };
@@ -1,6 +1,8 @@
1
+ import { Feature } from '@/types/file';
1
2
  import React from 'react';
2
3
  type NodeSidebarProps = {
3
4
  content: string;
5
+ features: Feature[];
4
6
  title: string;
5
7
  subtitle?: string;
6
8
  variant: 'event' | 'api' | 'noop' | 'cron';
@@ -1,10 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { JsonEditor } from '@/components/endpoints/json-editor';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
3
2
  import { Sidebar } from '@/components/sidebar/sidebar';
4
3
  import { X } from 'lucide-react';
5
- import { LanguageIndicator } from './language-indicator';
6
- export const NodeSidebar = ({ content, title, subtitle, language, isOpen, onClose }) => {
4
+ import { CodeDisplay } from './code-display';
5
+ export const NodeSidebar = ({ content, title, subtitle, language, isOpen, onClose, features, }) => {
7
6
  if (!isOpen)
8
7
  return null;
9
- return (_jsxs(Sidebar, { title: title, subtitle: subtitle, initialWidth: 600, contentClassName: "p-0 h-full gap-0", onClose: onClose, actions: [{ icon: _jsx(X, {}), onClick: onClose, label: 'Close' }], children: [_jsxs("div", { className: "flex items-center py-2 px-5 dark:bg-[#1e1e1e] gap-2 justify-center", children: [_jsx("div", { className: "text-sm text-muted-foreground", children: "Read only" }), _jsx("div", { className: "flex-1" }), _jsx(LanguageIndicator, { language: language, className: "w-4 h-4", size: 16, showLabel: true })] }), _jsx(JsonEditor, { value: content, language: language, height: 'calc(100vh - 160px)', readOnly: true })] }));
8
+ return (_jsx(Sidebar, { title: title, subtitle: subtitle, initialWidth: 900, contentClassName: "p-0 h-full gap-0", onClose: onClose, actions: [{ icon: _jsx(X, {}), onClick: onClose, label: 'Close' }], children: _jsx(CodeDisplay, { code: content, language: language, features: features }) }));
10
9
  };