@echothink-ui/todo 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.
@@ -0,0 +1,62 @@
1
+ import * as React from "react";
2
+ import { Badge, Checkbox, Tag } from "@echothink-ui/core";
3
+ import type { TodoTaskItem } from "../types";
4
+ import { formatDateTime, priorityLabel, prioritySeverity } from "../utils";
5
+
6
+ export interface TodoItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onToggle"> {
7
+ item: TodoTaskItem;
8
+ onToggle?: (id: string) => void;
9
+ }
10
+
11
+ function validDateTimeAttribute(value: string | undefined) {
12
+ if (!value) return undefined;
13
+ return Number.isNaN(new Date(value).getTime()) ? undefined : value;
14
+ }
15
+
16
+ export function TodoItem({ item, onToggle, className, ...props }: TodoItemProps) {
17
+ const hasMeta = Boolean(item.assignee || item.priority || item.dueAt);
18
+ const dueDateTime = validDateTimeAttribute(item.dueAt);
19
+ const itemClassName = [
20
+ "eth-todo-item",
21
+ hasMeta ? "eth-todo-item--has-meta" : "",
22
+ item.done ? "eth-todo-item--done" : "",
23
+ className ?? ""
24
+ ]
25
+ .filter(Boolean)
26
+ .join(" ");
27
+
28
+ return (
29
+ <div
30
+ {...props}
31
+ className={itemClassName}
32
+ data-priority={item.priority}
33
+ data-state={item.done ? "done" : "open"}
34
+ data-eth-component="TodoItem"
35
+ >
36
+ <div className="eth-todo-item__content eth-todo-item__primary">
37
+ <Checkbox
38
+ checked={item.done}
39
+ className="eth-todo-item__checkbox"
40
+ label={<span className="eth-todo-item__label">{item.label}</span>}
41
+ onChange={() => onToggle?.(item.id)}
42
+ />
43
+ </div>
44
+ {hasMeta ? (
45
+ <div className="eth-todo-item__meta" role="group" aria-label="Task details">
46
+ {item.priority ? (
47
+ <Badge className="eth-todo-item__tag" severity={prioritySeverity(item.priority)}>
48
+ {priorityLabel(item.priority)}
49
+ </Badge>
50
+ ) : null}
51
+ {item.assignee ? <Tag className="eth-todo-item__tag">{item.assignee}</Tag> : null}
52
+ {item.dueAt ? (
53
+ <span className="eth-todo-item__due">
54
+ <span className="eth-todo-item__due-label">Due</span>
55
+ <time dateTime={dueDateTime}>{formatDateTime(item.dueAt)}</time>
56
+ </span>
57
+ ) : null}
58
+ </div>
59
+ ) : null}
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,65 @@
1
+ import * as React from "react";
2
+ import { Button, Panel, TextInput } from "@echothink-ui/core";
3
+ import { PlusIcon } from "@echothink-ui/icons";
4
+ import type { TodoTaskItem } from "../types";
5
+ import { TodoItem } from "./TodoItem";
6
+
7
+ export interface TodoListProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onToggle"> {
8
+ items?: TodoTaskItem[];
9
+ onToggle?: (id: string) => void;
10
+ onAdd?: (label: string) => void;
11
+ }
12
+
13
+ export function TodoList({ items = [], onToggle, onAdd, className, ...props }: TodoListProps) {
14
+ const [label, setLabel] = React.useState("");
15
+ const completedCount = items.filter((item) => item.done).length;
16
+ const openCount = items.length - completedCount;
17
+ const summary = items.length
18
+ ? `${openCount} open / ${completedCount} done`
19
+ : "No tasks yet";
20
+
21
+ const submit = (event: React.FormEvent<HTMLFormElement>) => {
22
+ event.preventDefault();
23
+ if (!label.trim() || !onAdd) return;
24
+ onAdd(label.trim());
25
+ setLabel("");
26
+ };
27
+
28
+ return (
29
+ <Panel
30
+ {...props}
31
+ className={`eth-todo-list ${className ?? ""}`}
32
+ title="To-do"
33
+ subtitle={summary}
34
+ data-eth-component="TodoList"
35
+ >
36
+ {items.length ? (
37
+ <div
38
+ className="eth-todo-list__items"
39
+ role="list"
40
+ aria-label="Tasks"
41
+ >
42
+ {items.map((item) => (
43
+ <TodoItem key={item.id} item={item} onToggle={onToggle} role="listitem" />
44
+ ))}
45
+ </div>
46
+ ) : (
47
+ <p className="eth-todo-list__empty">No tasks have been added.</p>
48
+ )}
49
+ {onAdd ? (
50
+ <form className="eth-todo-list__add" aria-label="Add task" onSubmit={submit}>
51
+ <TextInput
52
+ value={label}
53
+ labelText="New task"
54
+ hideLabel
55
+ placeholder="Add a task"
56
+ onChange={(event) => setLabel(event.currentTarget.value)}
57
+ />
58
+ <Button type="submit" icon={<PlusIcon />} disabled={!label.trim()}>
59
+ Add
60
+ </Button>
61
+ </form>
62
+ ) : null}
63
+ </Panel>
64
+ );
65
+ }
@@ -0,0 +1,46 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { TodoList } from "./index";
4
+
5
+ const items = [
6
+ {
7
+ id: "approve",
8
+ label: "Approve campaign brief",
9
+ done: false,
10
+ dueAt: "2026-05-30T17:00:00+08:00",
11
+ assignee: "AL",
12
+ priority: "high"
13
+ },
14
+ {
15
+ id: "tag-docs",
16
+ label: "Tag new docs",
17
+ done: true,
18
+ assignee: "MK"
19
+ }
20
+ ];
21
+
22
+ describe("@echothink-ui/todo TodoList", () => {
23
+ it("renders a named task list with completion summary", () => {
24
+ render(<TodoList items={items} onAdd={vi.fn()} />);
25
+
26
+ const list = screen.getByRole("list", { name: "Tasks" });
27
+
28
+ expect(list.className).toContain("eth-todo-list__items");
29
+ expect(screen.getByText("1 open / 1 done")).toBeTruthy();
30
+ expect(screen.getByText("Approve campaign brief")).toBeTruthy();
31
+ expect(screen.getByText("AL")).toBeTruthy();
32
+ });
33
+
34
+ it("submits trimmed task labels through the add form", () => {
35
+ const onAdd = vi.fn();
36
+
37
+ render(<TodoList items={items} onAdd={onAdd} />);
38
+
39
+ fireEvent.change(screen.getByLabelText("New task"), {
40
+ target: { value: " Follow up with finance " }
41
+ });
42
+ fireEvent.click(screen.getByRole("button", { name: "Add" }));
43
+
44
+ expect(onAdd).toHaveBeenCalledWith("Follow up with finance");
45
+ });
46
+ });
package/src/index.tsx ADDED
@@ -0,0 +1,26 @@
1
+ import "./styles.css";
2
+
3
+ export * from "./types";
4
+ export { TodoList, type TodoListProps } from "./components/TodoList";
5
+ export { TodoItem, type TodoItemProps } from "./components/TodoItem";
6
+ export { KanbanBoard, type KanbanBoardProps } from "./components/KanbanBoard";
7
+ export { KanbanColumn, type KanbanColumnProps } from "./components/KanbanColumn";
8
+ export { TaskCard, type TaskCardProps } from "./components/TaskCard";
9
+ export { TaskTable, type TaskTableProps } from "./components/TaskTable";
10
+ export {
11
+ TaskDependencyList,
12
+ type TaskDependencyListProps
13
+ } from "./components/TaskDependencyList";
14
+ export { TaskTimeline, type TaskTimelineProps } from "./components/TaskTimeline";
15
+
16
+ export const TodoComponentNames = [
17
+ "TodoList",
18
+ "TodoItem",
19
+ "KanbanBoard",
20
+ "KanbanColumn",
21
+ "TaskCard",
22
+ "TaskTable",
23
+ "TaskDependencyList",
24
+ "TaskTimeline"
25
+ ] as const;
26
+ export type TodoComponentName = (typeof TodoComponentNames)[number];