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,190 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { Trash2, Check, X, Sparkles } from 'lucide-react';
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogHeader,
|
|
7
|
+
} from '@/components/ui/dialog';
|
|
8
|
+
import { Button } from '@/components/ui/button';
|
|
9
|
+
import type { Scope } from '@/types';
|
|
10
|
+
|
|
11
|
+
interface IdeaDetailModalProps {
|
|
12
|
+
scope: Scope | null;
|
|
13
|
+
open: boolean;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
onDelete: (id: number) => void;
|
|
16
|
+
onApprove: (id: number) => void;
|
|
17
|
+
onReject: (id: number) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function IdeaDetailModal({ scope, open, onClose, onDelete, onApprove, onReject }: IdeaDetailModalProps) {
|
|
21
|
+
const [title, setTitle] = useState('');
|
|
22
|
+
const [description, setDescription] = useState('');
|
|
23
|
+
const [savedTitle, setSavedTitle] = useState('');
|
|
24
|
+
const [savedDescription, setSavedDescription] = useState('');
|
|
25
|
+
const [saving, setSaving] = useState(false);
|
|
26
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
27
|
+
const titleRef = useRef<HTMLInputElement>(null);
|
|
28
|
+
|
|
29
|
+
const isGhost = !!scope?.is_ghost;
|
|
30
|
+
const isDirty = title !== savedTitle || description !== savedDescription;
|
|
31
|
+
|
|
32
|
+
// Sync state when scope changes
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (scope && open) {
|
|
35
|
+
const t = scope.title ?? '';
|
|
36
|
+
const d = scope.raw_content ?? '';
|
|
37
|
+
setTitle(t);
|
|
38
|
+
setDescription(d);
|
|
39
|
+
setSavedTitle(t);
|
|
40
|
+
setSavedDescription(d);
|
|
41
|
+
setConfirmDelete(false);
|
|
42
|
+
if (!isGhost) setTimeout(() => titleRef.current?.focus(), 100);
|
|
43
|
+
}
|
|
44
|
+
}, [scope?.id, open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
45
|
+
|
|
46
|
+
const save = useCallback(async () => {
|
|
47
|
+
if (!scope || !isDirty || saving || isGhost) return;
|
|
48
|
+
setSaving(true);
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`/api/orbital/ideas/${scope.id}`, {
|
|
51
|
+
method: 'PATCH',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ title, description }),
|
|
54
|
+
});
|
|
55
|
+
if (res.ok) {
|
|
56
|
+
setSavedTitle(title);
|
|
57
|
+
setSavedDescription(description);
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
setSaving(false);
|
|
61
|
+
}
|
|
62
|
+
}, [scope, title, description, isDirty, saving, isGhost]);
|
|
63
|
+
|
|
64
|
+
// Auto-save every 10s when dirty (not for ghosts)
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!isDirty || !open || isGhost) return;
|
|
67
|
+
const timer = setInterval(() => { save(); }, 10_000);
|
|
68
|
+
return () => clearInterval(timer);
|
|
69
|
+
}, [isDirty, open, save, isGhost]);
|
|
70
|
+
|
|
71
|
+
function handleClose() {
|
|
72
|
+
if (isDirty && !isGhost) {
|
|
73
|
+
if (window.confirm('Save changes before closing?')) {
|
|
74
|
+
save().then(onClose);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
onClose();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleDelete() {
|
|
82
|
+
if (!confirmDelete) {
|
|
83
|
+
setConfirmDelete(true);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (scope) onDelete(scope.id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!scope) return null;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) handleClose(); }}>
|
|
93
|
+
<DialogContent className="max-w-md p-0 gap-0 flex flex-col max-h-[70vh]">
|
|
94
|
+
{/* Header */}
|
|
95
|
+
<DialogHeader className="px-4 pt-3 pb-2">
|
|
96
|
+
<div className="flex items-center gap-2 pr-8">
|
|
97
|
+
<div className="flex-1 min-w-0">
|
|
98
|
+
{isGhost ? (
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<Sparkles className="h-3.5 w-3.5 text-purple-400 shrink-0" />
|
|
101
|
+
<span className="text-sm font-normal text-foreground truncate">{title}</span>
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<input
|
|
105
|
+
ref={titleRef}
|
|
106
|
+
className="w-full bg-transparent text-sm font-normal text-foreground border-none focus:outline-none focus:ring-0 placeholder:text-muted-foreground"
|
|
107
|
+
placeholder="Idea title..."
|
|
108
|
+
value={title}
|
|
109
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
{isGhost ? (
|
|
114
|
+
<span className="shrink-0 rounded border border-purple-500/30 bg-purple-500/10 px-1.5 py-0.5 text-[10px] text-purple-400 uppercase">
|
|
115
|
+
ai suggestion
|
|
116
|
+
</span>
|
|
117
|
+
) : confirmDelete ? (
|
|
118
|
+
<div className="flex items-center gap-1">
|
|
119
|
+
<Button size="sm" variant="destructive" className="h-6 text-xxs" onClick={handleDelete}>
|
|
120
|
+
Confirm
|
|
121
|
+
</Button>
|
|
122
|
+
<Button size="sm" variant="ghost" className="h-6 text-xxs" onClick={() => setConfirmDelete(false)}>
|
|
123
|
+
No
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
) : (
|
|
127
|
+
<Button
|
|
128
|
+
variant="ghost"
|
|
129
|
+
size="icon"
|
|
130
|
+
className="h-6 w-6 shrink-0 text-muted-foreground hover:text-destructive"
|
|
131
|
+
onClick={handleDelete}
|
|
132
|
+
title="Delete idea"
|
|
133
|
+
>
|
|
134
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
135
|
+
</Button>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</DialogHeader>
|
|
139
|
+
|
|
140
|
+
{/* Description */}
|
|
141
|
+
<div className="flex-1 min-h-0 px-4 pb-4">
|
|
142
|
+
{isGhost ? (
|
|
143
|
+
<div className="w-full min-h-[200px] rounded bg-muted/20 px-3 py-2.5 text-xs text-foreground/80 border border-border/50 whitespace-pre-wrap">
|
|
144
|
+
{description || <span className="text-muted-foreground italic">No description</span>}
|
|
145
|
+
</div>
|
|
146
|
+
) : (
|
|
147
|
+
<textarea
|
|
148
|
+
className="w-full h-full min-h-[200px] rounded bg-muted/30 px-3 py-2.5 text-xs text-foreground border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 resize-none placeholder:text-muted-foreground"
|
|
149
|
+
placeholder="Describe the idea... What problem does it solve? Any notes on approach?"
|
|
150
|
+
value={description}
|
|
151
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Footer */}
|
|
157
|
+
{isGhost ? (
|
|
158
|
+
<div className="px-4 pb-3 flex items-center gap-2">
|
|
159
|
+
<Button
|
|
160
|
+
size="sm"
|
|
161
|
+
className="flex-1 bg-green-600/20 border border-green-500/30 text-green-400 hover:bg-green-600/30 hover:text-green-300"
|
|
162
|
+
onClick={() => onApprove(scope.id)}
|
|
163
|
+
>
|
|
164
|
+
<Check className="h-3.5 w-3.5 mr-1.5" />
|
|
165
|
+
Approve
|
|
166
|
+
</Button>
|
|
167
|
+
<Button
|
|
168
|
+
size="sm"
|
|
169
|
+
variant="ghost"
|
|
170
|
+
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
|
171
|
+
onClick={() => onReject(scope.id)}
|
|
172
|
+
>
|
|
173
|
+
<X className="h-3.5 w-3.5 mr-1.5" />
|
|
174
|
+
Reject
|
|
175
|
+
</Button>
|
|
176
|
+
</div>
|
|
177
|
+
) : (
|
|
178
|
+
<div className="px-4 pb-3 flex items-center justify-between text-xxs text-muted-foreground">
|
|
179
|
+
<span>
|
|
180
|
+
{saving ? 'Saving...' : isDirty ? 'Unsaved changes' : 'Saved'}
|
|
181
|
+
</span>
|
|
182
|
+
<Button size="sm" variant="ghost" className="h-6" onClick={() => save()} disabled={!isDirty || saving}>
|
|
183
|
+
Save
|
|
184
|
+
</Button>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</DialogContent>
|
|
188
|
+
</Dialog>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Sparkles, Loader2 } from 'lucide-react';
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from '@/components/ui/dialog';
|
|
10
|
+
import { Button } from '@/components/ui/button';
|
|
11
|
+
|
|
12
|
+
interface IdeaFormDialogProps {
|
|
13
|
+
open: boolean;
|
|
14
|
+
loading: boolean;
|
|
15
|
+
onSubmit: (title: string, description: string) => void;
|
|
16
|
+
onCancel: () => void;
|
|
17
|
+
onSurprise: () => void;
|
|
18
|
+
surpriseLoading: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function IdeaFormDialog({ open, loading, onSubmit, onCancel, onSurprise, surpriseLoading }: IdeaFormDialogProps) {
|
|
22
|
+
const [title, setTitle] = useState('');
|
|
23
|
+
const [description, setDescription] = useState('');
|
|
24
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
25
|
+
|
|
26
|
+
// Reset fields and focus on open
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (open) {
|
|
29
|
+
setTitle('');
|
|
30
|
+
setDescription('');
|
|
31
|
+
setTimeout(() => inputRef.current?.focus(), 100);
|
|
32
|
+
}
|
|
33
|
+
}, [open]);
|
|
34
|
+
|
|
35
|
+
function handleSubmit() {
|
|
36
|
+
if (!title.trim()) return;
|
|
37
|
+
onSubmit(title.trim(), description.trim());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handleKeyDown(e: React.KeyboardEvent) {
|
|
41
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
handleSubmit();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) onCancel(); }}>
|
|
49
|
+
<DialogContent className="max-w-lg p-5 gap-0" onKeyDown={handleKeyDown}>
|
|
50
|
+
<DialogHeader className="mb-4">
|
|
51
|
+
<DialogTitle className="text-sm font-normal">New Idea</DialogTitle>
|
|
52
|
+
<DialogDescription className="text-xxs text-muted-foreground">
|
|
53
|
+
Capture a feature idea for the icebox
|
|
54
|
+
</DialogDescription>
|
|
55
|
+
</DialogHeader>
|
|
56
|
+
|
|
57
|
+
<input
|
|
58
|
+
ref={inputRef}
|
|
59
|
+
className="mb-3 w-full rounded bg-muted/50 px-3 py-2 text-sm text-foreground border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 placeholder:text-muted-foreground"
|
|
60
|
+
placeholder="Feature name..."
|
|
61
|
+
value={title}
|
|
62
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
<textarea
|
|
66
|
+
className="mb-4 w-full rounded bg-muted/50 px-3 py-2.5 text-xs text-foreground border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 resize-y placeholder:text-muted-foreground"
|
|
67
|
+
placeholder="Describe the idea... What problem does it solve? Any notes on approach?"
|
|
68
|
+
rows={6}
|
|
69
|
+
value={description}
|
|
70
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
<Button size="sm" onClick={handleSubmit} disabled={loading || !title.trim()} className="flex-1">
|
|
75
|
+
{loading ? 'Creating...' : 'Create'}
|
|
76
|
+
</Button>
|
|
77
|
+
<Button size="sm" variant="ghost" onClick={onCancel} disabled={loading}>
|
|
78
|
+
Cancel
|
|
79
|
+
</Button>
|
|
80
|
+
<span className="ml-auto text-[10px] text-muted-foreground/50">
|
|
81
|
+
{'\u2318'}+Enter
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Surprise Me */}
|
|
86
|
+
<div className="mt-4 pt-4 border-t border-border">
|
|
87
|
+
<Button
|
|
88
|
+
size="sm"
|
|
89
|
+
variant="outline"
|
|
90
|
+
className="w-full text-purple-400 border-purple-500/30 hover:bg-purple-500/10 hover:border-purple-500/50"
|
|
91
|
+
onClick={onSurprise}
|
|
92
|
+
disabled={surpriseLoading}
|
|
93
|
+
>
|
|
94
|
+
{surpriseLoading ? (
|
|
95
|
+
<>
|
|
96
|
+
<Loader2 className="h-3.5 w-3.5 mr-2 animate-spin" />
|
|
97
|
+
Generating ideas...
|
|
98
|
+
</>
|
|
99
|
+
) : (
|
|
100
|
+
<>
|
|
101
|
+
<Sparkles className="h-3.5 w-3.5 mr-2" />
|
|
102
|
+
Surprise Me
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
</Button>
|
|
106
|
+
<p className="mt-1.5 text-center text-[10px] text-muted-foreground">
|
|
107
|
+
AI analyzes the codebase and suggests feature ideas
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
</DialogContent>
|
|
111
|
+
</Dialog>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { useDroppable } from '@dnd-kit/core';
|
|
2
|
+
import type { Scope, ScopeStatus, Sprint, CardDisplayConfig } from '@/types';
|
|
3
|
+
import type { SortField, SortDirection } from '@/hooks/useBoardSettings';
|
|
4
|
+
import { ScopeCard } from './ScopeCard';
|
|
5
|
+
import { SprintContainer } from './SprintContainer';
|
|
6
|
+
import { ColumnMenu } from './ColumnMenu';
|
|
7
|
+
import { useTheme } from '@/hooks/useTheme';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
|
|
10
|
+
interface KanbanColumnProps {
|
|
11
|
+
id: ScopeStatus;
|
|
12
|
+
label: string;
|
|
13
|
+
color: string;
|
|
14
|
+
scopes: Scope[];
|
|
15
|
+
/** Sprints to render in this column (assembling in Ready, active in Implementing) */
|
|
16
|
+
sprints?: Sprint[];
|
|
17
|
+
scopeLookup?: Map<number, Scope>;
|
|
18
|
+
/** Global set of scope IDs in active groups across ALL columns (cross-column dedup) */
|
|
19
|
+
globalSprintScopeIds?: Set<number>;
|
|
20
|
+
onScopeClick?: (scope: Scope) => void;
|
|
21
|
+
onDeleteSprint?: (id: number) => void;
|
|
22
|
+
onDispatchSprint?: (id: number) => void;
|
|
23
|
+
onRenameSprint?: (id: number, name: string) => void;
|
|
24
|
+
/** ID of a sprint that was just created and should start with name editing */
|
|
25
|
+
editingSprintId?: number | null;
|
|
26
|
+
onSprintEditingDone?: () => void;
|
|
27
|
+
isValidDrop?: boolean;
|
|
28
|
+
isDragActive?: boolean;
|
|
29
|
+
headerExtra?: React.ReactNode;
|
|
30
|
+
collapsed?: boolean;
|
|
31
|
+
onToggleCollapse?: () => void;
|
|
32
|
+
sortField?: SortField;
|
|
33
|
+
sortDirection?: SortDirection;
|
|
34
|
+
onSetSort?: (field: SortField) => void;
|
|
35
|
+
cardDisplay?: CardDisplayConfig;
|
|
36
|
+
dimmedIds?: Set<number>;
|
|
37
|
+
onAddAllToSprint?: (sprintId: number, scopeIds: number[]) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function KanbanColumn({
|
|
41
|
+
id,
|
|
42
|
+
label,
|
|
43
|
+
color,
|
|
44
|
+
scopes,
|
|
45
|
+
sprints = [],
|
|
46
|
+
scopeLookup = new Map(),
|
|
47
|
+
globalSprintScopeIds,
|
|
48
|
+
onScopeClick,
|
|
49
|
+
onDeleteSprint,
|
|
50
|
+
onDispatchSprint,
|
|
51
|
+
onRenameSprint,
|
|
52
|
+
editingSprintId,
|
|
53
|
+
onSprintEditingDone,
|
|
54
|
+
isValidDrop,
|
|
55
|
+
isDragActive,
|
|
56
|
+
headerExtra,
|
|
57
|
+
collapsed,
|
|
58
|
+
onToggleCollapse,
|
|
59
|
+
sortField,
|
|
60
|
+
sortDirection,
|
|
61
|
+
onSetSort,
|
|
62
|
+
cardDisplay,
|
|
63
|
+
dimmedIds,
|
|
64
|
+
onAddAllToSprint,
|
|
65
|
+
}: KanbanColumnProps) {
|
|
66
|
+
const { neonGlass } = useTheme();
|
|
67
|
+
const { setNodeRef, isOver } = useDroppable({ id });
|
|
68
|
+
|
|
69
|
+
// Scopes that are in a sprint/batch should not appear as loose cards.
|
|
70
|
+
// Use the global set (cross-column dedup) if available, otherwise fall back to local.
|
|
71
|
+
const sprintScopeIds = globalSprintScopeIds ?? new Set(sprints.flatMap((s) => s.scope_ids));
|
|
72
|
+
const looseScopes = scopes.filter((s) => !sprintScopeIds.has(s.id));
|
|
73
|
+
const looseScopeIds = looseScopes.filter((s) => !s.is_ghost).map((s) => s.id);
|
|
74
|
+
const totalCount = scopes.length;
|
|
75
|
+
|
|
76
|
+
const showCollapsed = collapsed;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
ref={setNodeRef}
|
|
81
|
+
className={cn(
|
|
82
|
+
'flex h-full flex-shrink-0 flex-col rounded border bg-card/50 overflow-hidden transition-[width] duration-300 ease-in-out',
|
|
83
|
+
showCollapsed ? 'w-10 cursor-pointer items-center' : 'w-72',
|
|
84
|
+
neonGlass && 'card-glass neon-border-blue',
|
|
85
|
+
isDragActive && isOver && isValidDrop && 'ring-2 ring-green-500/60 border-green-500/40 bg-green-500/5',
|
|
86
|
+
isDragActive && isOver && !isValidDrop && 'ring-2 ring-red-500/50 border-red-500/30 bg-red-500/5',
|
|
87
|
+
isDragActive && !isOver && isValidDrop && 'border-green-500/20',
|
|
88
|
+
)}
|
|
89
|
+
onClick={showCollapsed ? onToggleCollapse : undefined}
|
|
90
|
+
>
|
|
91
|
+
{showCollapsed ? (
|
|
92
|
+
<>
|
|
93
|
+
{/* Menu at top */}
|
|
94
|
+
<div className="flex items-center justify-center py-1.5" onClick={(e) => e.stopPropagation()}>
|
|
95
|
+
{sortField && sortDirection && onSetSort && onToggleCollapse && (
|
|
96
|
+
<ColumnMenu
|
|
97
|
+
sortField={sortField}
|
|
98
|
+
sortDirection={sortDirection}
|
|
99
|
+
onSetSort={onSetSort}
|
|
100
|
+
collapsed
|
|
101
|
+
onToggleCollapse={onToggleCollapse}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Rotated label */}
|
|
107
|
+
{/* Rotated label */}
|
|
108
|
+
<div className="flex items-start justify-center overflow-hidden pt-2">
|
|
109
|
+
<div className="flex items-center gap-2 [writing-mode:vertical-lr]">
|
|
110
|
+
<div className={cn('h-2.5 w-2.5 rounded-full shrink-0', neonGlass && 'animate-glow-pulse')} style={{ backgroundColor: `hsl(${color})` }} />
|
|
111
|
+
<span className="text-xxs uppercase tracking-wider font-normal text-muted-foreground whitespace-nowrap">
|
|
112
|
+
{label}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Count badge — upright, below label */}
|
|
118
|
+
<span className="mt-2 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-normal text-muted-foreground">
|
|
119
|
+
{totalCount}
|
|
120
|
+
</span>
|
|
121
|
+
</>
|
|
122
|
+
) : (
|
|
123
|
+
<>
|
|
124
|
+
{/* Column header — click to collapse */}
|
|
125
|
+
<div className="flex items-center gap-2 border-b px-2.5 py-1.5 cursor-pointer" onClick={onToggleCollapse}>
|
|
126
|
+
<div className={cn('h-2.5 w-2.5 rounded-full', neonGlass && 'animate-glow-pulse')} style={{ backgroundColor: `hsl(${color})` }} />
|
|
127
|
+
<h2 className="text-xxs uppercase tracking-wider font-normal text-muted-foreground">{label}</h2>
|
|
128
|
+
<span className="ml-auto rounded-full bg-muted px-2 py-0.5 text-xs font-normal text-muted-foreground">
|
|
129
|
+
{totalCount}
|
|
130
|
+
</span>
|
|
131
|
+
{headerExtra && <span onClick={(e) => e.stopPropagation()}>{headerExtra}</span>}
|
|
132
|
+
{sortField && sortDirection && onSetSort && onToggleCollapse && (
|
|
133
|
+
<span onClick={(e) => e.stopPropagation()}>
|
|
134
|
+
<ColumnMenu
|
|
135
|
+
sortField={sortField}
|
|
136
|
+
sortDirection={sortDirection}
|
|
137
|
+
onSetSort={onSetSort}
|
|
138
|
+
collapsed={false}
|
|
139
|
+
onToggleCollapse={onToggleCollapse}
|
|
140
|
+
/>
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Cards */}
|
|
146
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-2">
|
|
147
|
+
<div className="space-y-1.5">
|
|
148
|
+
{sprints.map((sprint) => (
|
|
149
|
+
<SprintContainer
|
|
150
|
+
key={`sprint-${sprint.id}`}
|
|
151
|
+
sprint={sprint}
|
|
152
|
+
scopeLookup={scopeLookup}
|
|
153
|
+
onDelete={onDeleteSprint}
|
|
154
|
+
onDispatch={onDispatchSprint}
|
|
155
|
+
onRename={onRenameSprint}
|
|
156
|
+
onScopeClick={onScopeClick}
|
|
157
|
+
cardDisplay={cardDisplay}
|
|
158
|
+
dimmedIds={dimmedIds}
|
|
159
|
+
editingName={sprint.id === editingSprintId}
|
|
160
|
+
onEditingDone={onSprintEditingDone}
|
|
161
|
+
looseCount={sprint.status === 'assembling' ? looseScopeIds.length : 0}
|
|
162
|
+
onAddAll={sprint.status === 'assembling' && onAddAllToSprint
|
|
163
|
+
? (sprintId) => onAddAllToSprint(sprintId, looseScopeIds)
|
|
164
|
+
: undefined
|
|
165
|
+
}
|
|
166
|
+
/>
|
|
167
|
+
))}
|
|
168
|
+
|
|
169
|
+
{looseScopes.filter((s) => !s.is_ghost).map((scope) => (
|
|
170
|
+
<ScopeCard
|
|
171
|
+
key={scope.id}
|
|
172
|
+
scope={scope}
|
|
173
|
+
onClick={onScopeClick}
|
|
174
|
+
cardDisplay={cardDisplay}
|
|
175
|
+
dimmed={dimmedIds?.has(scope.id)}
|
|
176
|
+
/>
|
|
177
|
+
))}
|
|
178
|
+
{looseScopes.some((s) => s.is_ghost) && looseScopes.some((s) => !s.is_ghost) && (
|
|
179
|
+
<div className="my-2 border-t border-dashed border-purple-500/20" />
|
|
180
|
+
)}
|
|
181
|
+
{looseScopes.filter((s) => s.is_ghost).map((scope) => (
|
|
182
|
+
<ScopeCard
|
|
183
|
+
key={scope.id}
|
|
184
|
+
scope={scope}
|
|
185
|
+
onClick={onScopeClick}
|
|
186
|
+
cardDisplay={cardDisplay}
|
|
187
|
+
dimmed={dimmedIds?.has(scope.id)}
|
|
188
|
+
/>
|
|
189
|
+
))}
|
|
190
|
+
{totalCount === 0 && isDragActive && isOver && isValidDrop && (
|
|
191
|
+
<p className="py-8 text-center text-xs text-muted-foreground/50">
|
|
192
|
+
Drop here
|
|
193
|
+
</p>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import ReactMarkdown from 'react-markdown';
|
|
2
|
+
import remarkGfm from 'remark-gfm';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
interface MarkdownRendererProps {
|
|
6
|
+
content: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function MarkdownRenderer({ content, className }: MarkdownRendererProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className={cn(
|
|
13
|
+
'space-y-4 text-[13px] leading-[1.7]',
|
|
14
|
+
className,
|
|
15
|
+
)} style={{ fontFamily: "'Inter', system-ui, sans-serif" }}>
|
|
16
|
+
<ReactMarkdown
|
|
17
|
+
remarkPlugins={[remarkGfm]}
|
|
18
|
+
components={{
|
|
19
|
+
h1: ({ children }) => (
|
|
20
|
+
<h1 className="text-base font-semibold text-foreground border-b border-border pb-2 mb-3">{children}</h1>
|
|
21
|
+
),
|
|
22
|
+
h2: ({ children }) => (
|
|
23
|
+
<h2 className="text-sm font-semibold text-foreground mt-6 mb-2 border-b border-border/50 pb-1.5">{children}</h2>
|
|
24
|
+
),
|
|
25
|
+
h3: ({ children }) => (
|
|
26
|
+
<h3 className="text-[13px] font-semibold text-foreground mt-5 mb-1.5">{children}</h3>
|
|
27
|
+
),
|
|
28
|
+
h4: ({ children }) => (
|
|
29
|
+
<h4 className="text-[13px] font-medium text-foreground/90 mt-4 mb-1">{children}</h4>
|
|
30
|
+
),
|
|
31
|
+
p: ({ children }) => (
|
|
32
|
+
<p className="text-foreground/70 mb-2">{children}</p>
|
|
33
|
+
),
|
|
34
|
+
a: ({ href, children }) => (
|
|
35
|
+
<a href={href} className="text-accent-blue hover:text-accent-blue/80 underline underline-offset-2" target="_blank" rel="noopener noreferrer">
|
|
36
|
+
{children}
|
|
37
|
+
</a>
|
|
38
|
+
),
|
|
39
|
+
ul: ({ children }) => (
|
|
40
|
+
<ul className="ml-5 space-y-1.5 list-disc text-foreground/70 marker:text-muted-foreground">{children}</ul>
|
|
41
|
+
),
|
|
42
|
+
ol: ({ children }) => (
|
|
43
|
+
<ol className="ml-5 space-y-1.5 list-decimal text-foreground/70 marker:text-muted-foreground">{children}</ol>
|
|
44
|
+
),
|
|
45
|
+
li: ({ children, ...props }) => {
|
|
46
|
+
const checkbox = props.node?.properties?.className;
|
|
47
|
+
if (checkbox && String(checkbox).includes('task-list-item')) {
|
|
48
|
+
return <li className="list-none ml-[-1.25rem] flex items-start gap-2">{children}</li>;
|
|
49
|
+
}
|
|
50
|
+
return <li className="pl-1">{children}</li>;
|
|
51
|
+
},
|
|
52
|
+
input: ({ checked }) => (
|
|
53
|
+
<input
|
|
54
|
+
type="checkbox"
|
|
55
|
+
checked={checked}
|
|
56
|
+
readOnly
|
|
57
|
+
className="mt-1 h-3.5 w-3.5 rounded border-border accent-primary"
|
|
58
|
+
/>
|
|
59
|
+
),
|
|
60
|
+
code: ({ children, className: codeClassName }) => {
|
|
61
|
+
const isBlock = codeClassName?.startsWith('language-');
|
|
62
|
+
if (isBlock) {
|
|
63
|
+
return (
|
|
64
|
+
<code className={cn(
|
|
65
|
+
'block rounded p-3 font-mono text-xxs overflow-x-auto',
|
|
66
|
+
'bg-[#0d0d14] border border-border/50',
|
|
67
|
+
codeClassName,
|
|
68
|
+
)}>
|
|
69
|
+
{children}
|
|
70
|
+
</code>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return (
|
|
74
|
+
<code className="bg-[#0d0d14] border border-border/30 rounded px-1.5 py-0.5 font-mono text-xxs text-accent-blue/90">
|
|
75
|
+
{children}
|
|
76
|
+
</code>
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
pre: ({ children }) => (
|
|
80
|
+
<pre className="overflow-x-auto my-3">{children}</pre>
|
|
81
|
+
),
|
|
82
|
+
blockquote: ({ children }) => (
|
|
83
|
+
<blockquote className="border-l-2 border-accent-blue/40 pl-4 py-1 text-foreground/60 italic bg-surface/50 rounded-r">
|
|
84
|
+
{children}
|
|
85
|
+
</blockquote>
|
|
86
|
+
),
|
|
87
|
+
table: ({ children }) => (
|
|
88
|
+
<div className="overflow-x-auto my-3 rounded border border-border">
|
|
89
|
+
<table className="w-full text-xs">{children}</table>
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
thead: ({ children }) => (
|
|
93
|
+
<thead className="bg-[#0d0d14] text-muted-foreground">{children}</thead>
|
|
94
|
+
),
|
|
95
|
+
tr: ({ children }) => (
|
|
96
|
+
<tr className="border-b border-border/50 even:bg-surface/30">{children}</tr>
|
|
97
|
+
),
|
|
98
|
+
th: ({ children }) => (
|
|
99
|
+
<th className="px-3 py-2 text-left text-xxs font-medium uppercase tracking-wider">{children}</th>
|
|
100
|
+
),
|
|
101
|
+
td: ({ children }) => (
|
|
102
|
+
<td className="px-3 py-2 text-foreground/70">{children}</td>
|
|
103
|
+
),
|
|
104
|
+
hr: () => <hr className="border-border/50 my-4" />,
|
|
105
|
+
strong: ({ children }) => (
|
|
106
|
+
<strong className="font-semibold text-foreground">{children}</strong>
|
|
107
|
+
),
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{content}
|
|
111
|
+
</ReactMarkdown>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|