orbital-command 0.1.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 (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/bin/orbital.js +362 -0
  4. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
  5. package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
  6. package/dist/assets/charts-D__PA1zp.js +72 -0
  7. package/dist/assets/index-D1G6i0nS.css +1 -0
  8. package/dist/assets/index-DpItvKpf.js +419 -0
  9. package/dist/assets/ui-BvF022GT.js +53 -0
  10. package/dist/assets/vendor-Dzv9lrRc.js +59 -0
  11. package/dist/index.html +19 -0
  12. package/dist/scanner-sweep.png +0 -0
  13. package/dist/server/server/adapters/index.js +34 -0
  14. package/dist/server/server/adapters/iterm2-adapter.js +29 -0
  15. package/dist/server/server/adapters/subprocess-adapter.js +21 -0
  16. package/dist/server/server/adapters/terminal-adapter.js +1 -0
  17. package/dist/server/server/config.js +156 -0
  18. package/dist/server/server/database.js +90 -0
  19. package/dist/server/server/index.js +372 -0
  20. package/dist/server/server/init.js +811 -0
  21. package/dist/server/server/parsers/event-parser.js +64 -0
  22. package/dist/server/server/parsers/scope-parser.js +188 -0
  23. package/dist/server/server/routes/config-routes.js +163 -0
  24. package/dist/server/server/routes/data-routes.js +461 -0
  25. package/dist/server/server/routes/dispatch-routes.js +215 -0
  26. package/dist/server/server/routes/git-routes.js +92 -0
  27. package/dist/server/server/routes/scope-routes.js +215 -0
  28. package/dist/server/server/routes/sprint-routes.js +116 -0
  29. package/dist/server/server/routes/version-routes.js +130 -0
  30. package/dist/server/server/routes/workflow-routes.js +185 -0
  31. package/dist/server/server/schema.js +90 -0
  32. package/dist/server/server/services/batch-orchestrator.js +253 -0
  33. package/dist/server/server/services/claude-session-service.js +352 -0
  34. package/dist/server/server/services/config-service.js +132 -0
  35. package/dist/server/server/services/deploy-service.js +51 -0
  36. package/dist/server/server/services/event-service.js +63 -0
  37. package/dist/server/server/services/gate-service.js +83 -0
  38. package/dist/server/server/services/git-service.js +309 -0
  39. package/dist/server/server/services/github-service.js +145 -0
  40. package/dist/server/server/services/readiness-service.js +184 -0
  41. package/dist/server/server/services/scope-cache.js +72 -0
  42. package/dist/server/server/services/scope-service.js +424 -0
  43. package/dist/server/server/services/sprint-orchestrator.js +312 -0
  44. package/dist/server/server/services/sprint-service.js +293 -0
  45. package/dist/server/server/services/workflow-service.js +397 -0
  46. package/dist/server/server/utils/cc-hooks-parser.js +49 -0
  47. package/dist/server/server/utils/dispatch-utils.js +305 -0
  48. package/dist/server/server/utils/logger.js +86 -0
  49. package/dist/server/server/utils/terminal-launcher.js +388 -0
  50. package/dist/server/server/utils/worktree-manager.js +98 -0
  51. package/dist/server/server/watchers/event-watcher.js +81 -0
  52. package/dist/server/server/watchers/scope-watcher.js +33 -0
  53. package/dist/server/shared/api-types.js +5 -0
  54. package/dist/server/shared/default-workflow.json +616 -0
  55. package/dist/server/shared/workflow-config.js +44 -0
  56. package/dist/server/shared/workflow-engine.js +353 -0
  57. package/index.html +15 -0
  58. package/package.json +110 -0
  59. package/postcss.config.js +6 -0
  60. package/schemas/orbital.config.schema.json +83 -0
  61. package/scripts/postinstall.js +24 -0
  62. package/scripts/start.sh +20 -0
  63. package/server/adapters/index.ts +41 -0
  64. package/server/adapters/iterm2-adapter.ts +37 -0
  65. package/server/adapters/subprocess-adapter.ts +25 -0
  66. package/server/adapters/terminal-adapter.ts +24 -0
  67. package/server/config.ts +234 -0
  68. package/server/database.ts +107 -0
  69. package/server/index.ts +452 -0
  70. package/server/init.ts +891 -0
  71. package/server/parsers/event-parser.ts +74 -0
  72. package/server/parsers/scope-parser.ts +240 -0
  73. package/server/routes/config-routes.ts +182 -0
  74. package/server/routes/data-routes.ts +548 -0
  75. package/server/routes/dispatch-routes.ts +275 -0
  76. package/server/routes/git-routes.ts +112 -0
  77. package/server/routes/scope-routes.ts +262 -0
  78. package/server/routes/sprint-routes.ts +142 -0
  79. package/server/routes/version-routes.ts +156 -0
  80. package/server/routes/workflow-routes.ts +198 -0
  81. package/server/schema.ts +90 -0
  82. package/server/services/batch-orchestrator.ts +286 -0
  83. package/server/services/claude-session-service.ts +441 -0
  84. package/server/services/config-service.ts +151 -0
  85. package/server/services/deploy-service.ts +98 -0
  86. package/server/services/event-service.ts +98 -0
  87. package/server/services/gate-service.ts +126 -0
  88. package/server/services/git-service.ts +391 -0
  89. package/server/services/github-service.ts +183 -0
  90. package/server/services/readiness-service.ts +250 -0
  91. package/server/services/scope-cache.ts +81 -0
  92. package/server/services/scope-service.ts +476 -0
  93. package/server/services/sprint-orchestrator.ts +361 -0
  94. package/server/services/sprint-service.ts +415 -0
  95. package/server/services/workflow-service.ts +461 -0
  96. package/server/utils/cc-hooks-parser.ts +70 -0
  97. package/server/utils/dispatch-utils.ts +395 -0
  98. package/server/utils/logger.ts +109 -0
  99. package/server/utils/terminal-launcher.ts +462 -0
  100. package/server/utils/worktree-manager.ts +104 -0
  101. package/server/watchers/event-watcher.ts +100 -0
  102. package/server/watchers/scope-watcher.ts +38 -0
  103. package/shared/api-types.ts +20 -0
  104. package/shared/default-workflow.json +616 -0
  105. package/shared/workflow-config.ts +170 -0
  106. package/shared/workflow-engine.ts +427 -0
  107. package/src/App.tsx +33 -0
  108. package/src/components/AgentBadge.tsx +40 -0
  109. package/src/components/BatchPreflightModal.tsx +115 -0
  110. package/src/components/CardDisplayToggle.tsx +74 -0
  111. package/src/components/ColumnHeaderActions.tsx +55 -0
  112. package/src/components/ColumnMenu.tsx +99 -0
  113. package/src/components/DeployHistory.tsx +141 -0
  114. package/src/components/DispatchModal.tsx +164 -0
  115. package/src/components/DispatchPopover.tsx +139 -0
  116. package/src/components/DragOverlay.tsx +25 -0
  117. package/src/components/DriftSidebar.tsx +140 -0
  118. package/src/components/EnvironmentStrip.tsx +88 -0
  119. package/src/components/ErrorBoundary.tsx +62 -0
  120. package/src/components/FilterChip.tsx +105 -0
  121. package/src/components/GateIndicator.tsx +33 -0
  122. package/src/components/IdeaDetailModal.tsx +190 -0
  123. package/src/components/IdeaFormDialog.tsx +113 -0
  124. package/src/components/KanbanColumn.tsx +201 -0
  125. package/src/components/MarkdownRenderer.tsx +114 -0
  126. package/src/components/NeonGrid.tsx +128 -0
  127. package/src/components/PromotionQueue.tsx +89 -0
  128. package/src/components/ScopeCard.tsx +234 -0
  129. package/src/components/ScopeDetailModal.tsx +255 -0
  130. package/src/components/ScopeFilterBar.tsx +152 -0
  131. package/src/components/SearchInput.tsx +102 -0
  132. package/src/components/SessionPanel.tsx +335 -0
  133. package/src/components/SprintContainer.tsx +303 -0
  134. package/src/components/SprintDependencyDialog.tsx +78 -0
  135. package/src/components/SprintPreflightModal.tsx +138 -0
  136. package/src/components/StatusBar.tsx +168 -0
  137. package/src/components/SwimCell.tsx +67 -0
  138. package/src/components/SwimLaneRow.tsx +94 -0
  139. package/src/components/SwimlaneBoardView.tsx +108 -0
  140. package/src/components/VersionBadge.tsx +139 -0
  141. package/src/components/ViewModeSelector.tsx +114 -0
  142. package/src/components/config/AgentChip.tsx +53 -0
  143. package/src/components/config/AgentCreateDialog.tsx +321 -0
  144. package/src/components/config/AgentEditor.tsx +175 -0
  145. package/src/components/config/DirectoryTree.tsx +582 -0
  146. package/src/components/config/FileEditor.tsx +550 -0
  147. package/src/components/config/HookChip.tsx +50 -0
  148. package/src/components/config/StageCard.tsx +198 -0
  149. package/src/components/config/TransitionZone.tsx +173 -0
  150. package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
  151. package/src/components/config/WorkflowPipeline.tsx +161 -0
  152. package/src/components/source-control/BranchList.tsx +93 -0
  153. package/src/components/source-control/BranchPanel.tsx +105 -0
  154. package/src/components/source-control/CommitLog.tsx +100 -0
  155. package/src/components/source-control/CommitRow.tsx +47 -0
  156. package/src/components/source-control/GitHubPanel.tsx +110 -0
  157. package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
  158. package/src/components/source-control/GitOverviewBar.tsx +101 -0
  159. package/src/components/source-control/PullRequestList.tsx +69 -0
  160. package/src/components/source-control/WorktreeList.tsx +80 -0
  161. package/src/components/ui/badge.tsx +41 -0
  162. package/src/components/ui/button.tsx +55 -0
  163. package/src/components/ui/card.tsx +78 -0
  164. package/src/components/ui/dialog.tsx +94 -0
  165. package/src/components/ui/popover.tsx +33 -0
  166. package/src/components/ui/scroll-area.tsx +54 -0
  167. package/src/components/ui/separator.tsx +28 -0
  168. package/src/components/ui/tabs.tsx +52 -0
  169. package/src/components/ui/toggle-switch.tsx +35 -0
  170. package/src/components/ui/tooltip.tsx +27 -0
  171. package/src/components/workflow/AddEdgeDialog.tsx +217 -0
  172. package/src/components/workflow/AddListDialog.tsx +201 -0
  173. package/src/components/workflow/ChecklistEditor.tsx +239 -0
  174. package/src/components/workflow/CommandPrefixManager.tsx +118 -0
  175. package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
  176. package/src/components/workflow/DirectionSelector.tsx +133 -0
  177. package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
  178. package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
  179. package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
  180. package/src/components/workflow/EditToolbar.tsx +138 -0
  181. package/src/components/workflow/HookDetailPanel.tsx +250 -0
  182. package/src/components/workflow/HookExecutionLog.tsx +24 -0
  183. package/src/components/workflow/HookSourceModal.tsx +129 -0
  184. package/src/components/workflow/HooksDashboard.tsx +363 -0
  185. package/src/components/workflow/ListPropertyEditor.tsx +251 -0
  186. package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
  187. package/src/components/workflow/MovementRulesPanel.tsx +188 -0
  188. package/src/components/workflow/NodeDetailPanel.tsx +245 -0
  189. package/src/components/workflow/PresetSelector.tsx +414 -0
  190. package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
  191. package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
  192. package/src/components/workflow/WorkflowNode.tsx +147 -0
  193. package/src/components/workflow/graphLayout.ts +186 -0
  194. package/src/components/workflow/mergeHooks.ts +85 -0
  195. package/src/components/workflow/useEditHistory.ts +88 -0
  196. package/src/components/workflow/useWorkflowEditor.ts +262 -0
  197. package/src/components/workflow/validateConfig.ts +70 -0
  198. package/src/hooks/useActiveDispatches.ts +198 -0
  199. package/src/hooks/useBoardSettings.ts +170 -0
  200. package/src/hooks/useCardDisplay.ts +57 -0
  201. package/src/hooks/useCcHooks.ts +24 -0
  202. package/src/hooks/useConfigTree.ts +51 -0
  203. package/src/hooks/useEnforcementRules.ts +46 -0
  204. package/src/hooks/useEvents.ts +59 -0
  205. package/src/hooks/useFileEditor.ts +165 -0
  206. package/src/hooks/useGates.ts +57 -0
  207. package/src/hooks/useIdeaActions.ts +53 -0
  208. package/src/hooks/useKanbanDnd.ts +410 -0
  209. package/src/hooks/useOrbitalConfig.ts +54 -0
  210. package/src/hooks/usePipeline.ts +47 -0
  211. package/src/hooks/usePipelineData.ts +338 -0
  212. package/src/hooks/useReconnect.ts +25 -0
  213. package/src/hooks/useScopeFilters.ts +125 -0
  214. package/src/hooks/useScopeSessions.ts +44 -0
  215. package/src/hooks/useScopes.ts +67 -0
  216. package/src/hooks/useSearch.ts +67 -0
  217. package/src/hooks/useSettings.tsx +187 -0
  218. package/src/hooks/useSocket.ts +25 -0
  219. package/src/hooks/useSourceControl.ts +105 -0
  220. package/src/hooks/useSprintPreflight.ts +55 -0
  221. package/src/hooks/useSprints.ts +154 -0
  222. package/src/hooks/useStatusBarHighlight.ts +18 -0
  223. package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
  224. package/src/hooks/useTheme.ts +9 -0
  225. package/src/hooks/useTransitionReadiness.ts +53 -0
  226. package/src/hooks/useVersion.ts +155 -0
  227. package/src/hooks/useViolations.ts +65 -0
  228. package/src/hooks/useWorkflow.tsx +125 -0
  229. package/src/hooks/useZoomModifier.ts +19 -0
  230. package/src/index.css +797 -0
  231. package/src/layouts/DashboardLayout.tsx +113 -0
  232. package/src/lib/collisionDetection.ts +20 -0
  233. package/src/lib/scope-fields.ts +61 -0
  234. package/src/lib/swimlane.ts +146 -0
  235. package/src/lib/utils.ts +15 -0
  236. package/src/main.tsx +19 -0
  237. package/src/socket.ts +11 -0
  238. package/src/types/index.ts +497 -0
  239. package/src/views/AgentFeed.tsx +339 -0
  240. package/src/views/DeployPipeline.tsx +59 -0
  241. package/src/views/EnforcementView.tsx +378 -0
  242. package/src/views/PrimitivesConfig.tsx +500 -0
  243. package/src/views/QualityGates.tsx +1012 -0
  244. package/src/views/ScopeBoard.tsx +454 -0
  245. package/src/views/SessionTimeline.tsx +516 -0
  246. package/src/views/Settings.tsx +183 -0
  247. package/src/views/SourceControl.tsx +95 -0
  248. package/src/views/WorkflowVisualizer.tsx +382 -0
  249. package/tailwind.config.js +161 -0
  250. package/templates/agents/AUTO-INVOKE.md +180 -0
  251. package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
  252. package/templates/agents/QUICK-REFERENCE.md +122 -0
  253. package/templates/agents/README.md +188 -0
  254. package/templates/agents/SKILL-TRIGGERS.md +100 -0
  255. package/templates/agents/blue-team/frontend-designer.md +424 -0
  256. package/templates/agents/green-team/architect.md +526 -0
  257. package/templates/agents/green-team/rules-enforcer.md +131 -0
  258. package/templates/agents/red-team/attacker-learned.md +24 -0
  259. package/templates/agents/red-team/attacker.md +486 -0
  260. package/templates/agents/red-team/chaos.md +548 -0
  261. package/templates/agents/reference/component-registry.md +82 -0
  262. package/templates/agents/workflows/full-mode.md +218 -0
  263. package/templates/agents/workflows/quick-mode.md +118 -0
  264. package/templates/agents/workflows/security-mode.md +283 -0
  265. package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
  266. package/templates/config/agent-triggers.json +92 -0
  267. package/templates/hooks/agent-team-gate.sh +31 -0
  268. package/templates/hooks/agent-trigger.sh +97 -0
  269. package/templates/hooks/block-push.sh +66 -0
  270. package/templates/hooks/block-workarounds.sh +61 -0
  271. package/templates/hooks/blocker-check.sh +28 -0
  272. package/templates/hooks/completion-checklist.sh +28 -0
  273. package/templates/hooks/decision-capture.sh +15 -0
  274. package/templates/hooks/dependency-check.sh +27 -0
  275. package/templates/hooks/end-session.sh +31 -0
  276. package/templates/hooks/exploration-logger.sh +37 -0
  277. package/templates/hooks/files-changed-summary.sh +37 -0
  278. package/templates/hooks/get-session-id.sh +49 -0
  279. package/templates/hooks/git-commit-guard.sh +34 -0
  280. package/templates/hooks/init-session.sh +93 -0
  281. package/templates/hooks/orbital-emit.sh +79 -0
  282. package/templates/hooks/orbital-report-deploy.sh +78 -0
  283. package/templates/hooks/orbital-report-gates.sh +40 -0
  284. package/templates/hooks/orbital-report-violation.sh +36 -0
  285. package/templates/hooks/orbital-scope-update.sh +53 -0
  286. package/templates/hooks/phase-verify-reminder.sh +26 -0
  287. package/templates/hooks/review-gate-check.sh +82 -0
  288. package/templates/hooks/scope-commit-logger.sh +37 -0
  289. package/templates/hooks/scope-create-cleanup.sh +36 -0
  290. package/templates/hooks/scope-create-gate.sh +80 -0
  291. package/templates/hooks/scope-create-tracker.sh +17 -0
  292. package/templates/hooks/scope-file-sync.sh +53 -0
  293. package/templates/hooks/scope-gate.sh +35 -0
  294. package/templates/hooks/scope-helpers.sh +188 -0
  295. package/templates/hooks/scope-lifecycle-gate.sh +139 -0
  296. package/templates/hooks/scope-prepare.sh +244 -0
  297. package/templates/hooks/scope-transition.sh +172 -0
  298. package/templates/hooks/session-enforcer.sh +143 -0
  299. package/templates/hooks/time-tracker.sh +33 -0
  300. package/templates/lessons-learned.md +15 -0
  301. package/templates/orbital.config.json +35 -0
  302. package/templates/presets/development.json +42 -0
  303. package/templates/presets/gitflow.json +712 -0
  304. package/templates/presets/minimal.json +23 -0
  305. package/templates/quick/rules.md +218 -0
  306. package/templates/scopes/_template.md +255 -0
  307. package/templates/settings-hooks.json +98 -0
  308. package/templates/skills/git-commit/SKILL.md +85 -0
  309. package/templates/skills/git-dev/SKILL.md +99 -0
  310. package/templates/skills/git-hotfix/SKILL.md +223 -0
  311. package/templates/skills/git-main/SKILL.md +84 -0
  312. package/templates/skills/git-production/SKILL.md +165 -0
  313. package/templates/skills/git-staging/SKILL.md +112 -0
  314. package/templates/skills/scope-create/SKILL.md +81 -0
  315. package/templates/skills/scope-fix-review/SKILL.md +168 -0
  316. package/templates/skills/scope-implement/SKILL.md +110 -0
  317. package/templates/skills/scope-post-review/SKILL.md +144 -0
  318. package/templates/skills/scope-pre-review/SKILL.md +211 -0
  319. package/templates/skills/scope-verify/SKILL.md +201 -0
  320. package/templates/skills/session-init/SKILL.md +62 -0
  321. package/templates/skills/session-resume/SKILL.md +201 -0
  322. package/templates/skills/test-checks/SKILL.md +171 -0
  323. package/templates/skills/test-code-review/SKILL.md +252 -0
  324. package/tsconfig.json +25 -0
  325. package/vite.config.ts +38 -0
@@ -0,0 +1,183 @@
1
+ import { useEffect } from 'react';
2
+ import { Settings as SettingsIcon, Minus, Plus } from 'lucide-react';
3
+ import { ScrollArea } from '@/components/ui/scroll-area';
4
+ import { Separator } from '@/components/ui/separator';
5
+ import { ToggleSwitch } from '@/components/ui/toggle-switch';
6
+ import { useSettings, FONT_CATALOG, preloadFontPreviews, type FontCategory } from '@/hooks/useSettings';
7
+ import { cn } from '@/lib/utils';
8
+
9
+ const CATEGORY_LABELS: Record<FontCategory, string> = {
10
+ 'monospace': 'Monospace',
11
+ 'sans-serif': 'Sans-Serif',
12
+ 'display': 'Display',
13
+ };
14
+
15
+ const CATEGORY_ORDER: FontCategory[] = ['monospace', 'sans-serif', 'display'];
16
+
17
+ export function Settings() {
18
+ const { settings, updateSetting } = useSettings();
19
+
20
+ useEffect(() => { preloadFontPreviews(); }, []);
21
+
22
+ const decrementScale = () => {
23
+ const next = Math.max(0.8, Math.round((settings.fontScale - 0.05) * 100) / 100);
24
+ updateSetting('fontScale', next);
25
+ };
26
+
27
+ const incrementScale = () => {
28
+ const next = Math.min(1.3, Math.round((settings.fontScale + 0.05) * 100) / 100);
29
+ updateSetting('fontScale', next);
30
+ };
31
+
32
+ return (
33
+ <div className="flex h-full flex-col">
34
+ {/* Header */}
35
+ <div className="flex items-center gap-3 px-2 pb-4">
36
+ <SettingsIcon className="h-5 w-5 text-muted-foreground" />
37
+ <h1 className="text-lg font-medium tracking-wide">Settings</h1>
38
+ </div>
39
+
40
+ <ScrollArea className="flex-1">
41
+ <div className="flex flex-col gap-6 pr-4 pb-8">
42
+
43
+ {/* ── Appearance ── */}
44
+ <section className="card-glass rounded-xl p-5">
45
+ <h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground mb-5">
46
+ Appearance
47
+ </h2>
48
+
49
+ {/* Font Family */}
50
+ <div className="mb-5">
51
+ <label className="text-xs text-muted-foreground mb-3 block">Font Family</label>
52
+ {CATEGORY_ORDER.map(category => {
53
+ const fonts = FONT_CATALOG.filter(f => f.category === category);
54
+ return (
55
+ <div key={category} className="mb-4 last:mb-0">
56
+ <span className="text-xxs uppercase tracking-widest text-muted-foreground/60 mb-2 block">
57
+ {CATEGORY_LABELS[category]}
58
+ </span>
59
+ <div className="grid grid-cols-2 gap-2 xl:grid-cols-3">
60
+ {fonts.map(font => (
61
+ <button
62
+ key={font.family}
63
+ onClick={() => updateSetting('fontFamily', font.family)}
64
+ className={cn(
65
+ 'group relative flex flex-col items-start rounded-lg border px-3 py-2.5 text-left transition-all duration-200',
66
+ settings.fontFamily === font.family
67
+ ? 'border-[rgba(0,188,212,0.5)] bg-[rgba(0,188,212,0.08)] shadow-[0_0_12px_rgba(0,188,212,0.15)]'
68
+ : 'border-[rgba(255,255,255,0.06)] bg-[rgba(255,255,255,0.02)] hover:border-[rgba(255,255,255,0.12)] hover:bg-[rgba(255,255,255,0.04)]'
69
+ )}
70
+ >
71
+ <span
72
+ className="text-sm text-foreground truncate w-full"
73
+ style={{ fontFamily: `'${font.family}', ${category === 'monospace' ? 'monospace' : 'sans-serif'}` }}
74
+ >
75
+ {font.label}
76
+ </span>
77
+ <span
78
+ className="text-xs text-muted-foreground/50 mt-0.5"
79
+ style={{ fontFamily: `'${font.family}', ${category === 'monospace' ? 'monospace' : 'sans-serif'}` }}
80
+ >
81
+ Aa Bb 0123
82
+ </span>
83
+ </button>
84
+ ))}
85
+ </div>
86
+ </div>
87
+ );
88
+ })}
89
+ </div>
90
+
91
+ <Separator className="my-4" />
92
+
93
+ {/* Font Size Scale */}
94
+ <SettingRow label="Font Size" description="Scale text across the dashboard">
95
+ <div className="flex items-center gap-2">
96
+ <button
97
+ onClick={decrementScale}
98
+ disabled={settings.fontScale <= 0.8}
99
+ className="flex h-7 w-7 items-center justify-center rounded-md border border-[rgba(255,255,255,0.08)] bg-[rgba(255,255,255,0.03)] text-muted-foreground transition-colors hover:border-[rgba(0,188,212,0.3)] hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed"
100
+ >
101
+ <Minus className="h-3.5 w-3.5" />
102
+ </button>
103
+ <span className="w-12 text-center text-sm tabular-nums">
104
+ {Math.round(settings.fontScale * 100)}%
105
+ </span>
106
+ <button
107
+ onClick={incrementScale}
108
+ disabled={settings.fontScale >= 1.3}
109
+ className="flex h-7 w-7 items-center justify-center rounded-md border border-[rgba(255,255,255,0.08)] bg-[rgba(255,255,255,0.03)] text-muted-foreground transition-colors hover:border-[rgba(0,188,212,0.3)] hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed"
110
+ >
111
+ <Plus className="h-3.5 w-3.5" />
112
+ </button>
113
+ </div>
114
+ </SettingRow>
115
+
116
+ <Separator className="my-4" />
117
+
118
+ {/* Reduce Motion */}
119
+ <SettingRow label="Reduce Motion" description="Disable animations and transitions">
120
+ <ToggleSwitch
121
+ checked={settings.reduceMotion}
122
+ onCheckedChange={v => updateSetting('reduceMotion', v)}
123
+ />
124
+ </SettingRow>
125
+
126
+ <Separator className="my-4" />
127
+
128
+ {/* Background Effects */}
129
+ <SettingRow label="Background Effects" description="Animated orbs and grid overlay">
130
+ <ToggleSwitch
131
+ checked={settings.showBackgroundEffects}
132
+ onCheckedChange={v => updateSetting('showBackgroundEffects', v)}
133
+ />
134
+ </SettingRow>
135
+ </section>
136
+
137
+ {/* ── Display ── */}
138
+ <section className="card-glass rounded-xl p-5">
139
+ <h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground mb-5">
140
+ Display
141
+ </h2>
142
+
143
+ <SettingRow label="Status Bar" description="Scope progress bar at bottom">
144
+ <ToggleSwitch
145
+ checked={settings.showStatusBar}
146
+ onCheckedChange={v => updateSetting('showStatusBar', v)}
147
+ />
148
+ </SettingRow>
149
+
150
+ <Separator className="my-4" />
151
+
152
+ <SettingRow label="Compact Mode" description="Reduce spacing for denser layout">
153
+ <ToggleSwitch
154
+ checked={settings.compactMode}
155
+ onCheckedChange={v => updateSetting('compactMode', v)}
156
+ />
157
+ </SettingRow>
158
+ </section>
159
+ </div>
160
+ </ScrollArea>
161
+ </div>
162
+ );
163
+ }
164
+
165
+ function SettingRow({
166
+ label,
167
+ description,
168
+ children,
169
+ }: {
170
+ label: string;
171
+ description: string;
172
+ children: React.ReactNode;
173
+ }) {
174
+ return (
175
+ <div className="flex items-center justify-between gap-4">
176
+ <div className="min-w-0">
177
+ <div className="text-sm text-foreground">{label}</div>
178
+ <div className="text-xs text-muted-foreground/60">{description}</div>
179
+ </div>
180
+ <div className="flex-shrink-0">{children}</div>
181
+ </div>
182
+ );
183
+ }
@@ -0,0 +1,95 @@
1
+ import { useState } from 'react';
2
+ import { GitFork, ChevronDown, ChevronRight, Rocket } from 'lucide-react';
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Card, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { useSourceControl } from '@/hooks/useSourceControl';
6
+ import { usePipeline } from '@/hooks/usePipeline';
7
+ import { GitOverviewBar } from '@/components/source-control/GitOverviewBar';
8
+ import { CommitLog } from '@/components/source-control/CommitLog';
9
+ import { BranchPanel } from '@/components/source-control/BranchPanel';
10
+ import { GitHubPanel } from '@/components/source-control/GitHubPanel';
11
+ import { DeployHistory } from '@/components/DeployHistory';
12
+
13
+ export function SourceControl() {
14
+ const { overview, commits, branches, worktrees, github, drift, loading, loadMoreCommits, hasMoreCommits } = useSourceControl();
15
+ const { deployments } = usePipeline();
16
+ const [deployExpanded, setDeployExpanded] = useState(true);
17
+
18
+ if (loading) {
19
+ return (
20
+ <div className="flex h-64 items-center justify-center">
21
+ <div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
22
+ </div>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <div className="flex-1 min-h-0 overflow-y-auto">
28
+ {/* Header */}
29
+ <div className="mb-6 flex items-center gap-3">
30
+ <GitFork className="h-4 w-4 text-primary" />
31
+ <h1 className="text-xl font-light">Repo</h1>
32
+ {overview && (
33
+ <Badge variant="secondary">
34
+ {overview.currentBranch}
35
+ </Badge>
36
+ )}
37
+ </div>
38
+
39
+ {/* Section A: Git Overview Bar */}
40
+ {overview && <GitOverviewBar overview={overview} github={github} />}
41
+
42
+ {/* Section B + C: Commit Log + Branch Panel */}
43
+ <div className="grid gap-6 lg:grid-cols-3">
44
+ <CommitLog
45
+ commits={commits}
46
+ branches={branches}
47
+ hasMore={hasMoreCommits}
48
+ onLoadMore={loadMoreCommits}
49
+ />
50
+ <BranchPanel
51
+ branches={branches}
52
+ worktrees={worktrees}
53
+ drift={drift}
54
+ branchingMode={overview?.branchingMode ?? 'trunk'}
55
+ />
56
+ </div>
57
+
58
+ {/* Section D: GitHub Panel (collapsible) */}
59
+ <GitHubPanel github={github} />
60
+
61
+ {/* Section E: Deploy History (collapsible) */}
62
+ {deployments.length > 0 && (
63
+ <div className="mt-6">
64
+ {!deployExpanded ? (
65
+ <Card
66
+ className="cursor-pointer select-none"
67
+ onClick={() => setDeployExpanded(true)}
68
+ >
69
+ <CardHeader>
70
+ <CardTitle className="flex items-center gap-2 text-base">
71
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
72
+ <Rocket className="h-4 w-4 text-primary" />
73
+ Deploy History
74
+ <Badge variant="secondary">{deployments.length}</Badge>
75
+ </CardTitle>
76
+ </CardHeader>
77
+ </Card>
78
+ ) : (
79
+ <div>
80
+ <button
81
+ onClick={() => setDeployExpanded(false)}
82
+ className="mb-2 flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
83
+ >
84
+ <ChevronDown className="h-4 w-4" />
85
+ <Rocket className="h-4 w-4" />
86
+ Deploy History
87
+ </button>
88
+ <DeployHistory deployments={deployments} />
89
+ </div>
90
+ )}
91
+ </div>
92
+ )}
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,382 @@
1
+ import { useState, useMemo, useCallback, useEffect } from 'react';
2
+ import {
3
+ ReactFlow,
4
+ Background,
5
+ Controls,
6
+ useNodesState,
7
+ useEdgesState,
8
+ BackgroundVariant,
9
+ } from '@xyflow/react';
10
+ import type { NodeMouseHandler, EdgeMouseHandler } from '@xyflow/react';
11
+ import '@xyflow/react/dist/style.css';
12
+
13
+ import { Workflow, Zap, Pencil, LayoutGrid } from 'lucide-react';
14
+ import type { WorkflowHook, UnifiedHook } from '../../shared/workflow-config';
15
+ import { useWorkflow } from '@/hooks/useWorkflow';
16
+ import { useScopes } from '@/hooks/useScopes';
17
+ import { useCcHooks } from '@/hooks/useCcHooks';
18
+ import { WorkflowNode } from '@/components/workflow/WorkflowNode';
19
+ import type { WorkflowNodeType } from '@/components/workflow/WorkflowNode';
20
+ import { WorkflowEdgeComponent } from '@/components/workflow/WorkflowEdgeComponent';
21
+ import type { WorkflowEdgeType } from '@/components/workflow/WorkflowEdgeComponent';
22
+ import { NodeDetailPanel } from '@/components/workflow/NodeDetailPanel';
23
+ import { EdgeDetailPanel } from '@/components/workflow/EdgeDetailPanel';
24
+ import { PresetSelector } from '@/components/workflow/PresetSelector';
25
+ import { HookSourceModal } from '@/components/workflow/HookSourceModal';
26
+ import { HooksDashboard } from '@/components/workflow/HooksDashboard';
27
+ import { HookDetailPanel } from '@/components/workflow/HookDetailPanel';
28
+ import { ListPropertyEditor } from '@/components/workflow/ListPropertyEditor';
29
+ import { EdgePropertyEditor } from '@/components/workflow/EdgePropertyEditor';
30
+ import { EditToolbar } from '@/components/workflow/EditToolbar';
31
+ import { AddListDialog } from '@/components/workflow/AddListDialog';
32
+ import { AddEdgeDialog } from '@/components/workflow/AddEdgeDialog';
33
+ import { MigrationPreviewDialog } from '@/components/workflow/MigrationPreviewDialog';
34
+ import { ConfigSettingsPanel } from '@/components/workflow/ConfigSettingsPanel';
35
+ import { useWorkflowEditor } from '@/components/workflow/useWorkflowEditor';
36
+ import { computeLayout, computeEdges, computeActiveHandles } from '@/components/workflow/graphLayout';
37
+ import { mergeHooks } from '@/components/workflow/mergeHooks';
38
+
39
+ // ─── Constants ──────────────────────────────────────────
40
+
41
+ const NODE_TYPES = { workflow: WorkflowNode } as const;
42
+ const EDGE_TYPES = { workflow: WorkflowEdgeComponent } as const;
43
+
44
+ // ─── Detail Panel State ─────────────────────────────────
45
+
46
+ type DetailState =
47
+ | { type: 'none' }
48
+ | { type: 'node'; listId: string }
49
+ | { type: 'edge'; from: string; to: string };
50
+
51
+ type ActiveTab = 'graph' | 'hooks';
52
+
53
+ // ─── Component ──────────────────────────────────────────
54
+
55
+ export default function WorkflowVisualizer() {
56
+ const { engine } = useWorkflow();
57
+ const { scopes } = useScopes();
58
+ const { ccHooks } = useCcHooks();
59
+ const config = engine.getConfig();
60
+ const editor = useWorkflowEditor(config);
61
+
62
+ // The config to display: either the edit draft or the live config
63
+ const displayConfig = editor.editMode ? editor.editConfig : config;
64
+ const displayLists = useMemo(() => [...displayConfig.lists].sort((a, b) => a.order - b.order), [displayConfig.lists]);
65
+ const displayEdges = displayConfig.edges;
66
+ const displayHooks = displayConfig.hooks ?? [];
67
+
68
+ // Unified hooks: workflow + CC merged
69
+ const unifiedHooks = useMemo(() => mergeHooks(displayHooks, ccHooks), [displayHooks, ccHooks]);
70
+
71
+ // Tab state
72
+ const [activeTab, setActiveTab] = useState<ActiveTab>('graph');
73
+
74
+ // Source modal (shared across tabs)
75
+ const [sourceModalHook, setSourceModalHook] = useState<UnifiedHook | null>(null);
76
+
77
+ // Hooks tab: selected hook for detail panel
78
+ const [selectedHookForDetail, setSelectedHookForDetail] = useState<UnifiedHook | null>(null);
79
+
80
+ // Adapter: convert WorkflowHook click from Edge/NodeDetailPanel to source modal
81
+ const handleWorkflowHookClick = useCallback((wfHook: WorkflowHook) => {
82
+ const unified = unifiedHooks.find((u) => u.id === wfHook.id);
83
+ if (unified) setSourceModalHook(unified);
84
+ }, [unifiedHooks]);
85
+
86
+ // Cross-tab: navigate from HookDetailPanel edge click to Graph tab
87
+ const handleNavigateToEdge = useCallback((from: string, to: string) => {
88
+ setSelectedHookForDetail(null);
89
+ setActiveTab('graph');
90
+ setDetail({ type: 'edge', from, to });
91
+ }, []);
92
+
93
+ // Scope counts
94
+ const scopeCounts = useMemo(() => {
95
+ const counts = new Map<string, number>();
96
+ for (const scope of scopes) {
97
+ counts.set(scope.status, (counts.get(scope.status) ?? 0) + 1);
98
+ }
99
+ return counts;
100
+ }, [scopes]);
101
+
102
+ // Build graph data from display config
103
+ const activeHandles = useMemo(
104
+ () => computeActiveHandles(displayEdges, displayLists),
105
+ [displayEdges, displayLists],
106
+ );
107
+ const graphNodes = useMemo(
108
+ () => computeLayout(displayLists, displayConfig.groups ?? [], scopeCounts, displayEdges).map((node) => ({
109
+ ...node,
110
+ data: { ...node.data, activeHandles: activeHandles.get(node.id) },
111
+ })),
112
+ [displayLists, displayConfig.groups, scopeCounts, displayEdges, activeHandles],
113
+ );
114
+ const graphEdges = useMemo(
115
+ () => computeEdges(displayEdges, displayLists, displayHooks, false),
116
+ [displayEdges, displayLists, displayHooks],
117
+ );
118
+
119
+ const [nodes, setNodes, onNodesChange] = useNodesState(graphNodes);
120
+ const [edges, setEdges, onEdgesChange] = useEdgesState(graphEdges);
121
+
122
+ // Sync React Flow state when the computed graph data changes (e.g. preset switch)
123
+ useEffect(() => { setNodes(graphNodes); }, [graphNodes, setNodes]);
124
+ useEffect(() => { setEdges(graphEdges); }, [graphEdges, setEdges]);
125
+
126
+ // Detail panel (graph tab)
127
+ const [detail, setDetail] = useState<DetailState>({ type: 'none' });
128
+
129
+ const onNodeClick: NodeMouseHandler<WorkflowNodeType> = useCallback((_event, node) => {
130
+ setDetail({ type: 'node', listId: node.id });
131
+ }, []);
132
+
133
+ const onEdgeClick: EdgeMouseHandler<WorkflowEdgeType> = useCallback((_event, edge) => {
134
+ const [from, to] = edge.id.split(':');
135
+ setDetail({ type: 'edge', from, to });
136
+ }, []);
137
+
138
+ const onPaneClick = useCallback(() => setDetail({ type: 'none' }), []);
139
+
140
+ // Resolve selected items from display config
141
+ const selectedList = detail.type === 'node'
142
+ ? displayConfig.lists.find((l) => l.id === detail.listId) ?? null
143
+ : null;
144
+ const selectedEdge = detail.type === 'edge'
145
+ ? displayEdges.find((e) => e.from === detail.from && e.to === detail.to) ?? null
146
+ : null;
147
+
148
+ const nodeHooks = useMemo(() => {
149
+ if (!selectedList) return [];
150
+ const relevant = displayEdges.filter((e) => e.from === selectedList.id || e.to === selectedList.id);
151
+ const hookIds = new Set(relevant.flatMap((e) => e.hooks ?? []));
152
+ return displayHooks.filter((h) => hookIds.has(h.id));
153
+ }, [selectedList, displayEdges, displayHooks]);
154
+
155
+ const nodeConnectedEdges = useMemo(() => {
156
+ if (!selectedList) return [];
157
+ return displayEdges.filter((e) => e.from === selectedList.id || e.to === selectedList.id);
158
+ }, [selectedList, displayEdges]);
159
+
160
+ const edgeHooks = useMemo(() => {
161
+ if (!selectedEdge) return [];
162
+ return (selectedEdge.hooks ?? [])
163
+ .map((id) => displayHooks.find((h) => h.id === id))
164
+ .filter((h): h is NonNullable<typeof h> => h !== undefined);
165
+ }, [selectedEdge, displayHooks]);
166
+
167
+ return (
168
+ <div className="flex flex-1 min-h-0 flex-col">
169
+ {/* Header */}
170
+ <div className="mb-4 flex items-center justify-between">
171
+ <div className="flex items-center gap-3">
172
+ <Workflow className="h-4 w-4 text-primary" />
173
+ <h1 className="text-xl font-light">Workflow</h1>
174
+ {editor.editMode && (
175
+ <span className="rounded bg-cyan-500/20 px-2 py-0.5 text-[10px] font-semibold text-cyan-400">
176
+ EDIT MODE
177
+ </span>
178
+ )}
179
+
180
+ {/* Tab navigation */}
181
+ {!editor.editMode && (
182
+ <div className="ml-4 flex rounded-lg border border-zinc-800 bg-zinc-900/50 p-0.5">
183
+ <TabButton
184
+ active={activeTab === 'graph'}
185
+ onClick={() => setActiveTab('graph')}
186
+ icon={<LayoutGrid className="h-3 w-3" />}
187
+ label="Graph"
188
+ />
189
+ <TabButton
190
+ active={activeTab === 'hooks'}
191
+ onClick={() => setActiveTab('hooks')}
192
+ icon={<Zap className="h-3 w-3" />}
193
+ label="Hooks"
194
+ count={unifiedHooks.length}
195
+ />
196
+ </div>
197
+ )}
198
+ </div>
199
+ <div className="flex items-center gap-2">
200
+ {!editor.editMode && (
201
+ <button
202
+ onClick={editor.enterEditMode}
203
+ className="flex items-center gap-1.5 rounded border border-zinc-800 px-3 py-1.5 text-xs text-zinc-400 transition-colors hover:border-cyan-500/40 hover:text-cyan-400"
204
+ >
205
+ <Pencil className="h-3.5 w-3.5" />
206
+ Edit
207
+ </button>
208
+ )}
209
+ {!editor.editMode && <PresetSelector activeConfigName={config.name} />}
210
+ </div>
211
+ </div>
212
+
213
+ {/* Edit Toolbar */}
214
+ {editor.editMode && (
215
+ <div className="mb-3">
216
+ <EditToolbar
217
+ canUndo={editor.canUndo}
218
+ canRedo={editor.canRedo}
219
+ changeCount={editor.changeCount}
220
+ validation={editor.validation}
221
+ saving={editor.saving}
222
+ onAddList={() => editor.setShowAddList(true)}
223
+ onAddEdge={() => editor.setShowAddEdge(true)}
224
+ onConfigSettings={() => { editor.setShowConfigSettings(true); setDetail({ type: 'none' }); }}
225
+ onUndo={editor.undo}
226
+ onRedo={editor.redo}
227
+ onSave={editor.save}
228
+ onDiscard={editor.discard}
229
+ onPreview={editor.preview}
230
+ />
231
+ </div>
232
+ )}
233
+
234
+ {/* ─── Graph Tab ────────────────────────────────── */}
235
+ {(activeTab === 'graph' || editor.editMode) && (
236
+ <div className="flex min-h-0 flex-1 gap-3">
237
+ {/* React Flow Canvas */}
238
+ <div
239
+ className="min-h-0 flex-1 rounded-lg border bg-transparent"
240
+ style={{ borderColor: editor.editMode ? '#3b82f640' : '#27272a' }}
241
+ >
242
+ <ReactFlow
243
+ nodes={nodes}
244
+ edges={edges}
245
+ onNodesChange={onNodesChange}
246
+ onEdgesChange={onEdgesChange}
247
+ nodeTypes={NODE_TYPES}
248
+ edgeTypes={EDGE_TYPES}
249
+ onNodeClick={onNodeClick}
250
+ onEdgeClick={onEdgeClick}
251
+ onPaneClick={onPaneClick}
252
+ fitView
253
+ fitViewOptions={{ padding: 0.08 }}
254
+ minZoom={0.3}
255
+ maxZoom={2}
256
+ proOptions={{ hideAttribution: true }}
257
+ >
258
+ <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#27272a" />
259
+ <Controls className="!border-zinc-800 !bg-zinc-900 [&>button]:!border-zinc-800 [&>button]:!bg-zinc-900 [&>button]:!fill-zinc-400 [&>button:hover]:!bg-zinc-800" />
260
+ </ReactFlow>
261
+ </div>
262
+
263
+ {/* Edit mode panels */}
264
+ {editor.editMode && detail.type === 'node' && selectedList && (
265
+ <ListPropertyEditor
266
+ key={selectedList.id}
267
+ list={selectedList}
268
+ config={editor.editConfig}
269
+ onSave={(updated) => { editor.updateList(selectedList, updated); setDetail({ type: 'none' }); }}
270
+ onDelete={() => { editor.deleteList(selectedList.id); setDetail({ type: 'none' }); }}
271
+ onClose={() => setDetail({ type: 'none' })}
272
+ />
273
+ )}
274
+ {editor.editMode && detail.type === 'edge' && selectedEdge && (
275
+ <EdgePropertyEditor
276
+ key={`${selectedEdge.from}:${selectedEdge.to}`}
277
+ edge={selectedEdge}
278
+ config={editor.editConfig}
279
+ onSave={(updated) => { editor.updateEdge(selectedEdge, updated); setDetail({ type: 'none' }); }}
280
+ onDelete={() => { editor.deleteEdge(selectedEdge.from, selectedEdge.to); setDetail({ type: 'none' }); }}
281
+ onClose={() => setDetail({ type: 'none' })}
282
+ />
283
+ )}
284
+ {editor.editMode && editor.showConfigSettings && detail.type === 'none' && (
285
+ <ConfigSettingsPanel
286
+ config={editor.editConfig}
287
+ onUpdate={editor.updateConfig}
288
+ onClose={() => editor.setShowConfigSettings(false)}
289
+ />
290
+ )}
291
+
292
+ {/* Read-only panels (graph tab only, not edit mode) */}
293
+ {!editor.editMode && detail.type === 'node' && (
294
+ <NodeDetailPanel list={selectedList} hooks={nodeHooks} connectedEdges={nodeConnectedEdges} onClose={() => setDetail({ type: 'none' })} onHookClick={handleWorkflowHookClick} />
295
+ )}
296
+ {!editor.editMode && detail.type === 'edge' && (
297
+ <EdgeDetailPanel edge={selectedEdge} hooks={edgeHooks} onClose={() => setDetail({ type: 'none' })} onHookClick={handleWorkflowHookClick} />
298
+ )}
299
+ </div>
300
+ )}
301
+
302
+ {/* ─── Hooks Tab ────────────────────────────────── */}
303
+ {activeTab === 'hooks' && !editor.editMode && (
304
+ <div className="flex min-h-0 flex-1 gap-3">
305
+ <HooksDashboard
306
+ hooks={unifiedHooks}
307
+ edges={displayEdges}
308
+ onHookClick={setSelectedHookForDetail}
309
+ />
310
+ {selectedHookForDetail && (
311
+ <HookDetailPanel
312
+ hook={selectedHookForDetail}
313
+ edges={displayEdges}
314
+ onClose={() => setSelectedHookForDetail(null)}
315
+ onViewSource={setSourceModalHook}
316
+ onNavigateToEdge={handleNavigateToEdge}
317
+ />
318
+ )}
319
+ </div>
320
+ )}
321
+
322
+ {/* Dialogs */}
323
+ <AddListDialog
324
+ open={editor.showAddList}
325
+ onOpenChange={editor.setShowAddList}
326
+ config={editor.editConfig}
327
+ onAdd={editor.addList}
328
+ />
329
+ <AddEdgeDialog
330
+ open={editor.showAddEdge}
331
+ onOpenChange={editor.setShowAddEdge}
332
+ config={editor.editConfig}
333
+ onAdd={editor.addEdge}
334
+ />
335
+ <MigrationPreviewDialog
336
+ open={editor.showPreview}
337
+ onOpenChange={editor.setShowPreview}
338
+ config={editor.editConfig}
339
+ plan={editor.previewPlan}
340
+ loading={editor.previewLoading}
341
+ error={editor.previewError}
342
+ onApply={editor.applyMigration}
343
+ />
344
+ <HookSourceModal
345
+ hook={sourceModalHook}
346
+ open={sourceModalHook !== null}
347
+ onClose={() => setSourceModalHook(null)}
348
+ />
349
+ </div>
350
+ );
351
+ }
352
+
353
+ // ─── Sub-components ─────────────────────────────────────
354
+
355
+ function TabButton({ active, onClick, icon, label, count }: {
356
+ active: boolean; onClick: () => void; icon: React.ReactNode; label: string; count?: number;
357
+ }) {
358
+ return (
359
+ <button
360
+ onClick={onClick}
361
+ className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors"
362
+ style={{
363
+ backgroundColor: active ? '#27272a' : 'transparent',
364
+ color: active ? '#e4e4e7' : '#71717a',
365
+ }}
366
+ >
367
+ {icon}
368
+ {label}
369
+ {count !== undefined && count > 0 && (
370
+ <span
371
+ className="rounded-full px-1.5 py-0.5 text-[9px] font-semibold leading-none"
372
+ style={{
373
+ backgroundColor: active ? '#f9731630' : '#27272a',
374
+ color: active ? '#f97316' : '#71717a',
375
+ }}
376
+ >
377
+ {count}
378
+ </span>
379
+ )}
380
+ </button>
381
+ );
382
+ }