jettypod 4.4.118 → 4.4.121
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/.env +4 -3
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +228 -80
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
|
|
2
|
+
import { useState, useRef, useCallback, memo } from 'react';
|
|
3
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
4
|
+
import type { WorkItem } from '@/lib/db';
|
|
5
|
+
import { prefetch } from '@/lib/data-bridge';
|
|
6
|
+
import { EditableTitle } from './EditableTitle';
|
|
7
|
+
import { CardMenu } from './CardMenu';
|
|
8
|
+
import { CopyableId } from './CopyableId';
|
|
9
|
+
import { WaveCompletionAnimation } from './WaveCompletionAnimation';
|
|
10
|
+
import { Input } from '@/components/ui/Input';
|
|
11
|
+
import { Button } from '@/components/ui/Button';
|
|
12
|
+
import { MODE_LABELS } from '@/lib/constants';
|
|
13
|
+
import { TypeIcon } from './TypeIcon';
|
|
14
|
+
import { shadow } from '@/lib/shadows';
|
|
15
|
+
|
|
16
|
+
function formatRejectionTime(isoString: string): string {
|
|
17
|
+
try {
|
|
18
|
+
const date = new Date(isoString);
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const diffMs = now.getTime() - date.getTime();
|
|
21
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
22
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
23
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
24
|
+
|
|
25
|
+
if (diffMins < 1) return 'just now';
|
|
26
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
27
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
28
|
+
if (diffDays === 1) return 'yesterday';
|
|
29
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
30
|
+
return date.toLocaleDateString();
|
|
31
|
+
} catch {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getModeLabel(item: WorkItem): string {
|
|
37
|
+
if (!item.mode) return '';
|
|
38
|
+
const base = MODE_LABELS[item.mode]?.label || item.mode;
|
|
39
|
+
if (item.current_step && item.total_steps) {
|
|
40
|
+
return `${base} ${item.current_step}/${item.total_steps}`;
|
|
41
|
+
}
|
|
42
|
+
return base;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Session indicator icon - shows when item has an active Claude session
|
|
46
|
+
function SessionIndicatorIcon({ className }: { className?: string }) {
|
|
47
|
+
return (
|
|
48
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
49
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface RejectionHistoryEntry {
|
|
55
|
+
round: number;
|
|
56
|
+
reason: string;
|
|
57
|
+
at: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function RejectionTimeline({ item, allChores, allBugs, isDone }: { item: WorkItem; allChores: WorkItem[]; allBugs: WorkItem[]; isDone: boolean }) {
|
|
61
|
+
let history: RejectionHistoryEntry[] = [];
|
|
62
|
+
try {
|
|
63
|
+
history = item.rejection_history ? JSON.parse(item.rejection_history) : [];
|
|
64
|
+
} catch {
|
|
65
|
+
// Corrupt JSON — degrade gracefully, show segments without reasons
|
|
66
|
+
}
|
|
67
|
+
const allChildren = [...allChores, ...allBugs];
|
|
68
|
+
|
|
69
|
+
// Build timeline segments
|
|
70
|
+
const segments: Array<{ label: string; round: number | null; reason: string | null; isLatest: boolean; children: WorkItem[] }> = [];
|
|
71
|
+
|
|
72
|
+
// Original segment (children with null rejection_round)
|
|
73
|
+
const originalChildren = allChildren.filter(c => c.rejection_round == null);
|
|
74
|
+
if (originalChildren.length > 0) {
|
|
75
|
+
segments.push({ label: 'Original', round: null, reason: null, isLatest: false, children: originalChildren });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Rejection round segments
|
|
79
|
+
for (let r = 1; r <= (item.rejection_count || 0); r++) {
|
|
80
|
+
const roundChildren = allChildren.filter(c => c.rejection_round === r);
|
|
81
|
+
const entry = history.find(h => h.round === r);
|
|
82
|
+
segments.push({
|
|
83
|
+
label: `Rejection #${r}`,
|
|
84
|
+
round: r,
|
|
85
|
+
reason: entry?.reason || null,
|
|
86
|
+
isLatest: r === item.rejection_count,
|
|
87
|
+
children: roundChildren,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="relative pl-[18px]">
|
|
93
|
+
{segments.map((seg, idx) => {
|
|
94
|
+
const isLast = idx === segments.length - 1;
|
|
95
|
+
return (
|
|
96
|
+
<div key={seg.label} className="relative" style={{ paddingBottom: isLast ? 0 : 4 }}>
|
|
97
|
+
{/* Vertical connector line */}
|
|
98
|
+
{!isLast && (
|
|
99
|
+
<div
|
|
100
|
+
className="absolute border-l-2 border-zinc-200 dark:border-zinc-700"
|
|
101
|
+
style={{ left: -12, top: 14, bottom: -4 }}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
{/* Node marker + label */}
|
|
105
|
+
<div className="relative flex items-center gap-2 py-1">
|
|
106
|
+
<div
|
|
107
|
+
className="absolute rounded-full"
|
|
108
|
+
style={{
|
|
109
|
+
left: -16, top: '50%', transform: 'translateY(-50%)',
|
|
110
|
+
width: 10, height: 10,
|
|
111
|
+
background: seg.round === null ? '#819D9F' : '#ef4444',
|
|
112
|
+
border: '2px solid var(--card, #27272a)',
|
|
113
|
+
zIndex: 1,
|
|
114
|
+
...(seg.isLatest ? { boxShadow: '0 0 0 3px rgba(239, 68, 68, 0.2)' } : {}),
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
<span className={`text-[11px] font-semibold uppercase tracking-wide ${
|
|
118
|
+
seg.round === null ? 'text-[#819D9F]' : 'text-red-400 dark:text-red-400'
|
|
119
|
+
}`}>{seg.label}</span>
|
|
120
|
+
{seg.reason && (
|
|
121
|
+
<span className="text-[11px] italic text-zinc-500 dark:text-zinc-500 truncate max-w-[180px]">
|
|
122
|
+
{seg.reason}
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
{/* Children under this segment */}
|
|
127
|
+
<div className="py-0.5">
|
|
128
|
+
{seg.children.map((child) => {
|
|
129
|
+
const isComplete = child.status === 'done';
|
|
130
|
+
const isBug = child.type === 'bug';
|
|
131
|
+
return (
|
|
132
|
+
<Link
|
|
133
|
+
key={child.id}
|
|
134
|
+
to={`/work/${child.id}`}
|
|
135
|
+
viewTransition
|
|
136
|
+
className={`block py-1.5 px-2.5 text-base rounded transition-colors duration-200 ease-out ${
|
|
137
|
+
isComplete
|
|
138
|
+
? 'bg-zinc-100 dark:bg-zinc-800/50'
|
|
139
|
+
: 'hover:bg-zinc-100 dark:hover:bg-zinc-700'
|
|
140
|
+
}`}
|
|
141
|
+
>
|
|
142
|
+
<div className="flex items-center gap-3">
|
|
143
|
+
<span className={`font-mono text-xs ${isComplete ? 'text-zinc-500' : 'text-zinc-400'}`}>#{child.id}</span>
|
|
144
|
+
{isBug && <TypeIcon type="bug" />}
|
|
145
|
+
{!isDone && !isBug && child.mode && MODE_LABELS[child.mode] && (
|
|
146
|
+
<span className={`px-1 py-0.5 rounded text-[10px] ${MODE_LABELS[child.mode].color}`}>
|
|
147
|
+
{getModeLabel(child)}
|
|
148
|
+
</span>
|
|
149
|
+
)}
|
|
150
|
+
<span className={`truncate ${
|
|
151
|
+
isComplete ? 'text-zinc-500' : 'text-zinc-700 dark:text-zinc-300'
|
|
152
|
+
}`}>
|
|
153
|
+
{child.title || <span className="text-zinc-400 italic">(Untitled)</span>}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
</Link>
|
|
157
|
+
);
|
|
158
|
+
})}
|
|
159
|
+
{seg.children.length === 0 && (
|
|
160
|
+
<span className="text-xs text-zinc-500 italic pl-2.5">No items</span>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface KanbanCardProps {
|
|
171
|
+
item: WorkItem;
|
|
172
|
+
epicTitle?: string | null;
|
|
173
|
+
showEpic?: boolean;
|
|
174
|
+
isInFlight?: boolean;
|
|
175
|
+
onTitleSave?: (id: number, newTitle: string) => Promise<void>;
|
|
176
|
+
onStatusChange?: (id: number, newStatus: string) => Promise<void | { success: boolean; notFound?: boolean }>;
|
|
177
|
+
onReject?: (id: number, reason: string) => Promise<void>;
|
|
178
|
+
onTriggerClaude?: (id: number, title: string, type: string, conversational?: boolean, description?: string | null) => void;
|
|
179
|
+
hasActiveSession?: boolean;
|
|
180
|
+
onOpenSession?: (id: string) => void;
|
|
181
|
+
onCloseSession?: (id: string) => void;
|
|
182
|
+
onRestart?: (id: number) => void;
|
|
183
|
+
usageAllowed?: boolean;
|
|
184
|
+
// Animation state lifted to board level
|
|
185
|
+
isCompletingAnimation?: boolean;
|
|
186
|
+
onAnimationComplete?: () => void;
|
|
187
|
+
isHighlighted?: boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const KanbanCard = memo(function KanbanCard({ item, epicTitle, showEpic = false, isInFlight = false, onTitleSave, onStatusChange, onReject, onTriggerClaude, hasActiveSession, onOpenSession, onCloseSession, onRestart, usageAllowed = true, isCompletingAnimation = false, onAnimationComplete, isHighlighted = false }: KanbanCardProps) {
|
|
191
|
+
const [expanded, setExpanded] = useState(false);
|
|
192
|
+
const [showRejectInput, setShowRejectInput] = useState(false);
|
|
193
|
+
const [rejectReason, setRejectReason] = useState('');
|
|
194
|
+
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
|
195
|
+
const [rejectionExpanded, setRejectionExpanded] = useState(false);
|
|
196
|
+
const navigate = useNavigate();
|
|
197
|
+
|
|
198
|
+
const handleOpenSession = (e: React.MouseEvent) => {
|
|
199
|
+
e.stopPropagation(); // Prevent card navigation
|
|
200
|
+
if (onOpenSession) {
|
|
201
|
+
onOpenSession(String(item.id));
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleStart = async (e: React.MouseEvent) => {
|
|
206
|
+
e.stopPropagation(); // Prevent card navigation
|
|
207
|
+
if (onStatusChange) {
|
|
208
|
+
await onStatusChange(item.id, 'in_progress');
|
|
209
|
+
if (onTriggerClaude) {
|
|
210
|
+
onTriggerClaude(item.id, item.title, item.type, !!item.conversational, item.description);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const handleRestart = (e: React.MouseEvent) => {
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
if (onRestart) {
|
|
218
|
+
onRestart(item.id);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const canStart = item.status === 'backlog' || item.status === 'cancelled';
|
|
223
|
+
|
|
224
|
+
// An item is reviewable when it has ready_for_review flag set
|
|
225
|
+
// This applies to kanban-visible items: features, standalone chores/bugs, and items under epics
|
|
226
|
+
const isReviewable = !!item.ready_for_review;
|
|
227
|
+
|
|
228
|
+
const handleAccept = async (e: React.MouseEvent) => {
|
|
229
|
+
e.stopPropagation();
|
|
230
|
+
if (onStatusChange) {
|
|
231
|
+
await onStatusChange(item.id, 'done');
|
|
232
|
+
}
|
|
233
|
+
if (onCloseSession) {
|
|
234
|
+
onCloseSession(String(item.id));
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const handleRejectClick = (e: React.MouseEvent) => {
|
|
239
|
+
e.stopPropagation();
|
|
240
|
+
setShowRejectInput(true);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const handleRejectConfirm = async (e: React.MouseEvent) => {
|
|
244
|
+
e.stopPropagation();
|
|
245
|
+
if (onReject && rejectReason.trim()) {
|
|
246
|
+
await onReject(item.id, rejectReason.trim());
|
|
247
|
+
setShowRejectInput(false);
|
|
248
|
+
setRejectReason('');
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const handleRejectCancel = (e: React.MouseEvent) => {
|
|
253
|
+
e.stopPropagation();
|
|
254
|
+
setShowRejectInput(false);
|
|
255
|
+
setRejectReason('');
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Calculate chores for expandable section
|
|
259
|
+
const allChores = item.chores || [];
|
|
260
|
+
const incompleteChores = allChores.filter(c => c.status !== 'done');
|
|
261
|
+
const hasChores = allChores.length > 0;
|
|
262
|
+
const hasIncompleteChores = incompleteChores.length > 0;
|
|
263
|
+
|
|
264
|
+
// Calculate bugs for expandable section
|
|
265
|
+
const allBugs = item.bugs || [];
|
|
266
|
+
const incompleteBugs = allBugs.filter(b => b.status !== 'done');
|
|
267
|
+
const hasBugs = allBugs.length > 0;
|
|
268
|
+
|
|
269
|
+
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
270
|
+
|
|
271
|
+
const handleCardClick = () => {
|
|
272
|
+
navigate(`/work/${item.id}`);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const handleMouseEnter = useCallback(() => {
|
|
276
|
+
hoverTimer.current = setTimeout(() => {
|
|
277
|
+
prefetch.workItem(item.id);
|
|
278
|
+
}, 100);
|
|
279
|
+
}, [item.id]);
|
|
280
|
+
|
|
281
|
+
const handleMouseLeave = useCallback(() => {
|
|
282
|
+
if (hoverTimer.current) {
|
|
283
|
+
clearTimeout(hoverTimer.current);
|
|
284
|
+
hoverTimer.current = null;
|
|
285
|
+
}
|
|
286
|
+
}, []);
|
|
287
|
+
|
|
288
|
+
const handleTitleSave = async (id: number, newTitle: string) => {
|
|
289
|
+
if (onTitleSave) {
|
|
290
|
+
await onTitleSave(id, newTitle);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Status changes are now handled by the board-level wrapper that triggers animation
|
|
295
|
+
const handleStatusChange = async (id: number, newStatus: string) => {
|
|
296
|
+
if (onStatusChange) {
|
|
297
|
+
await onStatusChange(id, newStatus);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const isDone = item.status === 'done';
|
|
302
|
+
|
|
303
|
+
const cardStyles = {
|
|
304
|
+
className: 'bg-white dark:bg-zinc-800',
|
|
305
|
+
boxShadow: shadow.sm,
|
|
306
|
+
hoverBoxShadow: shadow.lg,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const cardContent = (
|
|
310
|
+
<WaveCompletionAnimation isPlaying={isCompletingAnimation} onComplete={onAnimationComplete || (() => {})}>
|
|
311
|
+
<div
|
|
312
|
+
className={`kanban-card rounded-xl overflow-hidden transition-[background-color,opacity] duration-200 ease-out ${cardStyles.className}`}
|
|
313
|
+
style={{ boxShadow: cardStyles.boxShadow, '--hover-shadow': cardStyles.hoverBoxShadow } as React.CSSProperties}
|
|
314
|
+
data-testid={`kanban-card-${item.id}`}>
|
|
315
|
+
<div
|
|
316
|
+
onClick={handleCardClick}
|
|
317
|
+
onMouseEnter={handleMouseEnter}
|
|
318
|
+
onMouseLeave={handleMouseLeave}
|
|
319
|
+
className={`block ${isDone ? 'pt-4 px-4 pb-3' : 'p-4'} cursor-pointer`}
|
|
320
|
+
>
|
|
321
|
+
<div className="flex items-start gap-3">
|
|
322
|
+
<span className="text-base flex-shrink-0"><TypeIcon type={item.type} /></span>
|
|
323
|
+
<div className="flex-1 min-w-0">
|
|
324
|
+
{isDone ? (
|
|
325
|
+
/* Compact layout for done cards: ID and title inline, no mode badge */
|
|
326
|
+
<div className="flex items-start gap-3">
|
|
327
|
+
<CopyableId id={item.id} title={item.title} type={item.type} />
|
|
328
|
+
{hasActiveSession && (
|
|
329
|
+
<button
|
|
330
|
+
onClick={handleOpenSession}
|
|
331
|
+
className="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-[#819D9F] transition-colors duration-200 ease-out"
|
|
332
|
+
aria-label="Open active session"
|
|
333
|
+
data-testid={`session-indicator-${item.id}`}
|
|
334
|
+
title="Open session"
|
|
335
|
+
>
|
|
336
|
+
<SessionIndicatorIcon className="w-4 h-4" />
|
|
337
|
+
</button>
|
|
338
|
+
)}
|
|
339
|
+
<span className="text-base font-medium leading-snug text-zinc-900 dark:text-zinc-100">
|
|
340
|
+
{item.title || <span className="text-zinc-400 italic">(Untitled)</span>}
|
|
341
|
+
</span>
|
|
342
|
+
</div>
|
|
343
|
+
) : (
|
|
344
|
+
/* Standard layout: ID + mode badge on line 1, title below */
|
|
345
|
+
<>
|
|
346
|
+
<div className="flex items-start justify-between mb-1.5">
|
|
347
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
348
|
+
<CopyableId id={item.id} title={item.title} type={item.type} />
|
|
349
|
+
{hasActiveSession && (
|
|
350
|
+
<button
|
|
351
|
+
onClick={handleOpenSession}
|
|
352
|
+
className="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-[#819D9F] transition-colors duration-200 ease-out"
|
|
353
|
+
aria-label="Open active session"
|
|
354
|
+
data-testid={`session-indicator-${item.id}`}
|
|
355
|
+
title="Open session"
|
|
356
|
+
>
|
|
357
|
+
<SessionIndicatorIcon className="w-4 h-4" />
|
|
358
|
+
</button>
|
|
359
|
+
)}
|
|
360
|
+
{item.mode && MODE_LABELS[item.mode] && (
|
|
361
|
+
<span className={`text-xs px-2 py-1 rounded ${MODE_LABELS[item.mode].color}`}>
|
|
362
|
+
{getModeLabel(item)}
|
|
363
|
+
</span>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
{isReviewable ? (
|
|
367
|
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
368
|
+
{item.type === 'feature' && (
|
|
369
|
+
<Button
|
|
370
|
+
onClick={(e: React.MouseEvent) => {
|
|
371
|
+
e.stopPropagation();
|
|
372
|
+
navigate(`/work/${item.id}/proof`);
|
|
373
|
+
}}
|
|
374
|
+
variant="secondary"
|
|
375
|
+
size="xs"
|
|
376
|
+
aria-label="Launch QA proof dashboard"
|
|
377
|
+
data-testid={`qa-button-${item.id}`}
|
|
378
|
+
>
|
|
379
|
+
QA
|
|
380
|
+
</Button>
|
|
381
|
+
)}
|
|
382
|
+
{item.status !== 'done' && onStatusChange && (
|
|
383
|
+
<Button
|
|
384
|
+
onClick={handleAccept}
|
|
385
|
+
variant="secondary"
|
|
386
|
+
size="xs"
|
|
387
|
+
aria-label="Accept work item"
|
|
388
|
+
data-testid={`accept-button-${item.id}`}
|
|
389
|
+
>
|
|
390
|
+
Accept
|
|
391
|
+
</Button>
|
|
392
|
+
)}
|
|
393
|
+
{onReject && (
|
|
394
|
+
<Button
|
|
395
|
+
onClick={handleRejectClick}
|
|
396
|
+
variant="secondary"
|
|
397
|
+
size="xs"
|
|
398
|
+
aria-label="Reject work item"
|
|
399
|
+
data-testid={`reject-button-${item.id}`}
|
|
400
|
+
>
|
|
401
|
+
Reject
|
|
402
|
+
</Button>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
) : item.rejection_reason && !isDone ? (
|
|
406
|
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
407
|
+
{!hasActiveSession && onRestart && (
|
|
408
|
+
<Button
|
|
409
|
+
onClick={handleRestart}
|
|
410
|
+
variant="secondary"
|
|
411
|
+
size="xs"
|
|
412
|
+
aria-label="Restart work on rejected item"
|
|
413
|
+
data-testid={`restart-button-${item.id}`}
|
|
414
|
+
>
|
|
415
|
+
restart
|
|
416
|
+
</Button>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
) : null}
|
|
420
|
+
{canStart && onStatusChange && (
|
|
421
|
+
<Button
|
|
422
|
+
onClick={handleStart}
|
|
423
|
+
variant="secondary"
|
|
424
|
+
size="xs"
|
|
425
|
+
aria-label="Start work"
|
|
426
|
+
data-testid={`start-button-${item.id}`}
|
|
427
|
+
>
|
|
428
|
+
Start
|
|
429
|
+
</Button>
|
|
430
|
+
)}
|
|
431
|
+
</div>
|
|
432
|
+
<EditableTitle
|
|
433
|
+
title={item.title}
|
|
434
|
+
itemId={item.id}
|
|
435
|
+
onSave={handleTitleSave}
|
|
436
|
+
clickToEdit={false}
|
|
437
|
+
isEditing={isEditingTitle}
|
|
438
|
+
onEditingChange={setIsEditingTitle}
|
|
439
|
+
/>
|
|
440
|
+
</>
|
|
441
|
+
)}
|
|
442
|
+
{showEpic && epicTitle && (() => {
|
|
443
|
+
const epicId = item.parent_id || item.epic_id;
|
|
444
|
+
return epicId ? (
|
|
445
|
+
<Link to={`/work/${epicId}`}
|
|
446
|
+
viewTransition
|
|
447
|
+
onClick={(e) => e.stopPropagation()}
|
|
448
|
+
className="group/epic text-sm text-zinc-400 dark:text-zinc-500 mt-2 flex items-center gap-1.5 hover:text-zinc-600 dark:hover:text-zinc-300"
|
|
449
|
+
>
|
|
450
|
+
<TypeIcon type="epic" className="w-5 h-5 inline" />
|
|
451
|
+
<span className="group-hover/epic:underline">{epicTitle}</span>
|
|
452
|
+
</Link>
|
|
453
|
+
) : (
|
|
454
|
+
<p className="text-sm text-zinc-400 dark:text-zinc-500 mt-2 flex items-center gap-1.5">
|
|
455
|
+
<TypeIcon type="epic" className="w-5 h-5 inline" />
|
|
456
|
+
<span>{epicTitle}</span>
|
|
457
|
+
</p>
|
|
458
|
+
);
|
|
459
|
+
})()}
|
|
460
|
+
</div>
|
|
461
|
+
<div className="flex items-center gap-1.5">
|
|
462
|
+
{onStatusChange && (
|
|
463
|
+
<CardMenu
|
|
464
|
+
itemId={item.id}
|
|
465
|
+
itemTitle={item.title}
|
|
466
|
+
itemType={item.type}
|
|
467
|
+
itemDescription={item.description}
|
|
468
|
+
conversational={!!item.conversational}
|
|
469
|
+
currentStatus={item.status}
|
|
470
|
+
onStatusChange={handleStatusChange}
|
|
471
|
+
onTriggerClaude={onTriggerClaude}
|
|
472
|
+
hasActiveSession={hasActiveSession}
|
|
473
|
+
onOpenSession={onOpenSession}
|
|
474
|
+
usageAllowed={usageAllowed}
|
|
475
|
+
onEditName={() => setIsEditingTitle(true)}
|
|
476
|
+
onUnaccept={item.status === 'done' && onReject ? () => setShowRejectInput(true) : undefined}
|
|
477
|
+
/>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
{/* Rejection reason input */}
|
|
483
|
+
{showRejectInput && (
|
|
484
|
+
<div className="px-4 pb-4 border-t border-zinc-200 dark:border-zinc-700" onClick={(e) => e.stopPropagation()}>
|
|
485
|
+
<div className="mt-3">
|
|
486
|
+
<Input
|
|
487
|
+
type="text"
|
|
488
|
+
value={rejectReason}
|
|
489
|
+
onChange={(e) => setRejectReason(e.target.value)}
|
|
490
|
+
onKeyDown={(e) => {
|
|
491
|
+
e.stopPropagation();
|
|
492
|
+
if (e.key === 'Enter' && rejectReason.trim()) {
|
|
493
|
+
handleRejectConfirm(e as unknown as React.MouseEvent);
|
|
494
|
+
}
|
|
495
|
+
if (e.key === 'Escape') {
|
|
496
|
+
handleRejectCancel(e as unknown as React.MouseEvent);
|
|
497
|
+
}
|
|
498
|
+
}}
|
|
499
|
+
placeholder="Rejection reason..."
|
|
500
|
+
size="sm"
|
|
501
|
+
error
|
|
502
|
+
autoFocus
|
|
503
|
+
data-testid={`reject-reason-input-${item.id}`}
|
|
504
|
+
/>
|
|
505
|
+
<div className="flex items-center gap-1.5 mt-2">
|
|
506
|
+
<Button
|
|
507
|
+
onClick={handleRejectConfirm}
|
|
508
|
+
disabled={!rejectReason.trim()}
|
|
509
|
+
variant="destructive"
|
|
510
|
+
size="sm"
|
|
511
|
+
data-testid={`reject-confirm-${item.id}`}
|
|
512
|
+
>
|
|
513
|
+
Reject
|
|
514
|
+
</Button>
|
|
515
|
+
<Button
|
|
516
|
+
onClick={handleRejectCancel}
|
|
517
|
+
variant="ghost"
|
|
518
|
+
size="sm"
|
|
519
|
+
data-testid={`reject-cancel-${item.id}`}
|
|
520
|
+
>
|
|
521
|
+
Cancel
|
|
522
|
+
</Button>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
)}
|
|
527
|
+
{/* Rejection banner — hidden for done cards */}
|
|
528
|
+
{item.rejection_reason && !isDone && (() => {
|
|
529
|
+
let history: RejectionHistoryEntry[] = [];
|
|
530
|
+
try {
|
|
531
|
+
history = item.rejection_history ? JSON.parse(item.rejection_history) : [];
|
|
532
|
+
} catch { /* degrade gracefully */ }
|
|
533
|
+
const currentRound = item.rejection_count || 1;
|
|
534
|
+
const hasMultipleRounds = history.length > 1;
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<div>
|
|
538
|
+
<div
|
|
539
|
+
onClick={(e) => {
|
|
540
|
+
e.stopPropagation();
|
|
541
|
+
if (hasMultipleRounds) setRejectionExpanded(!rejectionExpanded);
|
|
542
|
+
}}
|
|
543
|
+
className={`bg-red-50 dark:bg-red-950/40 ${hasMultipleRounds ? 'cursor-pointer' : ''}`}
|
|
544
|
+
style={{ padding: '10px 16px' }}
|
|
545
|
+
data-testid={`rejection-banner-${item.id}`}
|
|
546
|
+
>
|
|
547
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
|
548
|
+
<span className="text-red-700 dark:text-red-400" style={{ fontSize: '12px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em' }}>
|
|
549
|
+
Rejected
|
|
550
|
+
</span>
|
|
551
|
+
<span className="bg-red-100 dark:bg-red-900/40 text-red-600 dark:text-red-400" style={{ fontSize: '11px', padding: '1px 8px', borderRadius: '12px', fontWeight: 600 }}>
|
|
552
|
+
Round {currentRound}
|
|
553
|
+
</span>
|
|
554
|
+
{hasMultipleRounds && (
|
|
555
|
+
<span className="text-red-600 dark:text-red-400" style={{ fontSize: '11px', opacity: 0.5, marginLeft: 'auto' }}>
|
|
556
|
+
{rejectionExpanded ? '▲' : '▼'}
|
|
557
|
+
</span>
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
<div className="text-red-700/75 dark:text-red-400/75" style={{ fontSize: '14px', lineHeight: 1.4 }}>
|
|
561
|
+
{item.rejection_reason}
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
{/* Expanded stacked history */}
|
|
565
|
+
{rejectionExpanded && hasMultipleRounds && (
|
|
566
|
+
<div
|
|
567
|
+
onClick={(e) => e.stopPropagation()}
|
|
568
|
+
className="bg-red-50 dark:bg-red-950/40"
|
|
569
|
+
style={{ padding: '12px 16px' }}
|
|
570
|
+
>
|
|
571
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px' }}>
|
|
572
|
+
<span className="text-red-700 dark:text-red-400" style={{ fontSize: '12px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em' }}>
|
|
573
|
+
Rejections
|
|
574
|
+
</span>
|
|
575
|
+
<span className="bg-red-100 dark:bg-red-900/40 text-red-600 dark:text-red-400" style={{ fontSize: '11px', padding: '1px 8px', borderRadius: '12px', fontWeight: 600 }}>
|
|
576
|
+
{history.length}
|
|
577
|
+
</span>
|
|
578
|
+
</div>
|
|
579
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
580
|
+
{history.map((entry) => {
|
|
581
|
+
const isLatest = entry.round === currentRound;
|
|
582
|
+
return (
|
|
583
|
+
<div key={entry.round} style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
|
|
584
|
+
<span
|
|
585
|
+
className={isLatest
|
|
586
|
+
? 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-400'
|
|
587
|
+
: 'bg-red-100/60 dark:bg-red-900/20 text-red-600/60 dark:text-red-400/50'
|
|
588
|
+
}
|
|
589
|
+
style={{
|
|
590
|
+
fontSize: '11px',
|
|
591
|
+
fontWeight: 600,
|
|
592
|
+
padding: '2px 8px',
|
|
593
|
+
borderRadius: '8px',
|
|
594
|
+
flexShrink: 0,
|
|
595
|
+
marginTop: '1px',
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
R{entry.round}
|
|
599
|
+
</span>
|
|
600
|
+
<div>
|
|
601
|
+
<div className="text-red-700/75 dark:text-red-400/75" style={{ fontSize: '14px', lineHeight: 1.4 }}>
|
|
602
|
+
{entry.reason}
|
|
603
|
+
</div>
|
|
604
|
+
{entry.at && (
|
|
605
|
+
<div className="text-red-600/40 dark:text-red-400/30" style={{ fontSize: '11px', marginTop: '2px' }}>
|
|
606
|
+
{formatRejectionTime(entry.at)}
|
|
607
|
+
</div>
|
|
608
|
+
)}
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
);
|
|
612
|
+
})}
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
)}
|
|
616
|
+
</div>
|
|
617
|
+
);
|
|
618
|
+
})()}
|
|
619
|
+
{/* Show expandable section for features with chores or bugs */}
|
|
620
|
+
{(hasChores || hasBugs) && (
|
|
621
|
+
<div className={"border-t border-zinc-200 dark:border-zinc-700"}>
|
|
622
|
+
<button
|
|
623
|
+
onClick={() => setExpanded(!expanded)}
|
|
624
|
+
className={`w-full px-4 py-2 flex items-start gap-2 text-base transition-colors duration-200 ease-out ${
|
|
625
|
+
isDone
|
|
626
|
+
? 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-700/50'
|
|
627
|
+
: 'text-zinc-600 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700/50'
|
|
628
|
+
}`}
|
|
629
|
+
>
|
|
630
|
+
<span className="mt-0.5">{expanded ? '▼' : '▶'}</span>
|
|
631
|
+
<div className="flex flex-col gap-1">
|
|
632
|
+
{hasChores && (
|
|
633
|
+
<div className="flex items-center gap-2">
|
|
634
|
+
<TypeIcon type="chore" />
|
|
635
|
+
<span>
|
|
636
|
+
{isDone
|
|
637
|
+
? `${allChores.length === 0 ? 'no' : allChores.length} chore${allChores.length !== 1 ? 's' : ''}`
|
|
638
|
+
: `${incompleteChores.length === 0 ? 'no' : incompleteChores.length}${item.mode ? ` ${item.mode} mode` : ''} chore${incompleteChores.length !== 1 ? 's' : ''} left`}
|
|
639
|
+
</span>
|
|
640
|
+
</div>
|
|
641
|
+
)}
|
|
642
|
+
{hasBugs && (
|
|
643
|
+
<div className="flex items-center gap-2">
|
|
644
|
+
<TypeIcon type="bug" />
|
|
645
|
+
<span>
|
|
646
|
+
{isDone
|
|
647
|
+
? `${allBugs.length === 0 ? 'no' : allBugs.length} bug${allBugs.length !== 1 ? 's' : ''}`
|
|
648
|
+
: `${incompleteBugs.length === 0 ? 'no' : incompleteBugs.length} bug${incompleteBugs.length !== 1 ? 's' : ''} left`}
|
|
649
|
+
</span>
|
|
650
|
+
</div>
|
|
651
|
+
)}
|
|
652
|
+
</div>
|
|
653
|
+
</button>
|
|
654
|
+
{expanded && (
|
|
655
|
+
<div className="px-4 pb-3 space-y-1.5">
|
|
656
|
+
{item.rejection_count > 0 ? (
|
|
657
|
+
<RejectionTimeline item={item} allChores={allChores} allBugs={allBugs} isDone={isDone} />
|
|
658
|
+
) : (
|
|
659
|
+
<>
|
|
660
|
+
{allChores.map((chore) => {
|
|
661
|
+
const isComplete = chore.status === 'done';
|
|
662
|
+
return (
|
|
663
|
+
<Link
|
|
664
|
+
key={chore.id}
|
|
665
|
+
to={`/work/${chore.id}`}
|
|
666
|
+
viewTransition
|
|
667
|
+
className={`block py-1.5 px-3 text-base rounded transition-colors duration-200 ease-out ${
|
|
668
|
+
isComplete
|
|
669
|
+
? 'bg-zinc-100 dark:bg-zinc-800/50'
|
|
670
|
+
: 'hover:bg-zinc-100 dark:hover:bg-zinc-700'
|
|
671
|
+
}`}
|
|
672
|
+
>
|
|
673
|
+
<div className="flex items-center gap-3">
|
|
674
|
+
<span className={`font-mono ${isComplete ? 'text-zinc-500' : 'text-zinc-400'}`}>#{chore.id}</span>
|
|
675
|
+
{!isDone && chore.mode && MODE_LABELS[chore.mode] && (
|
|
676
|
+
<span className={`px-1 py-0.5 rounded text-[10px] ${MODE_LABELS[chore.mode].color}`}>
|
|
677
|
+
{getModeLabel(chore)}
|
|
678
|
+
</span>
|
|
679
|
+
)}
|
|
680
|
+
<span className={`truncate ${
|
|
681
|
+
isComplete
|
|
682
|
+
? 'text-zinc-500'
|
|
683
|
+
: 'text-zinc-700 dark:text-zinc-300'
|
|
684
|
+
}`}>
|
|
685
|
+
{chore.title || <span className="text-zinc-400 italic">(Untitled)</span>}
|
|
686
|
+
</span>
|
|
687
|
+
</div>
|
|
688
|
+
</Link>
|
|
689
|
+
);
|
|
690
|
+
})}
|
|
691
|
+
{allBugs.map((bug) => {
|
|
692
|
+
const isComplete = bug.status === 'done';
|
|
693
|
+
return (
|
|
694
|
+
<Link
|
|
695
|
+
key={bug.id}
|
|
696
|
+
to={`/work/${bug.id}`}
|
|
697
|
+
viewTransition
|
|
698
|
+
className={`block py-1.5 px-3 text-base rounded transition-colors duration-200 ease-out ${
|
|
699
|
+
isComplete
|
|
700
|
+
? 'bg-zinc-100 dark:bg-zinc-800/50'
|
|
701
|
+
: 'hover:bg-zinc-100 dark:hover:bg-zinc-700'
|
|
702
|
+
}`}
|
|
703
|
+
>
|
|
704
|
+
<div className="flex items-center gap-3">
|
|
705
|
+
<span className={`font-mono ${isComplete ? 'text-zinc-500' : 'text-zinc-400'}`}>#{bug.id}</span>
|
|
706
|
+
<TypeIcon type="bug" />
|
|
707
|
+
<span className={`truncate ${
|
|
708
|
+
isComplete
|
|
709
|
+
? 'text-zinc-500'
|
|
710
|
+
: 'text-zinc-700 dark:text-zinc-300'
|
|
711
|
+
}`}>
|
|
712
|
+
{bug.title || <span className="text-zinc-400 italic">(Untitled)</span>}
|
|
713
|
+
</span>
|
|
714
|
+
</div>
|
|
715
|
+
</Link>
|
|
716
|
+
);
|
|
717
|
+
})}
|
|
718
|
+
</>
|
|
719
|
+
)}
|
|
720
|
+
</div>
|
|
721
|
+
)}
|
|
722
|
+
</div>
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
</WaveCompletionAnimation>
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
if (isHighlighted) {
|
|
729
|
+
return (
|
|
730
|
+
<div
|
|
731
|
+
className="rounded-xl"
|
|
732
|
+
style={{ animation: 'highlight-pulse 2s ease-in-out infinite' }}
|
|
733
|
+
>
|
|
734
|
+
{cardContent}
|
|
735
|
+
</div>
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return cardContent;
|
|
740
|
+
});
|