@aphrody/m3-ai 3.3.1 → 3.3.4

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 (48) hide show
  1. package/README.md +7 -7
  2. package/dist/A2APeerInspector.d.ts +41 -0
  3. package/dist/A2APeerInspector.d.ts.map +1 -0
  4. package/dist/A2ATaskTimeline.d.ts +28 -0
  5. package/dist/A2ATaskTimeline.d.ts.map +1 -0
  6. package/dist/AgUiEventStream.d.ts +87 -0
  7. package/dist/AgUiEventStream.d.ts.map +1 -0
  8. package/dist/GeminiAccountMenu.d.ts +55 -0
  9. package/dist/GeminiAccountMenu.d.ts.map +1 -0
  10. package/dist/GeminiPersonalContextEdit.d.ts +29 -0
  11. package/dist/GeminiPersonalContextEdit.d.ts.map +1 -0
  12. package/dist/GoogleSpotlightBar.d.ts +32 -0
  13. package/dist/GoogleSpotlightBar.d.ts.map +1 -0
  14. package/dist/Markdown.d.ts +2 -1
  15. package/dist/Markdown.d.ts.map +1 -1
  16. package/dist/McpAppCard.d.ts +24 -0
  17. package/dist/McpAppCard.d.ts.map +1 -0
  18. package/dist/ModelSelector.d.ts.map +1 -1
  19. package/dist/TaskRow.d.ts.map +1 -1
  20. package/dist/ToolCall.d.ts.map +1 -1
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3629 -1950
  24. package/dist/index.js.map +32 -1
  25. package/package.json +17 -20
  26. package/src/A2APeerInspector.tsx +378 -0
  27. package/src/A2ATaskTimeline.tsx +218 -0
  28. package/src/AgUiEventStream.ts +488 -0
  29. package/src/ChatComposer.tsx +239 -0
  30. package/src/GeminiAccountMenu.tsx +212 -0
  31. package/src/GeminiPersonalContextEdit.tsx +147 -0
  32. package/src/GoogleSpotlightBar.tsx +165 -0
  33. package/src/InlineBranchCard.tsx +97 -0
  34. package/src/InlineWorkerCard.tsx +182 -0
  35. package/src/Markdown.tsx +701 -0
  36. package/src/McpAppCard.tsx +161 -0
  37. package/src/MessageBubble.tsx +95 -0
  38. package/src/ModelSelector.tsx +300 -0
  39. package/src/TaskCreateForm.tsx +123 -0
  40. package/src/TaskDetail.tsx +224 -0
  41. package/src/TaskList.tsx +136 -0
  42. package/src/TaskPriorityIcon.tsx +39 -0
  43. package/src/TaskRow.tsx +177 -0
  44. package/src/TaskStatusIcon.tsx +39 -0
  45. package/src/ToolCall.tsx +639 -0
  46. package/src/index.ts +107 -0
  47. package/src/internal.tsx +107 -0
  48. package/src/types.ts +212 -0
@@ -0,0 +1,224 @@
1
+ // SPDX-License-Identifier: Apache-2.0 AND MIT
2
+ // Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/TaskDetail.tsx), MIT, rebuilt on Material Design 3.
3
+
4
+ import { Button, CircleButton, Icon, Select, SelectOption } from "@aphrody/m3-primitives";
5
+ import { clsx } from "clsx";
6
+
7
+ import { focusRing } from "./internal";
8
+ import { Markdown } from "./Markdown";
9
+ import { TaskPriorityIcon } from "./TaskPriorityIcon";
10
+ import { TaskStatusIcon } from "./TaskStatusIcon";
11
+ import type { Task, TaskStatus } from "./types";
12
+ import { TASK_PRIORITY_LABEL, TASK_STATUS_LABEL, TASK_STATUS_ORDER } from "./types";
13
+
14
+ export interface TaskDetailProps {
15
+ task: Task;
16
+ resolveAgentName?: (agentId: string) => string;
17
+ onStatusChange?: (task: Task, status: TaskStatus) => void;
18
+ onSubtaskToggle?: (task: Task, index: number, completed: boolean) => void;
19
+ onDelete?: (task: Task) => void;
20
+ onClose?: () => void;
21
+ className?: string;
22
+ }
23
+
24
+ function formatDate(iso: string) {
25
+ return new Date(iso).toLocaleDateString(undefined, {
26
+ year: "numeric",
27
+ month: "short",
28
+ day: "numeric",
29
+ hour: "2-digit",
30
+ minute: "2-digit",
31
+ });
32
+ }
33
+
34
+ function Property({ label, children }: { label: string; children: React.ReactNode }) {
35
+ return (
36
+ <div className="flex min-w-0 flex-col gap-0.5">
37
+ <span className="text-label-medium text-on-surface-variant">{label}</span>
38
+ <span className="flex min-h-8 items-center gap-1.5 text-body-medium text-on-surface">
39
+ {children}
40
+ </span>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ /**
46
+ * A task on an M3 surface: `text-headline-small` title, a property grid, subtasks as an M3 list
47
+ * with checkboxes, markdown description, and actions as M3 buttons (filled for the primary
48
+ * transition, text for the others).
49
+ */
50
+ export function TaskDetail({
51
+ task,
52
+ resolveAgentName,
53
+ onStatusChange,
54
+ onSubtaskToggle,
55
+ onDelete,
56
+ onClose,
57
+ className,
58
+ }: TaskDetailProps) {
59
+ const ownerName = resolveAgentName?.(task.owner_agent_id) ?? task.owner_agent_id;
60
+ const assigneeName = resolveAgentName?.(task.assigned_agent_id) ?? task.assigned_agent_id;
61
+ const completedCount = task.subtasks.filter((s) => s.completed).length;
62
+
63
+ return (
64
+ <div className={clsx("flex flex-col gap-6 bg-surface p-6 text-on-surface", className)}>
65
+ {/* Header */}
66
+ <div className="flex flex-col gap-2">
67
+ <div className="flex items-center gap-2">
68
+ <div className="flex flex-1 items-center gap-2">
69
+ <TaskStatusIcon status={task.status} size={20} />
70
+ <TaskPriorityIcon priority={task.priority} size={18} />
71
+ <span className="font-code text-body-small text-on-surface-variant">
72
+ SPC-{task.task_number}
73
+ </span>
74
+ </div>
75
+ {onClose && (
76
+ <CircleButton icon="close" variant="standard" onClick={onClose} aria-label="Close" />
77
+ )}
78
+ </div>
79
+ <h2 className="text-headline-small text-on-surface">{task.title}</h2>
80
+ </div>
81
+
82
+ {/* Properties */}
83
+ <div className="grid grid-cols-2 gap-x-6 gap-y-4 medium:grid-cols-4">
84
+ <Property label="Status">
85
+ {onStatusChange ? (
86
+ <Select
87
+ value={task.status}
88
+ size="md"
89
+ onChange={(value) => onStatusChange(task, value as TaskStatus)}
90
+ >
91
+ {TASK_STATUS_ORDER.map((s) => (
92
+ <SelectOption key={s} value={s}>
93
+ {TASK_STATUS_LABEL[s]}
94
+ </SelectOption>
95
+ ))}
96
+ </Select>
97
+ ) : (
98
+ TASK_STATUS_LABEL[task.status]
99
+ )}
100
+ </Property>
101
+ <Property label="Priority">
102
+ <TaskPriorityIcon priority={task.priority} size={18} label="" />
103
+ {TASK_PRIORITY_LABEL[task.priority]}
104
+ </Property>
105
+ <Property label="Owner">
106
+ <span className="truncate">{ownerName}</span>
107
+ </Property>
108
+ <Property label="Assigned">
109
+ <span className="truncate">{assigneeName}</span>
110
+ </Property>
111
+ </div>
112
+
113
+ {/* Subtasks */}
114
+ {task.subtasks.length > 0 && (
115
+ <section className="flex flex-col gap-2">
116
+ <div className="flex items-center justify-between">
117
+ <h3 className="text-title-small text-on-surface">Subtasks</h3>
118
+ <span className="text-body-small text-on-surface-variant">
119
+ {completedCount}/{task.subtasks.length}
120
+ </span>
121
+ </div>
122
+ <ul className="m-0 list-none overflow-hidden rounded-medium border border-outline-variant p-0">
123
+ {task.subtasks.map((subtask, i) => (
124
+ <li key={subtask.title}>
125
+ <button
126
+ type="button"
127
+ // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role -- M3 list item with a leading checkbox: the whole 48 dp row toggles and carries the state layer; a native checkbox input cannot host the row.
128
+ role="checkbox"
129
+ aria-checked={subtask.completed}
130
+ onClick={() => onSubtaskToggle?.(task, i, !subtask.completed)}
131
+ className={clsx(
132
+ "flex min-h-12 w-full items-center gap-4 px-4 py-2 text-left text-body-large",
133
+ "transition-colors duration-200 ease-standard hover:bg-on-surface/8 active:bg-on-surface/10",
134
+ focusRing,
135
+ "focus-visible:outline-offset-[-3px]",
136
+ i > 0 && "border-t border-outline-variant",
137
+ )}
138
+ >
139
+ <span
140
+ className={clsx(
141
+ "flex size-[18px] shrink-0 items-center justify-center rounded-[2px] transition-colors duration-200 ease-standard",
142
+ subtask.completed
143
+ ? "bg-primary text-on-primary"
144
+ : "border-2 border-on-surface-variant",
145
+ )}
146
+ >
147
+ {subtask.completed && <Icon name="check" size={16} weight={700} />}
148
+ </span>
149
+ <span
150
+ className={clsx(
151
+ "min-w-0 flex-1",
152
+ subtask.completed
153
+ ? "text-on-surface-variant line-through"
154
+ : "text-on-surface",
155
+ )}
156
+ >
157
+ {subtask.title}
158
+ </span>
159
+ </button>
160
+ </li>
161
+ ))}
162
+ </ul>
163
+ </section>
164
+ )}
165
+
166
+ {/* Description */}
167
+ <section className="flex flex-col gap-2">
168
+ <h3 className="text-title-small text-on-surface">Description</h3>
169
+ {task.description ? (
170
+ <Markdown content={task.description} size="medium" />
171
+ ) : (
172
+ <p className="text-body-medium text-on-surface-variant">No description</p>
173
+ )}
174
+ </section>
175
+
176
+ {/* Timestamps */}
177
+ <div className="flex flex-col gap-1 text-body-small text-on-surface-variant">
178
+ <span>Created {formatDate(task.created_at)}</span>
179
+ <span>Updated {formatDate(task.updated_at)}</span>
180
+ {task.completed_at && <span>Completed {formatDate(task.completed_at)}</span>}
181
+ </div>
182
+
183
+ {/* Actions */}
184
+ {(onStatusChange || onDelete) && (
185
+ <div className="flex flex-wrap items-center gap-2">
186
+ {onStatusChange && task.status === "pending_approval" && (
187
+ <Button variant="filled" size="lg" onClick={() => onStatusChange(task, "ready")}>
188
+ Approve
189
+ </Button>
190
+ )}
191
+ {onStatusChange && task.status === "backlog" && (
192
+ <Button variant="filled" size="lg" onClick={() => onStatusChange(task, "in_progress")}>
193
+ Execute
194
+ </Button>
195
+ )}
196
+ {onStatusChange && (task.status === "in_progress" || task.status === "ready") && (
197
+ <Button variant="filled" size="lg" onClick={() => onStatusChange(task, "done")}>
198
+ Mark Done
199
+ </Button>
200
+ )}
201
+ {onStatusChange && task.status === "done" && (
202
+ <Button variant="text" size="lg" onClick={() => onStatusChange(task, "ready")}>
203
+ Reopen
204
+ </Button>
205
+ )}
206
+ {onDelete && (
207
+ <button
208
+ type="button"
209
+ onClick={() => onDelete(task)}
210
+ className={clsx(
211
+ "inline-flex h-10 items-center justify-center gap-2 rounded-full px-3 text-label-large text-error",
212
+ "transition-colors duration-200 ease-standard hover:bg-error/8 active:bg-error/10",
213
+ focusRing,
214
+ )}
215
+ >
216
+ <Icon name="delete" size={18} />
217
+ Delete
218
+ </button>
219
+ )}
220
+ </div>
221
+ )}
222
+ </div>
223
+ );
224
+ }
@@ -0,0 +1,136 @@
1
+ // SPDX-License-Identifier: Apache-2.0 AND MIT
2
+ // Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/TaskList.tsx), MIT, rebuilt on Material Design 3.
3
+
4
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger, Icon } from "@aphrody/m3-primitives";
5
+ import { clsx } from "clsx";
6
+ import { useMemo } from "react";
7
+
8
+ import { focusRing } from "./internal";
9
+ import { TASK_ROW_GRID, TaskRow } from "./TaskRow";
10
+ import { TaskStatusIcon } from "./TaskStatusIcon";
11
+ import type { Task, TaskStatus } from "./types";
12
+ import { TASK_STATUS_LABEL, TASK_STATUS_ORDER } from "./types";
13
+
14
+ export interface TaskListProps {
15
+ tasks: Task[];
16
+ groups?: TaskStatus[];
17
+ collapsedGroups?: Set<TaskStatus>;
18
+ onToggleGroup?: (status: TaskStatus) => void;
19
+ activeTaskId?: string;
20
+ onTaskClick: (task: Task) => void;
21
+ onStatusChange?: (task: Task, status: TaskStatus) => void;
22
+ onDelete?: (task: Task) => void;
23
+ resolveAgentName?: (agentId: string) => string;
24
+ className?: string;
25
+ }
26
+
27
+ const GRID_STYLE = {
28
+ gridTemplateColumns: TASK_ROW_GRID,
29
+ paddingLeft: 16,
30
+ paddingRight: 8,
31
+ gap: 12,
32
+ } as const;
33
+
34
+ /**
35
+ * Tasks grouped by status as an M3 list: a column header in `text-label-medium`, collapsible
36
+ * section headers in `text-title-small`, `outline-variant` dividers.
37
+ */
38
+ export function TaskList({
39
+ tasks,
40
+ groups = TASK_STATUS_ORDER,
41
+ collapsedGroups,
42
+ onToggleGroup,
43
+ activeTaskId,
44
+ onTaskClick,
45
+ onStatusChange,
46
+ onDelete,
47
+ resolveAgentName,
48
+ className,
49
+ }: TaskListProps) {
50
+ const grouped = useMemo(() => {
51
+ const map = new Map<TaskStatus, Task[]>();
52
+ for (const status of groups) map.set(status, []);
53
+ for (const task of tasks) {
54
+ const bucket = map.get(task.status);
55
+ if (bucket) bucket.push(task);
56
+ }
57
+ return map;
58
+ }, [tasks, groups]);
59
+
60
+ return (
61
+ <div className={clsx("flex flex-col bg-surface text-on-surface", className)}>
62
+ {/* Column header */}
63
+ <div
64
+ className="grid items-center border-b border-outline-variant text-label-medium text-on-surface-variant"
65
+ style={{ ...GRID_STYLE, height: 32 }}
66
+ >
67
+ <span />
68
+ <span />
69
+ <span>ID</span>
70
+ <span>Title</span>
71
+ <span className="text-right">Subs</span>
72
+ <span className="text-right">Assignee</span>
73
+ <span />
74
+ </div>
75
+
76
+ {/* Groups */}
77
+ {groups.map((status) => {
78
+ const items = grouped.get(status) ?? [];
79
+ const isOpen = !collapsedGroups?.has(status);
80
+
81
+ return (
82
+ <Collapsible key={status} open={isOpen} onOpenChange={() => onToggleGroup?.(status)}>
83
+ <CollapsibleTrigger
84
+ className={clsx(
85
+ "grid w-full items-center rounded-none border-b border-outline-variant bg-surface-container-low",
86
+ "text-title-small text-on-surface transition-colors duration-200 ease-standard",
87
+ "hover:bg-on-surface/8 active:bg-on-surface/10",
88
+ focusRing,
89
+ "focus-visible:outline-offset-[-3px]",
90
+ )}
91
+ style={{ ...GRID_STYLE, height: 40 }}
92
+ >
93
+ <span className="flex items-center justify-center text-on-surface-variant">
94
+ <Icon
95
+ name="chevron_right"
96
+ size={20}
97
+ className={clsx(
98
+ "shrink-0 transition-transform duration-300 ease-emphasized",
99
+ isOpen && "rotate-90",
100
+ )}
101
+ />
102
+ </span>
103
+ <span className="flex items-center justify-center">
104
+ <TaskStatusIcon status={status} size={18} label="" />
105
+ </span>
106
+ <span className="col-span-5 flex items-center gap-1.5 text-left">
107
+ {TASK_STATUS_LABEL[status]}
108
+ <span className="text-body-small text-on-surface-variant">({items.length})</span>
109
+ </span>
110
+ </CollapsibleTrigger>
111
+
112
+ <CollapsibleContent>
113
+ {items.length > 0 ? (
114
+ items.map((task) => (
115
+ <TaskRow
116
+ key={task.id}
117
+ task={task}
118
+ isActive={task.id === activeTaskId}
119
+ onClick={onTaskClick}
120
+ onStatusChange={onStatusChange}
121
+ onDelete={onDelete}
122
+ resolveAgentName={resolveAgentName}
123
+ />
124
+ ))
125
+ ) : (
126
+ <div className="border-b border-outline-variant px-4 py-3 text-body-small text-on-surface-variant">
127
+ No tasks
128
+ </div>
129
+ )}
130
+ </CollapsibleContent>
131
+ </Collapsible>
132
+ );
133
+ })}
134
+ </div>
135
+ );
136
+ }
@@ -0,0 +1,39 @@
1
+ // SPDX-License-Identifier: Apache-2.0 AND MIT
2
+ // Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/TaskPriorityIcon.tsx), MIT, rebuilt on Material Design 3.
3
+
4
+ import { Icon } from "@aphrody/m3-primitives";
5
+ import { clsx } from "clsx";
6
+
7
+ import type { TaskPriority } from "./types";
8
+ import { TASK_PRIORITY_LABEL } from "./types";
9
+
10
+ /** Material Symbols glyph and M3 color role per priority. */
11
+ const config: Record<TaskPriority, { icon: string; filled: boolean; color: string }> = {
12
+ critical: { icon: "priority_high", filled: true, color: "text-error" },
13
+ high: { icon: "keyboard_double_arrow_up", filled: false, color: "text-warning" },
14
+ medium: { icon: "drag_handle", filled: false, color: "text-primary" },
15
+ low: { icon: "keyboard_arrow_down", filled: false, color: "text-outline" },
16
+ };
17
+
18
+ export interface TaskPriorityIconProps {
19
+ priority: TaskPriority;
20
+ size?: number;
21
+ className?: string;
22
+ /** Accessible label; defaults to the priority label. Pass `""` for a decorative icon. */
23
+ label?: string;
24
+ }
25
+
26
+ export function TaskPriorityIcon({ priority, size = 14, className, label }: TaskPriorityIconProps) {
27
+ const { icon, filled, color } = config[priority];
28
+ return (
29
+ <Icon
30
+ name={icon}
31
+ size={size}
32
+ filled={filled}
33
+ weight={600}
34
+ label={label ?? `${TASK_PRIORITY_LABEL[priority]} priority`}
35
+ data-priority={priority}
36
+ className={clsx(color, className)}
37
+ />
38
+ );
39
+ }
@@ -0,0 +1,177 @@
1
+ // SPDX-License-Identifier: Apache-2.0 AND MIT
2
+ // Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/TaskRow.tsx), MIT, rebuilt on Material Design 3.
3
+
4
+ import { DropdownMenu, Icon } from "@aphrody/m3-primitives";
5
+ import { clsx } from "clsx";
6
+
7
+ import { focusRing } from "./internal";
8
+ import { TaskPriorityIcon } from "./TaskPriorityIcon";
9
+ import { TaskStatusIcon } from "./TaskStatusIcon";
10
+ import type { Task, TaskStatus } from "./types";
11
+ import { TASK_STATUS_LABEL, TASK_STATUS_ORDER } from "./types";
12
+
13
+ export interface TaskRowProps {
14
+ task: Task;
15
+ onClick?: (task: Task) => void;
16
+ isActive?: boolean;
17
+ resolveAgentName?: (agentId: string) => string;
18
+ onStatusChange?: (task: Task, status: TaskStatus) => void;
19
+ onDelete?: (task: Task) => void;
20
+ className?: string;
21
+ }
22
+
23
+ /** Column template shared with the {@link TaskList} header. */
24
+ export const TASK_ROW_GRID = "20px 18px 72px 1fr 56px 96px 40px";
25
+
26
+ /** M3 list item height at desktop density (56 dp baseline, two density steps). */
27
+ export const TASK_ROW_HEIGHT = 48;
28
+
29
+ /**
30
+ * A task as a dense M3 list item: leading status and priority icons, `text-body-large` headline,
31
+ * `text-body-small` supporting columns, `on-surface` 8 % hover, `secondary-container` selection
32
+ * and a trailing overflow-menu icon button.
33
+ */
34
+ export function TaskRow({
35
+ task,
36
+ onClick,
37
+ isActive,
38
+ resolveAgentName,
39
+ onStatusChange,
40
+ onDelete,
41
+ className,
42
+ }: TaskRowProps) {
43
+ const completedSubtasks = task.subtasks.filter((s) => s.completed).length;
44
+ const assigneeName = resolveAgentName?.(task.assigned_agent_id) ?? task.assigned_agent_id;
45
+ const hasActions = onStatusChange || onDelete;
46
+ const isRowInteractive = typeof onClick === "function";
47
+
48
+ const rowClassName = clsx(
49
+ "task-row group grid w-full items-center border-b border-outline-variant text-left",
50
+ "transition-colors duration-200 ease-standard",
51
+ isActive
52
+ ? "bg-secondary-container text-on-secondary-container"
53
+ : "bg-transparent text-on-surface hover:bg-on-surface/8",
54
+ isRowInteractive && !isActive && "active:bg-on-surface/10",
55
+ className,
56
+ );
57
+ const rowStyle: React.CSSProperties = {
58
+ gridTemplateColumns: TASK_ROW_GRID,
59
+ minHeight: TASK_ROW_HEIGHT,
60
+ paddingLeft: 16,
61
+ paddingRight: 8,
62
+ gap: 12,
63
+ };
64
+
65
+ const mainContent = (
66
+ <>
67
+ <TaskStatusIcon status={task.status} size={20} />
68
+ <TaskPriorityIcon priority={task.priority} size={18} />
69
+
70
+ <span
71
+ className={clsx(
72
+ "truncate font-code text-body-small",
73
+ isActive ? "text-on-secondary-container" : "text-on-surface-variant",
74
+ )}
75
+ >
76
+ SPC-{task.task_number}
77
+ </span>
78
+
79
+ <span className="min-w-0 truncate text-body-large">{task.title}</span>
80
+
81
+ <span
82
+ className={clsx(
83
+ "text-right text-body-small",
84
+ isActive ? "text-on-secondary-container" : "text-on-surface-variant",
85
+ )}
86
+ >
87
+ {task.subtasks.length > 0 ? `${completedSubtasks}/${task.subtasks.length}` : ""}
88
+ </span>
89
+
90
+ <span
91
+ className={clsx(
92
+ "truncate text-right text-body-small",
93
+ isActive ? "text-on-secondary-container" : "text-on-surface-variant",
94
+ )}
95
+ >
96
+ {assigneeName}
97
+ </span>
98
+ </>
99
+ );
100
+
101
+ const actions = (
102
+ <span className="flex items-center justify-center">
103
+ {hasActions ? (
104
+ <DropdownMenu.Root>
105
+ <DropdownMenu.Trigger asChild>
106
+ <button
107
+ type="button"
108
+ aria-label="Task Actions"
109
+ className={clsx(
110
+ "flex size-8 items-center justify-center rounded-full text-on-surface-variant",
111
+ "opacity-0 transition-[opacity,background-color] duration-200 ease-standard",
112
+ "hover:bg-on-surface/8 active:bg-on-surface/10",
113
+ "group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 data-popup-open:opacity-100",
114
+ focusRing,
115
+ )}
116
+ >
117
+ <Icon name="more_vert" size={20} />
118
+ </button>
119
+ </DropdownMenu.Trigger>
120
+ <DropdownMenu.Content align="end" sideOffset={4}>
121
+ {onStatusChange &&
122
+ TASK_STATUS_ORDER.filter((s) => s !== task.status).map((s) => (
123
+ <DropdownMenu.Item
124
+ key={s}
125
+ onClick={() => {
126
+ onStatusChange(task, s);
127
+ }}
128
+ >
129
+ <TaskStatusIcon status={s} size={20} label="" />
130
+ <span>{TASK_STATUS_LABEL[s]}</span>
131
+ </DropdownMenu.Item>
132
+ ))}
133
+ {onStatusChange && onDelete && <DropdownMenu.Separator />}
134
+ {onDelete && (
135
+ <DropdownMenu.Item
136
+ onClick={() => {
137
+ onDelete(task);
138
+ }}
139
+ className="text-error"
140
+ >
141
+ <Icon name="delete" size={20} />
142
+ <span>Delete</span>
143
+ </DropdownMenu.Item>
144
+ )}
145
+ </DropdownMenu.Content>
146
+ </DropdownMenu.Root>
147
+ ) : null}
148
+ </span>
149
+ );
150
+
151
+ if (!isRowInteractive) {
152
+ return (
153
+ <div aria-current={isActive ? "true" : undefined} className={rowClassName} style={rowStyle}>
154
+ {mainContent}
155
+ {actions}
156
+ </div>
157
+ );
158
+ }
159
+
160
+ return (
161
+ <div className={rowClassName} style={rowStyle}>
162
+ <button
163
+ type="button"
164
+ aria-current={isActive ? "true" : undefined}
165
+ onClick={() => onClick(task)}
166
+ className={clsx(
167
+ "col-span-6 grid grid-cols-subgrid items-center border-0 bg-transparent p-0 text-left font-inherit",
168
+ focusRing,
169
+ "focus-visible:outline-offset-[-3px]",
170
+ )}
171
+ >
172
+ {mainContent}
173
+ </button>
174
+ {actions}
175
+ </div>
176
+ );
177
+ }
@@ -0,0 +1,39 @@
1
+ // SPDX-License-Identifier: Apache-2.0 AND MIT
2
+ // Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/TaskStatusIcon.tsx), MIT, rebuilt on Material Design 3.
3
+
4
+ import { Icon } from "@aphrody/m3-primitives";
5
+ import { clsx } from "clsx";
6
+
7
+ import type { TaskStatus } from "./types";
8
+ import { TASK_STATUS_LABEL } from "./types";
9
+
10
+ /** Material Symbols glyph and M3 color role per status. */
11
+ const config: Record<TaskStatus, { icon: string; filled: boolean; color: string }> = {
12
+ done: { icon: "check_circle", filled: true, color: "text-success" },
13
+ in_progress: { icon: "clock_loader_40", filled: true, color: "text-primary" },
14
+ ready: { icon: "radio_button_checked", filled: false, color: "text-primary" },
15
+ pending_approval: { icon: "schedule", filled: false, color: "text-warning" },
16
+ backlog: { icon: "radio_button_unchecked", filled: false, color: "text-outline" },
17
+ };
18
+
19
+ export interface TaskStatusIconProps {
20
+ status: TaskStatus;
21
+ size?: number;
22
+ className?: string;
23
+ /** Accessible label; defaults to the status label. Pass `""` for a decorative icon. */
24
+ label?: string;
25
+ }
26
+
27
+ export function TaskStatusIcon({ status, size = 16, className, label }: TaskStatusIconProps) {
28
+ const { icon, filled, color } = config[status];
29
+ return (
30
+ <Icon
31
+ name={icon}
32
+ size={size}
33
+ filled={filled}
34
+ label={label ?? TASK_STATUS_LABEL[status]}
35
+ data-status={status}
36
+ className={clsx(color, className)}
37
+ />
38
+ );
39
+ }