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.
- package/LICENSE +21 -0
- package/README.md +396 -0
- package/bin/orbital.js +362 -0
- package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
- package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
- package/dist/assets/charts-D__PA1zp.js +72 -0
- package/dist/assets/index-D1G6i0nS.css +1 -0
- package/dist/assets/index-DpItvKpf.js +419 -0
- package/dist/assets/ui-BvF022GT.js +53 -0
- package/dist/assets/vendor-Dzv9lrRc.js +59 -0
- package/dist/index.html +19 -0
- package/dist/scanner-sweep.png +0 -0
- package/dist/server/server/adapters/index.js +34 -0
- package/dist/server/server/adapters/iterm2-adapter.js +29 -0
- package/dist/server/server/adapters/subprocess-adapter.js +21 -0
- package/dist/server/server/adapters/terminal-adapter.js +1 -0
- package/dist/server/server/config.js +156 -0
- package/dist/server/server/database.js +90 -0
- package/dist/server/server/index.js +372 -0
- package/dist/server/server/init.js +811 -0
- package/dist/server/server/parsers/event-parser.js +64 -0
- package/dist/server/server/parsers/scope-parser.js +188 -0
- package/dist/server/server/routes/config-routes.js +163 -0
- package/dist/server/server/routes/data-routes.js +461 -0
- package/dist/server/server/routes/dispatch-routes.js +215 -0
- package/dist/server/server/routes/git-routes.js +92 -0
- package/dist/server/server/routes/scope-routes.js +215 -0
- package/dist/server/server/routes/sprint-routes.js +116 -0
- package/dist/server/server/routes/version-routes.js +130 -0
- package/dist/server/server/routes/workflow-routes.js +185 -0
- package/dist/server/server/schema.js +90 -0
- package/dist/server/server/services/batch-orchestrator.js +253 -0
- package/dist/server/server/services/claude-session-service.js +352 -0
- package/dist/server/server/services/config-service.js +132 -0
- package/dist/server/server/services/deploy-service.js +51 -0
- package/dist/server/server/services/event-service.js +63 -0
- package/dist/server/server/services/gate-service.js +83 -0
- package/dist/server/server/services/git-service.js +309 -0
- package/dist/server/server/services/github-service.js +145 -0
- package/dist/server/server/services/readiness-service.js +184 -0
- package/dist/server/server/services/scope-cache.js +72 -0
- package/dist/server/server/services/scope-service.js +424 -0
- package/dist/server/server/services/sprint-orchestrator.js +312 -0
- package/dist/server/server/services/sprint-service.js +293 -0
- package/dist/server/server/services/workflow-service.js +397 -0
- package/dist/server/server/utils/cc-hooks-parser.js +49 -0
- package/dist/server/server/utils/dispatch-utils.js +305 -0
- package/dist/server/server/utils/logger.js +86 -0
- package/dist/server/server/utils/terminal-launcher.js +388 -0
- package/dist/server/server/utils/worktree-manager.js +98 -0
- package/dist/server/server/watchers/event-watcher.js +81 -0
- package/dist/server/server/watchers/scope-watcher.js +33 -0
- package/dist/server/shared/api-types.js +5 -0
- package/dist/server/shared/default-workflow.json +616 -0
- package/dist/server/shared/workflow-config.js +44 -0
- package/dist/server/shared/workflow-engine.js +353 -0
- package/index.html +15 -0
- package/package.json +110 -0
- package/postcss.config.js +6 -0
- package/schemas/orbital.config.schema.json +83 -0
- package/scripts/postinstall.js +24 -0
- package/scripts/start.sh +20 -0
- package/server/adapters/index.ts +41 -0
- package/server/adapters/iterm2-adapter.ts +37 -0
- package/server/adapters/subprocess-adapter.ts +25 -0
- package/server/adapters/terminal-adapter.ts +24 -0
- package/server/config.ts +234 -0
- package/server/database.ts +107 -0
- package/server/index.ts +452 -0
- package/server/init.ts +891 -0
- package/server/parsers/event-parser.ts +74 -0
- package/server/parsers/scope-parser.ts +240 -0
- package/server/routes/config-routes.ts +182 -0
- package/server/routes/data-routes.ts +548 -0
- package/server/routes/dispatch-routes.ts +275 -0
- package/server/routes/git-routes.ts +112 -0
- package/server/routes/scope-routes.ts +262 -0
- package/server/routes/sprint-routes.ts +142 -0
- package/server/routes/version-routes.ts +156 -0
- package/server/routes/workflow-routes.ts +198 -0
- package/server/schema.ts +90 -0
- package/server/services/batch-orchestrator.ts +286 -0
- package/server/services/claude-session-service.ts +441 -0
- package/server/services/config-service.ts +151 -0
- package/server/services/deploy-service.ts +98 -0
- package/server/services/event-service.ts +98 -0
- package/server/services/gate-service.ts +126 -0
- package/server/services/git-service.ts +391 -0
- package/server/services/github-service.ts +183 -0
- package/server/services/readiness-service.ts +250 -0
- package/server/services/scope-cache.ts +81 -0
- package/server/services/scope-service.ts +476 -0
- package/server/services/sprint-orchestrator.ts +361 -0
- package/server/services/sprint-service.ts +415 -0
- package/server/services/workflow-service.ts +461 -0
- package/server/utils/cc-hooks-parser.ts +70 -0
- package/server/utils/dispatch-utils.ts +395 -0
- package/server/utils/logger.ts +109 -0
- package/server/utils/terminal-launcher.ts +462 -0
- package/server/utils/worktree-manager.ts +104 -0
- package/server/watchers/event-watcher.ts +100 -0
- package/server/watchers/scope-watcher.ts +38 -0
- package/shared/api-types.ts +20 -0
- package/shared/default-workflow.json +616 -0
- package/shared/workflow-config.ts +170 -0
- package/shared/workflow-engine.ts +427 -0
- package/src/App.tsx +33 -0
- package/src/components/AgentBadge.tsx +40 -0
- package/src/components/BatchPreflightModal.tsx +115 -0
- package/src/components/CardDisplayToggle.tsx +74 -0
- package/src/components/ColumnHeaderActions.tsx +55 -0
- package/src/components/ColumnMenu.tsx +99 -0
- package/src/components/DeployHistory.tsx +141 -0
- package/src/components/DispatchModal.tsx +164 -0
- package/src/components/DispatchPopover.tsx +139 -0
- package/src/components/DragOverlay.tsx +25 -0
- package/src/components/DriftSidebar.tsx +140 -0
- package/src/components/EnvironmentStrip.tsx +88 -0
- package/src/components/ErrorBoundary.tsx +62 -0
- package/src/components/FilterChip.tsx +105 -0
- package/src/components/GateIndicator.tsx +33 -0
- package/src/components/IdeaDetailModal.tsx +190 -0
- package/src/components/IdeaFormDialog.tsx +113 -0
- package/src/components/KanbanColumn.tsx +201 -0
- package/src/components/MarkdownRenderer.tsx +114 -0
- package/src/components/NeonGrid.tsx +128 -0
- package/src/components/PromotionQueue.tsx +89 -0
- package/src/components/ScopeCard.tsx +234 -0
- package/src/components/ScopeDetailModal.tsx +255 -0
- package/src/components/ScopeFilterBar.tsx +152 -0
- package/src/components/SearchInput.tsx +102 -0
- package/src/components/SessionPanel.tsx +335 -0
- package/src/components/SprintContainer.tsx +303 -0
- package/src/components/SprintDependencyDialog.tsx +78 -0
- package/src/components/SprintPreflightModal.tsx +138 -0
- package/src/components/StatusBar.tsx +168 -0
- package/src/components/SwimCell.tsx +67 -0
- package/src/components/SwimLaneRow.tsx +94 -0
- package/src/components/SwimlaneBoardView.tsx +108 -0
- package/src/components/VersionBadge.tsx +139 -0
- package/src/components/ViewModeSelector.tsx +114 -0
- package/src/components/config/AgentChip.tsx +53 -0
- package/src/components/config/AgentCreateDialog.tsx +321 -0
- package/src/components/config/AgentEditor.tsx +175 -0
- package/src/components/config/DirectoryTree.tsx +582 -0
- package/src/components/config/FileEditor.tsx +550 -0
- package/src/components/config/HookChip.tsx +50 -0
- package/src/components/config/StageCard.tsx +198 -0
- package/src/components/config/TransitionZone.tsx +173 -0
- package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
- package/src/components/config/WorkflowPipeline.tsx +161 -0
- package/src/components/source-control/BranchList.tsx +93 -0
- package/src/components/source-control/BranchPanel.tsx +105 -0
- package/src/components/source-control/CommitLog.tsx +100 -0
- package/src/components/source-control/CommitRow.tsx +47 -0
- package/src/components/source-control/GitHubPanel.tsx +110 -0
- package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
- package/src/components/source-control/GitOverviewBar.tsx +101 -0
- package/src/components/source-control/PullRequestList.tsx +69 -0
- package/src/components/source-control/WorktreeList.tsx +80 -0
- package/src/components/ui/badge.tsx +41 -0
- package/src/components/ui/button.tsx +55 -0
- package/src/components/ui/card.tsx +78 -0
- package/src/components/ui/dialog.tsx +94 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/toggle-switch.tsx +35 -0
- package/src/components/ui/tooltip.tsx +27 -0
- package/src/components/workflow/AddEdgeDialog.tsx +217 -0
- package/src/components/workflow/AddListDialog.tsx +201 -0
- package/src/components/workflow/ChecklistEditor.tsx +239 -0
- package/src/components/workflow/CommandPrefixManager.tsx +118 -0
- package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
- package/src/components/workflow/DirectionSelector.tsx +133 -0
- package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
- package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
- package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
- package/src/components/workflow/EditToolbar.tsx +138 -0
- package/src/components/workflow/HookDetailPanel.tsx +250 -0
- package/src/components/workflow/HookExecutionLog.tsx +24 -0
- package/src/components/workflow/HookSourceModal.tsx +129 -0
- package/src/components/workflow/HooksDashboard.tsx +363 -0
- package/src/components/workflow/ListPropertyEditor.tsx +251 -0
- package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
- package/src/components/workflow/MovementRulesPanel.tsx +188 -0
- package/src/components/workflow/NodeDetailPanel.tsx +245 -0
- package/src/components/workflow/PresetSelector.tsx +414 -0
- package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
- package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
- package/src/components/workflow/WorkflowNode.tsx +147 -0
- package/src/components/workflow/graphLayout.ts +186 -0
- package/src/components/workflow/mergeHooks.ts +85 -0
- package/src/components/workflow/useEditHistory.ts +88 -0
- package/src/components/workflow/useWorkflowEditor.ts +262 -0
- package/src/components/workflow/validateConfig.ts +70 -0
- package/src/hooks/useActiveDispatches.ts +198 -0
- package/src/hooks/useBoardSettings.ts +170 -0
- package/src/hooks/useCardDisplay.ts +57 -0
- package/src/hooks/useCcHooks.ts +24 -0
- package/src/hooks/useConfigTree.ts +51 -0
- package/src/hooks/useEnforcementRules.ts +46 -0
- package/src/hooks/useEvents.ts +59 -0
- package/src/hooks/useFileEditor.ts +165 -0
- package/src/hooks/useGates.ts +57 -0
- package/src/hooks/useIdeaActions.ts +53 -0
- package/src/hooks/useKanbanDnd.ts +410 -0
- package/src/hooks/useOrbitalConfig.ts +54 -0
- package/src/hooks/usePipeline.ts +47 -0
- package/src/hooks/usePipelineData.ts +338 -0
- package/src/hooks/useReconnect.ts +25 -0
- package/src/hooks/useScopeFilters.ts +125 -0
- package/src/hooks/useScopeSessions.ts +44 -0
- package/src/hooks/useScopes.ts +67 -0
- package/src/hooks/useSearch.ts +67 -0
- package/src/hooks/useSettings.tsx +187 -0
- package/src/hooks/useSocket.ts +25 -0
- package/src/hooks/useSourceControl.ts +105 -0
- package/src/hooks/useSprintPreflight.ts +55 -0
- package/src/hooks/useSprints.ts +154 -0
- package/src/hooks/useStatusBarHighlight.ts +18 -0
- package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
- package/src/hooks/useTheme.ts +9 -0
- package/src/hooks/useTransitionReadiness.ts +53 -0
- package/src/hooks/useVersion.ts +155 -0
- package/src/hooks/useViolations.ts +65 -0
- package/src/hooks/useWorkflow.tsx +125 -0
- package/src/hooks/useZoomModifier.ts +19 -0
- package/src/index.css +797 -0
- package/src/layouts/DashboardLayout.tsx +113 -0
- package/src/lib/collisionDetection.ts +20 -0
- package/src/lib/scope-fields.ts +61 -0
- package/src/lib/swimlane.ts +146 -0
- package/src/lib/utils.ts +15 -0
- package/src/main.tsx +19 -0
- package/src/socket.ts +11 -0
- package/src/types/index.ts +497 -0
- package/src/views/AgentFeed.tsx +339 -0
- package/src/views/DeployPipeline.tsx +59 -0
- package/src/views/EnforcementView.tsx +378 -0
- package/src/views/PrimitivesConfig.tsx +500 -0
- package/src/views/QualityGates.tsx +1012 -0
- package/src/views/ScopeBoard.tsx +454 -0
- package/src/views/SessionTimeline.tsx +516 -0
- package/src/views/Settings.tsx +183 -0
- package/src/views/SourceControl.tsx +95 -0
- package/src/views/WorkflowVisualizer.tsx +382 -0
- package/tailwind.config.js +161 -0
- package/templates/agents/AUTO-INVOKE.md +180 -0
- package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
- package/templates/agents/QUICK-REFERENCE.md +122 -0
- package/templates/agents/README.md +188 -0
- package/templates/agents/SKILL-TRIGGERS.md +100 -0
- package/templates/agents/blue-team/frontend-designer.md +424 -0
- package/templates/agents/green-team/architect.md +526 -0
- package/templates/agents/green-team/rules-enforcer.md +131 -0
- package/templates/agents/red-team/attacker-learned.md +24 -0
- package/templates/agents/red-team/attacker.md +486 -0
- package/templates/agents/red-team/chaos.md +548 -0
- package/templates/agents/reference/component-registry.md +82 -0
- package/templates/agents/workflows/full-mode.md +218 -0
- package/templates/agents/workflows/quick-mode.md +118 -0
- package/templates/agents/workflows/security-mode.md +283 -0
- package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
- package/templates/config/agent-triggers.json +92 -0
- package/templates/hooks/agent-team-gate.sh +31 -0
- package/templates/hooks/agent-trigger.sh +97 -0
- package/templates/hooks/block-push.sh +66 -0
- package/templates/hooks/block-workarounds.sh +61 -0
- package/templates/hooks/blocker-check.sh +28 -0
- package/templates/hooks/completion-checklist.sh +28 -0
- package/templates/hooks/decision-capture.sh +15 -0
- package/templates/hooks/dependency-check.sh +27 -0
- package/templates/hooks/end-session.sh +31 -0
- package/templates/hooks/exploration-logger.sh +37 -0
- package/templates/hooks/files-changed-summary.sh +37 -0
- package/templates/hooks/get-session-id.sh +49 -0
- package/templates/hooks/git-commit-guard.sh +34 -0
- package/templates/hooks/init-session.sh +93 -0
- package/templates/hooks/orbital-emit.sh +79 -0
- package/templates/hooks/orbital-report-deploy.sh +78 -0
- package/templates/hooks/orbital-report-gates.sh +40 -0
- package/templates/hooks/orbital-report-violation.sh +36 -0
- package/templates/hooks/orbital-scope-update.sh +53 -0
- package/templates/hooks/phase-verify-reminder.sh +26 -0
- package/templates/hooks/review-gate-check.sh +82 -0
- package/templates/hooks/scope-commit-logger.sh +37 -0
- package/templates/hooks/scope-create-cleanup.sh +36 -0
- package/templates/hooks/scope-create-gate.sh +80 -0
- package/templates/hooks/scope-create-tracker.sh +17 -0
- package/templates/hooks/scope-file-sync.sh +53 -0
- package/templates/hooks/scope-gate.sh +35 -0
- package/templates/hooks/scope-helpers.sh +188 -0
- package/templates/hooks/scope-lifecycle-gate.sh +139 -0
- package/templates/hooks/scope-prepare.sh +244 -0
- package/templates/hooks/scope-transition.sh +172 -0
- package/templates/hooks/session-enforcer.sh +143 -0
- package/templates/hooks/time-tracker.sh +33 -0
- package/templates/lessons-learned.md +15 -0
- package/templates/orbital.config.json +35 -0
- package/templates/presets/development.json +42 -0
- package/templates/presets/gitflow.json +712 -0
- package/templates/presets/minimal.json +23 -0
- package/templates/quick/rules.md +218 -0
- package/templates/scopes/_template.md +255 -0
- package/templates/settings-hooks.json +98 -0
- package/templates/skills/git-commit/SKILL.md +85 -0
- package/templates/skills/git-dev/SKILL.md +99 -0
- package/templates/skills/git-hotfix/SKILL.md +223 -0
- package/templates/skills/git-main/SKILL.md +84 -0
- package/templates/skills/git-production/SKILL.md +165 -0
- package/templates/skills/git-staging/SKILL.md +112 -0
- package/templates/skills/scope-create/SKILL.md +81 -0
- package/templates/skills/scope-fix-review/SKILL.md +168 -0
- package/templates/skills/scope-implement/SKILL.md +110 -0
- package/templates/skills/scope-post-review/SKILL.md +144 -0
- package/templates/skills/scope-pre-review/SKILL.md +211 -0
- package/templates/skills/scope-verify/SKILL.md +201 -0
- package/templates/skills/session-init/SKILL.md +62 -0
- package/templates/skills/session-resume/SKILL.md +201 -0
- package/templates/skills/test-checks/SKILL.md +171 -0
- package/templates/skills/test-code-review/SKILL.md +252 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +38 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AlertTriangle, Plus, X } from 'lucide-react';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogHeader,
|
|
6
|
+
DialogTitle,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
} from '@/components/ui/dialog';
|
|
9
|
+
import { Button } from '@/components/ui/button';
|
|
10
|
+
import { Badge } from '@/components/ui/badge';
|
|
11
|
+
import { formatScopeId } from '@/lib/utils';
|
|
12
|
+
|
|
13
|
+
interface UnmetDep {
|
|
14
|
+
scope_id: number;
|
|
15
|
+
missing: Array<{ scope_id: number; title: string; status: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SprintDependencyDialogProps {
|
|
19
|
+
open: boolean;
|
|
20
|
+
unmetDeps: UnmetDep[];
|
|
21
|
+
onAddAll: (scopeIds: number[]) => void;
|
|
22
|
+
onCancel: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function SprintDependencyDialog({ open, unmetDeps, onAddAll, onCancel }: SprintDependencyDialogProps) {
|
|
26
|
+
// Collect all unique missing scope IDs
|
|
27
|
+
const allMissing = new Map<number, { title: string; status: string }>();
|
|
28
|
+
for (const dep of unmetDeps) {
|
|
29
|
+
for (const m of dep.missing) {
|
|
30
|
+
if (!allMissing.has(m.scope_id)) {
|
|
31
|
+
allMissing.set(m.scope_id, { title: m.title, status: m.status });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Dialog open={open} onOpenChange={(o) => !o && onCancel()}>
|
|
38
|
+
<DialogContent className="sm:max-w-md">
|
|
39
|
+
<DialogHeader>
|
|
40
|
+
<DialogTitle className="flex items-center gap-2 text-sm">
|
|
41
|
+
<AlertTriangle className="h-4 w-4 text-warning-amber" />
|
|
42
|
+
Unmet Dependencies
|
|
43
|
+
</DialogTitle>
|
|
44
|
+
<DialogDescription className="text-xs">
|
|
45
|
+
Some scopes you added depend on scopes not yet in this sprint.
|
|
46
|
+
</DialogDescription>
|
|
47
|
+
</DialogHeader>
|
|
48
|
+
|
|
49
|
+
<div className="space-y-3 my-2">
|
|
50
|
+
{unmetDeps.map((dep) => (
|
|
51
|
+
<div key={dep.scope_id} className="text-xs">
|
|
52
|
+
<span className="font-mono text-muted-foreground">{formatScopeId(dep.scope_id)}</span>
|
|
53
|
+
<span className="ml-1">depends on:</span>
|
|
54
|
+
<div className="ml-4 mt-1 space-y-1">
|
|
55
|
+
{dep.missing.map((m) => (
|
|
56
|
+
<div key={m.scope_id} className="flex items-center gap-2">
|
|
57
|
+
<span className="font-mono text-muted-foreground">{formatScopeId(m.scope_id)}</span>
|
|
58
|
+
<span className="truncate">{m.title}</span>
|
|
59
|
+
<Badge variant="outline" className="text-[10px] ml-auto shrink-0">{m.status}</Badge>
|
|
60
|
+
</div>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="flex justify-end gap-2 pt-2 border-t">
|
|
68
|
+
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
69
|
+
<X className="h-3 w-3 mr-1" /> Cancel
|
|
70
|
+
</Button>
|
|
71
|
+
<Button size="sm" onClick={() => onAddAll([...allMissing.keys()])}>
|
|
72
|
+
<Plus className="h-3 w-3 mr-1" /> Add All Dependencies
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</DialogContent>
|
|
76
|
+
</Dialog>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { Play, ArrowRight, AlertTriangle } from 'lucide-react';
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogHeader,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
} from '@/components/ui/dialog';
|
|
10
|
+
import { Button } from '@/components/ui/button';
|
|
11
|
+
import { Badge } from '@/components/ui/badge';
|
|
12
|
+
import { formatScopeId } from '@/lib/utils';
|
|
13
|
+
import type { Sprint } from '@/types';
|
|
14
|
+
|
|
15
|
+
interface SprintPreflightModalProps {
|
|
16
|
+
open: boolean;
|
|
17
|
+
sprint: Sprint | null;
|
|
18
|
+
graph: { layers: number[][]; edges: Array<{ from: number; to: number }> } | null;
|
|
19
|
+
loading: boolean;
|
|
20
|
+
onConfirm: () => void;
|
|
21
|
+
onCancel: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function SprintPreflightModal({
|
|
25
|
+
open,
|
|
26
|
+
sprint,
|
|
27
|
+
graph,
|
|
28
|
+
loading,
|
|
29
|
+
onConfirm,
|
|
30
|
+
onCancel,
|
|
31
|
+
}: SprintPreflightModalProps) {
|
|
32
|
+
const [dispatching, setDispatching] = useState(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (open) setDispatching(false);
|
|
36
|
+
}, [open]);
|
|
37
|
+
|
|
38
|
+
if (!sprint) return null;
|
|
39
|
+
|
|
40
|
+
const scopeMap = new Map(sprint.scopes.map((s) => [s.scope_id, s]));
|
|
41
|
+
const layers = graph?.layers ?? [];
|
|
42
|
+
const totalScopes = sprint.scope_ids.length;
|
|
43
|
+
|
|
44
|
+
const handleConfirm = async () => {
|
|
45
|
+
setDispatching(true);
|
|
46
|
+
onConfirm();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Dialog open={open} onOpenChange={(o) => !o && onCancel()}>
|
|
51
|
+
<DialogContent className="sm:max-w-lg">
|
|
52
|
+
<DialogHeader>
|
|
53
|
+
<DialogTitle className="flex items-center gap-2 text-sm">
|
|
54
|
+
<Play className="h-4 w-4 text-cyan-400" />
|
|
55
|
+
Dispatch Sprint: {sprint.name}
|
|
56
|
+
</DialogTitle>
|
|
57
|
+
<DialogDescription className="text-xs">
|
|
58
|
+
{totalScopes} scope{totalScopes !== 1 ? 's' : ''} will be dispatched in{' '}
|
|
59
|
+
{layers.length} layer{layers.length !== 1 ? 's' : ''} with max{' '}
|
|
60
|
+
{sprint.concurrency_cap} concurrent agents.
|
|
61
|
+
</DialogDescription>
|
|
62
|
+
</DialogHeader>
|
|
63
|
+
|
|
64
|
+
{/* Execution Graph */}
|
|
65
|
+
<div className="my-3 space-y-3 max-h-64 overflow-y-auto">
|
|
66
|
+
{layers.map((layer, idx) => (
|
|
67
|
+
<div key={idx}>
|
|
68
|
+
<div className="flex items-center gap-2 mb-1">
|
|
69
|
+
<Badge variant="outline" className="text-[10px]">Layer {idx}</Badge>
|
|
70
|
+
{idx === 0 && <span className="text-[10px] text-cyan-400">Launches first</span>}
|
|
71
|
+
{idx > 0 && (
|
|
72
|
+
<span className="text-[10px] text-muted-foreground">
|
|
73
|
+
Waits for Layer {idx - 1}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
<div className="ml-4 space-y-1">
|
|
78
|
+
{layer.map((scopeId) => {
|
|
79
|
+
const ss = scopeMap.get(scopeId);
|
|
80
|
+
return (
|
|
81
|
+
<div key={scopeId} className="flex items-center gap-2 text-xs">
|
|
82
|
+
<span className="font-mono text-muted-foreground w-8 shrink-0">
|
|
83
|
+
{formatScopeId(scopeId)}
|
|
84
|
+
</span>
|
|
85
|
+
<span className="truncate flex-1">{ss?.title ?? 'Unknown'}</span>
|
|
86
|
+
{ss?.effort_estimate && (
|
|
87
|
+
<span className="text-[10px] text-muted-foreground shrink-0">
|
|
88
|
+
{ss.effort_estimate}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
{idx < layers.length - 1 && (
|
|
96
|
+
<div className="flex justify-center my-1">
|
|
97
|
+
<ArrowRight className="h-3 w-3 text-muted-foreground rotate-90" />
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Warnings */}
|
|
105
|
+
{layers.length === 0 && !loading && (
|
|
106
|
+
<div className="flex items-center gap-2 rounded border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-400">
|
|
107
|
+
<AlertTriangle className="h-3.5 w-3.5 shrink-0" />
|
|
108
|
+
Could not compute execution layers. Sprint may have issues.
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* Actions */}
|
|
113
|
+
<div className="flex justify-end gap-2 pt-2 border-t">
|
|
114
|
+
<Button variant="ghost" size="sm" onClick={onCancel} disabled={dispatching}>
|
|
115
|
+
Cancel
|
|
116
|
+
</Button>
|
|
117
|
+
<Button
|
|
118
|
+
size="sm"
|
|
119
|
+
onClick={handleConfirm}
|
|
120
|
+
disabled={dispatching || loading || layers.length === 0}
|
|
121
|
+
className="bg-cyan-600 hover:bg-cyan-700"
|
|
122
|
+
>
|
|
123
|
+
{dispatching ? (
|
|
124
|
+
<span className="flex items-center gap-1">
|
|
125
|
+
<span className="h-3 w-3 animate-spin rounded-full border border-white border-t-transparent" />
|
|
126
|
+
Dispatching...
|
|
127
|
+
</span>
|
|
128
|
+
) : (
|
|
129
|
+
<>
|
|
130
|
+
<Play className="h-3 w-3 mr-1" /> Dispatch Sprint
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</DialogContent>
|
|
136
|
+
</Dialog>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { useScopes } from '@/hooks/useScopes';
|
|
4
|
+
import { useWorkflow } from '@/hooks/useWorkflow.tsx';
|
|
5
|
+
import { useTheme } from '@/hooks/useTheme';
|
|
6
|
+
import { formatScopeId } from '@/lib/utils';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
import { VersionBadge } from '@/components/VersionBadge';
|
|
9
|
+
import type { Scope } from '@/types';
|
|
10
|
+
|
|
11
|
+
export function StatusBar() {
|
|
12
|
+
const { scopes } = useScopes();
|
|
13
|
+
const { engine } = useWorkflow();
|
|
14
|
+
const { neonGlass } = useTheme();
|
|
15
|
+
const navigate = useNavigate();
|
|
16
|
+
|
|
17
|
+
const boardColumns = useMemo(() => engine.getBoardColumns(), [engine]);
|
|
18
|
+
const entryPointId = useMemo(() => engine.getEntryPoint().id, [engine]);
|
|
19
|
+
|
|
20
|
+
const columnOrder = useMemo(() => {
|
|
21
|
+
const map = new Map<string, number>();
|
|
22
|
+
boardColumns.forEach((col, i) => map.set(col.id, i));
|
|
23
|
+
return map;
|
|
24
|
+
}, [boardColumns]);
|
|
25
|
+
|
|
26
|
+
const columnColorMap = useMemo(() => {
|
|
27
|
+
const map = new Map<string, string>();
|
|
28
|
+
for (const col of boardColumns) map.set(col.id, col.color);
|
|
29
|
+
return map;
|
|
30
|
+
}, [boardColumns]);
|
|
31
|
+
|
|
32
|
+
const { inProgress, needsAttention } = useMemo(() => {
|
|
33
|
+
const prog: Scope[] = [];
|
|
34
|
+
const attn: Scope[] = [];
|
|
35
|
+
|
|
36
|
+
for (const scope of scopes) {
|
|
37
|
+
if (scope.status === entryPointId) continue;
|
|
38
|
+
if (engine.isTerminalStatus(scope.status)) continue;
|
|
39
|
+
|
|
40
|
+
if (scope.blocked_by.length > 0) {
|
|
41
|
+
attn.push(scope);
|
|
42
|
+
} else {
|
|
43
|
+
prog.push(scope);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { inProgress: groupByStatus(prog, columnOrder), needsAttention: groupByStatus(attn, columnOrder) };
|
|
48
|
+
}, [scopes, entryPointId, engine, columnOrder]);
|
|
49
|
+
|
|
50
|
+
const handleBadgeClick = (e: React.MouseEvent, scopeId: number) => {
|
|
51
|
+
e.stopPropagation();
|
|
52
|
+
navigate(`/?highlight=${scopeId}`);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const hasScopes = inProgress.size > 0 || needsAttention.size > 0;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className={cn(
|
|
59
|
+
'fixed bottom-0 left-24 right-0 z-40 border-t border-border bg-surface/95 backdrop-blur-sm',
|
|
60
|
+
neonGlass && 'ticker-glass'
|
|
61
|
+
)}>
|
|
62
|
+
<div className="flex items-center px-4 py-2">
|
|
63
|
+
{/* Scrollable scope badges */}
|
|
64
|
+
{hasScopes && (
|
|
65
|
+
<div className="flex min-w-0 flex-1 items-center gap-4 overflow-x-auto">
|
|
66
|
+
{inProgress.size > 0 && (
|
|
67
|
+
<>
|
|
68
|
+
<span className="flex-shrink-0 text-xxs uppercase tracking-wider font-normal text-muted-foreground">
|
|
69
|
+
In Progress
|
|
70
|
+
</span>
|
|
71
|
+
<ScopeBadges groups={inProgress} colorMap={columnColorMap} onClick={handleBadgeClick} />
|
|
72
|
+
</>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{inProgress.size > 0 && needsAttention.size > 0 && (
|
|
76
|
+
<div className="h-4 w-px flex-shrink-0 bg-border" />
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{needsAttention.size > 0 && (
|
|
80
|
+
<>
|
|
81
|
+
<span className="flex-shrink-0 text-xxs uppercase tracking-wider font-normal text-warning-amber">
|
|
82
|
+
Needs Attention
|
|
83
|
+
</span>
|
|
84
|
+
<ScopeBadges groups={needsAttention} colorMap={columnColorMap} onClick={handleBadgeClick} />
|
|
85
|
+
</>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Spacer when no scopes */}
|
|
91
|
+
{!hasScopes && <div className="flex-1" />}
|
|
92
|
+
|
|
93
|
+
{/* Version badge — pinned right */}
|
|
94
|
+
<div className="flex-shrink-0 ml-4">
|
|
95
|
+
<VersionBadge />
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ScopeBadges({
|
|
103
|
+
groups,
|
|
104
|
+
colorMap,
|
|
105
|
+
onClick,
|
|
106
|
+
}: {
|
|
107
|
+
groups: Map<string, Scope[]>;
|
|
108
|
+
colorMap: Map<string, string>;
|
|
109
|
+
onClick: (e: React.MouseEvent, scopeId: number) => void;
|
|
110
|
+
}) {
|
|
111
|
+
return (
|
|
112
|
+
<>
|
|
113
|
+
{Array.from(groups.entries()).map(([status, scopeList]) => {
|
|
114
|
+
const color = colorMap.get(status) ?? '220 70% 50%';
|
|
115
|
+
const hex = hslToHex(color);
|
|
116
|
+
return scopeList.map((scope) => (
|
|
117
|
+
<button
|
|
118
|
+
key={scope.id}
|
|
119
|
+
onClick={(e) => onClick(e, scope.id)}
|
|
120
|
+
className="flex flex-shrink-0 items-center gap-1.5 rounded-md border px-2 py-0.5 text-xxs transition-colors hover:brightness-125 cursor-pointer"
|
|
121
|
+
style={{
|
|
122
|
+
backgroundColor: `${hex}15`,
|
|
123
|
+
borderColor: `${hex}40`,
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<span
|
|
127
|
+
className="h-2 w-2 rounded-full flex-shrink-0"
|
|
128
|
+
style={{ backgroundColor: `hsl(${color})` }}
|
|
129
|
+
/>
|
|
130
|
+
<span className="max-w-[120px] truncate">
|
|
131
|
+
{formatScopeId(scope.id)} {scope.title}
|
|
132
|
+
</span>
|
|
133
|
+
</button>
|
|
134
|
+
));
|
|
135
|
+
})}
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function groupByStatus(scopes: Scope[], columnOrder: Map<string, number>): Map<string, Scope[]> {
|
|
141
|
+
const map = new Map<string, Scope[]>();
|
|
142
|
+
for (const scope of scopes) {
|
|
143
|
+
const list = map.get(scope.status);
|
|
144
|
+
if (list) list.push(scope);
|
|
145
|
+
else map.set(scope.status, [scope]);
|
|
146
|
+
}
|
|
147
|
+
// Sort by column order
|
|
148
|
+
return new Map(
|
|
149
|
+
Array.from(map.entries()).sort(
|
|
150
|
+
([a], [b]) => (columnOrder.get(a) ?? 999) - (columnOrder.get(b) ?? 999)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function hslToHex(hsl: string): string {
|
|
156
|
+
const parts = hsl.match(/[\d.]+/g);
|
|
157
|
+
if (!parts || parts.length < 3) return '#888888';
|
|
158
|
+
const h = parseFloat(parts[0]);
|
|
159
|
+
const s = parseFloat(parts[1]) / 100;
|
|
160
|
+
const l = parseFloat(parts[2]) / 100;
|
|
161
|
+
const a = s * Math.min(l, 1 - l);
|
|
162
|
+
const f = (n: number) => {
|
|
163
|
+
const k = (n + h / 30) % 12;
|
|
164
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
165
|
+
return Math.round(255 * color).toString(16).padStart(2, '0');
|
|
166
|
+
};
|
|
167
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
168
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useDroppable } from '@dnd-kit/core';
|
|
2
|
+
import type { Scope, ScopeStatus, CardDisplayConfig } from '@/types';
|
|
3
|
+
import { ScopeCard } from './ScopeCard';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
interface SwimCellProps {
|
|
7
|
+
laneValue: string;
|
|
8
|
+
status: ScopeStatus;
|
|
9
|
+
scopes: Scope[];
|
|
10
|
+
onScopeClick?: (scope: Scope) => void;
|
|
11
|
+
cardDisplay?: CardDisplayConfig;
|
|
12
|
+
dimmedIds?: Set<number>;
|
|
13
|
+
isDragActive: boolean;
|
|
14
|
+
isValidDrop: boolean;
|
|
15
|
+
isCollapsed: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SwimCell({
|
|
19
|
+
laneValue,
|
|
20
|
+
status,
|
|
21
|
+
scopes = [],
|
|
22
|
+
onScopeClick,
|
|
23
|
+
cardDisplay,
|
|
24
|
+
dimmedIds,
|
|
25
|
+
isDragActive,
|
|
26
|
+
isValidDrop,
|
|
27
|
+
isCollapsed,
|
|
28
|
+
}: SwimCellProps) {
|
|
29
|
+
const droppableId = `swim::${laneValue}::${status}`;
|
|
30
|
+
const { setNodeRef, isOver } = useDroppable({ id: droppableId });
|
|
31
|
+
|
|
32
|
+
if (isCollapsed) return null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
ref={setNodeRef}
|
|
37
|
+
className={cn(
|
|
38
|
+
'swim-cell min-h-[48px] rounded border border-white/[0.04] p-1 transition-colors',
|
|
39
|
+
isDragActive && isOver && isValidDrop && 'ring-2 ring-green-500/60 border-green-500/40 bg-green-500/5',
|
|
40
|
+
isDragActive && isOver && !isValidDrop && 'ring-2 ring-red-500/50 border-red-500/30 bg-red-500/5',
|
|
41
|
+
isDragActive && !isOver && isValidDrop && 'border-green-500/20',
|
|
42
|
+
scopes.length === 0 && 'border-dashed border-white/[0.06]',
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<div className="space-y-1.5">
|
|
46
|
+
{scopes.filter((s) => !s.is_ghost).map((scope) => (
|
|
47
|
+
<ScopeCard
|
|
48
|
+
key={scope.id}
|
|
49
|
+
scope={scope}
|
|
50
|
+
onClick={onScopeClick}
|
|
51
|
+
cardDisplay={cardDisplay}
|
|
52
|
+
dimmed={dimmedIds?.has(scope.id)}
|
|
53
|
+
/>
|
|
54
|
+
))}
|
|
55
|
+
{scopes.filter((s) => s.is_ghost).map((scope) => (
|
|
56
|
+
<ScopeCard
|
|
57
|
+
key={scope.id}
|
|
58
|
+
scope={scope}
|
|
59
|
+
onClick={onScopeClick}
|
|
60
|
+
cardDisplay={cardDisplay}
|
|
61
|
+
dimmed={dimmedIds?.has(scope.id)}
|
|
62
|
+
/>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ChevronRight } from 'lucide-react';
|
|
2
|
+
import type { SwimLane } from '@/lib/swimlane';
|
|
3
|
+
import type { Scope, ScopeStatus, CardDisplayConfig, BoardColumn } from '@/types';
|
|
4
|
+
import { SwimCell } from './SwimCell';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
interface SwimLaneRowProps {
|
|
8
|
+
lane: SwimLane;
|
|
9
|
+
columns: BoardColumn[];
|
|
10
|
+
collapsedColumns: Set<string>;
|
|
11
|
+
isLaneCollapsed: boolean;
|
|
12
|
+
onToggleLane: () => void;
|
|
13
|
+
onScopeClick?: (scope: Scope) => void;
|
|
14
|
+
cardDisplay?: CardDisplayConfig;
|
|
15
|
+
dimmedIds?: Set<number>;
|
|
16
|
+
isDragActive: boolean;
|
|
17
|
+
validTargets: Set<ScopeStatus>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SwimLaneRow({
|
|
21
|
+
lane,
|
|
22
|
+
columns,
|
|
23
|
+
collapsedColumns,
|
|
24
|
+
isLaneCollapsed,
|
|
25
|
+
onToggleLane,
|
|
26
|
+
onScopeClick,
|
|
27
|
+
cardDisplay,
|
|
28
|
+
dimmedIds,
|
|
29
|
+
isDragActive,
|
|
30
|
+
validTargets,
|
|
31
|
+
}: SwimLaneRowProps) {
|
|
32
|
+
if (isLaneCollapsed) {
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{/* Lane label cell — collapsed */}
|
|
36
|
+
<button
|
|
37
|
+
onClick={onToggleLane}
|
|
38
|
+
className="swim-lane-header flex items-center gap-2 rounded-l px-3 py-1.5 text-left hover:bg-white/[0.04] transition-colors cursor-pointer sticky left-0 z-10 bg-background"
|
|
39
|
+
>
|
|
40
|
+
<div className={cn('h-full w-0.5 rounded-full shrink-0 self-stretch', lane.color)} />
|
|
41
|
+
<ChevronRight className="h-3 w-3 text-muted-foreground shrink-0" />
|
|
42
|
+
<span className="text-xxs font-medium text-muted-foreground truncate capitalize">
|
|
43
|
+
{lane.label}
|
|
44
|
+
</span>
|
|
45
|
+
<span className="ml-auto rounded-full bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground shrink-0">
|
|
46
|
+
{lane.count}
|
|
47
|
+
</span>
|
|
48
|
+
</button>
|
|
49
|
+
{/* Empty cells across columns */}
|
|
50
|
+
{columns.map((col) => (
|
|
51
|
+
<div key={col.id} className={cn(collapsedColumns.has(col.id) && 'hidden')} />
|
|
52
|
+
))}
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
{/* Lane label cell */}
|
|
60
|
+
<button
|
|
61
|
+
onClick={onToggleLane}
|
|
62
|
+
className="swim-lane-header flex items-start gap-2 rounded-l px-3 py-2 text-left hover:bg-white/[0.04] transition-colors cursor-pointer sticky left-0 z-10 bg-background"
|
|
63
|
+
>
|
|
64
|
+
<div className={cn('w-0.5 rounded-full shrink-0 min-h-[32px] self-stretch', lane.color)} />
|
|
65
|
+
<div className="flex flex-col gap-1 min-w-0">
|
|
66
|
+
<div className="flex items-center gap-1.5">
|
|
67
|
+
<ChevronRight className="h-3 w-3 text-muted-foreground shrink-0 rotate-90 transition-transform" />
|
|
68
|
+
<span className="swim-lane-label text-xxs font-medium text-foreground/80 truncate capitalize">
|
|
69
|
+
{lane.label}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
<span className="text-[10px] text-muted-foreground">
|
|
73
|
+
{lane.count} scope{lane.count !== 1 ? 's' : ''}
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
</button>
|
|
77
|
+
{/* Cells per column */}
|
|
78
|
+
{columns.map((col) => (
|
|
79
|
+
<SwimCell
|
|
80
|
+
key={col.id}
|
|
81
|
+
laneValue={lane.value}
|
|
82
|
+
status={col.id}
|
|
83
|
+
scopes={lane.cells[col.id]}
|
|
84
|
+
onScopeClick={onScopeClick}
|
|
85
|
+
cardDisplay={cardDisplay}
|
|
86
|
+
dimmedIds={dimmedIds}
|
|
87
|
+
isDragActive={isDragActive}
|
|
88
|
+
isValidDrop={validTargets.has(col.id)}
|
|
89
|
+
isCollapsed={collapsedColumns.has(col.id)}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Info } from 'lucide-react';
|
|
2
|
+
import type { SwimLane } from '@/lib/swimlane';
|
|
3
|
+
import type { Scope, ScopeStatus, CardDisplayConfig, BoardColumn, Sprint } from '@/types';
|
|
4
|
+
import { SwimLaneRow } from './SwimLaneRow';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import { useTheme } from '@/hooks/useTheme';
|
|
7
|
+
|
|
8
|
+
interface SwimlaneBoardViewProps {
|
|
9
|
+
lanes: SwimLane[];
|
|
10
|
+
columns: BoardColumn[];
|
|
11
|
+
collapsedColumns: Set<string>;
|
|
12
|
+
collapsedLanes: Set<string>;
|
|
13
|
+
onToggleLane: (laneValue: string) => void;
|
|
14
|
+
onToggleCollapse: (columnId: string) => void;
|
|
15
|
+
onScopeClick?: (scope: Scope) => void;
|
|
16
|
+
cardDisplay?: CardDisplayConfig;
|
|
17
|
+
dimmedIds?: Set<number>;
|
|
18
|
+
isDragActive: boolean;
|
|
19
|
+
validTargets: Set<ScopeStatus>;
|
|
20
|
+
sprints: Sprint[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SwimlaneBoardView({
|
|
24
|
+
lanes,
|
|
25
|
+
columns,
|
|
26
|
+
collapsedColumns,
|
|
27
|
+
collapsedLanes,
|
|
28
|
+
onToggleLane,
|
|
29
|
+
onToggleCollapse,
|
|
30
|
+
onScopeClick,
|
|
31
|
+
cardDisplay,
|
|
32
|
+
dimmedIds,
|
|
33
|
+
isDragActive,
|
|
34
|
+
validTargets,
|
|
35
|
+
sprints,
|
|
36
|
+
}: SwimlaneBoardViewProps) {
|
|
37
|
+
const { neonGlass } = useTheme();
|
|
38
|
+
|
|
39
|
+
// Filter out collapsed columns for grid sizing
|
|
40
|
+
const visibleColumns = columns.filter((c) => !collapsedColumns.has(c.id));
|
|
41
|
+
const gridTemplateColumns = `140px ${visibleColumns.map(() => '200px').join(' ')}`;
|
|
42
|
+
|
|
43
|
+
const hasActiveSprints = sprints.some((s) =>
|
|
44
|
+
s.group_type === 'sprint' || (s.group_type === 'batch' && s.status !== 'completed')
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
49
|
+
{/* Sprint info banner */}
|
|
50
|
+
{hasActiveSprints && (
|
|
51
|
+
<div className="mb-2 flex items-center gap-2 rounded border border-primary/20 bg-primary/5 px-3 py-1.5 text-xs text-muted-foreground">
|
|
52
|
+
<Info className="h-3.5 w-3.5 text-primary shrink-0" />
|
|
53
|
+
Sprint groups are hidden in swimlane view
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
<div
|
|
58
|
+
className="grid gap-px pb-4"
|
|
59
|
+
style={{ gridTemplateColumns, width: 'max-content' }}
|
|
60
|
+
>
|
|
61
|
+
{/* Column headers — sticky top row */}
|
|
62
|
+
<div className="sticky top-0 z-20 bg-background" />
|
|
63
|
+
{visibleColumns.map((col) => (
|
|
64
|
+
<button
|
|
65
|
+
key={col.id}
|
|
66
|
+
onClick={() => onToggleCollapse(col.id)}
|
|
67
|
+
className={cn(
|
|
68
|
+
'sticky top-0 z-20 flex items-center gap-1.5 rounded-t border-b border-white/[0.06] bg-background px-2 py-1.5 text-left cursor-pointer hover:bg-white/[0.03] transition-colors',
|
|
69
|
+
neonGlass && 'border-b-white/[0.08]',
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
<div className={cn('h-2 w-2 rounded-full shrink-0', neonGlass && 'animate-glow-pulse')} style={{ backgroundColor: `hsl(${col.color})` }} />
|
|
73
|
+
<span className="text-xxs uppercase tracking-wider font-normal text-muted-foreground truncate">
|
|
74
|
+
{col.label}
|
|
75
|
+
</span>
|
|
76
|
+
</button>
|
|
77
|
+
))}
|
|
78
|
+
|
|
79
|
+
{/* Lane rows */}
|
|
80
|
+
{lanes.map((lane) => (
|
|
81
|
+
<SwimLaneRow
|
|
82
|
+
key={lane.value}
|
|
83
|
+
lane={lane}
|
|
84
|
+
columns={visibleColumns}
|
|
85
|
+
collapsedColumns={collapsedColumns}
|
|
86
|
+
isLaneCollapsed={collapsedLanes.has(lane.value)}
|
|
87
|
+
onToggleLane={() => onToggleLane(lane.value)}
|
|
88
|
+
onScopeClick={onScopeClick}
|
|
89
|
+
cardDisplay={cardDisplay}
|
|
90
|
+
dimmedIds={dimmedIds}
|
|
91
|
+
isDragActive={isDragActive}
|
|
92
|
+
validTargets={validTargets}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
|
|
96
|
+
{/* Empty state */}
|
|
97
|
+
{lanes.length === 0 && (
|
|
98
|
+
<>
|
|
99
|
+
<div />
|
|
100
|
+
<div className={cn('col-span-full py-12 text-center text-xs text-muted-foreground')}>
|
|
101
|
+
No scopes to display
|
|
102
|
+
</div>
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|