@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,117 @@
1
+ import * as React from "react";
2
+ import {
3
+ ActionGroup,
4
+ Badge,
5
+ Button,
6
+ StatusDot,
7
+ statusLabel,
8
+ type EthAction
9
+ } from "@echothink-ui/core";
10
+ import type { ProjectBaseProps, ProjectSummary } from "../types";
11
+ import {
12
+ classNames,
13
+ formatNumber,
14
+ healthLabel,
15
+ healthToStatus,
16
+ projectHtmlProps,
17
+ statusToSeverity
18
+ } from "../utils";
19
+
20
+ export interface ProjectCardProps extends ProjectBaseProps {
21
+ project?: ProjectSummary;
22
+ onSelect?: (projectId: string) => void;
23
+ actions?: EthAction[];
24
+ }
25
+
26
+ export function ProjectCard({
27
+ project,
28
+ onSelect,
29
+ actions,
30
+ className,
31
+ ...props
32
+ }: ProjectCardProps) {
33
+ const resolvedProject: ProjectSummary = project ?? {
34
+ id: "project",
35
+ name: "Project",
36
+ status: "not-started",
37
+ description: "Project summary"
38
+ };
39
+ const titleId = `${resolvedProject.id}-title`;
40
+ const descriptionId = resolvedProject.description
41
+ ? `${resolvedProject.id}-description`
42
+ : undefined;
43
+
44
+ return (
45
+ <article
46
+ {...projectHtmlProps(props)}
47
+ className={classNames(
48
+ "eth-project-card",
49
+ `eth-project-card--${resolvedProject.status}`,
50
+ className
51
+ )}
52
+ data-eth-component="ProjectCard"
53
+ data-status={resolvedProject.status}
54
+ role="region"
55
+ aria-labelledby={titleId}
56
+ aria-describedby={descriptionId}
57
+ tabIndex={-1}
58
+ >
59
+ <header className="eth-project-card__header">
60
+ <div className="eth-project-card__heading">
61
+ <h3 id={titleId}>{resolvedProject.name}</h3>
62
+ {resolvedProject.description ? (
63
+ <p id={descriptionId}>{resolvedProject.description}</p>
64
+ ) : null}
65
+ </div>
66
+ <div className="eth-project-card__status">
67
+ <Badge severity={statusToSeverity(resolvedProject.status)}>
68
+ {statusLabel(resolvedProject.status)}
69
+ </Badge>
70
+ </div>
71
+ </header>
72
+
73
+ <dl className="eth-project-card__metadata">
74
+ <div>
75
+ <dt>Owner</dt>
76
+ <dd>{resolvedProject.ownerLabel ?? "Unassigned"}</dd>
77
+ </div>
78
+ <div>
79
+ <dt>Updated</dt>
80
+ <dd>{resolvedProject.updatedAt ?? "Not updated"}</dd>
81
+ </div>
82
+ <div>
83
+ <dt>App domains</dt>
84
+ <dd>{formatNumber(resolvedProject.appDomainsCount)}</dd>
85
+ </div>
86
+ <div>
87
+ <dt>Open tasks</dt>
88
+ <dd>{formatNumber(resolvedProject.openTasksCount)}</dd>
89
+ </div>
90
+ <div>
91
+ <dt>Health</dt>
92
+ <dd>
93
+ <StatusDot
94
+ status={healthToStatus(resolvedProject.healthStatus)}
95
+ label={healthLabel(resolvedProject.healthStatus)}
96
+ />
97
+ </dd>
98
+ </div>
99
+ </dl>
100
+
101
+ {onSelect || actions?.length ? (
102
+ <footer className="eth-project-card__actions">
103
+ {onSelect ? (
104
+ <Button
105
+ intent="secondary"
106
+ density="compact"
107
+ onClick={() => onSelect(resolvedProject.id)}
108
+ >
109
+ Open project
110
+ </Button>
111
+ ) : null}
112
+ <ActionGroup actions={actions} />
113
+ </footer>
114
+ ) : null}
115
+ </article>
116
+ );
117
+ }
@@ -0,0 +1,45 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ProjectCreateForm } from "./ProjectCreateForm";
4
+
5
+ describe("ProjectCreateForm", () => {
6
+ it("renders a named create form with supporting description", () => {
7
+ render(<ProjectCreateForm />);
8
+
9
+ const form = screen.getByRole("form", { name: "Create project" });
10
+ const description = screen.getByText(
11
+ "Start a project workspace with an owner, template, and operating context."
12
+ );
13
+
14
+ expect(form.getAttribute("aria-describedby")).toBe(description.id);
15
+ });
16
+
17
+ it("validates required name and submits trimmed project values", () => {
18
+ const onSubmit = vi.fn();
19
+ render(
20
+ <ProjectCreateForm
21
+ defaults={{ description: "Initial scope", ownerLabel: "Jane Doe" }}
22
+ onSubmit={onSubmit}
23
+ />
24
+ );
25
+
26
+ fireEvent.click(screen.getByRole("button", { name: "Create project" }));
27
+ expect(screen.getAllByText("Project name is required.")).toHaveLength(2);
28
+ expect(onSubmit).not.toHaveBeenCalled();
29
+
30
+ fireEvent.change(screen.getByLabelText("Name"), {
31
+ target: { value: " Q3 launch workspace " }
32
+ });
33
+ fireEvent.change(screen.getByLabelText("Template"), {
34
+ target: { value: "software-delivery" }
35
+ });
36
+ fireEvent.click(screen.getByRole("button", { name: "Create project" }));
37
+
38
+ expect(onSubmit).toHaveBeenCalledWith({
39
+ name: "Q3 launch workspace",
40
+ description: "Initial scope",
41
+ ownerLabel: "Jane Doe",
42
+ template: "software-delivery"
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,176 @@
1
+ import * as React from "react";
2
+ import {
3
+ Button,
4
+ FormField,
5
+ FormSection,
6
+ FormValidationMessage,
7
+ Select,
8
+ TextInput,
9
+ Textarea
10
+ } from "@echothink-ui/core";
11
+ import type { ProjectBaseProps, ProjectCreateValues } from "../types";
12
+ import { classNames, projectHtmlProps } from "../utils";
13
+
14
+ export interface ProjectCreateFormProps extends ProjectBaseProps {
15
+ defaults?: Partial<ProjectCreateValues>;
16
+ onSubmit?: (values: ProjectCreateValues) => void;
17
+ onCancel?: () => void;
18
+ }
19
+
20
+ const templateOptions = [
21
+ { value: "blank", label: "Blank project" },
22
+ { value: "software-delivery", label: "Software delivery" },
23
+ { value: "research", label: "Research workspace" },
24
+ { value: "operations", label: "Operations runbook" }
25
+ ];
26
+
27
+ export function ProjectCreateForm({
28
+ defaults,
29
+ onSubmit,
30
+ onCancel,
31
+ title = "Create project",
32
+ description = "Start a project workspace with an owner, template, and operating context.",
33
+ className,
34
+ ...props
35
+ }: ProjectCreateFormProps) {
36
+ const [values, setValues] = React.useState<ProjectCreateValues>({
37
+ name: defaults?.name ?? "",
38
+ description: defaults?.description ?? "",
39
+ ownerLabel: defaults?.ownerLabel ?? "",
40
+ template: defaults?.template ?? "blank"
41
+ });
42
+ const [nameError, setNameError] = React.useState<string>();
43
+ const formId = React.useId().replace(/:/g, "");
44
+ const titleId = `eth-project-create-form-${formId}-title`;
45
+ const descriptionId = `eth-project-create-form-${formId}-description`;
46
+ const htmlProps = projectHtmlProps(props);
47
+ const describedBy =
48
+ [htmlProps["aria-describedby"], description ? descriptionId : undefined]
49
+ .filter(Boolean)
50
+ .join(" ") || undefined;
51
+
52
+ const update = (field: keyof ProjectCreateValues, value: string) => {
53
+ setValues((current) => ({ ...current, [field]: value }));
54
+ if (field === "name" && value.trim()) setNameError(undefined);
55
+ };
56
+
57
+ const submit = (event: React.FormEvent<HTMLFormElement>) => {
58
+ event.preventDefault();
59
+ if (!values.name.trim()) {
60
+ setNameError("Project name is required.");
61
+ return;
62
+ }
63
+ onSubmit?.({ ...values, name: values.name.trim() });
64
+ };
65
+
66
+ return (
67
+ <form
68
+ {...htmlProps}
69
+ className={classNames("eth-project-create-form", className)}
70
+ data-eth-component="ProjectCreateForm"
71
+ aria-labelledby={htmlProps["aria-labelledby"] ?? (title ? titleId : undefined)}
72
+ aria-describedby={describedBy}
73
+ onSubmit={submit}
74
+ noValidate
75
+ >
76
+ {title || description ? (
77
+ <header className="eth-project-create-form__header">
78
+ {title ? (
79
+ <h2 id={titleId} className="eth-project-create-form__title">
80
+ {title}
81
+ </h2>
82
+ ) : null}
83
+ {description ? (
84
+ <p id={descriptionId} className="eth-project-create-form__description">
85
+ {description}
86
+ </p>
87
+ ) : null}
88
+ </header>
89
+ ) : null}
90
+
91
+ {nameError ? (
92
+ <FormValidationMessage className="eth-project-create-form__validation">
93
+ {nameError}
94
+ </FormValidationMessage>
95
+ ) : null}
96
+
97
+ <FormSection
98
+ className="eth-project-create-form__section"
99
+ title="Project details"
100
+ description="Define the visible identity and initial scope for the workspace."
101
+ >
102
+ <div className="eth-project-create-form__grid">
103
+ <FormField
104
+ id="project-name"
105
+ label="Name"
106
+ helperText="Shown in project lists, tabs, and approval trails."
107
+ required
108
+ error={nameError}
109
+ className="eth-project-create-form__field"
110
+ >
111
+ <TextInput
112
+ value={values.name}
113
+ onChange={(event) => update("name", event.currentTarget.value)}
114
+ autoFocus
115
+ />
116
+ </FormField>
117
+ <FormField
118
+ id="project-owner"
119
+ label="Owner"
120
+ helperText="Person or group accountable for project changes."
121
+ className="eth-project-create-form__field"
122
+ >
123
+ <TextInput
124
+ value={values.ownerLabel}
125
+ onChange={(event) => update("ownerLabel", event.currentTarget.value)}
126
+ />
127
+ </FormField>
128
+ <FormField
129
+ id="project-description"
130
+ label="Description"
131
+ helperText="Summarize the purpose, key outputs, or operating window."
132
+ className="eth-project-create-form__field eth-project-create-form__field--wide"
133
+ >
134
+ <Textarea
135
+ value={values.description}
136
+ onChange={(event) => update("description", event.currentTarget.value)}
137
+ rows={4}
138
+ />
139
+ </FormField>
140
+ </div>
141
+ </FormSection>
142
+
143
+ <FormSection
144
+ className="eth-project-create-form__section"
145
+ title="Setup"
146
+ description="Select the starting structure for tasks, resources, and app-domain links."
147
+ >
148
+ <div className="eth-project-create-form__grid eth-project-create-form__grid--setup">
149
+ <FormField
150
+ id="project-template"
151
+ label="Template"
152
+ helperText="Templates prefill common sections without locking future changes."
153
+ className="eth-project-create-form__field"
154
+ >
155
+ <Select
156
+ options={templateOptions}
157
+ value={values.template}
158
+ onChange={(event) => update("template", event.currentTarget.value)}
159
+ />
160
+ </FormField>
161
+ </div>
162
+ </FormSection>
163
+
164
+ <footer className="eth-project-create-form__actions">
165
+ {onCancel ? (
166
+ <Button intent="secondary" onClick={onCancel}>
167
+ Cancel
168
+ </Button>
169
+ ) : null}
170
+ <Button type="submit" intent="primary">
171
+ Create project
172
+ </Button>
173
+ </footer>
174
+ </form>
175
+ );
176
+ }
@@ -0,0 +1,107 @@
1
+ import * as React from "react";
2
+ import { StatusDot, type EthAction } from "@echothink-ui/core";
3
+ import { KPIBlock } from "@echothink-ui/charts";
4
+ import { PageHeader } from "@echothink-ui/layouts";
5
+ import type {
6
+ ProjectActivityEvent,
7
+ ProjectAppDomainInstance,
8
+ ProjectBaseProps,
9
+ ProjectResource,
10
+ ProjectSummary
11
+ } from "../types";
12
+ import { projectHtmlProps } from "../utils";
13
+ import { ProjectActivityTimeline } from "./ProjectActivityTimeline";
14
+ import { ProjectAppDomainPanel } from "./ProjectAppDomainPanel";
15
+ import { ProjectResourcePanel } from "./ProjectResourcePanel";
16
+ import { ProjectStatusSummary } from "./ProjectStatusSummary";
17
+
18
+ export interface ProjectDashboardTemplateProps extends ProjectBaseProps {
19
+ project?: ProjectSummary;
20
+ projects?: ProjectSummary[];
21
+ events?: ProjectActivityEvent[];
22
+ resources?: ProjectResource[];
23
+ instances?: ProjectAppDomainInstance[];
24
+ actions?: EthAction[];
25
+ }
26
+
27
+ const demoProject: ProjectSummary = {
28
+ id: "project-demo",
29
+ name: "Project dashboard",
30
+ status: "in-progress",
31
+ description: "Project overview for resources, tasks, activity, and app domains.",
32
+ ownerLabel: "Operations",
33
+ updatedAt: "Today",
34
+ appDomainsCount: 2,
35
+ openTasksCount: 7,
36
+ healthStatus: "healthy"
37
+ };
38
+
39
+ const demoEvents: ProjectActivityEvent[] = [
40
+ {
41
+ id: "event-1",
42
+ timestamp: "2026-05-27 09:20",
43
+ actor: "EchoThink",
44
+ kind: "resource",
45
+ summary: "Resource inventory refreshed"
46
+ },
47
+ {
48
+ id: "event-2",
49
+ timestamp: "2026-05-27 10:05",
50
+ actor: "Project lead",
51
+ kind: "app-domain",
52
+ summary: "Analytics domain configured"
53
+ }
54
+ ];
55
+
56
+ const demoResources: ProjectResource[] = [
57
+ { id: "res-1", label: "Launch brief", kind: "document", status: "synced", updatedAt: "Today" },
58
+ { id: "res-2", label: "Task backlog", kind: "table", status: "in-progress", updatedAt: "Today" }
59
+ ];
60
+
61
+ const demoInstances: ProjectAppDomainInstance[] = [
62
+ { id: "inst-1", appDomainLabel: "Analytics", status: "running", health: "healthy" },
63
+ { id: "inst-2", appDomainLabel: "Document review", status: "active", health: "warning" }
64
+ ];
65
+
66
+ export function ProjectDashboardTemplate({
67
+ project = demoProject,
68
+ projects,
69
+ events = demoEvents,
70
+ resources = demoResources,
71
+ instances = demoInstances,
72
+ className,
73
+ actions,
74
+ ...props
75
+ }: ProjectDashboardTemplateProps) {
76
+ const projectSet = projects ?? [project];
77
+
78
+ return (
79
+ <main
80
+ {...projectHtmlProps(props)}
81
+ className={`eth-project-dashboard-template ${className ?? ""}`}
82
+ data-eth-component="ProjectDashboardTemplate"
83
+ >
84
+ <PageHeader
85
+ title={project.name}
86
+ subtitle={project.description}
87
+ status={project.status}
88
+ actions={actions}
89
+ />
90
+ <div className="eth-project-dashboard-template__kpis" role="region" aria-label="Project KPIs">
91
+ <KPIBlock title="Open tasks" value={project.openTasksCount ?? 0} />
92
+ <KPIBlock title="App domains" value={project.appDomainsCount ?? 0} />
93
+ <KPIBlock
94
+ title="Health"
95
+ value={project.healthStatus ?? "healthy"}
96
+ status={<StatusDot status="synced" label="Operational" />}
97
+ />
98
+ </div>
99
+ <ProjectStatusSummary projects={projectSet} />
100
+ <div className="eth-project-dashboard-template__grid">
101
+ <ProjectActivityTimeline events={events} />
102
+ <ProjectResourcePanel resources={resources} />
103
+ <ProjectAppDomainPanel instances={instances} />
104
+ </div>
105
+ </main>
106
+ );
107
+ }
@@ -0,0 +1,112 @@
1
+ import * as React from "react";
2
+ import { Button, EmptyState, type EthAction } from "@echothink-ui/core";
3
+ import { PageHeader } from "@echothink-ui/layouts";
4
+ import type { ProjectBaseProps, ProjectSummary } from "../types";
5
+ import { projectHtmlProps } from "../utils";
6
+ import { ProjectStatusSummary } from "./ProjectStatusSummary";
7
+ import { ProjectTable } from "./ProjectTable";
8
+
9
+ export interface ProjectManagementPageProps extends ProjectBaseProps {
10
+ projects?: ProjectSummary[];
11
+ onCreate?: () => void;
12
+ onSelect?: (projectId: string) => void;
13
+ onFilterSelect?: (filterId: string) => void;
14
+ filters?: Array<{ id: string; label: React.ReactNode; active?: boolean }>;
15
+ activeProjectId?: string;
16
+ actions?: EthAction[];
17
+ }
18
+
19
+ export function ProjectManagementPage({
20
+ projects = [],
21
+ onCreate,
22
+ onSelect,
23
+ onFilterSelect,
24
+ filters = [],
25
+ activeProjectId,
26
+ title = "Projects",
27
+ subtitle = "Manage project workspaces, resources, and installed app domains.",
28
+ className,
29
+ actions,
30
+ ...props
31
+ }: ProjectManagementPageProps) {
32
+ const headerActions = React.useMemo<EthAction[]>(() => {
33
+ const providedActions = actions ?? [];
34
+ const hasCreateAction = providedActions.some((action) => {
35
+ if (action.id === "new-project") return true;
36
+ return (
37
+ typeof action.label === "string" &&
38
+ action.label.trim().toLowerCase() === "new project"
39
+ );
40
+ });
41
+
42
+ if (!onCreate || hasCreateAction) return providedActions;
43
+
44
+ return [
45
+ ...providedActions,
46
+ {
47
+ id: "new-project",
48
+ label: "New project",
49
+ intent: "primary",
50
+ onSelect: onCreate
51
+ }
52
+ ];
53
+ }, [actions, onCreate]);
54
+
55
+ return (
56
+ <main
57
+ {...projectHtmlProps(props)}
58
+ className={`eth-project-management-page ${className ?? ""}`}
59
+ data-eth-component="ProjectManagementPage"
60
+ >
61
+ <PageHeader title={title} subtitle={subtitle} actions={headerActions} />
62
+
63
+ {filters.length ? (
64
+ <div
65
+ className="eth-project-management-page__filters"
66
+ role="group"
67
+ aria-label="Project filters"
68
+ >
69
+ {filters.map((filter) => (
70
+ <button
71
+ key={filter.id}
72
+ type="button"
73
+ className={`eth-project-management-page__filter ${
74
+ filter.active ? "eth-project-management-page__filter--active" : ""
75
+ }`}
76
+ aria-pressed={Boolean(filter.active)}
77
+ aria-disabled={!onFilterSelect ? true : undefined}
78
+ tabIndex={!onFilterSelect ? -1 : undefined}
79
+ onClick={onFilterSelect ? () => onFilterSelect(filter.id) : undefined}
80
+ >
81
+ {filter.label}
82
+ </button>
83
+ ))}
84
+ </div>
85
+ ) : null}
86
+
87
+ {projects.length ? (
88
+ <>
89
+ <ProjectStatusSummary projects={projects} />
90
+ <ProjectTable
91
+ projects={projects}
92
+ onRowSelect={onSelect}
93
+ selectedProjectId={activeProjectId}
94
+ aria-label={activeProjectId ? `Projects, active ${activeProjectId}` : "Projects"}
95
+ />
96
+ </>
97
+ ) : (
98
+ <EmptyState
99
+ title="No projects yet"
100
+ description="Create a project to connect resources, members, model settings, and app domains."
101
+ action={
102
+ onCreate ? (
103
+ <Button intent="primary" onClick={onCreate}>
104
+ New project
105
+ </Button>
106
+ ) : undefined
107
+ }
108
+ />
109
+ )}
110
+ </main>
111
+ );
112
+ }