@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.
- package/README.md +5 -0
- package/dist/components/KanbanBoard.d.ts +8 -0
- package/dist/components/KanbanColumn.d.ts +8 -0
- package/dist/components/TaskCard.d.ts +6 -0
- package/dist/components/TaskDependencyList.d.ts +6 -0
- package/dist/components/TaskTable.d.ts +9 -0
- package/dist/components/TaskTimeline.d.ts +6 -0
- package/dist/components/TodoItem.d.ts +7 -0
- package/dist/components/TodoList.d.ts +8 -0
- package/dist/index.cjs +590 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +500 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +545 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +49 -0
- package/dist/utils.d.ts +3 -0
- package/package.json +43 -0
- package/src/components/KanbanBoard.tsx +161 -0
- package/src/components/KanbanColumn.tsx +105 -0
- package/src/components/TaskCard.tsx +74 -0
- package/src/components/TaskDependencyList.tsx +56 -0
- package/src/components/TaskTable.tsx +69 -0
- package/src/components/TaskTimeline.tsx +40 -0
- package/src/components/TodoItem.test.tsx +46 -0
- package/src/components/TodoItem.tsx +62 -0
- package/src/components/TodoList.tsx +65 -0
- package/src/index.test.tsx +46 -0
- package/src/index.tsx +26 -0
- package/src/styles.css +591 -0
- package/src/types.ts +56 -0
- package/src/utils.ts +26 -0
|
@@ -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];
|