feat(HAI-012): complete Steps 1-2 — add activity timeline to TaskDetailModal with styling

This commit is contained in:
Dustin Byrne
2026-03-25 21:03:12 -04:00
parent 32d1210f01
commit c8aacc3622
2 changed files with 112 additions and 0 deletions

View File

@@ -5,6 +5,21 @@ import type { Task, TaskDetail, Column, MergeResult } from "@hai/core";
import { COLUMN_LABELS, VALID_TRANSITIONS } from "@hai/core";
import type { ToastType } from "../hooks/useToast";
function formatTimestamp(iso: string): string {
const date = new Date(iso);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMin = Math.floor(diffMs / 60000);
const diffHr = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHr / 24);
if (diffMin < 1) return "just now";
if (diffMin < 60) return `${diffMin}m ago`;
if (diffHr < 24) return `${diffHr}h ago`;
if (diffDay < 7) return `${diffDay}d ago`;
return date.toLocaleDateString();
}
interface TaskDetailModalProps {
task: TaskDetail;
onClose: () => void;
@@ -121,6 +136,28 @@ export function TaskDetailModal({
</ul>
</div>
)}
<div className="detail-section detail-activity">
<h4>Activity</h4>
{task.log && task.log.length > 0 ? (
<div className="detail-activity-list">
{[...task.log].reverse().map((entry, i) => (
<div key={i} className="detail-log-entry">
<div className="detail-log-header">
<span className="detail-log-timestamp">
{formatTimestamp(entry.timestamp)}
</span>
<span className="detail-log-action">{entry.action}</span>
</div>
{entry.outcome && (
<div className="detail-log-outcome">{entry.outcome}</div>
)}
</div>
))}
</div>
) : (
<div className="detail-log-empty">(no activity)</div>
)}
</div>
</div>
<div className="modal-actions">
<button className="btn btn-danger btn-sm" onClick={handleDelete}>

View File

@@ -527,6 +527,81 @@ html, body {
font-family: "SF Mono", Monaco, Consolas, monospace;
}
/* === Activity Timeline === */
.detail-activity {
margin-top: 20px;
}
.detail-activity-list {
max-height: 260px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0;
}
.detail-activity-list::-webkit-scrollbar { width: 4px; }
.detail-activity-list::-webkit-scrollbar-track { background: transparent; }
.detail-activity-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.detail-log-entry {
padding: 8px 0 8px 14px;
border-left: 2px solid var(--border);
position: relative;
}
.detail-log-entry::before {
content: "";
position: absolute;
left: -4px;
top: 13px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-dim);
}
.detail-log-entry:first-child::before {
background: var(--todo);
}
.detail-log-header {
display: flex;
flex-direction: column;
gap: 2px;
}
.detail-log-timestamp {
font-size: 11px;
color: var(--text-dim);
font-family: "SF Mono", Monaco, Consolas, monospace;
}
.detail-log-action {
font-size: 13px;
color: var(--text-muted);
line-height: 1.4;
}
.detail-log-outcome {
margin-top: 6px;
padding: 8px 12px;
background: var(--bg);
border-left: 3px solid var(--border);
border-radius: 0 var(--radius) var(--radius) 0;
font-size: 12px;
line-height: 1.5;
color: var(--text-muted);
white-space: pre-wrap;
word-break: break-word;
}
.detail-log-empty {
font-size: 13px;
color: var(--text-dim);
padding: 12px 0;
}
/* === Toasts === */
.toast-container {
position: fixed;