@echothink-ui/project 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 (55) hide show
  1. package/README.md +5 -0
  2. package/dist/components/ProjectActivityTimeline.d.ts +5 -0
  3. package/dist/components/ProjectAppDomainPanel.d.ts +8 -0
  4. package/dist/components/ProjectCard.d.ts +8 -0
  5. package/dist/components/ProjectCreateForm.d.ts +7 -0
  6. package/dist/components/ProjectDashboardTemplate.d.ts +11 -0
  7. package/dist/components/ProjectManagementPage.d.ts +17 -0
  8. package/dist/components/ProjectMembersPanel.d.ts +9 -0
  9. package/dist/components/ProjectModelConfigPanel.d.ts +9 -0
  10. package/dist/components/ProjectPermissionPanel.d.ts +9 -0
  11. package/dist/components/ProjectResourcePanel.d.ts +6 -0
  12. package/dist/components/ProjectScopeSelector.d.ts +7 -0
  13. package/dist/components/ProjectSettingsPanel.d.ts +6 -0
  14. package/dist/components/ProjectStatusSummary.d.ts +6 -0
  15. package/dist/components/ProjectSummaryPanel.d.ts +5 -0
  16. package/dist/components/ProjectTab.d.ts +3 -0
  17. package/dist/components/ProjectTabGroup.d.ts +12 -0
  18. package/dist/components/ProjectTable.d.ts +9 -0
  19. package/dist/index.cjs +2112 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.css +2059 -0
  22. package/dist/index.css.map +1 -0
  23. package/dist/index.d.ts +21 -0
  24. package/dist/index.js +2098 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/types.d.ts +99 -0
  27. package/dist/utils.d.ts +288 -0
  28. package/package.json +45 -0
  29. package/src/components/ProjectActivityTimeline.test.tsx +43 -0
  30. package/src/components/ProjectActivityTimeline.tsx +118 -0
  31. package/src/components/ProjectAppDomainPanel.tsx +147 -0
  32. package/src/components/ProjectCard.tsx +117 -0
  33. package/src/components/ProjectCreateForm.test.tsx +45 -0
  34. package/src/components/ProjectCreateForm.tsx +176 -0
  35. package/src/components/ProjectDashboardTemplate.tsx +107 -0
  36. package/src/components/ProjectManagementPage.tsx +112 -0
  37. package/src/components/ProjectMembersPanel.tsx +181 -0
  38. package/src/components/ProjectModelConfigPanel.tsx +294 -0
  39. package/src/components/ProjectPermissionPanel.tsx +174 -0
  40. package/src/components/ProjectResourcePanel.tsx +154 -0
  41. package/src/components/ProjectScopeSelector.test.tsx +50 -0
  42. package/src/components/ProjectScopeSelector.tsx +92 -0
  43. package/src/components/ProjectSettingsPanel.test.tsx +25 -0
  44. package/src/components/ProjectSettingsPanel.tsx +244 -0
  45. package/src/components/ProjectStatusSummary.tsx +165 -0
  46. package/src/components/ProjectSummaryPanel.test.tsx +37 -0
  47. package/src/components/ProjectSummaryPanel.tsx +85 -0
  48. package/src/components/ProjectTab.tsx +8 -0
  49. package/src/components/ProjectTabGroup.tsx +38 -0
  50. package/src/components/ProjectTable.tsx +138 -0
  51. package/src/index.test.tsx +337 -0
  52. package/src/index.tsx +41 -0
  53. package/src/styles.css +2431 -0
  54. package/src/types.ts +111 -0
  55. package/src/utils.ts +96 -0
@@ -0,0 +1,337 @@
1
+ import { fireEvent, render, screen, within } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import {
4
+ ProjectAppDomainPanel,
5
+ ProjectComponentNames,
6
+ ProjectMembersPanel,
7
+ ProjectModelConfigPanel,
8
+ ProjectPermissionPanel,
9
+ ProjectResourcePanel,
10
+ ProjectStatusSummary,
11
+ ProjectTab,
12
+ ProjectTabGroup,
13
+ ProjectTable
14
+ } from "./index";
15
+
16
+ const instances = [
17
+ {
18
+ id: "mailbox",
19
+ appDomainLabel: "Mailbox",
20
+ status: "running" as const,
21
+ health: "healthy" as const
22
+ },
23
+ {
24
+ id: "documents",
25
+ appDomainLabel: "Documents",
26
+ status: "in-progress" as const,
27
+ health: "warning" as const
28
+ }
29
+ ];
30
+
31
+ describe("@echothink-ui/project ProjectTable", () => {
32
+ it("renders a named table with stable project name and description blocks", () => {
33
+ render(
34
+ <ProjectTable
35
+ projects={[
36
+ {
37
+ id: "p1",
38
+ name: "Marketing Campaign",
39
+ status: "in-progress",
40
+ description: "Q3 product launch and lifecycle messaging.",
41
+ ownerLabel: "JD",
42
+ updatedAt: "May 27",
43
+ appDomainsCount: 4,
44
+ openTasksCount: 12
45
+ }
46
+ ]}
47
+ />
48
+ );
49
+
50
+ const table = screen.getByRole("table", { name: "Project list" });
51
+ const nameGroup = screen.getByText("Marketing Campaign").closest(".eth-project-table__name");
52
+
53
+ expect(table.className).toContain("eth-project-table__table");
54
+ expect(nameGroup?.querySelector(".eth-project-table__title")?.textContent).toBe(
55
+ "Marketing Campaign"
56
+ );
57
+ expect(nameGroup?.querySelector(".eth-project-table__description")?.textContent).toBe(
58
+ "Q3 product launch and lifecycle messaging."
59
+ );
60
+ expect(screen.getByRole("columnheader", { name: "App domains" })).toBeTruthy();
61
+ });
62
+ });
63
+
64
+ describe("@echothink-ui/project ProjectAppDomainPanel", () => {
65
+ it("omits the actions column when no action handlers are available", () => {
66
+ render(<ProjectAppDomainPanel instances={instances} />);
67
+
68
+ expect(screen.queryByText("Actions")).toBeNull();
69
+ expect(screen.queryByRole("button", { name: "Open" })).toBeNull();
70
+ });
71
+
72
+ it("shows configured row actions when handlers are provided", () => {
73
+ render(
74
+ <ProjectAppDomainPanel
75
+ instances={instances}
76
+ onOpen={() => undefined}
77
+ onConfigure={() => undefined}
78
+ onRemove={() => undefined}
79
+ />
80
+ );
81
+
82
+ expect(screen.getByText("Actions")).toBeTruthy();
83
+ expect(screen.getAllByRole("button", { name: "Open" })).toHaveLength(2);
84
+ expect(screen.getAllByRole("button", { name: "Configure" })).toHaveLength(2);
85
+ expect(screen.getAllByRole("button", { name: "Remove" })).toHaveLength(2);
86
+ });
87
+ });
88
+
89
+ describe("@echothink-ui/project ProjectModelConfigPanel", () => {
90
+ const providers = [
91
+ {
92
+ id: "anthropic",
93
+ label: "Anthropic",
94
+ models: [{ id: "claude-opus-4-7", label: "Claude Opus 4.7" }]
95
+ },
96
+ {
97
+ id: "openai",
98
+ label: "OpenAI",
99
+ models: [{ id: "gpt-4o", label: "GPT-4o" }]
100
+ }
101
+ ];
102
+
103
+ it("renders selected model summary and generation parameter context", () => {
104
+ render(
105
+ <ProjectModelConfigPanel
106
+ providers={providers}
107
+ selectedProvider="anthropic"
108
+ model="claude-opus-4-7"
109
+ params={{ temperature: 0.4, topP: 0.95, maxTokens: 4096 }}
110
+ />
111
+ );
112
+
113
+ expect(screen.getByRole("region", { name: "Project model configuration" })).toBeTruthy();
114
+ expect(screen.getByRole("heading", { name: "Project model configuration" })).toBeTruthy();
115
+ expect(screen.getAllByText("Anthropic").length).toBeGreaterThan(0);
116
+ expect(screen.getAllByText("Claude Opus 4.7").length).toBeGreaterThan(0);
117
+ expect(screen.getByText("Controls response randomness.")).toBeTruthy();
118
+ expect(screen.getByText("Caps the response length for project agent runs.")).toBeTruthy();
119
+ });
120
+
121
+ it("emits the first available model when the provider changes", () => {
122
+ const handleChange = vi.fn();
123
+
124
+ render(
125
+ <ProjectModelConfigPanel
126
+ providers={providers}
127
+ selectedProvider="anthropic"
128
+ model="claude-opus-4-7"
129
+ params={{ temperature: 0.4, topP: 0.95, maxTokens: 4096 }}
130
+ onChange={handleChange}
131
+ />
132
+ );
133
+
134
+ fireEvent.change(screen.getByLabelText("Provider"), { target: { value: "openai" } });
135
+
136
+ expect(handleChange).toHaveBeenCalledWith({
137
+ provider: "openai",
138
+ model: "gpt-4o",
139
+ params: { temperature: 0.4, topP: 0.95, maxTokens: 4096 }
140
+ });
141
+ });
142
+ });
143
+
144
+ describe("@echothink-ui/project ProjectMembersPanel", () => {
145
+ it("renders members as active by default and keeps roles read-only without a change handler", () => {
146
+ render(<ProjectMembersPanel members={[{ id: "m1", label: "Jane Doe", role: "owner" }]} />);
147
+
148
+ expect(screen.getByText("Active")).toBeTruthy();
149
+ expect(screen.queryByText("-")).toBeNull();
150
+ expect(screen.queryByRole("combobox", { name: "Role for Jane Doe" })).toBeNull();
151
+ expect(screen.getByText("Owner")).toBeTruthy();
152
+ });
153
+
154
+ it("shows editable role controls and row actions when handlers are provided", () => {
155
+ render(
156
+ <ProjectMembersPanel
157
+ members={[{ id: "m1", label: "Jane Doe", role: "owner", status: "active" }]}
158
+ onChangeRole={() => undefined}
159
+ onRemove={() => undefined}
160
+ />
161
+ );
162
+
163
+ expect(screen.getByRole("combobox", { name: "Role for Jane Doe" })).toBeTruthy();
164
+ expect(screen.getByRole("button", { name: "Remove" })).toBeTruthy();
165
+ });
166
+ });
167
+
168
+ describe("@echothink-ui/project ProjectPermissionPanel", () => {
169
+ const subjects = [
170
+ { id: "jd", label: "Jane Doe" },
171
+ { id: "mk", label: "Mark K." }
172
+ ];
173
+ const resources = [
174
+ { id: "brief", label: "Brief.md" },
175
+ { id: "tasks", label: "Tasks" }
176
+ ];
177
+ const actions = [
178
+ { id: "read", label: "Read" },
179
+ { id: "write", label: "Write" }
180
+ ];
181
+ const assignments = {
182
+ "jd:brief:read": true,
183
+ "jd:brief:write": true,
184
+ "mk:brief:read": true
185
+ };
186
+
187
+ it("summarizes the matrix and announces editable permission controls", () => {
188
+ const handleChange = vi.fn();
189
+
190
+ render(
191
+ <ProjectPermissionPanel
192
+ subjects={subjects}
193
+ resources={resources}
194
+ actions={actions}
195
+ assignments={assignments}
196
+ onChange={handleChange}
197
+ />
198
+ );
199
+
200
+ expect(screen.getByRole("heading", { name: "Project permissions" })).toBeTruthy();
201
+ expect(
202
+ within(screen.getByLabelText("Project permission summary")).getByText("3")
203
+ ).toBeTruthy();
204
+ expect(
205
+ within(screen.getByLabelText("Project permission summary")).getByText("5")
206
+ ).toBeTruthy();
207
+
208
+ fireEvent.click(
209
+ screen.getByRole("checkbox", { name: "Write permission for Mark K. on Tasks" })
210
+ );
211
+
212
+ expect(handleChange).toHaveBeenCalledWith("mk:tasks:write", true);
213
+ });
214
+
215
+ it("uses read-only permission states when no change handler is provided", () => {
216
+ render(
217
+ <ProjectPermissionPanel
218
+ subjects={subjects.slice(0, 1)}
219
+ resources={resources.slice(0, 1)}
220
+ actions={actions}
221
+ assignments={assignments}
222
+ />
223
+ );
224
+
225
+ expect(screen.queryByRole("checkbox")).toBeNull();
226
+ expect(within(screen.getByRole("table")).getAllByText("Granted")).toHaveLength(2);
227
+ });
228
+ });
229
+
230
+ describe("@echothink-ui/project ProjectResourcePanel", () => {
231
+ const resources = [
232
+ {
233
+ id: "brief",
234
+ kind: "document",
235
+ label: "Brief.md",
236
+ status: "synced" as const,
237
+ updatedAt: "May 27"
238
+ },
239
+ {
240
+ id: "hero",
241
+ kind: "image",
242
+ label: "hero.png",
243
+ status: "in-progress" as const,
244
+ updatedAt: "May 26"
245
+ },
246
+ {
247
+ id: "dataset",
248
+ kind: "table",
249
+ label: "Audience segments",
250
+ status: "stale" as const,
251
+ updatedAt: "May 24"
252
+ }
253
+ ];
254
+
255
+ it("summarizes resource counts and attention states", () => {
256
+ render(<ProjectResourcePanel resources={resources} />);
257
+ const summary = screen.getByLabelText("Resource summary");
258
+
259
+ expect(screen.getByRole("region", { name: "Project resources" })).toBeTruthy();
260
+ expect(within(summary).getByText("Resources")).toBeTruthy();
261
+ expect(within(summary).getByText("Synced")).toBeTruthy();
262
+ expect(within(summary).getByText("Attention")).toBeTruthy();
263
+ expect(screen.queryByText("Actions")).toBeNull();
264
+ });
265
+
266
+ it("shows an open action only when selection is available", () => {
267
+ render(<ProjectResourcePanel resources={resources} onSelect={() => undefined} />);
268
+
269
+ expect(screen.getByText("Actions")).toBeTruthy();
270
+ expect(screen.getAllByRole("button", { name: "Open" })).toHaveLength(3);
271
+ });
272
+ });
273
+
274
+ describe("@echothink-ui/project ProjectStatusSummary", () => {
275
+ it("includes attention statuses in the distribution legend", () => {
276
+ render(
277
+ <ProjectStatusSummary
278
+ summary={{
279
+ byStatus: { running: 1, "approval-required": 1 },
280
+ healthyPercent: 50,
281
+ blockedCount: 1
282
+ }}
283
+ />
284
+ );
285
+
286
+ expect(screen.getByText("Approval Required 1")).toBeTruthy();
287
+ });
288
+ });
289
+
290
+ describe("@echothink-ui/project ProjectTab", () => {
291
+ it("exports a project-scoped browser tab with tab semantics and status", () => {
292
+ render(<ProjectTab id="marketing" label="Marketing Campaign" status="active" active />);
293
+
294
+ expect(ProjectComponentNames).toContain("ProjectTab");
295
+ const tab = screen.getByRole("tab", { name: /Marketing Campaign/ });
296
+ expect(tab.getAttribute("aria-selected")).toBe("true");
297
+ expect(screen.getByText("Active")).toBeTruthy();
298
+ });
299
+ });
300
+
301
+ describe("@echothink-ui/project ProjectTabGroup", () => {
302
+ it("renders project-scoped tabs with status, active state, and close affordances", () => {
303
+ const handleActivate = vi.fn();
304
+ const handleClose = vi.fn();
305
+
306
+ const { container } = render(
307
+ <ProjectTabGroup
308
+ projectId="marketing"
309
+ projectLabel="Marketing Campaign"
310
+ activeTabId="dashboard"
311
+ tabs={[
312
+ { id: "dashboard", label: "Dashboard", status: "active" },
313
+ { id: "tasks", label: "Tasks", status: "running", dirty: true },
314
+ { id: "brief", label: "Brief.md", status: "in-progress" }
315
+ ]}
316
+ onActivateTab={handleActivate}
317
+ onCloseTab={handleClose}
318
+ />
319
+ );
320
+
321
+ expect(container.querySelector('[data-eth-component="ProjectTabGroup"]')).toBeTruthy();
322
+ expect(screen.getByRole("heading", { name: "Marketing Campaign" })).toBeTruthy();
323
+ expect(screen.getByRole("tablist", { name: "Marketing Campaign" })).toBeTruthy();
324
+
325
+ const activeTab = screen.getByRole("tab", { name: /Dashboard/ });
326
+ expect(activeTab.getAttribute("aria-selected")).toBe("true");
327
+ expect(screen.getByText("Active")).toBeTruthy();
328
+ expect(screen.getByText("Running")).toBeTruthy();
329
+ expect(screen.getByLabelText("Unsaved changes")).toBeTruthy();
330
+
331
+ fireEvent.click(screen.getByRole("tab", { name: /Tasks/ }));
332
+ expect(handleActivate).toHaveBeenCalledWith("tasks");
333
+
334
+ fireEvent.click(screen.getByRole("button", { name: "Close Brief.md" }));
335
+ expect(handleClose).toHaveBeenCalledWith("brief");
336
+ });
337
+ });
package/src/index.tsx ADDED
@@ -0,0 +1,41 @@
1
+ import "./styles.css";
2
+
3
+ export * from "./types";
4
+ export * from "./components/ProjectManagementPage";
5
+ export * from "./components/ProjectCard";
6
+ export * from "./components/ProjectTable";
7
+ export * from "./components/ProjectTab";
8
+ export * from "./components/ProjectStatusSummary";
9
+ export * from "./components/ProjectCreateForm";
10
+ export * from "./components/ProjectSummaryPanel";
11
+ export * from "./components/ProjectSettingsPanel";
12
+ export * from "./components/ProjectScopeSelector";
13
+ export * from "./components/ProjectResourcePanel";
14
+ export * from "./components/ProjectActivityTimeline";
15
+ export * from "./components/ProjectTabGroup";
16
+ export * from "./components/ProjectMembersPanel";
17
+ export * from "./components/ProjectPermissionPanel";
18
+ export * from "./components/ProjectAppDomainPanel";
19
+ export * from "./components/ProjectModelConfigPanel";
20
+ export * from "./components/ProjectDashboardTemplate";
21
+
22
+ export const ProjectComponentNames = [
23
+ "ProjectManagementPage",
24
+ "ProjectCard",
25
+ "ProjectTable",
26
+ "ProjectTab",
27
+ "ProjectStatusSummary",
28
+ "ProjectCreateForm",
29
+ "ProjectSummaryPanel",
30
+ "ProjectSettingsPanel",
31
+ "ProjectScopeSelector",
32
+ "ProjectResourcePanel",
33
+ "ProjectActivityTimeline",
34
+ "ProjectTabGroup",
35
+ "ProjectMembersPanel",
36
+ "ProjectPermissionPanel",
37
+ "ProjectAppDomainPanel",
38
+ "ProjectModelConfigPanel",
39
+ "ProjectDashboardTemplate"
40
+ ] as const;
41
+ export type ProjectComponentName = (typeof ProjectComponentNames)[number];