diff --git a/apps/web/package.json b/apps/web/package.json index 054c429..55f3aab 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,6 +13,7 @@ "@code/auth": "workspace:*", "@code/backend": "workspace:*", "@code/env": "workspace:*", + "@code/primitives": "workspace:*", "@code/ui": "workspace:*", "@flue/react": "1.0.0-beta.9", "@flue/sdk": "1.0.0-beta.9", @@ -40,7 +41,7 @@ "tailwindcss": "catalog:", "typescript": "catalog:", "vite": "catalog:", - "vitest": "catalog:", - "vite-tsconfig-paths": "^6.1.1" + "vite-tsconfig-paths": "^6.1.1", + "vitest": "catalog:" } } diff --git a/apps/web/src/components/projects/project-workspace-page.tsx b/apps/web/src/components/projects/project-workspace-page.tsx index b171063..3c317ba 100644 --- a/apps/web/src/components/projects/project-workspace-page.tsx +++ b/apps/web/src/components/projects/project-workspace-page.tsx @@ -3,296 +3,702 @@ import { Button } from "@code/ui/components/button"; import { Input } from "@code/ui/components/input"; import { Textarea } from "@code/ui/components/textarea"; import { + ArrowUpRight, Bot, + CircleAlert, ExternalLink, - FileText, + FolderGit2, GitFork, - Play, + GitPullRequest, + LoaderCircle, Plus, + Sparkles, } from "lucide-react"; import UserMenu from "@/components/user-menu"; import { useProjectWorkspace } from "@/hooks/use-project-workspace"; +import type { + ProjectIssueSummary, + ProjectLoopView, +} from "@/lib/projects/project-evidence"; -const statusStyle: Record["status"], string> = { - completed: - "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", - failed: "border-destructive/40 bg-destructive/10 text-destructive", - "needs-input": - "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300", - open: "border-border bg-muted/40 text-muted-foreground", - queued: "border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300", - working: "border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300", +interface Project { + readonly id: string; + readonly name: string; + readonly sources: readonly { + readonly defaultBranch?: string; + readonly host: string; + readonly repositoryPath: string; + readonly url: string; + }[]; +} + +const formatStatus = (status: Doc<"projectIssues">["status"]): string => { + if (status === "needs-input") { + return "Needs input"; + } + if (status === "working") { + return "In progress"; + } + return status.charAt(0).toUpperCase() + status.slice(1); }; +const statusClasses: Record["status"], string> = { + completed: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300", + failed: "bg-destructive/15 text-destructive", + "needs-input": "bg-orange-500/15 text-orange-700 dark:text-orange-300", + open: "bg-muted text-muted-foreground", + queued: "bg-violet-500/15 text-violet-700 dark:text-violet-300", + working: "bg-blue-500/15 text-blue-700 dark:text-blue-300", +}; + +const verificationIconClass = ( + verification: ProjectLoopView["verification"] +): string => { + if (verification === "passed") { + return "text-emerald-600"; + } + if (verification === "failed" || verification === "blocked") { + return "text-orange-600"; + } + return "text-muted-foreground"; +}; + +const checkStateClass = ( + state: ProjectLoopView["verificationChecks"][number]["state"] +): string => { + if (state === "complete") { + return "bg-emerald-500 text-white"; + } + if (state === "attention") { + return "bg-orange-500 text-white"; + } + if (state === "current") { + return "bg-blue-500 text-white"; + } + return "bg-muted text-muted-foreground"; +}; + +const LoadingLine = ({ label }: { readonly label: string }) => ( +
+ + {label} +
+); + interface RepositoryFormProps { readonly busy: boolean; - readonly onChange: (value: string) => void; - readonly onSubmit: () => void; + readonly onRepositoryChange: (value: string) => void; + readonly onRepositorySubmit: () => void; readonly value: string; } const RepositoryForm = ({ busy, - onChange, - onSubmit, + onRepositoryChange, + onRepositorySubmit, value, }: RepositoryFormProps) => (
{ event.preventDefault(); - onSubmit(); + onRepositorySubmit(); }} > -
- -

- Import with a public HTTP(S) Git URL. +

+

+ Connect project +

+

+ Bring a public repository into the project loop.

onChange(event.target.value)} + aria-label="Public Git repository URL" + onChange={(event) => onRepositoryChange(event.target.value)} placeholder="https://github.com/owner/repo" required value={value} /> ); -interface ProjectListProps { - readonly projects: - | readonly { - readonly id: string; - readonly name: string; - readonly sources: readonly { - readonly host: string; - readonly repositoryPath: string; - readonly url: string; - }[]; - }[] - | undefined; +interface ProjectSidebarProps { + readonly onProjectSelect: (projectId: Id<"projects">) => void; + readonly projects: readonly Project[] | undefined; readonly selectedId: Id<"projects"> | null; - readonly onSelect: (projectId: Id<"projects">) => void; } -const ProjectList = ({ projects, selectedId, onSelect }: ProjectListProps) => { - const renderContent = () => { +const ProjectSidebar = ({ + onProjectSelect, + projects, + selectedId, +}: ProjectSidebarProps) => { + const renderProjects = () => { if (projects === undefined) { - return ( -

Loading projects...

- ); + return ; } if (projects.length === 0) { return ( -

- Import a repository to create its project artifacts and issue queue. +

+ Connect a repository to create the first project workspace.

); } return ( -
- {projects.map((project) => ( - - ))} +
+ {projects.map((project) => { + const [source] = project.sources; + const selected = selectedId === project.id; + return ( + + ); + })}
); }; return ( - - ); -}; - -interface ArtifactGridProps { - readonly artifacts: readonly Doc<"projectArtifacts">[] | undefined; -} - -const renderArtifacts = (artifacts: ArtifactGridProps["artifacts"]) => { - if (artifacts === undefined) { - return

Loading artifacts...

; - } - if (artifacts.length === 0) { - return

No artifacts yet.

; - } - return ( -
- {artifacts.map((artifact) => ( -
- -

{artifact.path}

-

- rev {artifact.revision} +

+ +
+
+ +

+ Project context stays attached to every issue and run. +

+
+
+ ); }; -const ArtifactGrid = ({ artifacts }: ArtifactGridProps) => ( -
-

- Project artifacts -

- {renderArtifacts(artifacts)} -
-); - interface IssueComposerProps { readonly body: string; readonly busy: boolean; - readonly onBodyChange: (value: string) => void; - readonly onSubmit: () => void; - readonly onTitleChange: (value: string) => void; + readonly onIssueBodyChange: (value: string) => void; + readonly onIssueSubmit: () => void; + readonly onIssueTitleChange: (value: string) => void; readonly title: string; } const IssueComposer = ({ body, busy, - onBodyChange, - onSubmit, - onTitleChange, + onIssueBodyChange, + onIssueSubmit, + onIssueTitleChange, title, }: IssueComposerProps) => (
{ event.preventDefault(); - onSubmit(); + onIssueSubmit(); }} > -

New issue

- onTitleChange(event.target.value)} - placeholder="Issue title" - required - value={title} - /> -