@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,165 @@
1
+ import * as React from "react";
2
+ import {
3
+ Badge,
4
+ StatusDot,
5
+ statusLabel,
6
+ type EthOperationalStatus,
7
+ type EthSeverity
8
+ } from "@echothink-ui/core";
9
+ import { KPIBlock } from "@echothink-ui/charts";
10
+ import type { ProjectBaseProps, ProjectStatusSummaryData, ProjectSummary } from "../types";
11
+ import { classNames, projectHtmlProps, summarizeProjects } from "../utils";
12
+
13
+ export interface ProjectStatusSummaryProps extends ProjectBaseProps {
14
+ projects?: ProjectSummary[];
15
+ summary?: ProjectStatusSummaryData;
16
+ }
17
+
18
+ const priorityStatuses: EthOperationalStatus[] = [
19
+ "running",
20
+ "in-progress",
21
+ "approval-required",
22
+ "blocked",
23
+ "failed",
24
+ "queued",
25
+ "pending-approval",
26
+ "warning",
27
+ "stale",
28
+ "paused",
29
+ "completed",
30
+ "succeeded",
31
+ "synced",
32
+ "active",
33
+ "not-started",
34
+ "inactive"
35
+ ];
36
+
37
+ const statusClassNames: Partial<Record<EthOperationalStatus, string>> = {
38
+ "approval-required": "eth-project-status-summary__bar-segment--attention",
39
+ "pending-approval": "eth-project-status-summary__bar-segment--warning",
40
+ "in-progress": "eth-project-status-summary__bar-segment--inflight",
41
+ blocked: "eth-project-status-summary__bar-segment--attention",
42
+ failed: "eth-project-status-summary__bar-segment--attention",
43
+ running: "eth-project-status-summary__bar-segment--inflight",
44
+ queued: "eth-project-status-summary__bar-segment--queued",
45
+ warning: "eth-project-status-summary__bar-segment--warning",
46
+ stale: "eth-project-status-summary__bar-segment--warning",
47
+ paused: "eth-project-status-summary__bar-segment--paused",
48
+ completed: "eth-project-status-summary__bar-segment--complete",
49
+ succeeded: "eth-project-status-summary__bar-segment--complete",
50
+ synced: "eth-project-status-summary__bar-segment--complete",
51
+ active: "eth-project-status-summary__bar-segment--complete",
52
+ "not-started": "eth-project-status-summary__bar-segment--queued",
53
+ inactive: "eth-project-status-summary__bar-segment--queued"
54
+ };
55
+
56
+ export function ProjectStatusSummary({
57
+ projects = [],
58
+ summary,
59
+ title = "Project status",
60
+ className,
61
+ ...props
62
+ }: ProjectStatusSummaryProps) {
63
+ const headingId = React.useId();
64
+ const computed = summary ?? summarizeProjects(projects);
65
+ const total = Object.values(computed.byStatus).reduce((sum, value) => sum + (value ?? 0), 0);
66
+ const activeCount = (computed.byStatus.running ?? 0) + (computed.byStatus["in-progress"] ?? 0);
67
+ const prioritizedStatuses = priorityStatuses.filter((status) => computed.byStatus[status]);
68
+ const additionalStatuses = Object.keys(computed.byStatus).filter(
69
+ (status): status is EthOperationalStatus =>
70
+ !priorityStatuses.includes(status as EthOperationalStatus) &&
71
+ Boolean(computed.byStatus[status as EthOperationalStatus])
72
+ );
73
+ const displayedStatuses = [...prioritizedStatuses, ...additionalStatuses];
74
+ const statusSummary = total
75
+ ? `${total} project${total === 1 ? "" : "s"} across ${displayedStatuses.length} status${
76
+ displayedStatuses.length === 1 ? "" : "es"
77
+ }`
78
+ : "No tracked projects";
79
+ const attentionLabel = total
80
+ ? computed.blockedCount
81
+ ? `${computed.blockedCount} needs attention`
82
+ : "Operational"
83
+ : "No projects";
84
+ const attentionSeverity: EthSeverity = total
85
+ ? computed.blockedCount
86
+ ? "danger"
87
+ : "success"
88
+ : "neutral";
89
+
90
+ return (
91
+ <section
92
+ {...projectHtmlProps(props)}
93
+ className={classNames("eth-project-status-summary", className)}
94
+ data-eth-component="ProjectStatusSummary"
95
+ role="region"
96
+ aria-labelledby={headingId}
97
+ tabIndex={-1}
98
+ >
99
+ <header className="eth-project-status-summary__header">
100
+ <div className="eth-project-status-summary__heading">
101
+ <h3 id={headingId}>{title}</h3>
102
+ <p>{statusSummary}</p>
103
+ </div>
104
+ <Badge severity={attentionSeverity}>{attentionLabel}</Badge>
105
+ </header>
106
+
107
+ <div className="eth-project-status-summary__kpis">
108
+ <KPIBlock title="Projects" value={total} description="Total tracked" />
109
+ <KPIBlock title="Healthy" value={`${computed.healthyPercent}%`} description="Operational" />
110
+ <KPIBlock title="Active" value={activeCount} description="Running or in progress" />
111
+ <KPIBlock title="Blocked" value={computed.blockedCount} description="Needs attention" />
112
+ </div>
113
+
114
+ <div className="eth-project-status-summary__distribution">
115
+ <div
116
+ className="eth-project-status-summary__bar"
117
+ aria-label="Project status distribution"
118
+ role="list"
119
+ >
120
+ {displayedStatuses.length ? (
121
+ displayedStatuses.map((status) => {
122
+ const count = computed.byStatus[status] ?? 0;
123
+ return (
124
+ <span
125
+ key={status}
126
+ aria-label={`${statusLabel(status)}: ${count}`}
127
+ className={classNames(
128
+ "eth-project-status-summary__bar-segment",
129
+ statusClassNames[status],
130
+ `eth-project-status-summary__bar-segment--${status}`
131
+ )}
132
+ role="listitem"
133
+ style={{ flexGrow: count }}
134
+ title={`${statusLabel(status)}: ${count}`}
135
+ />
136
+ );
137
+ })
138
+ ) : (
139
+ <span
140
+ aria-label="No project status data"
141
+ className="eth-project-status-summary__bar-segment"
142
+ role="listitem"
143
+ style={{ flexGrow: 1 }}
144
+ />
145
+ )}
146
+ </div>
147
+
148
+ {displayedStatuses.length ? (
149
+ <div className="eth-project-status-summary__legend" role="list">
150
+ {displayedStatuses.map((status) => (
151
+ <span key={status} role="listitem">
152
+ <StatusDot
153
+ status={status}
154
+ label={`${statusLabel(status)} ${computed.byStatus[status]}`}
155
+ />
156
+ </span>
157
+ ))}
158
+ </div>
159
+ ) : (
160
+ <p className="eth-project-status-summary__empty">No project status data</p>
161
+ )}
162
+ </div>
163
+ </section>
164
+ );
165
+ }
@@ -0,0 +1,37 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { ProjectSummaryPanel } from "./ProjectSummaryPanel";
4
+
5
+ describe("@echothink-ui/project ProjectSummaryPanel", () => {
6
+ it("renders an accessible inspector summary with status and metadata", () => {
7
+ render(
8
+ <ProjectSummaryPanel
9
+ project={{
10
+ id: "p1",
11
+ name: "Marketing Campaign",
12
+ status: "in-progress",
13
+ description: "Q3 product launch and lifecycle messaging.",
14
+ ownerLabel: "JD",
15
+ updatedAt: "May 27",
16
+ appDomainsCount: 4,
17
+ openTasksCount: 12,
18
+ healthStatus: "healthy"
19
+ }}
20
+ />
21
+ );
22
+
23
+ const panel = screen.getByRole("region", { name: "Marketing Campaign" });
24
+
25
+ expect(panel.className).toContain("eth-project-summary-panel--in-progress");
26
+ expect(panel.getAttribute("data-status")).toBe("in-progress");
27
+ expect(panel.getAttribute("aria-describedby")).toBe("p1-summary-description");
28
+ expect(screen.getByText("Q3 product launch and lifecycle messaging.").id).toBe(
29
+ "p1-summary-description"
30
+ );
31
+ expect(screen.getByText("Project ID")).toBeTruthy();
32
+ expect(screen.getByText("p1")).toBeTruthy();
33
+ expect(screen.getByText("Open tasks")).toBeTruthy();
34
+ expect(screen.getByText("12")).toBeTruthy();
35
+ expect(screen.getByText("Healthy")).toBeTruthy();
36
+ });
37
+ });
@@ -0,0 +1,85 @@
1
+ import * as React from "react";
2
+ import { Badge, StatusDot, statusLabel } from "@echothink-ui/core";
3
+ import type { ProjectBaseProps, ProjectSummary } from "../types";
4
+ import {
5
+ classNames,
6
+ formatNumber,
7
+ healthLabel,
8
+ healthToStatus,
9
+ projectHtmlProps,
10
+ statusToSeverity
11
+ } from "../utils";
12
+
13
+ export interface ProjectSummaryPanelProps extends ProjectBaseProps {
14
+ project?: ProjectSummary;
15
+ }
16
+
17
+ export function ProjectSummaryPanel({ project, className, ...props }: ProjectSummaryPanelProps) {
18
+ const resolvedProject: ProjectSummary = project ?? {
19
+ id: "project",
20
+ name: "Project",
21
+ status: "not-started",
22
+ description: "Project summary"
23
+ };
24
+ const titleId = `${resolvedProject.id}-summary-title`;
25
+ const descriptionId = resolvedProject.description
26
+ ? `${resolvedProject.id}-summary-description`
27
+ : undefined;
28
+ const metadataItems = [
29
+ { label: "Project ID", value: resolvedProject.id },
30
+ { label: "Owner", value: resolvedProject.ownerLabel ?? "Unassigned" },
31
+ { label: "Updated", value: resolvedProject.updatedAt ?? "Not updated" },
32
+ { label: "App domains", value: formatNumber(resolvedProject.appDomainsCount) },
33
+ { label: "Open tasks", value: formatNumber(resolvedProject.openTasksCount) }
34
+ ];
35
+
36
+ return (
37
+ <section
38
+ {...projectHtmlProps(props)}
39
+ className={classNames(
40
+ "eth-project-summary-panel",
41
+ `eth-project-summary-panel--${resolvedProject.status}`,
42
+ className
43
+ )}
44
+ data-eth-component="ProjectSummaryPanel"
45
+ data-status={resolvedProject.status}
46
+ role="region"
47
+ aria-labelledby={titleId}
48
+ aria-describedby={descriptionId}
49
+ tabIndex={-1}
50
+ >
51
+ <header className="eth-project-summary-panel__header">
52
+ <div className="eth-project-summary-panel__heading">
53
+ <h3 id={titleId}>{resolvedProject.name}</h3>
54
+ {resolvedProject.description ? (
55
+ <p id={descriptionId}>{resolvedProject.description}</p>
56
+ ) : null}
57
+ </div>
58
+ <div className="eth-project-summary-panel__status">
59
+ <Badge severity={statusToSeverity(resolvedProject.status)}>
60
+ {statusLabel(resolvedProject.status)}
61
+ </Badge>
62
+ </div>
63
+ </header>
64
+ <dl className="eth-project-summary-panel__metadata">
65
+ {metadataItems.map((item) => (
66
+ <div className="eth-project-summary-panel__metadata-item" key={item.label}>
67
+ <dt>{item.label}</dt>
68
+ <dd>{item.value}</dd>
69
+ </div>
70
+ ))}
71
+ <div
72
+ className="eth-project-summary-panel__metadata-item eth-project-summary-panel__metadata-item--health"
73
+ >
74
+ <dt>Health</dt>
75
+ <dd>
76
+ <StatusDot
77
+ status={healthToStatus(resolvedProject.healthStatus)}
78
+ label={healthLabel(resolvedProject.healthStatus)}
79
+ />
80
+ </dd>
81
+ </div>
82
+ </dl>
83
+ </section>
84
+ );
85
+ }
@@ -0,0 +1,8 @@
1
+ import { BrowserTab, type BrowserTabProps } from "@echothink-ui/layouts";
2
+ import { classNames } from "../utils";
3
+
4
+ export type ProjectTabProps = BrowserTabProps;
5
+
6
+ export function ProjectTab({ className, ...props }: ProjectTabProps) {
7
+ return <BrowserTab {...props} className={classNames("eth-project-tab", className)} />;
8
+ }
@@ -0,0 +1,38 @@
1
+ import * as React from "react";
2
+ import { ProjectTabCollection, type ProjectTabItem } from "@echothink-ui/layouts";
3
+ import type { ProjectBaseProps } from "../types";
4
+ import { classNames, projectHtmlProps } from "../utils";
5
+
6
+ export interface ProjectTabGroupProps extends ProjectBaseProps {
7
+ projectId: string;
8
+ projectLabel: React.ReactNode;
9
+ tabs: ProjectTabItem[];
10
+ activeTabId?: string;
11
+ onActivateTab?: (id: string) => void;
12
+ onCloseTab?: (id: string) => void;
13
+ }
14
+
15
+ export function ProjectTabGroup({
16
+ projectId,
17
+ projectLabel,
18
+ tabs,
19
+ activeTabId,
20
+ onActivateTab,
21
+ onCloseTab,
22
+ className,
23
+ ...props
24
+ }: ProjectTabGroupProps) {
25
+ return (
26
+ <ProjectTabCollection
27
+ {...projectHtmlProps(props)}
28
+ className={classNames("eth-project-tab-group", className)}
29
+ data-eth-component="ProjectTabGroup"
30
+ projectId={projectId}
31
+ projectLabel={projectLabel}
32
+ tabs={tabs}
33
+ activeTabId={activeTabId}
34
+ onActivateTab={onActivateTab}
35
+ onCloseTab={onCloseTab}
36
+ />
37
+ );
38
+ }
@@ -0,0 +1,138 @@
1
+ import * as React from "react";
2
+ import {
3
+ Badge,
4
+ Button,
5
+ EmptyState,
6
+ statusLabel,
7
+ type EthAction
8
+ } from "@echothink-ui/core";
9
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
10
+ import type { ProjectBaseProps, ProjectSummary } from "../types";
11
+ import {
12
+ formatNumber,
13
+ projectHtmlProps,
14
+ statusToSeverity
15
+ } from "../utils";
16
+
17
+ export interface ProjectTableProps extends ProjectBaseProps {
18
+ projects?: ProjectSummary[];
19
+ onRowSelect?: (projectId: string) => void;
20
+ selectedProjectId?: string;
21
+ rowActions?: (project: ProjectSummary) => EthAction[];
22
+ }
23
+
24
+ export function ProjectTable({
25
+ projects = [],
26
+ onRowSelect,
27
+ selectedProjectId,
28
+ rowActions,
29
+ density = "compact",
30
+ className,
31
+ ...props
32
+ }: ProjectTableProps) {
33
+ const columns = React.useMemo<DataColumn<ProjectSummary>[]>(
34
+ () => [
35
+ {
36
+ key: "name",
37
+ header: "Name",
38
+ width: "34%",
39
+ render: (project) => (
40
+ <div className="eth-project-table__name">
41
+ {onRowSelect ? (
42
+ <Button
43
+ className="eth-project-table__title eth-project-table__title-button"
44
+ intent="ghost"
45
+ onClick={() => onRowSelect(project.id)}
46
+ >
47
+ {project.name}
48
+ </Button>
49
+ ) : (
50
+ <strong className="eth-project-table__title">{project.name}</strong>
51
+ )}
52
+ {project.description ? (
53
+ <small className="eth-project-table__description">{project.description}</small>
54
+ ) : null}
55
+ </div>
56
+ )
57
+ },
58
+ {
59
+ key: "status",
60
+ header: "Status",
61
+ width: "10rem",
62
+ render: (project) => (
63
+ <span className="eth-project-table__status">
64
+ <Badge severity={statusToSeverity(project.status)}>{statusLabel(project.status)}</Badge>
65
+ </span>
66
+ )
67
+ },
68
+ {
69
+ key: "ownerLabel",
70
+ header: "Owner",
71
+ width: "7rem",
72
+ render: (project) => (
73
+ <span className="eth-project-table__owner">{project.ownerLabel ?? "Unassigned"}</span>
74
+ )
75
+ },
76
+ {
77
+ key: "appDomainsCount",
78
+ header: "App domains",
79
+ width: "7.5rem",
80
+ align: "end",
81
+ render: (project) => (
82
+ <span className="eth-project-table__metric">
83
+ {formatNumber(project.appDomainsCount)}
84
+ </span>
85
+ )
86
+ },
87
+ {
88
+ key: "openTasksCount",
89
+ header: "Open tasks",
90
+ width: "7.5rem",
91
+ align: "end",
92
+ render: (project) => (
93
+ <span className="eth-project-table__metric">
94
+ {formatNumber(project.openTasksCount)}
95
+ </span>
96
+ )
97
+ },
98
+ {
99
+ key: "updatedAt",
100
+ header: "Updated",
101
+ width: "7.5rem",
102
+ render: (project) => (
103
+ <span className="eth-project-table__updated">{project.updatedAt ?? "-"}</span>
104
+ )
105
+ }
106
+ ],
107
+ [onRowSelect]
108
+ );
109
+ const htmlProps = projectHtmlProps(props);
110
+
111
+ return (
112
+ <section
113
+ {...htmlProps}
114
+ className={`eth-project-table ${className ?? ""}`}
115
+ data-eth-component="ProjectTable"
116
+ role="region"
117
+ aria-label={htmlProps["aria-label"] ?? "Project list"}
118
+ tabIndex={-1}
119
+ >
120
+ <DataTable
121
+ aria-label="Project list"
122
+ className="eth-project-table__table"
123
+ density={density}
124
+ rows={projects}
125
+ columns={columns}
126
+ rowKey="id"
127
+ selectedRows={selectedProjectId ? [selectedProjectId] : []}
128
+ rowActions={rowActions}
129
+ emptyState={
130
+ <EmptyState
131
+ title="No projects"
132
+ description="Create a project to start organizing resources and app domains."
133
+ />
134
+ }
135
+ />
136
+ </section>
137
+ );
138
+ }