yadflow 2.6.0 → 2.8.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 +2 -11
- package/README.md +30 -5
- package/bin/yad.mjs +36 -1
- package/cli/docs.mjs +298 -0
- package/cli/manifest.mjs +6 -1
- package/cli/roster.mjs +164 -0
- package/cli/setup.mjs +128 -2
- package/package.json +3 -4
- 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-connect-repos/SKILL.md +4 -0
- package/skills/yad-connect-repos/references/hub-config.md +3 -1
- 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 +131 -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
- package/skills/yad-hub-bridge/references/login-roster.md +1 -0
- package/docs/index.html +0 -1323
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import { useFlowStore } from '../../store/useFlowStore';
|
|
5
|
+
import { PATHS } from '../../data/paths';
|
|
6
|
+
import type { PathCategory } from '../../data/types';
|
|
7
|
+
import { Icon } from '../shared/Icon';
|
|
8
|
+
import { CATEGORY_ICONS } from '../../utils/iconMap';
|
|
9
|
+
|
|
10
|
+
const CATEGORY_LABELS: Record<PathCategory, string> = {
|
|
11
|
+
success: 'Success',
|
|
12
|
+
'rider-cancel': 'Rider Cancellations',
|
|
13
|
+
'driver-cancel': 'Driver Cancellations',
|
|
14
|
+
timeout: 'Timeouts',
|
|
15
|
+
ops: 'Ops Actions',
|
|
16
|
+
'active-cancel': 'Active Trip Cancels',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CATEGORY_ORDER: PathCategory[] = [
|
|
20
|
+
'success',
|
|
21
|
+
'rider-cancel',
|
|
22
|
+
'timeout',
|
|
23
|
+
'driver-cancel',
|
|
24
|
+
'ops',
|
|
25
|
+
'active-cancel',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const PathSelector = () => {
|
|
29
|
+
const { selectedPath, selectPath } = useFlowStore();
|
|
30
|
+
const navigate = useNavigate();
|
|
31
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
32
|
+
|
|
33
|
+
const grouped = useMemo(() => {
|
|
34
|
+
const groups: Record<string, typeof PATHS> = {};
|
|
35
|
+
for (const path of PATHS) {
|
|
36
|
+
if (searchQuery) {
|
|
37
|
+
const q = searchQuery.toLowerCase();
|
|
38
|
+
if (!path.label.toLowerCase().includes(q) && !path.description.toLowerCase().includes(q)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!groups[path.category]) groups[path.category] = [];
|
|
43
|
+
groups[path.category].push(path);
|
|
44
|
+
}
|
|
45
|
+
return groups;
|
|
46
|
+
}, [searchQuery]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-col gap-3">
|
|
50
|
+
<div className="flex items-center justify-between mb-1">
|
|
51
|
+
<h3 className="text-slate-100 text-sm font-bold font-display uppercase tracking-wider">
|
|
52
|
+
Path Selection
|
|
53
|
+
</h3>
|
|
54
|
+
<button className="text-xs font-medium" style={{ color: 'var(--color-primary)' }}>
|
|
55
|
+
View All
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Search */}
|
|
60
|
+
<div className="flex items-center rounded-lg border transition-colors"
|
|
61
|
+
style={{
|
|
62
|
+
background: 'var(--color-surface-highlight)',
|
|
63
|
+
borderColor: 'transparent',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<Icon name="search" size={20} className="text-slate-400 ml-3" />
|
|
67
|
+
<input
|
|
68
|
+
type="text"
|
|
69
|
+
placeholder="Search paths..."
|
|
70
|
+
value={searchQuery}
|
|
71
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
72
|
+
className="bg-transparent border-none text-sm text-white w-full placeholder-slate-500 ml-2 p-2 focus:outline-none focus:ring-0"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Path list */}
|
|
77
|
+
<div className="space-y-1">
|
|
78
|
+
{CATEGORY_ORDER.map((cat) => {
|
|
79
|
+
const paths = grouped[cat];
|
|
80
|
+
if (!paths || paths.length === 0) return null;
|
|
81
|
+
return (
|
|
82
|
+
<div key={cat}>
|
|
83
|
+
<div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-wider"
|
|
84
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
85
|
+
>
|
|
86
|
+
{CATEGORY_LABELS[cat]}
|
|
87
|
+
</div>
|
|
88
|
+
{paths.map((path) => {
|
|
89
|
+
const isSelected = selectedPath.id === path.id;
|
|
90
|
+
const iconName = CATEGORY_ICONS[path.category] || 'circle';
|
|
91
|
+
return (
|
|
92
|
+
<motion.button
|
|
93
|
+
key={path.id}
|
|
94
|
+
onClick={() => selectPath(path.id)}
|
|
95
|
+
className="w-full flex items-center justify-between gap-3 px-3 py-2.5 rounded-lg text-left transition-all relative overflow-hidden"
|
|
96
|
+
style={{
|
|
97
|
+
background: isSelected ? 'rgba(97,22,218,0.1)' : 'transparent',
|
|
98
|
+
border: isSelected ? '1px solid rgba(97,22,218,0.2)' : '1px solid transparent',
|
|
99
|
+
color: isSelected ? 'white' : 'var(--color-text-secondary)',
|
|
100
|
+
}}
|
|
101
|
+
whileHover={{
|
|
102
|
+
backgroundColor: isSelected ? undefined : 'var(--color-surface-highlight)',
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{isSelected && (
|
|
106
|
+
<div className="absolute left-0 top-0 bottom-0 w-1"
|
|
107
|
+
style={{ background: 'var(--color-primary)' }}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
<div className="flex items-center gap-3">
|
|
111
|
+
<Icon
|
|
112
|
+
name={iconName}
|
|
113
|
+
size={20}
|
|
114
|
+
filled={isSelected}
|
|
115
|
+
className={isSelected ? 'text-[var(--color-primary)]' : 'text-slate-500'}
|
|
116
|
+
/>
|
|
117
|
+
<span className="text-sm font-medium">{path.label}</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="flex items-center gap-1.5">
|
|
120
|
+
{isSelected && (
|
|
121
|
+
<button
|
|
122
|
+
onClick={(e) => { e.stopPropagation(); navigate(`/path/${path.id}`); }}
|
|
123
|
+
className="p-1 rounded hover:bg-white/10 transition-colors"
|
|
124
|
+
title="View details"
|
|
125
|
+
>
|
|
126
|
+
<Icon name="open_in_new" size={14} className="text-[var(--color-primary)]" />
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
{isSelected ? (
|
|
130
|
+
<div className="h-2 w-2 rounded-full shadow-[0_0_8px_rgba(34,197,94,0.6)]"
|
|
131
|
+
style={{ background: '#22c55e' }}
|
|
132
|
+
/>
|
|
133
|
+
) : (
|
|
134
|
+
<span className="text-[10px]" style={{ color: 'var(--color-text-muted)' }}>
|
|
135
|
+
P{path.id}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</motion.button>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Icon } from '../shared/Icon';
|
|
2
|
+
import { useFlowStore } from '../../store/useFlowStore';
|
|
3
|
+
|
|
4
|
+
export function SidebarFooter() {
|
|
5
|
+
const { playbackState, play, stop } = useFlowStore();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="p-4 border-t flex items-center justify-between gap-4"
|
|
9
|
+
style={{
|
|
10
|
+
borderColor: 'var(--color-border-default)',
|
|
11
|
+
background: 'var(--color-bg-primary)',
|
|
12
|
+
}}
|
|
13
|
+
>
|
|
14
|
+
<button
|
|
15
|
+
onClick={stop}
|
|
16
|
+
className="flex-1 flex items-center justify-center gap-2 h-10 rounded-lg border text-slate-200 text-sm font-medium transition-colors"
|
|
17
|
+
style={{
|
|
18
|
+
borderColor: 'var(--color-surface-highlight)',
|
|
19
|
+
background: 'var(--color-surface-dark)',
|
|
20
|
+
}}
|
|
21
|
+
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--color-surface-highlight)'}
|
|
22
|
+
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--color-surface-dark)'}
|
|
23
|
+
>
|
|
24
|
+
<Icon name="restart_alt" size={18} />
|
|
25
|
+
Reset Flow
|
|
26
|
+
</button>
|
|
27
|
+
<button
|
|
28
|
+
onClick={play}
|
|
29
|
+
className="flex-1 flex items-center justify-center gap-2 h-10 rounded-lg text-white text-sm font-medium transition-colors"
|
|
30
|
+
style={{
|
|
31
|
+
background: 'var(--color-primary)',
|
|
32
|
+
boxShadow: '0 4px 15px rgba(97,22,218,0.25)',
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<Icon name={playbackState === 'playing' ? 'pause' : 'play_arrow'} size={18} />
|
|
36
|
+
{playbackState === 'playing' ? 'Pause' : 'Resume'}
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { useFlowStore } from '../../store/useFlowStore';
|
|
4
|
+
import type { ActorType } from '../../data/types';
|
|
5
|
+
import { Icon } from '../shared/Icon';
|
|
6
|
+
import { ACTOR_ICONS } from '../../utils/iconMap';
|
|
7
|
+
|
|
8
|
+
const ACTOR_COLORS: Record<ActorType, string> = {
|
|
9
|
+
rider: '#fb2576',
|
|
10
|
+
driver: '#6316db',
|
|
11
|
+
ops: '#06b6d4',
|
|
12
|
+
system: '#f59e0b',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const StepList = () => {
|
|
16
|
+
const {
|
|
17
|
+
selectedPath,
|
|
18
|
+
activeSubPathId,
|
|
19
|
+
activeStepIndex,
|
|
20
|
+
setActiveStep,
|
|
21
|
+
selectSubPath,
|
|
22
|
+
getCurrentSteps,
|
|
23
|
+
} = useFlowStore();
|
|
24
|
+
|
|
25
|
+
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
|
26
|
+
|
|
27
|
+
const hasSubPaths = selectedPath.subPaths && selectedPath.subPaths.length > 0;
|
|
28
|
+
const steps = getCurrentSteps();
|
|
29
|
+
|
|
30
|
+
// Sync expanded state: auto-expand the active step, allow user to close it
|
|
31
|
+
const isExpanded = (index: number) => {
|
|
32
|
+
if (expandedIndex !== null) return expandedIndex === index;
|
|
33
|
+
return index === activeStepIndex;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleStepClick = (index: number) => {
|
|
37
|
+
setActiveStep(index);
|
|
38
|
+
// Toggle: if already expanded, close it; otherwise expand it
|
|
39
|
+
if (isExpanded(index)) {
|
|
40
|
+
setExpandedIndex(-1); // -1 means "nothing expanded"
|
|
41
|
+
} else {
|
|
42
|
+
setExpandedIndex(index);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Reset expanded state when active step changes externally (playback)
|
|
47
|
+
const prevActiveRef = { current: activeStepIndex };
|
|
48
|
+
if (prevActiveRef.current !== activeStepIndex) {
|
|
49
|
+
prevActiveRef.current = activeStepIndex;
|
|
50
|
+
if (expandedIndex !== null && expandedIndex !== activeStepIndex) {
|
|
51
|
+
// Auto-expand the new active step during playback
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex flex-col gap-2">
|
|
57
|
+
<div className="flex items-center justify-between mb-2">
|
|
58
|
+
<h3 className="text-slate-400 text-xs font-bold font-display uppercase tracking-wider">
|
|
59
|
+
Sequence Timeline
|
|
60
|
+
</h3>
|
|
61
|
+
<span className="text-xs font-mono px-2 py-1 rounded"
|
|
62
|
+
style={{ color: 'var(--color-text-secondary)', background: 'var(--color-surface-highlight)' }}
|
|
63
|
+
>
|
|
64
|
+
Path {selectedPath.id}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Sub-path selector */}
|
|
69
|
+
{hasSubPaths && (
|
|
70
|
+
<div className="flex flex-wrap gap-1 mb-2">
|
|
71
|
+
{selectedPath.subPaths!.map((sub) => (
|
|
72
|
+
<button
|
|
73
|
+
key={sub.id}
|
|
74
|
+
onClick={() => selectSubPath(sub.id)}
|
|
75
|
+
className="rounded-md px-2 py-1 text-[10px] font-semibold transition-colors"
|
|
76
|
+
style={{
|
|
77
|
+
background: activeSubPathId === sub.id ? `${selectedPath.color}25` : 'var(--color-surface-highlight)',
|
|
78
|
+
color: activeSubPathId === sub.id ? selectedPath.color : 'var(--color-text-secondary)',
|
|
79
|
+
border: `1px solid ${activeSubPathId === sub.id ? `${selectedPath.color}50` : 'var(--color-border-default)'}`,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{sub.id.toUpperCase()}: {sub.label}
|
|
83
|
+
</button>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{/* Timeline — uses absolute line to avoid overflow clipping */}
|
|
89
|
+
<div className="relative" style={{ paddingLeft: '36px' }}>
|
|
90
|
+
{/* Vertical timeline line */}
|
|
91
|
+
<div
|
|
92
|
+
className="absolute top-0 bottom-0"
|
|
93
|
+
style={{
|
|
94
|
+
left: '15px',
|
|
95
|
+
width: '2px',
|
|
96
|
+
background: 'var(--color-surface-highlight)',
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
{steps.map((step, index) => {
|
|
101
|
+
const isCurrent = index === activeStepIndex;
|
|
102
|
+
const isCompleted = index < activeStepIndex;
|
|
103
|
+
const expanded = isExpanded(index);
|
|
104
|
+
const actorColor = ACTOR_COLORS[step.actor];
|
|
105
|
+
const actorIcon = ACTOR_ICONS[step.actor] || 'person';
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div key={step.id} className="relative pb-4">
|
|
109
|
+
{/* Timeline dot — centered on the line at left:15px */}
|
|
110
|
+
<div
|
|
111
|
+
className="absolute"
|
|
112
|
+
style={{
|
|
113
|
+
left: '-36px',
|
|
114
|
+
top: '4px',
|
|
115
|
+
width: '32px',
|
|
116
|
+
display: 'flex',
|
|
117
|
+
justifyContent: 'center',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{isCurrent ? (
|
|
121
|
+
<motion.div
|
|
122
|
+
className="h-7 w-7 rounded-full flex items-center justify-center"
|
|
123
|
+
style={{
|
|
124
|
+
background: 'var(--color-primary)',
|
|
125
|
+
boxShadow: '0 0 0 3px rgba(97,22,218,0.25)',
|
|
126
|
+
}}
|
|
127
|
+
animate={{ scale: [1, 1.08, 1] }}
|
|
128
|
+
transition={{ duration: 2, repeat: Infinity }}
|
|
129
|
+
>
|
|
130
|
+
<Icon name="sync" size={14} className="text-white" />
|
|
131
|
+
</motion.div>
|
|
132
|
+
) : isCompleted ? (
|
|
133
|
+
<div
|
|
134
|
+
className="h-5 w-5 rounded-full flex items-center justify-center"
|
|
135
|
+
style={{
|
|
136
|
+
background: 'var(--color-surface-highlight)',
|
|
137
|
+
border: '2px solid var(--color-border-light)',
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<Icon name="check" size={12} className="text-slate-500" />
|
|
141
|
+
</div>
|
|
142
|
+
) : (
|
|
143
|
+
<div
|
|
144
|
+
className="h-5 w-5 rounded-full flex items-center justify-center"
|
|
145
|
+
style={{
|
|
146
|
+
background: 'var(--color-bg-primary)',
|
|
147
|
+
border: '2px solid var(--color-surface-highlight)',
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<div className="h-1.5 w-1.5 rounded-full" style={{ background: 'var(--color-surface-highlight)' }} />
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Clickable step area */}
|
|
156
|
+
<button
|
|
157
|
+
onClick={() => handleStepClick(index)}
|
|
158
|
+
className="text-left w-full block"
|
|
159
|
+
>
|
|
160
|
+
{/* Compact header — always visible */}
|
|
161
|
+
<div className={`flex items-center gap-2 ${!isCurrent && !expanded ? (isCompleted ? 'opacity-60' : 'opacity-40') : ''} hover:opacity-100 transition-opacity`}>
|
|
162
|
+
<span className="text-xs font-bold uppercase" style={{ color: isCurrent ? 'var(--color-primary)' : 'var(--color-text-secondary)' }}>
|
|
163
|
+
Step {index + 1}
|
|
164
|
+
</span>
|
|
165
|
+
<span
|
|
166
|
+
className="text-[10px] px-1.5 py-0.5 rounded font-medium"
|
|
167
|
+
style={{
|
|
168
|
+
background: isCurrent ? 'var(--color-primary)' : 'var(--color-surface-highlight)',
|
|
169
|
+
color: isCurrent ? 'white' : isCompleted ? 'var(--color-text-secondary)' : 'var(--color-text-muted)',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{isCurrent ? 'Current' : isCompleted ? 'Completed' : 'Pending'}
|
|
173
|
+
</span>
|
|
174
|
+
{/* Expand/collapse indicator */}
|
|
175
|
+
<Icon
|
|
176
|
+
name={expanded ? 'expand_less' : 'expand_more'}
|
|
177
|
+
size={16}
|
|
178
|
+
className="text-slate-500 ml-auto"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
<h4 className={`font-medium text-sm mt-1 ${isCurrent ? 'text-white font-bold' : 'text-slate-200'}`}>
|
|
182
|
+
{step.title}
|
|
183
|
+
</h4>
|
|
184
|
+
{!expanded && (
|
|
185
|
+
<div className="flex items-center gap-2 mt-1">
|
|
186
|
+
<Icon name={actorIcon} size={14} className="text-slate-400" />
|
|
187
|
+
<span className="text-xs" style={{ color: 'var(--color-text-secondary)' }}>
|
|
188
|
+
{step.actor}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</button>
|
|
193
|
+
|
|
194
|
+
{/* Expandable detail card */}
|
|
195
|
+
<AnimatePresence>
|
|
196
|
+
{expanded && (
|
|
197
|
+
<motion.div
|
|
198
|
+
initial={{ opacity: 0, height: 0 }}
|
|
199
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
200
|
+
exit={{ opacity: 0, height: 0 }}
|
|
201
|
+
className="overflow-hidden"
|
|
202
|
+
>
|
|
203
|
+
<div
|
|
204
|
+
className="flex flex-col gap-2 p-3 mt-2 rounded-lg border"
|
|
205
|
+
style={{
|
|
206
|
+
background: 'var(--color-surface-dark)',
|
|
207
|
+
borderColor: isCurrent ? 'rgba(97,22,218,0.3)' : 'var(--color-border-default)',
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
<p className="text-xs leading-relaxed" style={{ color: 'var(--color-text-secondary)' }}>
|
|
211
|
+
{step.description}
|
|
212
|
+
</p>
|
|
213
|
+
<div
|
|
214
|
+
className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-2"
|
|
215
|
+
style={{ borderTop: '1px solid var(--color-surface-highlight)' }}
|
|
216
|
+
>
|
|
217
|
+
<div className="flex items-center gap-1.5" style={{ color: actorColor }}>
|
|
218
|
+
<Icon name={actorIcon} size={16} />
|
|
219
|
+
<span className="text-xs font-medium">{step.actor}</span>
|
|
220
|
+
</div>
|
|
221
|
+
<Icon name="arrow_forward" size={12} className="text-slate-500" />
|
|
222
|
+
<span className="text-xs text-slate-400 font-medium break-all">{step.bookingStatus}</span>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</motion.div>
|
|
226
|
+
)}
|
|
227
|
+
</AnimatePresence>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
})}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { MessageType } from "../../data/types";
|
|
3
|
+
import { MESSAGE_COLORS } from "../../data/types";
|
|
4
|
+
|
|
5
|
+
interface BadgeProps {
|
|
6
|
+
type: MessageType;
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Badge: React.FC<BadgeProps> = React.memo(({ type, label }) => {
|
|
11
|
+
const color = MESSAGE_COLORS[type];
|
|
12
|
+
return (
|
|
13
|
+
<span
|
|
14
|
+
className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
|
|
15
|
+
style={{
|
|
16
|
+
backgroundColor: `${color}20`,
|
|
17
|
+
color: color,
|
|
18
|
+
border: `1px solid ${color}40`,
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<span
|
|
22
|
+
className="h-1.5 w-1.5 rounded-full"
|
|
23
|
+
style={{ backgroundColor: color }}
|
|
24
|
+
/>
|
|
25
|
+
{label || type}
|
|
26
|
+
</span>
|
|
27
|
+
);
|
|
28
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { useFlowStore } from '../../store/useFlowStore';
|
|
4
|
+
import { PATHS } from '../../data/paths';
|
|
5
|
+
import { Icon } from './Icon';
|
|
6
|
+
import { CATEGORY_ICONS } from '../../utils/iconMap';
|
|
7
|
+
|
|
8
|
+
export function CommandPalette() {
|
|
9
|
+
const isOpen = useFlowStore((s) => s.isCommandPaletteOpen);
|
|
10
|
+
const toggle = useFlowStore((s) => s.toggleCommandPalette);
|
|
11
|
+
const selectPath = useFlowStore((s) => s.selectPath);
|
|
12
|
+
const setActiveStep = useFlowStore((s) => s.setActiveStep);
|
|
13
|
+
const [query, setQuery] = useState('');
|
|
14
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
15
|
+
|
|
16
|
+
// Keyboard shortcut to open
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const handler = (e: KeyboardEvent) => {
|
|
19
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
toggle();
|
|
22
|
+
}
|
|
23
|
+
if (e.key === 'Escape' && isOpen) {
|
|
24
|
+
toggle();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
window.addEventListener('keydown', handler);
|
|
28
|
+
return () => window.removeEventListener('keydown', handler);
|
|
29
|
+
}, [isOpen, toggle]);
|
|
30
|
+
|
|
31
|
+
// Reset on open
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (isOpen) {
|
|
34
|
+
setQuery('');
|
|
35
|
+
setSelectedIndex(0);
|
|
36
|
+
}
|
|
37
|
+
}, [isOpen]);
|
|
38
|
+
|
|
39
|
+
const results = useMemo(() => {
|
|
40
|
+
if (!query.trim()) {
|
|
41
|
+
return PATHS.map((p) => ({
|
|
42
|
+
id: `path-${p.id}`,
|
|
43
|
+
type: 'path' as const,
|
|
44
|
+
label: `Path ${p.id}: ${p.label}`,
|
|
45
|
+
description: p.description,
|
|
46
|
+
category: p.category,
|
|
47
|
+
pathId: p.id,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const q = query.toLowerCase();
|
|
52
|
+
const items: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
type: 'path' | 'step';
|
|
55
|
+
label: string;
|
|
56
|
+
description: string;
|
|
57
|
+
category?: string;
|
|
58
|
+
pathId: number;
|
|
59
|
+
stepIndex?: number;
|
|
60
|
+
}> = [];
|
|
61
|
+
|
|
62
|
+
for (const path of PATHS) {
|
|
63
|
+
if (path.label.toLowerCase().includes(q) || path.description.toLowerCase().includes(q)) {
|
|
64
|
+
items.push({
|
|
65
|
+
id: `path-${path.id}`,
|
|
66
|
+
type: 'path',
|
|
67
|
+
label: `Path ${path.id}: ${path.label}`,
|
|
68
|
+
description: path.description,
|
|
69
|
+
category: path.category,
|
|
70
|
+
pathId: path.id,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (let i = 0; i < path.steps.length; i++) {
|
|
74
|
+
const step = path.steps[i];
|
|
75
|
+
if (step.title.toLowerCase().includes(q) || step.handler.toLowerCase().includes(q)) {
|
|
76
|
+
items.push({
|
|
77
|
+
id: `step-${path.id}-${i}`,
|
|
78
|
+
type: 'step',
|
|
79
|
+
label: `${step.title}`,
|
|
80
|
+
description: `Path ${path.id} > Step ${i + 1} > ${step.handler}`,
|
|
81
|
+
pathId: path.id,
|
|
82
|
+
stepIndex: i,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return items.slice(0, 20);
|
|
89
|
+
}, [query]);
|
|
90
|
+
|
|
91
|
+
const handleSelect = useCallback((item: typeof results[0]) => {
|
|
92
|
+
selectPath(item.pathId);
|
|
93
|
+
if (item.type === 'step' && item.stepIndex !== undefined) {
|
|
94
|
+
setTimeout(() => setActiveStep(item.stepIndex!), 50);
|
|
95
|
+
}
|
|
96
|
+
toggle();
|
|
97
|
+
}, [selectPath, setActiveStep, toggle]);
|
|
98
|
+
|
|
99
|
+
// Keyboard navigation
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!isOpen) return;
|
|
102
|
+
const handler = (e: KeyboardEvent) => {
|
|
103
|
+
if (e.key === 'ArrowDown') {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
setSelectedIndex((i) => Math.min(i + 1, results.length - 1));
|
|
106
|
+
} else if (e.key === 'ArrowUp') {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
109
|
+
} else if (e.key === 'Enter' && results[selectedIndex]) {
|
|
110
|
+
handleSelect(results[selectedIndex]);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
window.addEventListener('keydown', handler);
|
|
114
|
+
return () => window.removeEventListener('keydown', handler);
|
|
115
|
+
}, [isOpen, selectedIndex, results, handleSelect]);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<AnimatePresence>
|
|
119
|
+
{isOpen && (
|
|
120
|
+
<>
|
|
121
|
+
<motion.div
|
|
122
|
+
initial={{ opacity: 0 }}
|
|
123
|
+
animate={{ opacity: 1 }}
|
|
124
|
+
exit={{ opacity: 0 }}
|
|
125
|
+
className="fixed inset-0 z-50 bg-black/60"
|
|
126
|
+
onClick={toggle}
|
|
127
|
+
/>
|
|
128
|
+
<motion.div
|
|
129
|
+
initial={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
130
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
131
|
+
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
132
|
+
transition={{ duration: 0.15 }}
|
|
133
|
+
className="fixed top-[15%] left-1/2 -translate-x-1/2 w-full max-w-lg z-50 rounded-xl shadow-2xl border overflow-hidden"
|
|
134
|
+
style={{
|
|
135
|
+
background: 'var(--color-surface-dark)',
|
|
136
|
+
borderColor: 'var(--color-border-default)',
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{/* Search input */}
|
|
140
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b"
|
|
141
|
+
style={{ borderColor: 'var(--color-border-default)' }}
|
|
142
|
+
>
|
|
143
|
+
<Icon name="search" size={20} className="text-slate-400" />
|
|
144
|
+
<input
|
|
145
|
+
type="text"
|
|
146
|
+
placeholder="Search paths, steps, handlers..."
|
|
147
|
+
value={query}
|
|
148
|
+
onChange={(e) => { setQuery(e.target.value); setSelectedIndex(0); }}
|
|
149
|
+
className="flex-1 bg-transparent border-none text-white text-sm placeholder-slate-500 focus:outline-none focus:ring-0"
|
|
150
|
+
autoFocus
|
|
151
|
+
/>
|
|
152
|
+
<kbd className="text-[10px] text-slate-500 px-1.5 py-0.5 rounded border"
|
|
153
|
+
style={{ background: 'var(--color-bg-primary)', borderColor: 'var(--color-border-default)' }}
|
|
154
|
+
>
|
|
155
|
+
ESC
|
|
156
|
+
</kbd>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Results */}
|
|
160
|
+
<div className="max-h-[400px] overflow-y-auto p-2">
|
|
161
|
+
{results.length === 0 ? (
|
|
162
|
+
<div className="text-center py-8 text-sm" style={{ color: 'var(--color-text-muted)' }}>
|
|
163
|
+
No results found
|
|
164
|
+
</div>
|
|
165
|
+
) : (
|
|
166
|
+
results.map((item, index) => (
|
|
167
|
+
<button
|
|
168
|
+
key={item.id}
|
|
169
|
+
onClick={() => handleSelect(item)}
|
|
170
|
+
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors"
|
|
171
|
+
style={{
|
|
172
|
+
background: index === selectedIndex ? 'var(--color-surface-highlight)' : 'transparent',
|
|
173
|
+
}}
|
|
174
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
175
|
+
>
|
|
176
|
+
<Icon
|
|
177
|
+
name={item.type === 'path' ? (CATEGORY_ICONS[item.category || ''] || 'route') : 'arrow_forward'}
|
|
178
|
+
size={18}
|
|
179
|
+
className={item.type === 'path' ? 'text-[var(--color-primary)]' : 'text-slate-500'}
|
|
180
|
+
/>
|
|
181
|
+
<div className="flex-1 min-w-0">
|
|
182
|
+
<p className="text-sm font-medium text-white truncate">{item.label}</p>
|
|
183
|
+
<p className="text-xs truncate" style={{ color: 'var(--color-text-muted)' }}>
|
|
184
|
+
{item.description}
|
|
185
|
+
</p>
|
|
186
|
+
</div>
|
|
187
|
+
<span className="text-[10px] uppercase font-bold px-2 py-0.5 rounded"
|
|
188
|
+
style={{
|
|
189
|
+
background: item.type === 'path' ? 'rgba(97,22,218,0.15)' : 'rgba(255,255,255,0.05)',
|
|
190
|
+
color: item.type === 'path' ? 'var(--color-primary)' : 'var(--color-text-muted)',
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
{item.type}
|
|
194
|
+
</span>
|
|
195
|
+
</button>
|
|
196
|
+
))
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Footer hint */}
|
|
201
|
+
<div className="px-4 py-2 border-t flex items-center gap-4 text-[10px]"
|
|
202
|
+
style={{ borderColor: 'var(--color-border-default)', color: 'var(--color-text-muted)' }}
|
|
203
|
+
>
|
|
204
|
+
<span><kbd className="font-mono">↑↓</kbd> Navigate</span>
|
|
205
|
+
<span><kbd className="font-mono">⏎</kbd> Select</span>
|
|
206
|
+
<span><kbd className="font-mono">Esc</kbd> Close</span>
|
|
207
|
+
</div>
|
|
208
|
+
</motion.div>
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
</AnimatePresence>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface IconProps {
|
|
2
|
+
name: string;
|
|
3
|
+
size?: number;
|
|
4
|
+
className?: string;
|
|
5
|
+
filled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Icon({ name, size = 24, className = '', filled }: IconProps) {
|
|
9
|
+
return (
|
|
10
|
+
<span
|
|
11
|
+
className={`material-symbols-outlined ${className}`}
|
|
12
|
+
style={{
|
|
13
|
+
fontSize: size,
|
|
14
|
+
fontVariationSettings: filled ? "'FILL' 1" : undefined,
|
|
15
|
+
}}
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
>
|
|
18
|
+
{name}
|
|
19
|
+
</span>
|
|
20
|
+
);
|
|
21
|
+
}
|