yadflow 2.5.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +8 -2
  2. package/README.md +65 -22
  3. package/bin/yad.mjs +27 -1
  4. package/cli/docs.mjs +298 -0
  5. package/cli/doctor.mjs +1 -0
  6. package/cli/manifest.mjs +23 -2
  7. package/cli/ship.mjs +37 -0
  8. package/docs/index.html +44 -13
  9. package/package.json +2 -2
  10. package/skills/sdlc/config.yaml +26 -2
  11. package/skills/sdlc/install.sh +1 -1
  12. package/skills/sdlc/module-help.csv +11 -4
  13. package/skills/yad-checks/references/check-gates.md +58 -2
  14. package/skills/yad-checks/templates/checks/commit-message.sh +82 -0
  15. package/skills/yad-checks/templates/github/yad-checks.yml +27 -0
  16. package/skills/yad-checks/templates/github/yad-hub-checks.yml +36 -0
  17. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +20 -0
  18. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +39 -0
  19. package/skills/yad-commit/SKILL.md +66 -0
  20. package/skills/yad-connect-docs/SKILL.md +132 -0
  21. package/skills/yad-connect-docs/references/docs-registry.md +74 -0
  22. package/skills/yad-docs/SKILL.md +159 -0
  23. package/skills/yad-docs/references/data-mapping.md +75 -0
  24. package/skills/yad-docs/references/theme-map.md +69 -0
  25. package/skills/yad-docs/templates/app/README.md +31 -0
  26. package/skills/yad-docs/templates/app/eslint.config.js +23 -0
  27. package/skills/yad-docs/templates/app/index.html +17 -0
  28. package/skills/yad-docs/templates/app/package-lock.json +4030 -0
  29. package/skills/yad-docs/templates/app/package.json +35 -0
  30. package/skills/yad-docs/templates/app/public/favicon.svg +28 -0
  31. package/skills/yad-docs/templates/app/public/logo.svg +39 -0
  32. package/skills/yad-docs/templates/app/public/vite.svg +1 -0
  33. package/skills/yad-docs/templates/app/src/App.tsx +98 -0
  34. package/skills/yad-docs/templates/app/src/components/Auth/LoginPage.tsx +101 -0
  35. package/skills/yad-docs/templates/app/src/components/Canvas/AnimatedMessage.tsx +101 -0
  36. package/skills/yad-docs/templates/app/src/components/Canvas/ConnectionLine.tsx +90 -0
  37. package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +216 -0
  38. package/skills/yad-docs/templates/app/src/components/Canvas/SystemComponent.tsx +153 -0
  39. package/skills/yad-docs/templates/app/src/components/Controls/PlaybackBar.tsx +284 -0
  40. package/skills/yad-docs/templates/app/src/components/Controls/StepDetail.tsx +167 -0
  41. package/skills/yad-docs/templates/app/src/components/DetailPanel/HandlerLogicSnippet.tsx +41 -0
  42. package/skills/yad-docs/templates/app/src/components/DetailPanel/RequestPayloadPreview.tsx +46 -0
  43. package/skills/yad-docs/templates/app/src/components/DetailPanel/RightPanel.tsx +88 -0
  44. package/skills/yad-docs/templates/app/src/components/DetailPanel/StatusCard.tsx +76 -0
  45. package/skills/yad-docs/templates/app/src/components/DetailPanel/TriggerEventCard.tsx +45 -0
  46. package/skills/yad-docs/templates/app/src/components/DocLayout/DocPageShell.tsx +80 -0
  47. package/skills/yad-docs/templates/app/src/components/DocLayout/DocSectionCard.tsx +55 -0
  48. package/skills/yad-docs/templates/app/src/components/DocLayout/DocTableOfContents.tsx +79 -0
  49. package/skills/yad-docs/templates/app/src/components/DocLayout/RoleCard.tsx +67 -0
  50. package/skills/yad-docs/templates/app/src/components/DocSections/ApiReferenceSection.tsx +108 -0
  51. package/skills/yad-docs/templates/app/src/components/DocSections/CancelabilitySection.tsx +73 -0
  52. package/skills/yad-docs/templates/app/src/components/DocSections/CriticalRunbookSection.tsx +177 -0
  53. package/skills/yad-docs/templates/app/src/components/DocSections/DataMigrationSection.tsx +102 -0
  54. package/skills/yad-docs/templates/app/src/components/DocSections/DbSchemaSection.tsx +98 -0
  55. package/skills/yad-docs/templates/app/src/components/DocSections/DeploymentGuideSection.tsx +104 -0
  56. package/skills/yad-docs/templates/app/src/components/DocSections/DriverIntegrationSection.tsx +127 -0
  57. package/skills/yad-docs/templates/app/src/components/DocSections/ExecutiveSummarySection.tsx +69 -0
  58. package/skills/yad-docs/templates/app/src/components/DocSections/FlowOverviewSection.tsx +73 -0
  59. package/skills/yad-docs/templates/app/src/components/DocSections/FlowPathsChecklistSection.tsx +96 -0
  60. package/skills/yad-docs/templates/app/src/components/DocSections/MiddlewareChainSection.tsx +107 -0
  61. package/skills/yad-docs/templates/app/src/components/DocSections/MonitoringAlertingSection.tsx +106 -0
  62. package/skills/yad-docs/templates/app/src/components/DocSections/NotificationLocalizationSection.tsx +102 -0
  63. package/skills/yad-docs/templates/app/src/components/DocSections/PMRoadmapSection.tsx +133 -0
  64. package/skills/yad-docs/templates/app/src/components/DocSections/PerformanceTestingSection.tsx +91 -0
  65. package/skills/yad-docs/templates/app/src/components/DocSections/RiderIntegrationSection.tsx +99 -0
  66. package/skills/yad-docs/templates/app/src/components/DocSections/SecuritySection.tsx +74 -0
  67. package/skills/yad-docs/templates/app/src/components/DocSections/StatusMachineSection.tsx +90 -0
  68. package/skills/yad-docs/templates/app/src/components/DocSections/TestPlanSection.tsx +163 -0
  69. package/skills/yad-docs/templates/app/src/components/Logs/SystemLogsTerminal.tsx +126 -0
  70. package/skills/yad-docs/templates/app/src/components/Navigation/TopNavBar.tsx +90 -0
  71. package/skills/yad-docs/templates/app/src/components/Reference/BullMQJobsList.tsx +60 -0
  72. package/skills/yad-docs/templates/app/src/components/Reference/DecisionTreeView.tsx +49 -0
  73. package/skills/yad-docs/templates/app/src/components/Reference/DeeplinkActionsChips.tsx +69 -0
  74. package/skills/yad-docs/templates/app/src/components/Reference/DriverUIStatesTable.tsx +61 -0
  75. package/skills/yad-docs/templates/app/src/components/Reference/FeatureFlagMatrix.tsx +73 -0
  76. package/skills/yad-docs/templates/app/src/components/Reference/RiderUIStatesTable.tsx +61 -0
  77. package/skills/yad-docs/templates/app/src/components/Reference/RulesLegendPanel.tsx +217 -0
  78. package/skills/yad-docs/templates/app/src/components/Reference/StakeholderToggle.tsx +41 -0
  79. package/skills/yad-docs/templates/app/src/components/Reference/TroubleshootingSection.tsx +93 -0
  80. package/skills/yad-docs/templates/app/src/components/Sidebar/PathSelector.tsx +148 -0
  81. package/skills/yad-docs/templates/app/src/components/Sidebar/SidebarFooter.tsx +40 -0
  82. package/skills/yad-docs/templates/app/src/components/Sidebar/StepList.tsx +234 -0
  83. package/skills/yad-docs/templates/app/src/components/shared/Badge.tsx +28 -0
  84. package/skills/yad-docs/templates/app/src/components/shared/CommandPalette.tsx +213 -0
  85. package/skills/yad-docs/templates/app/src/components/shared/Icon.tsx +21 -0
  86. package/skills/yad-docs/templates/app/src/components/shared/Tooltip.tsx +42 -0
  87. package/skills/yad-docs/templates/app/src/data/components.ts +74 -0
  88. package/skills/yad-docs/templates/app/src/data/docSections.ts +231 -0
  89. package/skills/yad-docs/templates/app/src/data/paths.ts +2319 -0
  90. package/skills/yad-docs/templates/app/src/data/referenceData.ts +392 -0
  91. package/skills/yad-docs/templates/app/src/data/roles.ts +145 -0
  92. package/skills/yad-docs/templates/app/src/data/types.ts +79 -0
  93. package/skills/yad-docs/templates/app/src/hooks/useAnimationQueue.ts +41 -0
  94. package/skills/yad-docs/templates/app/src/hooks/usePlayback.ts +100 -0
  95. package/skills/yad-docs/templates/app/src/hooks/useStakeholderFilter.ts +10 -0
  96. package/skills/yad-docs/templates/app/src/index.css +121 -0
  97. package/skills/yad-docs/templates/app/src/main.tsx +13 -0
  98. package/skills/yad-docs/templates/app/src/pages/RoleSelectPage.tsx +34 -0
  99. package/skills/yad-docs/templates/app/src/pages/StakeholderDocPage.tsx +98 -0
  100. package/skills/yad-docs/templates/app/src/pages/SubPathDetailPage.tsx +282 -0
  101. package/skills/yad-docs/templates/app/src/store/useAuthStore.ts +42 -0
  102. package/skills/yad-docs/templates/app/src/store/useFlowStore.ts +197 -0
  103. package/skills/yad-docs/templates/app/src/utils/iconMap.ts +46 -0
  104. package/skills/yad-docs/templates/app/tsconfig.app.json +28 -0
  105. package/skills/yad-docs/templates/app/tsconfig.json +7 -0
  106. package/skills/yad-docs/templates/app/tsconfig.node.json +26 -0
  107. package/skills/yad-docs/templates/app/vite.config.ts +10 -0
  108. package/skills/yad-docs-overview/SKILL.md +129 -0
  109. package/skills/yad-docs-overview/references/pipeline-model.md +102 -0
  110. package/skills/yad-docs-sync/SKILL.md +99 -0
  111. package/skills/yad-docs-sync/references/staleness.md +81 -0
  112. package/skills/yad-engineer-review/SKILL.md +86 -0
  113. package/skills/{yad-ship → yad-engineer-review}/references/ship-and-record.md +2 -2
  114. package/skills/{yad-ship → yad-engineer-review}/templates/.coderabbit.yaml +1 -1
  115. package/skills/yad-epic/references/state-schema.md +1 -1
  116. package/skills/yad-implement/SKILL.md +1 -1
  117. package/skills/yad-implement/references/implement-conventions.md +1 -1
  118. package/skills/yad-open-pr/SKILL.md +72 -0
  119. package/skills/yad-pr-template/templates/checks/pr-template.sh +62 -0
  120. package/skills/yad-pr-template/templates/checks/pr-title.sh +51 -0
  121. package/skills/yad-run/SKILL.md +2 -2
  122. package/skills/yad-run/references/run-loop.md +4 -4
  123. package/skills/yad-ship/SKILL.md +44 -66
  124. package/skills/yad-spec/SKILL.md +1 -1
@@ -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">&#8593;&#8595;</kbd> Navigate</span>
205
+ <span><kbd className="font-mono">&#9166;</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
+ }