@echothink-ui/task 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.
Files changed (64) hide show
  1. package/README.md +5 -0
  2. package/dist/components/BackendThinkingChain.d.ts +2 -0
  3. package/dist/components/BlockingReasonPanel.d.ts +2 -0
  4. package/dist/components/DAGEdge.d.ts +2 -0
  5. package/dist/components/DAGLegend.d.ts +2 -0
  6. package/dist/components/DAGNode.d.ts +4 -0
  7. package/dist/components/DecisionRequiredPanel.d.ts +2 -0
  8. package/dist/components/HumanInterventionPanel.d.ts +2 -0
  9. package/dist/components/MobileTaskShell.d.ts +12 -0
  10. package/dist/components/TaskApprovalPanel.d.ts +2 -0
  11. package/dist/components/TaskCard.d.ts +2 -0
  12. package/dist/components/TaskDependencyList.d.ts +2 -0
  13. package/dist/components/TaskDetailPanel.d.ts +2 -0
  14. package/dist/components/TaskHandoffPanel.d.ts +2 -0
  15. package/dist/components/TaskProgressIndicator.d.ts +2 -0
  16. package/dist/components/TaskRetryPanel.d.ts +2 -0
  17. package/dist/components/TaskRunLog.d.ts +2 -0
  18. package/dist/components/TaskStatusBadge.d.ts +2 -0
  19. package/dist/components/TaskTable.d.ts +5 -0
  20. package/dist/components/TaskTimeline.d.ts +2 -0
  21. package/dist/components/TaskWaveDAG.d.ts +2 -0
  22. package/dist/components/TaskWaveHeader.d.ts +2 -0
  23. package/dist/components/TaskWaveTable.d.ts +2 -0
  24. package/dist/components/utils.d.ts +13 -0
  25. package/dist/index.cjs +2434 -0
  26. package/dist/index.cjs.map +1 -0
  27. package/dist/index.css +2402 -0
  28. package/dist/index.css.map +1 -0
  29. package/dist/index.d.ts +27 -0
  30. package/dist/index.js +2388 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/types.d.ts +249 -0
  33. package/package.json +45 -0
  34. package/src/components/BackendThinkingChain.tsx +129 -0
  35. package/src/components/BlockingReasonPanel.tsx +67 -0
  36. package/src/components/DAGEdge.tsx +97 -0
  37. package/src/components/DAGLegend.tsx +86 -0
  38. package/src/components/DAGNode.tsx +103 -0
  39. package/src/components/DecisionRequiredPanel.tsx +166 -0
  40. package/src/components/HumanInterventionPanel.tsx +82 -0
  41. package/src/components/MobileTaskShell.tsx +52 -0
  42. package/src/components/TaskApprovalPanel.tsx +159 -0
  43. package/src/components/TaskCard.tsx +71 -0
  44. package/src/components/TaskDependencyList.test.tsx +54 -0
  45. package/src/components/TaskDependencyList.tsx +105 -0
  46. package/src/components/TaskDetailPanel.test.tsx +49 -0
  47. package/src/components/TaskDetailPanel.tsx +139 -0
  48. package/src/components/TaskHandoffPanel.tsx +125 -0
  49. package/src/components/TaskProgressIndicator.tsx +70 -0
  50. package/src/components/TaskRetryPanel.test.tsx +29 -0
  51. package/src/components/TaskRetryPanel.tsx +103 -0
  52. package/src/components/TaskRunLog.tsx +156 -0
  53. package/src/components/TaskStatusBadge.tsx +29 -0
  54. package/src/components/TaskTable.tsx +294 -0
  55. package/src/components/TaskTimeline.tsx +98 -0
  56. package/src/components/TaskWaveDAG.tsx +202 -0
  57. package/src/components/TaskWaveHeader.tsx +82 -0
  58. package/src/components/TaskWaveTable.tsx +151 -0
  59. package/src/components/css.d.ts +1 -0
  60. package/src/components/utils.ts +116 -0
  61. package/src/index.test.tsx +316 -0
  62. package/src/index.tsx +90 -0
  63. package/src/styles.css +2889 -0
  64. package/src/types.ts +289 -0
@@ -0,0 +1,151 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge, EmptyState, StatusDot, Surface } from "@echothink-ui/core";
4
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
5
+ import type { Task, TaskWaveTableProps } from "../types";
6
+ import { formatDuration, labelForStatus } from "./utils";
7
+
8
+ function taskRowsFromItems(items: TaskWaveTableProps["items"]): Task[] {
9
+ return (
10
+ items?.map((item, index) => ({
11
+ id: item.id,
12
+ title: String(item.label),
13
+ status: item.status ?? "not-started",
14
+ order: index + 1
15
+ })) ?? []
16
+ );
17
+ }
18
+
19
+ function dependencyCount(task: Task) {
20
+ if (typeof task.dependencyCount === "number") return task.dependencyCount;
21
+ if (task.dependencyIds?.length) return task.dependencyIds.length;
22
+ return task.dependencies?.length ?? 0;
23
+ }
24
+
25
+ function EmptyCell() {
26
+ return <span className="eth-task-wave-table__empty">-</span>;
27
+ }
28
+
29
+ export function TaskWaveTable({
30
+ wave,
31
+ tasks,
32
+ items,
33
+ onTaskSelect,
34
+ rowActions,
35
+ className,
36
+ title,
37
+ description,
38
+ ..._surfaceProps
39
+ }: TaskWaveTableProps) {
40
+ const rows = tasks ?? taskRowsFromItems(items);
41
+ const columns = React.useMemo<DataColumn<Task>[]>(
42
+ () => [
43
+ {
44
+ key: "order",
45
+ header: "Order",
46
+ width: "4.5rem",
47
+ render: (task, index) => (
48
+ <span className="eth-task-wave-table__order">{task.order ?? index + 1}</span>
49
+ )
50
+ },
51
+ {
52
+ key: "title",
53
+ header: "Task",
54
+ width: "22rem",
55
+ render: (task) =>
56
+ onTaskSelect ? (
57
+ <button
58
+ className="eth-task-wave-table__task-button"
59
+ type="button"
60
+ onClick={() => onTaskSelect(task)}
61
+ >
62
+ <span className="eth-task-wave-table__task-title">{task.title}</span>
63
+ </button>
64
+ ) : (
65
+ <span className="eth-task-wave-table__task-title">{task.title}</span>
66
+ )
67
+ },
68
+ {
69
+ key: "status",
70
+ header: "Status",
71
+ width: "12rem",
72
+ render: (task) => (
73
+ <StatusDot
74
+ className="eth-task-wave-table__status"
75
+ status={task.status}
76
+ label={labelForStatus(task.status)}
77
+ />
78
+ )
79
+ },
80
+ {
81
+ key: "assignee",
82
+ header: "Assignee",
83
+ width: "8rem",
84
+ render: (task) =>
85
+ task.assignee ? (
86
+ <span className="eth-task-wave-table__assignee">{task.assignee}</span>
87
+ ) : (
88
+ <EmptyCell />
89
+ )
90
+ },
91
+ {
92
+ key: "dependencies",
93
+ header: "Dependencies",
94
+ width: "7rem",
95
+ align: "center",
96
+ render: (task) => {
97
+ const count = dependencyCount(task);
98
+ return (
99
+ <Badge
100
+ className="eth-task-wave-table__dependency-count"
101
+ severity={count > 0 ? "info" : "neutral"}
102
+ >
103
+ {count}
104
+ </Badge>
105
+ );
106
+ }
107
+ },
108
+ {
109
+ key: "durationMs",
110
+ header: "Duration",
111
+ width: "7rem",
112
+ render: (task) => (
113
+ <span
114
+ className={clsx(
115
+ "eth-task-wave-table__duration",
116
+ task.durationMs == null && "eth-task-wave-table__empty"
117
+ )}
118
+ >
119
+ {formatDuration(task.durationMs)}
120
+ </span>
121
+ )
122
+ }
123
+ ],
124
+ [onTaskSelect]
125
+ );
126
+
127
+ return (
128
+ <Surface
129
+ className={clsx("eth-task-wave-table", className)}
130
+ title={wave?.label ?? title ?? "Task wave"}
131
+ subtitle={wave?.id}
132
+ description={description}
133
+ data-eth-component="TaskWaveTable"
134
+ >
135
+ <DataTable<Task>
136
+ aria-label={
137
+ typeof (wave?.label ?? title) === "string"
138
+ ? `${wave?.label ?? title} tasks`
139
+ : "Wave tasks"
140
+ }
141
+ className="eth-task-wave-table__table"
142
+ rows={rows}
143
+ columns={columns}
144
+ rowKey="id"
145
+ density="compact"
146
+ rowActions={rowActions}
147
+ emptyState={<EmptyState title="No wave tasks" />}
148
+ />
149
+ </Surface>
150
+ );
151
+ }
@@ -0,0 +1 @@
1
+ declare module "*.css";
@@ -0,0 +1,116 @@
1
+ import type { EthOperationalStatus, EthSeverity } from "@echothink-ui/core";
2
+ import { statusLabel } from "@echothink-ui/core";
3
+ import type { RiskLevel, TaskPriority, TaskWave } from "../types";
4
+
5
+ export function severityForStatus(status: EthOperationalStatus): EthSeverity {
6
+ if (status === "running" || status === "queued" || status === "in-progress") return "info";
7
+ if (status === "blocked" || status === "failed") return "danger";
8
+ if (status === "pending-approval" || status === "approval-required" || status === "warning") {
9
+ return "warning";
10
+ }
11
+ if (status === "succeeded" || status === "completed" || status === "synced") return "success";
12
+ return "neutral";
13
+ }
14
+
15
+ export function severityForPriority(priority?: TaskPriority): EthSeverity {
16
+ if (priority === "critical") return "danger";
17
+ if (priority === "high") return "warning";
18
+ if (priority === "medium") return "info";
19
+ return "neutral";
20
+ }
21
+
22
+ export function severityForRisk(riskLevel?: RiskLevel): EthSeverity {
23
+ if (riskLevel === "critical" || riskLevel === "high") return "danger";
24
+ if (riskLevel === "medium") return "warning";
25
+ if (riskLevel === "low") return "info";
26
+ return "neutral";
27
+ }
28
+
29
+ export function labelForStatus(status: EthOperationalStatus) {
30
+ return statusLabel(status);
31
+ }
32
+
33
+ export function formatDateTime(value?: string | Date) {
34
+ if (!value) return "-";
35
+ const date = value instanceof Date ? value : new Date(value);
36
+ if (Number.isNaN(date.valueOf())) return String(value);
37
+ return new Intl.DateTimeFormat(undefined, {
38
+ dateStyle: "medium",
39
+ timeStyle: "short"
40
+ }).format(date);
41
+ }
42
+
43
+ export function relativeTime(value?: string | Date, now = new Date()) {
44
+ if (!value) return "";
45
+ const date = value instanceof Date ? value : new Date(value);
46
+ if (Number.isNaN(date.valueOf())) return "";
47
+ const deltaSeconds = Math.round((date.getTime() - now.getTime()) / 1000);
48
+ const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
49
+ ["year", 31536000],
50
+ ["month", 2592000],
51
+ ["week", 604800],
52
+ ["day", 86400],
53
+ ["hour", 3600],
54
+ ["minute", 60]
55
+ ];
56
+ const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
57
+ for (const [unit, seconds] of units) {
58
+ if (Math.abs(deltaSeconds) >= seconds) {
59
+ return formatter.format(Math.round(deltaSeconds / seconds), unit);
60
+ }
61
+ }
62
+ return formatter.format(deltaSeconds, "second");
63
+ }
64
+
65
+ export function formatDuration(durationMs?: number) {
66
+ if (durationMs == null) return "-";
67
+ if (durationMs < 1000) return `${durationMs}ms`;
68
+ const seconds = Math.round(durationMs / 1000);
69
+ if (seconds < 60) return `${seconds}s`;
70
+ const minutes = Math.floor(seconds / 60);
71
+ const remainder = seconds % 60;
72
+ if (minutes < 60) return remainder ? `${minutes}m ${remainder}s` : `${minutes}m`;
73
+ const hours = Math.floor(minutes / 60);
74
+ const minuteRemainder = minutes % 60;
75
+ return minuteRemainder ? `${hours}h ${minuteRemainder}m` : `${hours}h`;
76
+ }
77
+
78
+ export function clampProgress(value?: number, total = 100) {
79
+ if (value == null || Number.isNaN(value)) return 0;
80
+ if (total <= 0) return 0;
81
+ const percent = total === 100 ? value : (value / total) * 100;
82
+ return Math.max(0, Math.min(100, Math.round(percent)));
83
+ }
84
+
85
+ export function waveProgress(wave?: TaskWave) {
86
+ if (!wave?.totalTasks) return 0;
87
+ const completed = (wave.statuses.completed ?? 0) + (wave.statuses.succeeded ?? 0);
88
+ return clampProgress(completed, wave.totalTasks);
89
+ }
90
+
91
+ export function statusColor(status?: EthOperationalStatus) {
92
+ switch (status) {
93
+ case "completed":
94
+ case "succeeded":
95
+ case "synced":
96
+ case "active":
97
+ return "var(--eth-color-success)";
98
+ case "running":
99
+ case "in-progress":
100
+ case "queued":
101
+ return "var(--eth-color-info)";
102
+ case "blocked":
103
+ case "failed":
104
+ case "approval-required":
105
+ return "var(--eth-color-danger)";
106
+ case "pending-approval":
107
+ case "warning":
108
+ return "var(--eth-color-warning)";
109
+ default:
110
+ return "var(--eth-color-border-strong)";
111
+ }
112
+ }
113
+
114
+ export function fallbackId(prefix: string) {
115
+ return `${prefix}-${Math.random().toString(36).slice(2)}`;
116
+ }
@@ -0,0 +1,316 @@
1
+ import { render, screen, within } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ TaskCard,
5
+ TaskProgressIndicator,
6
+ TaskStatusBadge,
7
+ TaskTable,
8
+ TaskTimeline,
9
+ TaskWaveDAG,
10
+ TaskWaveHeader,
11
+ TaskWaveTable,
12
+ type Task
13
+ } from "./index";
14
+
15
+ const tasks: Task[] = [
16
+ {
17
+ id: "t1",
18
+ title: "Create landing page hero",
19
+ assignee: "JD",
20
+ dueAt: "2026-05-30",
21
+ status: "in-progress",
22
+ priority: "high"
23
+ },
24
+ {
25
+ id: "t2",
26
+ title: "Approve pricing copy",
27
+ assignee: "Legal",
28
+ status: "approval-required"
29
+ },
30
+ {
31
+ id: "t3",
32
+ title: "Wire analytics",
33
+ assignee: "AL",
34
+ status: "queued"
35
+ },
36
+ {
37
+ id: "t4",
38
+ title: "QA pass",
39
+ assignee: "MK",
40
+ status: "blocked"
41
+ }
42
+ ];
43
+
44
+ describe("@echothink-ui/task TaskCard", () => {
45
+ it("renders the task summary, metadata, tags, and actions in one compact surface", () => {
46
+ render(
47
+ <TaskCard
48
+ task={{
49
+ id: "t1",
50
+ title: "Create landing page hero",
51
+ assignee: "JD",
52
+ dueAt: "2026-05-30T20:00:00Z",
53
+ status: "in-progress",
54
+ priority: "high",
55
+ tags: ["q3-launch", "design"],
56
+ description: "Design hero with social-proof modules and animated stat strip."
57
+ }}
58
+ actions={[{ id: "view", label: "View" }]}
59
+ />
60
+ );
61
+
62
+ const card = screen
63
+ .getByRole("heading", { name: "Create landing page hero" })
64
+ .closest('[data-eth-component="TaskCard"]');
65
+
66
+ expect(card?.className).toContain("eth-task-card");
67
+ expect(
68
+ screen.getByText("Design hero with social-proof modules and animated stat strip.")
69
+ ).toBeTruthy();
70
+ expect(card ? within(card).getByText("Assignee") : null).toBeTruthy();
71
+ expect(card ? within(card).getByText("Priority") : null).toBeTruthy();
72
+ expect(card ? within(card).getByText("q3-launch") : null).toBeTruthy();
73
+ expect(screen.getByRole("button", { name: "View" })).toBeTruthy();
74
+ });
75
+ });
76
+
77
+ describe("@echothink-ui/task TaskWaveTable", () => {
78
+ it("renders wave rows with stable table cell affordances", () => {
79
+ const { container } = render(
80
+ <TaskWaveTable
81
+ wave={{
82
+ id: "wave-q3",
83
+ label: "Wave Q3",
84
+ totalTasks: 1,
85
+ statuses: { "in-progress": 1 }
86
+ }}
87
+ tasks={[
88
+ {
89
+ id: "task-1",
90
+ title: "Create landing page hero",
91
+ status: "in-progress",
92
+ assignee: "JD",
93
+ order: 1,
94
+ dependencyCount: 2,
95
+ durationMs: 5400000
96
+ }
97
+ ]}
98
+ />
99
+ );
100
+
101
+ const waveTable = container.querySelector('[data-eth-component="TaskWaveTable"]');
102
+
103
+ expect(waveTable?.className).toContain("eth-task-wave-table");
104
+ expect(waveTable?.querySelector(".eth-task-wave-table__order")?.textContent).toBe("1");
105
+ expect(waveTable?.querySelector(".eth-task-wave-table__task-title")?.textContent).toBe(
106
+ "Create landing page hero"
107
+ );
108
+ expect(waveTable?.querySelector(".eth-task-wave-table__dependency-count")?.textContent).toBe(
109
+ "2"
110
+ );
111
+ expect(waveTable?.querySelector(".eth-task-wave-table__duration")?.textContent).toBe(
112
+ "1h 30m"
113
+ );
114
+ });
115
+ });
116
+
117
+ describe("@echothink-ui/task TaskTimeline", () => {
118
+ it("renders task events as a readable semantic activity timeline", () => {
119
+ const { container } = render(
120
+ <TaskTimeline
121
+ events={[
122
+ {
123
+ id: "created",
124
+ timestamp: "2026-05-25T09:00:00Z",
125
+ actor: "JD",
126
+ kind: "status_change",
127
+ summary: "Created task"
128
+ },
129
+ {
130
+ id: "comment",
131
+ timestamp: "2026-05-26T11:30:00Z",
132
+ actor: "Agent",
133
+ kind: "comment",
134
+ summary: "Drafted initial copy",
135
+ details: "Added the launch message and review checklist."
136
+ }
137
+ ]}
138
+ />
139
+ );
140
+
141
+ const timeline = screen.getByRole("region", { name: "Task timeline" });
142
+ expect(within(timeline).getByRole("list")).toBeTruthy();
143
+ expect(within(timeline).getByText("Created task")).toBeTruthy();
144
+ expect(within(timeline).getByText("Drafted initial copy")).toBeTruthy();
145
+ expect(
146
+ within(timeline).getByText("Added the launch message and review checklist.")
147
+ ).toBeTruthy();
148
+ expect(within(timeline).getByText("By Agent")).toBeTruthy();
149
+
150
+ const times = Array.from(container.querySelectorAll("time"));
151
+ expect(times[0]?.getAttribute("datetime")).toBe("2026-05-25T09:00:00.000Z");
152
+ expect(times[0]?.textContent).not.toContain("2026-05-25T09:00:00.000Z");
153
+ expect(times[0]?.textContent).not.toContain("ago");
154
+
155
+ expect(within(timeline).getByText("Status").closest("li")?.className).toContain(
156
+ "eth-task-timeline__event--status_change"
157
+ );
158
+ expect(within(timeline).getByText("Comment").closest("li")?.className).toContain(
159
+ "eth-task-timeline__event--comment"
160
+ );
161
+ expect(container.querySelector(".eth-task-timeline__marker")?.textContent).toBe("");
162
+ });
163
+ });
164
+
165
+ describe("@echothink-ui/task TaskProgressIndicator", () => {
166
+ it("renders step progress as an ordered workflow with separate labels and statuses", () => {
167
+ const { container } = render(
168
+ <TaskProgressIndicator
169
+ label="Deployment progress"
170
+ steps={[
171
+ { id: "plan", label: "Plan", status: "completed" },
172
+ { id: "build", label: "Build", status: "running" },
173
+ { id: "ship", label: "Ship", status: "not-started" }
174
+ ]}
175
+ />
176
+ );
177
+
178
+ const list = screen.getByRole("list", { name: "Deployment progress" });
179
+ const steps = screen.getAllByRole("listitem");
180
+
181
+ expect(container.querySelector('[data-eth-component="TaskProgressIndicator"]')).toBeTruthy();
182
+ expect(steps).toHaveLength(3);
183
+ expect(within(steps[0]).getByText("Plan").className).toContain(
184
+ "eth-task-progress__step-label"
185
+ );
186
+ expect(within(steps[0]).getByText("Completed").className).toContain(
187
+ "eth-task-progress__step-status"
188
+ );
189
+ expect(steps[1].getAttribute("aria-current")).toBe("step");
190
+ expect(steps[2].getAttribute("aria-current")).toBeNull();
191
+ expect(list.querySelectorAll(".eth-task-progress__connector")).toHaveLength(2);
192
+ expect(container.querySelector(".eth-status-dot")).toBeNull();
193
+ });
194
+ });
195
+
196
+ describe("@echothink-ui/task TaskStatusBadge", () => {
197
+ it("marks task status badges with state-specific semantics and an indicator", () => {
198
+ render(<TaskStatusBadge status="approval-required" aria-label="Task approval required" />);
199
+
200
+ const badge = screen.getByText("Approval Required").closest(".eth-task-status-badge");
201
+
202
+ expect(badge?.className).toContain("eth-task-status-badge--approval-required");
203
+ expect(badge?.getAttribute("data-eth-component")).toBe("TaskStatusBadge");
204
+ expect(badge?.getAttribute("aria-label")).toBe("Task approval required");
205
+ expect(
206
+ badge?.querySelector(".eth-task-status-badge__indicator")?.getAttribute("aria-hidden")
207
+ ).toBe("true");
208
+ });
209
+
210
+ it("allows status copy to be overridden without losing status semantics", () => {
211
+ render(<TaskStatusBadge status="completed" label="Done" />);
212
+
213
+ const badge = screen.getByText("Done").closest(".eth-task-status-badge");
214
+
215
+ expect(badge?.className).toContain("eth-task-status-badge--completed");
216
+ expect(badge?.getAttribute("data-status")).toBe("completed");
217
+ });
218
+ });
219
+
220
+ describe("@echothink-ui/task TaskWaveDAG", () => {
221
+ it("renders a compact accessible dependency graph with a viewport, statuses, and edge labels", () => {
222
+ render(
223
+ <TaskWaveDAG
224
+ title="Launch dependencies"
225
+ nodes={[
226
+ { id: "research", label: "Research", status: "completed" },
227
+ { id: "brief", label: "Brief", status: "in-progress" },
228
+ { id: "legal", label: "Legal gate", status: "pending-approval" },
229
+ { id: "publish", label: "Publish", status: "not-started" }
230
+ ]}
231
+ edges={[
232
+ { from: "research", to: "brief", label: "handoff" },
233
+ { from: "research", to: "legal", label: "approval" },
234
+ { from: "brief", to: "publish" },
235
+ { from: "legal", to: "publish" }
236
+ ]}
237
+ />
238
+ );
239
+
240
+ const graph = screen.getByRole("img", { name: "Launch dependencies" });
241
+
242
+ expect(graph.className.baseVal).toContain("eth-dag");
243
+ expect(graph.closest(".eth-task-wave-dag__viewport")).toBeTruthy();
244
+ expect(graph.querySelector(".eth-dag__canvas")).toBeTruthy();
245
+ expect(screen.getByText("Research")).toBeTruthy();
246
+ expect(screen.getByText("Legal gate")).toBeTruthy();
247
+ expect(screen.getByText("approval")).toBeTruthy();
248
+ expect(screen.getAllByText("Pending Approval").length).toBeGreaterThan(0);
249
+ });
250
+ });
251
+
252
+ describe("@echothink-ui/task TaskTable", () => {
253
+ it("renders a named table with Carbon data-table classes", () => {
254
+ render(<TaskTable tasks={tasks} title="Tasks" />);
255
+
256
+ const table = screen.getByRole("table", { name: "Tasks table" });
257
+
258
+ expect(table.getAttribute("data-eth-component")).toBe("TaskTable");
259
+ expect(table.className).toContain("eth-task-table");
260
+ expect(table.className).toContain("eth-data-table");
261
+ });
262
+
263
+ it("filters rows by operational status when a status filter is supplied", () => {
264
+ render(<TaskTable tasks={tasks} title="Tasks" showFilters statusFilter="blocked" />);
265
+
266
+ expect(screen.getByText("QA pass")).toBeTruthy();
267
+ expect(screen.queryByText("Create landing page hero")).toBeNull();
268
+ expect(screen.getByText("1 of 4 tasks shown")).toBeTruthy();
269
+ });
270
+
271
+ it("renders selectable bulk actions when bulk actions are supplied", () => {
272
+ render(
273
+ <TaskTable
274
+ tasks={tasks}
275
+ title="Tasks"
276
+ selectedRows={["t2"]}
277
+ bulkActions={[{ id: "approve", label: "Approve selected", requiresConfirmation: false }]}
278
+ />
279
+ );
280
+
281
+ expect(screen.getByText("1 row selected")).toBeTruthy();
282
+ expect(screen.getByRole("button", { name: "Approve selected" })).toBeTruthy();
283
+ });
284
+ });
285
+
286
+ describe("@echothink-ui/task TaskWaveHeader", () => {
287
+ it("renders compact wave progress, status counts, and actions", () => {
288
+ const { container } = render(
289
+ <TaskWaveHeader
290
+ wave={{
291
+ id: "wave-q3",
292
+ label: "Wave Q3",
293
+ totalTasks: 4,
294
+ statuses: { completed: 1, running: 1, "approval-required": 1, queued: 1 },
295
+ startedAt: "2026-05-25T08:00:00",
296
+ eta: "2026-06-01T08:00:00"
297
+ }}
298
+ actions={[{ id: "pause", label: "Pause wave" }]}
299
+ />
300
+ );
301
+
302
+ const header = screen
303
+ .getByRole("heading", { name: "Wave Q3" })
304
+ .closest('[data-eth-component="TaskWaveHeader"]');
305
+
306
+ expect(header?.className).toContain("eth-task-wave-header");
307
+ expect(header ? within(header).getByText("Wave progress") : null).toBeTruthy();
308
+ expect(container.querySelector(".eth-task-wave-header__count--queued")?.textContent).toContain(
309
+ "Queued: 1"
310
+ );
311
+ expect(
312
+ container.querySelector(".eth-task-wave-header__count--approval-required")?.textContent
313
+ ).toContain("Approval Required: 1");
314
+ expect(screen.getByRole("button", { name: "Pause wave" })).toBeTruthy();
315
+ });
316
+ });
package/src/index.tsx ADDED
@@ -0,0 +1,90 @@
1
+ import "./styles.css";
2
+
3
+ export { BackendThinkingChain } from "./components/BackendThinkingChain";
4
+ export { BlockingReasonPanel } from "./components/BlockingReasonPanel";
5
+ export { DAGEdge } from "./components/DAGEdge";
6
+ export { DAGLegend } from "./components/DAGLegend";
7
+ export { DAGNode } from "./components/DAGNode";
8
+ export { DecisionRequiredPanel } from "./components/DecisionRequiredPanel";
9
+ export { HumanInterventionPanel } from "./components/HumanInterventionPanel";
10
+ export { TaskApprovalPanel } from "./components/TaskApprovalPanel";
11
+ export { TaskCard } from "./components/TaskCard";
12
+ export { TaskDependencyList } from "./components/TaskDependencyList";
13
+ export { TaskDetailPanel } from "./components/TaskDetailPanel";
14
+ export { TaskHandoffPanel } from "./components/TaskHandoffPanel";
15
+ export { TaskProgressIndicator } from "./components/TaskProgressIndicator";
16
+ export { TaskRetryPanel } from "./components/TaskRetryPanel";
17
+ export { TaskRunLog } from "./components/TaskRunLog";
18
+ export { TaskStatusBadge } from "./components/TaskStatusBadge";
19
+ export { TaskTable } from "./components/TaskTable";
20
+ export { TaskTimeline } from "./components/TaskTimeline";
21
+ export { TaskWaveDAG } from "./components/TaskWaveDAG";
22
+ export { TaskWaveHeader } from "./components/TaskWaveHeader";
23
+ export { TaskWaveTable } from "./components/TaskWaveTable";
24
+ export { MobileTaskShell } from "./components/MobileTaskShell";
25
+ export type { MobileTaskShellProps } from "./components/MobileTaskShell";
26
+ export type {
27
+ BackendThinkingChainProps,
28
+ BackendThinkingStep,
29
+ BlockingReasonPanelProps,
30
+ DAGEdgeLegendItem,
31
+ DAGEdgeLegendVariant,
32
+ DAGEdgeModel,
33
+ DAGEdgeProps,
34
+ DAGLegendProps,
35
+ DAGNodeModel,
36
+ DAGNodeProps,
37
+ DecisionEvidence,
38
+ DecisionOption,
39
+ DecisionRequiredPanelProps,
40
+ HumanInterventionPanelProps,
41
+ PositionedDAGNode,
42
+ RiskLevel,
43
+ Task,
44
+ TaskApprovalPanelProps,
45
+ TaskCardProps,
46
+ TaskDependency,
47
+ TaskDependencyListProps,
48
+ TaskDetailPanelProps,
49
+ TaskHandoffPanelProps,
50
+ TaskPriority,
51
+ TaskProgressIndicatorProps,
52
+ TaskRetryPanelProps,
53
+ TaskRunLogEntry,
54
+ TaskRunLogProps,
55
+ TaskStatusBadgeProps,
56
+ TaskStep,
57
+ TaskTableProps,
58
+ TaskTimelineEvent,
59
+ TaskTimelineProps,
60
+ TaskWave,
61
+ TaskWaveDAGProps,
62
+ TaskWaveHeaderProps,
63
+ TaskWaveTableProps
64
+ } from "./types";
65
+
66
+ export const TaskComponentNames = [
67
+ "TaskCard",
68
+ "TaskTable",
69
+ "TaskDetailPanel",
70
+ "TaskStatusBadge",
71
+ "TaskProgressIndicator",
72
+ "TaskDependencyList",
73
+ "TaskTimeline",
74
+ "TaskWaveTable",
75
+ "TaskWaveHeader",
76
+ "TaskWaveDAG",
77
+ "DAGNode",
78
+ "DAGEdge",
79
+ "DAGLegend",
80
+ "TaskApprovalPanel",
81
+ "DecisionRequiredPanel",
82
+ "BackendThinkingChain",
83
+ "HumanInterventionPanel",
84
+ "BlockingReasonPanel",
85
+ "TaskHandoffPanel",
86
+ "TaskRunLog",
87
+ "TaskRetryPanel",
88
+ "MobileTaskShell"
89
+ ] as const;
90
+ export type TaskComponentName = (typeof TaskComponentNames)[number];