chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
@@ -0,0 +1,45 @@
import type { Task, TaskStatus } from '@/lib/types';
import { TaskCard } from './task-card';
interface KanbanBoardProps {
tasks: Task[];
onTaskClick: (task: Task) => void;
}
const columns: { id: TaskStatus; label: string }[] = [
{ id: 'not-started', label: 'Not Started' },
{ id: 'in-progress', label: 'In Progress' },
{ id: 'blocked', label: 'Blocked' },
{ id: 'done', label: 'Done' },
];
export function KanbanBoard({ tasks, onTaskClick }: KanbanBoardProps): React.ReactElement {
return (
<div className="flex gap-4 overflow-x-auto pb-4">
{columns.map((col) => {
const columnTasks = tasks.filter((t) => t.status === col.id);
return (
<div
key={col.id}
className="flex w-72 shrink-0 flex-col rounded-lg border border-surface-border bg-surface-elevated"
>
<div className="flex items-center justify-between border-b border-surface-border px-3 py-2">
<h3 className="text-sm font-medium text-text-secondary">{col.label}</h3>
<span className="rounded-full bg-surface-card px-2 py-0.5 text-xs text-text-muted">
{columnTasks.length}
</span>
</div>
<div className="flex-1 space-y-2 p-2">
{columnTasks.length === 0 && (
<p className="py-4 text-center text-xs text-text-muted">No tasks</p>
)}
{columnTasks.map((task) => (
<TaskCard key={task.id} task={task} onClick={onTaskClick} />
))}
</div>
</div>
);
})}
</div>
);
}
@@ -0,0 +1,55 @@
import { cn } from '@/lib/cn';
import type { Task } from '@/lib/types';
interface TaskCardProps {
task: Task;
onClick: (task: Task) => void;
}
const priorityColors: Record<string, string> = {
critical: 'text-error',
high: 'text-warning',
medium: 'text-blue-400',
low: 'text-text-muted',
};
const statusBadgeColors: Record<string, string> = {
'not-started': 'bg-gray-600/20 text-gray-300',
'in-progress': 'bg-blue-600/20 text-blue-400',
blocked: 'bg-error/20 text-error',
done: 'bg-success/20 text-success',
cancelled: 'bg-gray-600/20 text-gray-500',
};
export function TaskCard({ task, onClick }: TaskCardProps): React.ReactElement {
return (
<button
type="button"
onClick={() => onClick(task)}
className="w-full rounded-lg border border-surface-border bg-surface-card p-3 text-left transition-colors hover:border-gray-500"
>
<div className="flex items-start justify-between gap-2">
<span className="text-sm font-medium text-text-primary">{task.title}</span>
<span className={cn('text-xs', priorityColors[task.priority])}>{task.priority}</span>
</div>
{task.description && (
<p className="mt-1 line-clamp-2 text-xs text-text-muted">{task.description}</p>
)}
<div className="mt-2 flex items-center gap-2">
<span
className={cn(
'rounded-full px-2 py-0.5 text-xs',
statusBadgeColors[task.status] ?? 'bg-gray-600/20 text-gray-400',
)}
>
{task.status}
</span>
{task.dueDate && (
<span className="text-xs text-text-muted">
{new Date(task.dueDate).toLocaleDateString()}
</span>
)}
</div>
</button>
);
}
@@ -0,0 +1,229 @@
import { useEffect, useRef } from 'react';
import type { ReactNode } from 'react';
import { cn } from '@/lib/cn';
import type { Task } from '@/lib/types';
interface TaskDetailModalProps {
task: Task;
onClose: () => void;
}
const priorityColors: Record<string, string> = {
critical: 'text-error',
high: 'text-warning',
medium: 'text-blue-400',
low: 'text-text-muted',
};
const statusBadgeColors: Record<string, string> = {
'not-started': 'bg-gray-600/20 text-gray-300',
'in-progress': 'bg-blue-600/20 text-blue-400',
blocked: 'bg-error/20 text-error',
done: 'bg-success/20 text-success',
cancelled: 'bg-gray-600/20 text-gray-500',
};
function DetailRow({
label,
children,
}: {
label: string;
children: ReactNode;
}): React.ReactElement {
return (
<div className="flex gap-2 py-2 text-sm">
<span className="w-24 shrink-0 text-text-muted">{label}</span>
<span className="text-text-primary">{children}</span>
</div>
);
}
export function TaskDetailModal({ task, onClose }: TaskDetailModalProps): React.ReactElement {
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleKeyDown(e: KeyboardEvent): void {
if (e.key === 'Escape') onClose();
}
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
// Trap focus on mount
useEffect(() => {
dialogRef.current?.focus();
}, []);
const prLinks = extractPrLinks(task.metadata);
return (
/* backdrop */
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
role="presentation"
>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="task-detail-title"
tabIndex={-1}
className="flex max-h-[90vh] w-full max-w-lg flex-col overflow-hidden rounded-xl border border-surface-border bg-surface-card outline-none"
>
{/* Header */}
<div className="flex items-start justify-between gap-3 border-b border-surface-border p-4">
<h2 id="task-detail-title" className="text-base font-semibold text-text-primary">
{task.title}
</h2>
<button
type="button"
onClick={onClose}
aria-label="Close task details"
className="shrink-0 rounded p-1 text-text-muted hover:text-text-primary"
>
✕
</button>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto p-4">
{/* Badges */}
<div className="mb-4 flex flex-wrap gap-2">
<span
className={cn(
'rounded-full px-2 py-0.5 text-xs',
statusBadgeColors[task.status] ?? 'bg-gray-600/20 text-gray-400',
)}
>
{task.status}
</span>
<span className={cn('text-xs', priorityColors[task.priority])}>
{task.priority} priority
</span>
</div>
{/* Description */}
{task.description && (
<div className="mb-4 rounded-lg bg-surface-elevated p-3">
<p className="text-sm text-text-secondary">{task.description}</p>
</div>
)}
{/* Details */}
<div className="divide-y divide-surface-border rounded-lg border border-surface-border px-3">
{task.assignee ? <DetailRow label="Assignee">{task.assignee}</DetailRow> : null}
{task.dueDate ? (
<DetailRow label="Due date">
{new Date(task.dueDate).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</DetailRow>
) : null}
<DetailRow label="Created">
{new Date(task.createdAt).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</DetailRow>
<DetailRow label="Updated">
{new Date(task.updatedAt).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</DetailRow>
</div>
{/* Tags */}
{task.tags != null && task.tags.length > 0 ? (
<div className="mt-4">
<p className="mb-2 text-xs text-text-muted">Tags</p>
<div className="flex flex-wrap gap-1">
{task.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-surface-elevated px-2 py-0.5 text-xs text-text-secondary"
>
{tag}
</span>
))}
</div>
</div>
) : null}
{/* PR Links */}
{prLinks.length > 0 ? (
<div className="mt-4">
<p className="mb-2 text-xs text-text-muted">Pull Requests</p>
<ul className="space-y-1">
{prLinks.map((link) => (
<li key={link.url}>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 underline hover:text-blue-300"
>
{link.label ?? link.url}
</a>
</li>
))}
</ul>
</div>
) : null}
{/* Notes from metadata */}
<NotesSection notes={getNotesString(task.metadata)} />
</div>
</div>
</div>
);
}
interface PrLink {
url: string;
label?: string;
}
function extractPrLinks(metadata: Record<string, unknown> | null): PrLink[] {
if (!metadata) return [];
const raw = metadata['pr_links'] ?? metadata['prLinks'];
if (!Array.isArray(raw)) return [];
return raw
.filter((item): item is PrLink => {
if (typeof item === 'string') return true;
if (typeof item === 'object' && item !== null && 'url' in item) return true;
return false;
})
.map((item): PrLink => {
if (typeof item === 'string') return { url: item };
return item as PrLink;
});
}
function getNotesString(metadata: Record<string, unknown> | null): string | null {
if (!metadata) return null;
const notes = metadata['notes'];
if (typeof notes === 'string' && notes.trim().length > 0) return notes;
return null;
}
function NotesSection({ notes }: { notes: string | null }): React.ReactElement | null {
if (!notes) return null;
return (
<div className="mt-4">
<p className="mb-2 text-xs text-text-muted">Notes</p>
<div className="rounded-lg bg-surface-elevated p-3">
<p className="whitespace-pre-wrap text-xs text-text-secondary">{notes}</p>
</div>
</div>
);
}
@@ -0,0 +1,65 @@
import { cn } from '@/lib/cn';
import type { Task } from '@/lib/types';
interface TaskListViewProps {
tasks: Task[];
onTaskClick: (task: Task) => void;
}
const priorityColors: Record<string, string> = {
critical: 'text-error',
high: 'text-warning',
medium: 'text-blue-400',
low: 'text-text-muted',
};
const statusColors: Record<string, string> = {
'not-started': 'text-gray-400',
'in-progress': 'text-blue-400',
blocked: 'text-error',
done: 'text-success',
cancelled: 'text-gray-500',
};
export function TaskListView({ tasks, onTaskClick }: TaskListViewProps): React.ReactElement {
if (tasks.length === 0) {
return <p className="py-8 text-center text-sm text-text-muted">No tasks found</p>;
}
return (
<div className="overflow-hidden rounded-lg border border-surface-border">
<table className="w-full">
<thead>
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
<th className="px-4 py-2 font-medium">Title</th>
<th className="px-4 py-2 font-medium">Status</th>
<th className="px-4 py-2 font-medium">Priority</th>
<th className="hidden px-4 py-2 font-medium md:table-cell">Due</th>
</tr>
</thead>
<tbody>
{tasks.map((task) => (
<tr
key={task.id}
onClick={() => onTaskClick(task)}
className="cursor-pointer border-b border-surface-border transition-colors last:border-b-0 hover:bg-surface-elevated"
>
<td className="px-4 py-3 text-sm text-text-primary">{task.title}</td>
<td className="px-4 py-3">
<span className={cn('text-xs', statusColors[task.status])}>{task.status}</span>
</td>
<td className="px-4 py-3">
<span className={cn('text-xs', priorityColors[task.priority])}>
{task.priority}
</span>
</td>
<td className="hidden px-4 py-3 text-xs text-text-muted md:table-cell">
{task.dueDate ? new Date(task.dueDate).toLocaleDateString() : '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
@@ -0,0 +1,97 @@
import { cn } from '@/lib/cn';
import type { Task, TaskStatus } from '@/lib/types';
interface TaskStatusSummaryProps {
tasks: Task[];
activeFilter: TaskStatus | 'all';
onFilterChange: (filter: TaskStatus | 'all') => void;
}
const statusConfig: {
id: TaskStatus | 'all';
label: string;
color: string;
activeColor: string;
}[] = [
{
id: 'all',
label: 'All',
color: 'text-text-muted',
activeColor: 'bg-surface-elevated text-text-primary',
},
{
id: 'not-started',
label: 'Not Started',
color: 'text-gray-400',
activeColor: 'bg-gray-600/20 text-gray-300',
},
{
id: 'in-progress',
label: 'In Progress',
color: 'text-blue-400',
activeColor: 'bg-blue-600/20 text-blue-400',
},
{
id: 'blocked',
label: 'Blocked',
color: 'text-error',
activeColor: 'bg-error/20 text-error',
},
{
id: 'done',
label: 'Done',
color: 'text-success',
activeColor: 'bg-success/20 text-success',
},
];
export function TaskStatusSummary({
tasks,
activeFilter,
onFilterChange,
}: TaskStatusSummaryProps): React.ReactElement {
const counts: Record<TaskStatus | 'all', number> = {
all: tasks.length,
'not-started': 0,
'in-progress': 0,
blocked: 0,
done: 0,
cancelled: 0,
};
for (const task of tasks) {
counts[task.status] = (counts[task.status] ?? 0) + 1;
}
return (
<div className="flex flex-wrap gap-2">
{statusConfig.map((config) => {
const count = counts[config.id];
const isActive = activeFilter === config.id;
return (
<button
key={config.id}
type="button"
onClick={() => onFilterChange(config.id)}
className={cn(
'flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs transition-colors',
isActive
? cn('border-transparent', config.activeColor)
: 'border-surface-border text-text-muted hover:border-gray-500',
)}
>
<span>{config.label}</span>
<span
className={cn(
'rounded-full px-1.5 py-0.5 text-xs font-medium',
isActive ? 'bg-black/20' : 'bg-surface-elevated',
)}
>
{count}
</span>
</button>
);
})}
</div>
);
}