yadflow 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -7
- package/README.md +23 -2
- package/bin/yad.mjs +22 -1
- package/cli/docs.mjs +298 -0
- package/cli/manifest.mjs +6 -1
- package/docs/index.html +4 -4
- package/package.json +2 -2
- package/skills/sdlc/config.yaml +19 -0
- package/skills/sdlc/install.sh +1 -1
- package/skills/sdlc/module-help.csv +4 -0
- package/skills/yad-connect-docs/SKILL.md +132 -0
- package/skills/yad-connect-docs/references/docs-registry.md +74 -0
- package/skills/yad-docs/SKILL.md +159 -0
- package/skills/yad-docs/references/data-mapping.md +75 -0
- package/skills/yad-docs/references/theme-map.md +69 -0
- package/skills/yad-docs/templates/app/README.md +31 -0
- package/skills/yad-docs/templates/app/eslint.config.js +23 -0
- package/skills/yad-docs/templates/app/index.html +17 -0
- package/skills/yad-docs/templates/app/package-lock.json +4030 -0
- package/skills/yad-docs/templates/app/package.json +35 -0
- package/skills/yad-docs/templates/app/public/favicon.svg +28 -0
- package/skills/yad-docs/templates/app/public/logo.svg +39 -0
- package/skills/yad-docs/templates/app/public/vite.svg +1 -0
- package/skills/yad-docs/templates/app/src/App.tsx +98 -0
- package/skills/yad-docs/templates/app/src/components/Auth/LoginPage.tsx +101 -0
- package/skills/yad-docs/templates/app/src/components/Canvas/AnimatedMessage.tsx +101 -0
- package/skills/yad-docs/templates/app/src/components/Canvas/ConnectionLine.tsx +90 -0
- package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +216 -0
- package/skills/yad-docs/templates/app/src/components/Canvas/SystemComponent.tsx +153 -0
- package/skills/yad-docs/templates/app/src/components/Controls/PlaybackBar.tsx +284 -0
- package/skills/yad-docs/templates/app/src/components/Controls/StepDetail.tsx +167 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/HandlerLogicSnippet.tsx +41 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/RequestPayloadPreview.tsx +46 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/RightPanel.tsx +88 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/StatusCard.tsx +76 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/TriggerEventCard.tsx +45 -0
- package/skills/yad-docs/templates/app/src/components/DocLayout/DocPageShell.tsx +80 -0
- package/skills/yad-docs/templates/app/src/components/DocLayout/DocSectionCard.tsx +55 -0
- package/skills/yad-docs/templates/app/src/components/DocLayout/DocTableOfContents.tsx +79 -0
- package/skills/yad-docs/templates/app/src/components/DocLayout/RoleCard.tsx +67 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/ApiReferenceSection.tsx +108 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/CancelabilitySection.tsx +73 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/CriticalRunbookSection.tsx +177 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/DataMigrationSection.tsx +102 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/DbSchemaSection.tsx +98 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/DeploymentGuideSection.tsx +104 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/DriverIntegrationSection.tsx +127 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/ExecutiveSummarySection.tsx +69 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/FlowOverviewSection.tsx +73 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/FlowPathsChecklistSection.tsx +96 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/MiddlewareChainSection.tsx +107 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/MonitoringAlertingSection.tsx +106 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/NotificationLocalizationSection.tsx +102 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/PMRoadmapSection.tsx +133 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/PerformanceTestingSection.tsx +91 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/RiderIntegrationSection.tsx +99 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/SecuritySection.tsx +74 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/StatusMachineSection.tsx +90 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/TestPlanSection.tsx +163 -0
- package/skills/yad-docs/templates/app/src/components/Logs/SystemLogsTerminal.tsx +126 -0
- package/skills/yad-docs/templates/app/src/components/Navigation/TopNavBar.tsx +90 -0
- package/skills/yad-docs/templates/app/src/components/Reference/BullMQJobsList.tsx +60 -0
- package/skills/yad-docs/templates/app/src/components/Reference/DecisionTreeView.tsx +49 -0
- package/skills/yad-docs/templates/app/src/components/Reference/DeeplinkActionsChips.tsx +69 -0
- package/skills/yad-docs/templates/app/src/components/Reference/DriverUIStatesTable.tsx +61 -0
- package/skills/yad-docs/templates/app/src/components/Reference/FeatureFlagMatrix.tsx +73 -0
- package/skills/yad-docs/templates/app/src/components/Reference/RiderUIStatesTable.tsx +61 -0
- package/skills/yad-docs/templates/app/src/components/Reference/RulesLegendPanel.tsx +217 -0
- package/skills/yad-docs/templates/app/src/components/Reference/StakeholderToggle.tsx +41 -0
- package/skills/yad-docs/templates/app/src/components/Reference/TroubleshootingSection.tsx +93 -0
- package/skills/yad-docs/templates/app/src/components/Sidebar/PathSelector.tsx +148 -0
- package/skills/yad-docs/templates/app/src/components/Sidebar/SidebarFooter.tsx +40 -0
- package/skills/yad-docs/templates/app/src/components/Sidebar/StepList.tsx +234 -0
- package/skills/yad-docs/templates/app/src/components/shared/Badge.tsx +28 -0
- package/skills/yad-docs/templates/app/src/components/shared/CommandPalette.tsx +213 -0
- package/skills/yad-docs/templates/app/src/components/shared/Icon.tsx +21 -0
- package/skills/yad-docs/templates/app/src/components/shared/Tooltip.tsx +42 -0
- package/skills/yad-docs/templates/app/src/data/components.ts +74 -0
- package/skills/yad-docs/templates/app/src/data/docSections.ts +231 -0
- package/skills/yad-docs/templates/app/src/data/paths.ts +2319 -0
- package/skills/yad-docs/templates/app/src/data/referenceData.ts +392 -0
- package/skills/yad-docs/templates/app/src/data/roles.ts +145 -0
- package/skills/yad-docs/templates/app/src/data/types.ts +79 -0
- package/skills/yad-docs/templates/app/src/hooks/useAnimationQueue.ts +41 -0
- package/skills/yad-docs/templates/app/src/hooks/usePlayback.ts +100 -0
- package/skills/yad-docs/templates/app/src/hooks/useStakeholderFilter.ts +10 -0
- package/skills/yad-docs/templates/app/src/index.css +121 -0
- package/skills/yad-docs/templates/app/src/main.tsx +13 -0
- package/skills/yad-docs/templates/app/src/pages/RoleSelectPage.tsx +34 -0
- package/skills/yad-docs/templates/app/src/pages/StakeholderDocPage.tsx +98 -0
- package/skills/yad-docs/templates/app/src/pages/SubPathDetailPage.tsx +282 -0
- package/skills/yad-docs/templates/app/src/store/useAuthStore.ts +42 -0
- package/skills/yad-docs/templates/app/src/store/useFlowStore.ts +197 -0
- package/skills/yad-docs/templates/app/src/utils/iconMap.ts +46 -0
- package/skills/yad-docs/templates/app/tsconfig.app.json +28 -0
- package/skills/yad-docs/templates/app/tsconfig.json +7 -0
- package/skills/yad-docs/templates/app/tsconfig.node.json +26 -0
- package/skills/yad-docs/templates/app/vite.config.ts +10 -0
- package/skills/yad-docs-overview/SKILL.md +129 -0
- package/skills/yad-docs-overview/references/pipeline-model.md +102 -0
- package/skills/yad-docs-sync/SKILL.md +99 -0
- package/skills/yad-docs-sync/references/staleness.md +81 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { useRef, useMemo, useCallback, useState, useEffect } from 'react';
|
|
2
|
+
import { COMPONENTS } from '../../data/components';
|
|
3
|
+
import { useFlowStore } from '../../store/useFlowStore';
|
|
4
|
+
import { useAnimationQueue } from '../../hooks/useAnimationQueue';
|
|
5
|
+
import { SystemComponent } from './SystemComponent';
|
|
6
|
+
import { ConnectionLine } from './ConnectionLine';
|
|
7
|
+
import { AnimatedMessage } from './AnimatedMessage';
|
|
8
|
+
import { Icon } from '../shared/Icon';
|
|
9
|
+
|
|
10
|
+
const ALL_CONNECTIONS = [
|
|
11
|
+
['rider-app', 'backend-api'],
|
|
12
|
+
['driver-app', 'backend-api'],
|
|
13
|
+
['backend-api', 'bullmq'],
|
|
14
|
+
['backend-api', 'database'],
|
|
15
|
+
['backend-api', 'pubsub'],
|
|
16
|
+
['backend-api', 'dac'],
|
|
17
|
+
['backend-api', 'ops-dashboard'],
|
|
18
|
+
['ops-dashboard', 'backend-api'],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const FlowCanvas: React.FC = () => {
|
|
22
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const [dims, setDims] = useState({ width: 0, height: 0 });
|
|
24
|
+
const getCurrentStep = useFlowStore((s) => s.getCurrentStep);
|
|
25
|
+
const speed = useFlowStore((s) => s.speed);
|
|
26
|
+
const zoomLevel = useFlowStore((s) => s.zoomLevel);
|
|
27
|
+
const zoomIn = useFlowStore((s) => s.zoomIn);
|
|
28
|
+
const zoomOut = useFlowStore((s) => s.zoomOut);
|
|
29
|
+
const resetZoom = useFlowStore((s) => s.resetZoom);
|
|
30
|
+
const selectedPath = useFlowStore((s) => s.selectedPath);
|
|
31
|
+
const currentStep = getCurrentStep();
|
|
32
|
+
const { completedTargets, animKey } = useAnimationQueue();
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const el = containerRef.current;
|
|
36
|
+
if (!el) return;
|
|
37
|
+
|
|
38
|
+
const observer = new ResizeObserver((entries) => {
|
|
39
|
+
const entry = entries[0];
|
|
40
|
+
if (entry) {
|
|
41
|
+
setDims({
|
|
42
|
+
width: entry.contentRect.width,
|
|
43
|
+
height: entry.contentRect.height,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
observer.observe(el);
|
|
48
|
+
return () => observer.disconnect();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const activeComponents = useMemo(
|
|
52
|
+
() => new Set(currentStep?.activeComponents || []),
|
|
53
|
+
[currentStep]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const activeConnections = useMemo(() => {
|
|
57
|
+
if (!currentStep) return new Set<string>();
|
|
58
|
+
const conns = new Set<string>();
|
|
59
|
+
for (const msg of currentStep.messages) {
|
|
60
|
+
conns.add(`${msg.from}-${msg.to}`);
|
|
61
|
+
conns.add(`${msg.to}-${msg.from}`);
|
|
62
|
+
}
|
|
63
|
+
return conns;
|
|
64
|
+
}, [currentStep]);
|
|
65
|
+
|
|
66
|
+
const isConnectionActive = useCallback(
|
|
67
|
+
(from: string, to: string) => {
|
|
68
|
+
return (
|
|
69
|
+
activeConnections.has(`${from}-${to}`) ||
|
|
70
|
+
activeConnections.has(`${to}-${from}`)
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
[activeConnections]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const messages = currentStep?.messages || [];
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div ref={containerRef} className="relative h-full w-full overflow-hidden">
|
|
80
|
+
{/* Canvas Header Overlay */}
|
|
81
|
+
<div className="absolute top-4 left-4 right-4 z-30 flex justify-between items-start pointer-events-none">
|
|
82
|
+
<div className="pointer-events-auto rounded-lg p-3 border shadow-xl"
|
|
83
|
+
style={{
|
|
84
|
+
background: 'rgba(30,26,37,0.8)',
|
|
85
|
+
backdropFilter: 'blur(12px)',
|
|
86
|
+
borderColor: 'rgba(255,255,255,0.05)',
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<h1 className="text-white text-xl font-bold font-display">
|
|
90
|
+
{selectedPath.label}
|
|
91
|
+
</h1>
|
|
92
|
+
<div className="flex items-center gap-2 mt-1">
|
|
93
|
+
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
|
94
|
+
<span className="text-xs text-slate-300 uppercase tracking-wide">Live Simulation</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="pointer-events-auto flex gap-2">
|
|
98
|
+
<button
|
|
99
|
+
onClick={zoomIn}
|
|
100
|
+
className="h-10 w-10 flex items-center justify-center rounded-lg text-white border transition-colors"
|
|
101
|
+
style={{
|
|
102
|
+
background: 'rgba(30,26,37,0.8)',
|
|
103
|
+
borderColor: 'rgba(255,255,255,0.05)',
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<Icon name="zoom_in" size={20} />
|
|
107
|
+
</button>
|
|
108
|
+
<button
|
|
109
|
+
onClick={zoomOut}
|
|
110
|
+
className="h-10 w-10 flex items-center justify-center rounded-lg text-white border transition-colors"
|
|
111
|
+
style={{
|
|
112
|
+
background: 'rgba(30,26,37,0.8)',
|
|
113
|
+
borderColor: 'rgba(255,255,255,0.05)',
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<Icon name="zoom_out" size={20} />
|
|
117
|
+
</button>
|
|
118
|
+
<button
|
|
119
|
+
onClick={resetZoom}
|
|
120
|
+
className="h-10 w-10 flex items-center justify-center rounded-lg text-white border transition-colors"
|
|
121
|
+
style={{
|
|
122
|
+
background: 'rgba(30,26,37,0.8)',
|
|
123
|
+
borderColor: 'rgba(255,255,255,0.05)',
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<Icon name="center_focus_strong" size={20} />
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Zoomable content */}
|
|
132
|
+
<div
|
|
133
|
+
className="absolute inset-0"
|
|
134
|
+
style={{
|
|
135
|
+
transform: `scale(${zoomLevel})`,
|
|
136
|
+
transformOrigin: 'center center',
|
|
137
|
+
transition: 'transform 0.2s ease',
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
{/* SVG layer for connection lines */}
|
|
141
|
+
{dims.width > 0 && (
|
|
142
|
+
<svg className="pointer-events-none absolute inset-0 h-full w-full">
|
|
143
|
+
<defs>
|
|
144
|
+
<linearGradient id="gradientPath" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
145
|
+
<stop offset="0%" stopColor="#6116da" stopOpacity={1} />
|
|
146
|
+
<stop offset="100%" stopColor="#ff6490" stopOpacity={1} />
|
|
147
|
+
</linearGradient>
|
|
148
|
+
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
149
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="#ff6490" />
|
|
150
|
+
</marker>
|
|
151
|
+
</defs>
|
|
152
|
+
{ALL_CONNECTIONS.map(([from, to]) => (
|
|
153
|
+
<ConnectionLine
|
|
154
|
+
key={`${from}-${to}`}
|
|
155
|
+
from={from}
|
|
156
|
+
to={to}
|
|
157
|
+
isActive={isConnectionActive(from, to)}
|
|
158
|
+
containerWidth={dims.width}
|
|
159
|
+
containerHeight={dims.height}
|
|
160
|
+
/>
|
|
161
|
+
))}
|
|
162
|
+
</svg>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{/* Animated messages */}
|
|
166
|
+
{dims.width > 0 &&
|
|
167
|
+
messages.map((msg) => (
|
|
168
|
+
<AnimatedMessage
|
|
169
|
+
key={`${animKey}-${msg.id}`}
|
|
170
|
+
from={msg.from}
|
|
171
|
+
to={msg.to}
|
|
172
|
+
label={msg.label}
|
|
173
|
+
color={msg.color}
|
|
174
|
+
delay={msg.delay}
|
|
175
|
+
duration={msg.duration}
|
|
176
|
+
speed={speed}
|
|
177
|
+
containerWidth={dims.width}
|
|
178
|
+
containerHeight={dims.height}
|
|
179
|
+
/>
|
|
180
|
+
))}
|
|
181
|
+
|
|
182
|
+
{/* System component nodes */}
|
|
183
|
+
{COMPONENTS.map((comp) => (
|
|
184
|
+
<SystemComponent
|
|
185
|
+
key={comp.id}
|
|
186
|
+
component={comp}
|
|
187
|
+
isActive={activeComponents.has(comp.id)}
|
|
188
|
+
isReceiving={completedTargets.has(comp.id)}
|
|
189
|
+
/>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Current status badge */}
|
|
194
|
+
{currentStep && (
|
|
195
|
+
<div
|
|
196
|
+
className="absolute left-4 bottom-4 z-30 rounded-lg border px-3 py-2"
|
|
197
|
+
style={{
|
|
198
|
+
background: 'rgba(30,26,37,0.85)',
|
|
199
|
+
backdropFilter: 'blur(8px)',
|
|
200
|
+
borderColor: 'var(--color-border-default)',
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<div className="text-[10px] uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }}>
|
|
204
|
+
Status
|
|
205
|
+
</div>
|
|
206
|
+
<div className="text-sm font-bold" style={{ color: 'var(--color-accent)' }}>
|
|
207
|
+
{currentStep.status}
|
|
208
|
+
</div>
|
|
209
|
+
<div className="mt-0.5 text-[10px]" style={{ color: 'var(--color-text-secondary)' }}>
|
|
210
|
+
{currentStep.bookingStatus}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import type { SystemComponent as SystemComponentType } from '../../data/types';
|
|
4
|
+
import { Icon } from '../shared/Icon';
|
|
5
|
+
import { COMPONENT_ICONS, COMPONENT_ROLES } from '../../utils/iconMap';
|
|
6
|
+
|
|
7
|
+
interface SystemComponentProps {
|
|
8
|
+
component: SystemComponentType;
|
|
9
|
+
isActive: boolean;
|
|
10
|
+
isReceiving: boolean;
|
|
11
|
+
onClick?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const SystemComponent: React.FC<SystemComponentProps> = React.memo(
|
|
15
|
+
({ component, isActive, isReceiving, onClick }) => {
|
|
16
|
+
const [hovered, setHovered] = useState(false);
|
|
17
|
+
const iconName = COMPONENT_ICONS[component.id] || 'circle';
|
|
18
|
+
const role = COMPONENT_ROLES[component.id] || 'Service';
|
|
19
|
+
|
|
20
|
+
const glowStyle = useMemo(() => {
|
|
21
|
+
if (isReceiving) {
|
|
22
|
+
return {
|
|
23
|
+
boxShadow: `0 0 30px ${component.color}40, 0 0 60px ${component.color}20`,
|
|
24
|
+
borderColor: `${component.color}80`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (isActive) {
|
|
28
|
+
return {
|
|
29
|
+
boxShadow: `0 0 20px ${component.color}30, 0 0 40px ${component.color}15`,
|
|
30
|
+
borderColor: `${component.color}50`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
boxShadow: '0 4px 20px rgba(0,0,0,0.2)',
|
|
35
|
+
borderColor: 'rgba(255,255,255,0.05)',
|
|
36
|
+
};
|
|
37
|
+
}, [isActive, isReceiving, component.color]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<motion.button
|
|
42
|
+
onClick={onClick}
|
|
43
|
+
onMouseEnter={() => setHovered(true)}
|
|
44
|
+
onMouseLeave={() => setHovered(false)}
|
|
45
|
+
className="absolute z-10 flex cursor-pointer flex-col items-center gap-2 rounded-2xl p-4 transition-colors glass-panel group"
|
|
46
|
+
style={{
|
|
47
|
+
left: `${component.position.x}%`,
|
|
48
|
+
top: `${component.position.y}%`,
|
|
49
|
+
transform: 'translate(-50%, -50%)',
|
|
50
|
+
background: isActive
|
|
51
|
+
? `linear-gradient(135deg, ${component.color}12, ${component.color}06)`
|
|
52
|
+
: 'rgba(47, 41, 56, 0.4)',
|
|
53
|
+
backdropFilter: 'blur(12px)',
|
|
54
|
+
...glowStyle,
|
|
55
|
+
minWidth: '130px',
|
|
56
|
+
minHeight: '140px',
|
|
57
|
+
}}
|
|
58
|
+
animate={
|
|
59
|
+
isReceiving
|
|
60
|
+
? { scale: [1, 1.05, 1] }
|
|
61
|
+
: { scale: 1 }
|
|
62
|
+
}
|
|
63
|
+
transition={
|
|
64
|
+
isReceiving
|
|
65
|
+
? { duration: 0.6, ease: 'easeInOut' }
|
|
66
|
+
: { duration: 0.3 }
|
|
67
|
+
}
|
|
68
|
+
whileHover={{ scale: 1.05 }}
|
|
69
|
+
aria-label={`${component.label}: ${component.description}`}
|
|
70
|
+
tabIndex={0}
|
|
71
|
+
>
|
|
72
|
+
{/* Role tag */}
|
|
73
|
+
<div className="absolute -top-3 px-3 py-1 rounded-full text-[10px] uppercase tracking-wider font-bold"
|
|
74
|
+
style={{
|
|
75
|
+
background: 'var(--color-surface-dark)',
|
|
76
|
+
border: '1px solid var(--color-surface-highlight)',
|
|
77
|
+
color: 'var(--color-text-muted)',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{role}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Icon with gradient background */}
|
|
84
|
+
<div className="h-14 w-14 rounded-2xl flex items-center justify-center text-white shadow-lg mb-1"
|
|
85
|
+
style={{
|
|
86
|
+
background: `linear-gradient(135deg, ${component.color}, ${component.color}99)`,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<Icon name={iconName} size={28} className="text-white" />
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Label */}
|
|
93
|
+
<h3 className="text-white font-bold font-display text-sm text-center break-words w-full">{component.label}</h3>
|
|
94
|
+
|
|
95
|
+
{/* Status */}
|
|
96
|
+
<div className="flex items-center gap-1.5">
|
|
97
|
+
<motion.div
|
|
98
|
+
className="h-2 w-2 rounded-full"
|
|
99
|
+
style={{
|
|
100
|
+
backgroundColor: isActive ? '#22c55e' : isReceiving ? component.color : 'var(--color-text-muted)',
|
|
101
|
+
}}
|
|
102
|
+
animate={
|
|
103
|
+
isActive || isReceiving
|
|
104
|
+
? { opacity: [0.7, 1, 0.7] }
|
|
105
|
+
: {}
|
|
106
|
+
}
|
|
107
|
+
transition={{ duration: 1.5, repeat: Infinity }}
|
|
108
|
+
/>
|
|
109
|
+
<span className="text-xs text-slate-300">
|
|
110
|
+
{isReceiving ? 'Processing' : isActive ? 'Online' : 'Idle'}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Connection points */}
|
|
115
|
+
<div className="absolute right-[-6px] top-1/2 -translate-y-1/2 h-3 w-3 rounded-full border-2 border-white"
|
|
116
|
+
style={{ background: component.color, opacity: isActive ? 1 : 0.3 }}
|
|
117
|
+
/>
|
|
118
|
+
<div className="absolute left-[-6px] top-1/2 -translate-y-1/2 h-3 w-3 rounded-full border-2 border-white"
|
|
119
|
+
style={{ background: component.color, opacity: isActive ? 1 : 0.3 }}
|
|
120
|
+
/>
|
|
121
|
+
</motion.button>
|
|
122
|
+
|
|
123
|
+
{/* Tooltip */}
|
|
124
|
+
<AnimatePresence>
|
|
125
|
+
{hovered && (
|
|
126
|
+
<motion.div
|
|
127
|
+
initial={{ opacity: 0, y: 4 }}
|
|
128
|
+
animate={{ opacity: 1, y: 0 }}
|
|
129
|
+
exit={{ opacity: 0, y: 4 }}
|
|
130
|
+
transition={{ duration: 0.15 }}
|
|
131
|
+
className="pointer-events-none absolute z-50 max-w-[220px] rounded-lg px-3 py-2 text-xs"
|
|
132
|
+
style={{
|
|
133
|
+
left: `${component.position.x}%`,
|
|
134
|
+
top: `${component.position.y}%`,
|
|
135
|
+
transform: 'translate(-50%, calc(-100% - 50px))',
|
|
136
|
+
backgroundColor: 'var(--color-surface-dark)',
|
|
137
|
+
color: 'var(--color-text-primary)',
|
|
138
|
+
border: '1px solid var(--color-border-light)',
|
|
139
|
+
boxShadow: '0 4px 20px rgba(0,0,0,0.4)',
|
|
140
|
+
backdropFilter: 'blur(8px)',
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<div className="font-semibold font-display">{component.label}</div>
|
|
144
|
+
<div className="mt-0.5 text-[10px]" style={{ color: 'var(--color-text-secondary)' }}>
|
|
145
|
+
{component.description}
|
|
146
|
+
</div>
|
|
147
|
+
</motion.div>
|
|
148
|
+
)}
|
|
149
|
+
</AnimatePresence>
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
);
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { motion } from 'framer-motion';
|
|
2
|
+
import { useFlowStore } from '../../store/useFlowStore';
|
|
3
|
+
import { Icon } from '../shared/Icon';
|
|
4
|
+
|
|
5
|
+
function formatTime(seconds: number): string {
|
|
6
|
+
const mins = Math.floor(seconds / 60);
|
|
7
|
+
const secs = Math.floor(seconds % 60);
|
|
8
|
+
const cs = Math.floor((seconds % 1) * 100);
|
|
9
|
+
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}.${String(cs).padStart(2, '0')}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const PlaybackBar = () => {
|
|
13
|
+
const {
|
|
14
|
+
playbackState,
|
|
15
|
+
speed,
|
|
16
|
+
activeStepIndex,
|
|
17
|
+
elapsedTime,
|
|
18
|
+
play,
|
|
19
|
+
pause,
|
|
20
|
+
stop,
|
|
21
|
+
nextStep,
|
|
22
|
+
prevStep,
|
|
23
|
+
setSpeed,
|
|
24
|
+
setActiveStep,
|
|
25
|
+
getTotalSteps,
|
|
26
|
+
getCurrentStep,
|
|
27
|
+
getCurrentSteps,
|
|
28
|
+
toggleLogsPanel,
|
|
29
|
+
replayStep,
|
|
30
|
+
addLog,
|
|
31
|
+
} = useFlowStore();
|
|
32
|
+
|
|
33
|
+
const totalSteps = getTotalSteps();
|
|
34
|
+
const currentStep = getCurrentStep();
|
|
35
|
+
const steps = getCurrentSteps();
|
|
36
|
+
const progress = totalSteps > 0 ? ((activeStepIndex + 1) / totalSteps) * 100 : 0;
|
|
37
|
+
const estimatedTotal = totalSteps * 8;
|
|
38
|
+
|
|
39
|
+
const handleFlagIssue = () => {
|
|
40
|
+
if (currentStep) {
|
|
41
|
+
addLog({
|
|
42
|
+
level: 'warn',
|
|
43
|
+
source: 'USER',
|
|
44
|
+
message: `Flagged step ${activeStepIndex + 1}: ${currentStep.title}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="relative z-20">
|
|
51
|
+
{/* Ambient glow */}
|
|
52
|
+
<div
|
|
53
|
+
className="absolute inset-0 pointer-events-none overflow-hidden"
|
|
54
|
+
style={{ background: 'rgba(30,26,37,0.9)' }}
|
|
55
|
+
>
|
|
56
|
+
<div
|
|
57
|
+
className="absolute left-1/2 -translate-x-1/2 top-0 w-1/3 h-full"
|
|
58
|
+
style={{
|
|
59
|
+
background: 'rgba(97,22,218,0.05)',
|
|
60
|
+
filter: 'blur(100px)',
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div
|
|
66
|
+
className="relative"
|
|
67
|
+
style={{
|
|
68
|
+
borderTop: '1px solid rgba(255,255,255,0.1)',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{/* Header: label + timer */}
|
|
72
|
+
<div className="flex items-center justify-between px-6 pt-3 pb-1">
|
|
73
|
+
<span
|
|
74
|
+
className="text-[10px] font-semibold uppercase tracking-widest"
|
|
75
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
76
|
+
>
|
|
77
|
+
Booking Flow Progress
|
|
78
|
+
</span>
|
|
79
|
+
<div className="font-mono text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
|
80
|
+
<span style={{ color: 'var(--color-primary)' }}>{formatTime(elapsedTime)}</span>
|
|
81
|
+
{' / '}
|
|
82
|
+
<span>{formatTime(estimatedTotal)}</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Step title */}
|
|
87
|
+
<div className="px-6 pb-2">
|
|
88
|
+
<h3 className="text-white text-base font-bold">
|
|
89
|
+
{currentStep
|
|
90
|
+
? `Step ${activeStepIndex + 1}: ${currentStep.title}`
|
|
91
|
+
: 'Select a path to begin'}
|
|
92
|
+
</h3>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Step markers + progress bar */}
|
|
96
|
+
<div className="px-6 pb-3">
|
|
97
|
+
{/* Markers row */}
|
|
98
|
+
<div className="relative flex items-start justify-between mb-1">
|
|
99
|
+
{steps.map((step, i) => {
|
|
100
|
+
const isCompleted = i < activeStepIndex;
|
|
101
|
+
const isActive = i === activeStepIndex;
|
|
102
|
+
const shortLabel = step.title.split(' ')[0];
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<button
|
|
106
|
+
key={step.id}
|
|
107
|
+
onClick={() => setActiveStep(i)}
|
|
108
|
+
className="flex flex-col items-center gap-1 z-10 group cursor-pointer"
|
|
109
|
+
style={{ flex: '1 1 0' }}
|
|
110
|
+
title={step.title}
|
|
111
|
+
>
|
|
112
|
+
<div
|
|
113
|
+
className="rounded-full transition-all duration-300"
|
|
114
|
+
style={{
|
|
115
|
+
width: isActive ? 14 : 10,
|
|
116
|
+
height: isActive ? 14 : 10,
|
|
117
|
+
background: isCompleted
|
|
118
|
+
? 'var(--color-primary)'
|
|
119
|
+
: isActive
|
|
120
|
+
? 'white'
|
|
121
|
+
: 'rgba(255,255,255,0.15)',
|
|
122
|
+
border: isActive ? '3px solid var(--color-primary)' : 'none',
|
|
123
|
+
boxShadow: isActive
|
|
124
|
+
? '0 0 12px rgba(97,22,218,0.6)'
|
|
125
|
+
: isCompleted
|
|
126
|
+
? '0 0 6px rgba(97,22,218,0.3)'
|
|
127
|
+
: 'none',
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
<span
|
|
131
|
+
className="text-[9px] font-medium transition-colors"
|
|
132
|
+
style={{
|
|
133
|
+
color: isActive
|
|
134
|
+
? 'white'
|
|
135
|
+
: isCompleted
|
|
136
|
+
? 'var(--color-primary)'
|
|
137
|
+
: 'var(--color-text-muted)',
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
{shortLabel}
|
|
141
|
+
</span>
|
|
142
|
+
</button>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Progress bar */}
|
|
148
|
+
<div
|
|
149
|
+
className="relative h-1.5 w-full rounded-full overflow-hidden"
|
|
150
|
+
style={{ background: 'rgba(255,255,255,0.08)' }}
|
|
151
|
+
>
|
|
152
|
+
<motion.div
|
|
153
|
+
className="h-full rounded-full"
|
|
154
|
+
style={{
|
|
155
|
+
background: 'linear-gradient(90deg, var(--color-primary), #a855f7)',
|
|
156
|
+
}}
|
|
157
|
+
animate={{ width: `${progress}%` }}
|
|
158
|
+
transition={{ duration: 0.3 }}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Controls row */}
|
|
164
|
+
<div className="flex items-center gap-3 px-6 pb-3">
|
|
165
|
+
{/* Skip Prev */}
|
|
166
|
+
<button
|
|
167
|
+
onClick={prevStep}
|
|
168
|
+
disabled={activeStepIndex === 0}
|
|
169
|
+
className="group flex flex-col items-center gap-0.5 p-1.5 rounded-md transition-colors disabled:opacity-30"
|
|
170
|
+
style={{ color: 'var(--color-text-secondary)' }}
|
|
171
|
+
>
|
|
172
|
+
<Icon name="skip_previous" size={22} />
|
|
173
|
+
<span className="text-[8px] opacity-0 group-hover:opacity-100 transition-opacity">Prev</span>
|
|
174
|
+
</button>
|
|
175
|
+
|
|
176
|
+
{/* Play / Pause */}
|
|
177
|
+
<motion.button
|
|
178
|
+
onClick={playbackState === 'playing' ? pause : play}
|
|
179
|
+
className="h-14 w-14 rounded-full flex items-center justify-center text-white transition-transform active:scale-95"
|
|
180
|
+
style={{
|
|
181
|
+
background: 'var(--color-primary)',
|
|
182
|
+
boxShadow: '0 0 20px rgba(97,22,218,0.5)',
|
|
183
|
+
}}
|
|
184
|
+
whileHover={{ scale: 1.05, boxShadow: '0 0 30px rgba(97,22,218,0.8)' }}
|
|
185
|
+
whileTap={{ scale: 0.95 }}
|
|
186
|
+
aria-label={playbackState === 'playing' ? 'Pause' : 'Play'}
|
|
187
|
+
>
|
|
188
|
+
<Icon name={playbackState === 'playing' ? 'pause' : 'play_arrow'} size={32} filled />
|
|
189
|
+
</motion.button>
|
|
190
|
+
|
|
191
|
+
{/* Skip Next */}
|
|
192
|
+
<button
|
|
193
|
+
onClick={nextStep}
|
|
194
|
+
disabled={activeStepIndex >= totalSteps - 1}
|
|
195
|
+
className="group flex flex-col items-center gap-0.5 p-1.5 rounded-md transition-colors disabled:opacity-30"
|
|
196
|
+
style={{ color: 'var(--color-text-secondary)' }}
|
|
197
|
+
>
|
|
198
|
+
<Icon name="skip_next" size={22} />
|
|
199
|
+
<span className="text-[8px] opacity-0 group-hover:opacity-100 transition-opacity">Next</span>
|
|
200
|
+
</button>
|
|
201
|
+
|
|
202
|
+
{/* Stop */}
|
|
203
|
+
<button
|
|
204
|
+
onClick={stop}
|
|
205
|
+
className="p-1.5 rounded-md transition-colors"
|
|
206
|
+
style={{ color: 'var(--color-text-secondary)' }}
|
|
207
|
+
aria-label="Stop"
|
|
208
|
+
>
|
|
209
|
+
<Icon name="stop" size={22} />
|
|
210
|
+
</button>
|
|
211
|
+
|
|
212
|
+
{/* Divider */}
|
|
213
|
+
<div className="h-8 w-px" style={{ background: 'rgba(255,255,255,0.1)' }} />
|
|
214
|
+
|
|
215
|
+
{/* Speed control */}
|
|
216
|
+
<div className="flex rounded-lg p-0.5" style={{ background: 'rgba(255,255,255,0.05)' }}>
|
|
217
|
+
{[1, 2, 3].map((s) => (
|
|
218
|
+
<button
|
|
219
|
+
key={s}
|
|
220
|
+
onClick={() => setSpeed(s)}
|
|
221
|
+
className="px-3 py-1 rounded-md text-xs font-bold transition-all"
|
|
222
|
+
style={{
|
|
223
|
+
background: speed === s ? 'rgba(97,22,218,0.2)' : 'transparent',
|
|
224
|
+
color: speed === s ? 'white' : 'var(--color-text-muted)',
|
|
225
|
+
border: speed === s ? '1px solid rgba(97,22,218,0.3)' : '1px solid transparent',
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
{s}x
|
|
229
|
+
</button>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Divider */}
|
|
234
|
+
<div className="h-8 w-px" style={{ background: 'rgba(255,255,255,0.1)' }} />
|
|
235
|
+
|
|
236
|
+
{/* Replay Step */}
|
|
237
|
+
<button
|
|
238
|
+
onClick={replayStep}
|
|
239
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs font-medium transition-colors"
|
|
240
|
+
style={{
|
|
241
|
+
background: 'var(--color-surface-dark)',
|
|
242
|
+
borderColor: 'var(--color-border-default)',
|
|
243
|
+
color: 'var(--color-text-secondary)',
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<Icon name="history" size={16} />
|
|
247
|
+
Replay Step
|
|
248
|
+
</button>
|
|
249
|
+
|
|
250
|
+
{/* Flag Issue */}
|
|
251
|
+
<button
|
|
252
|
+
onClick={handleFlagIssue}
|
|
253
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs font-medium transition-colors"
|
|
254
|
+
style={{
|
|
255
|
+
background: 'var(--color-surface-dark)',
|
|
256
|
+
borderColor: 'var(--color-border-default)',
|
|
257
|
+
color: 'var(--color-text-secondary)',
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
<Icon name="bug_report" size={16} />
|
|
261
|
+
Flag Issue
|
|
262
|
+
</button>
|
|
263
|
+
|
|
264
|
+
{/* Divider */}
|
|
265
|
+
<div className="h-8 w-px" style={{ background: 'rgba(255,255,255,0.1)' }} />
|
|
266
|
+
|
|
267
|
+
{/* Logs toggle */}
|
|
268
|
+
<button
|
|
269
|
+
onClick={toggleLogsPanel}
|
|
270
|
+
className="flex items-center gap-2 px-3 py-1.5 rounded-lg border text-xs font-medium transition-colors"
|
|
271
|
+
style={{
|
|
272
|
+
background: 'var(--color-surface-dark)',
|
|
273
|
+
borderColor: 'var(--color-border-default)',
|
|
274
|
+
color: 'var(--color-text-secondary)',
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
<Icon name="terminal" size={16} />
|
|
278
|
+
Logs
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
};
|