Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ Frontend:
npm run build --prefix frontend
```

## Frontend UI

Before writing a new component from scratch, check
`frontend/src/app/components/ui/` for a primitive that already covers it — most
new UI should be composed from that shared layer rather than hand-rolled. If
there's no primitive but the pattern is a standard one (dialog, tooltip,
popover, select, tabs), take it from the [shadcn/ui](https://ui.shadcn.com)
registry so we inherit Radix's keyboard and ARIA behaviour instead of
reimplementing it. Hand-write a one-off only when it's used on a single screen
and has no interaction semantics worth sharing, and keep it in the feature
folder. When you find yourself copying the same markup into a third place —
especially a `shadow-[...]` glass recipe — promote it to `components/ui/` with a
test and migrate every call site in that PR. Tokens, the de facto spacing and
type scales, and the accessibility rules for primitives are documented in
[`docs/design-system.md`](docs/design-system.md).

## Testing

```bash
Expand Down
337 changes: 337 additions & 0 deletions docs/design-system.md

Large diffs are not rendered by default.

17 changes: 5 additions & 12 deletions frontend/src/app/components/assistant/QuickActionsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { Check } from "lucide-react";
import { CheckSquare } from "@/app/components/ui/check-square";
import { Modal } from "../modals/Modal";
import { QUICK_ACTIONS, type QuickActionId } from "./quickActionsPreferences";

Expand Down Expand Up @@ -61,17 +61,10 @@ export function QuickActionsModal({
<span className="min-w-0 truncate">
{action.label}
</span>
<span
className={`ml-auto flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border ${
checked
? "bg-gray-900 border-gray-900"
: "border-gray-300"
}`}
>
{checked && (
<Check className="h-2.5 w-2.5 text-white" />
)}
</span>
<CheckSquare
checked={checked}
className="ml-auto"
/>
</button>
);
})}
Expand Down
15 changes: 3 additions & 12 deletions frontend/src/app/components/modals/AddProjectDocsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { Check, Loader2, Upload, X } from "lucide-react";
import { Loader2, Upload, X } from "lucide-react";

Check warning on line 4 in frontend/src/app/components/modals/AddProjectDocsModal.tsx

View workflow job for this annotation

GitHub Actions / Frontend build and tests

'X' is defined but never used
import { CheckSquare } from "@/app/components/ui/check-square";
import { SearchBar } from "@/app/components/ui/search-bar";
import { getProject, uploadProjectDocument } from "@/app/lib/mikeApi";
import type { Document } from "../shared/types";
Expand Down Expand Up @@ -83,7 +84,7 @@
}
setSelectedIds((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);

Check warning on line 87 in frontend/src/app/components/modals/AddProjectDocsModal.tsx

View workflow job for this annotation

GitHub Actions / Frontend build and tests

Expected an assignment or function call and instead saw an expression
return next;
});
}
Expand Down Expand Up @@ -202,17 +203,7 @@
: "hover:bg-gray-100/70"
}`}
>
<span
className={`shrink-0 h-3.5 w-3.5 rounded border flex items-center justify-center ${
checked
? "bg-gray-900 border-gray-900"
: "border-gray-300"
}`}
>
{checked && (
<Check className="h-2.5 w-2.5 text-white" />
)}
</span>
<CheckSquare checked={checked} />
<DocFileIcon fileType={doc.file_type} />
<span
className={`flex-1 truncate ${
Expand Down
9 changes: 3 additions & 6 deletions frontend/src/app/components/modals/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { X } from "lucide-react";
import { GlassIconButton } from "@/app/components/ui/glass-icon-button";
import { PillButton } from "@/app/components/ui/pill-button";
import { cn } from "@/app/lib/utils";

Expand Down Expand Up @@ -116,13 +117,9 @@ export function Modal({
</div>
{headerAction}
</div>
<button
onClick={onClose}
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-white/70 bg-white/55 text-gray-500 shadow-[inset_0_1px_0_rgba(255,255,255,0.75),inset_0_-1px_0_rgba(255,255,255,0.55),0_6px_18px_rgba(15,23,42,0.08)] backdrop-blur-xl transition-colors hover:bg-white/75 hover:text-gray-700"
aria-label="Close"
>
<GlassIconButton onClick={onClose} aria-label="Close">
<X className="h-3.5 w-3.5" />
</button>
</GlassIconButton>
</div>
)}
{/* Body never scrolls itself (so children's edge shadows are
Expand Down
31 changes: 15 additions & 16 deletions frontend/src/app/components/projects/ProjectAssistantTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
type TableSortDirection,
TableStickyCell,
} from "@/app/components/shared/TablePrimitive";
import { EmptyState } from "@/app/components/ui/empty-state";
import { PillButton } from "@/app/components/ui/pill-button";
import { ChatSkeuoIcon } from "@/app/components/shared/AppSidebarSkeuoIcons";
import type { Chat } from "@/app/components/shared/types";
Expand Down Expand Up @@ -225,23 +226,21 @@ export function ProjectAssistantTable({
<ProjectAssistantLoadingRows />
) : chats.length === 0 ? (
<TableEmptyState>
<ChatSkeuoIcon className="mb-4 h-8 w-8" />
<p className="text-2xl font-medium font-serif text-gray-900">
Assistant
</p>
<p className="mt-1 text-xs text-gray-400 max-w-xs">
Ask questions and get answers grounded in the documents
in this project.
</p>
<PillButton
tone="black"
size="sm"
onClick={onCreateChat}
className="mt-4 px-3"
<EmptyState
icon={ChatSkeuoIcon}
title="Assistant"
description="Ask questions and get answers grounded in the documents in this project."
>
<Plus className="h-3.5 w-3.5" />
Create
</PillButton>
<PillButton
tone="black"
size="sm"
onClick={onCreateChat}
className="mt-4 px-3"
>
<Plus className="h-3.5 w-3.5" />
Create
</PillButton>
</EmptyState>
</TableEmptyState>
) : (
<TableBody>
Expand Down
17 changes: 7 additions & 10 deletions frontend/src/app/components/projects/ProjectReviewsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type TableSortDirection,
TableStickyCell,
} from "@/app/components/shared/TablePrimitive";
import { EmptyState } from "@/app/components/ui/empty-state";
import { PillButton } from "@/app/components/ui/pill-button";
import { TabularReviewSkeuoIcon } from "@/app/components/shared/AppSidebarSkeuoIcons";
import type { Document, TabularReview } from "@/app/components/shared/types";
Expand Down Expand Up @@ -247,15 +248,11 @@ export function ProjectReviewsTable({
No reviews found
</p>
) : (
<>
<TabularReviewSkeuoIcon className="mb-4 h-8 w-8" />
<p className="text-2xl font-medium font-serif text-gray-900">
Tabular Reviews
</p>
<p className="mt-1 text-xs text-gray-400 max-w-xs">
Extract data from project documents into tables
using AI.
</p>
<EmptyState
icon={TabularReviewSkeuoIcon}
title="Tabular Reviews"
description="Extract data from project documents into tables using AI."
>
<PillButton
tone="black"
size="sm"
Expand All @@ -266,7 +263,7 @@ export function ProjectReviewsTable({
<Plus className="h-3.5 w-3.5" />
Create
</PillButton>
</>
</EmptyState>
)}
</TableEmptyState>
) : (
Expand Down
46 changes: 21 additions & 25 deletions frontend/src/app/components/projects/ProjectsOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
type TableSortDirection,
TableStickyCell,
} from "@/app/components/shared/TablePrimitive";
import { EmptyState } from "@/app/components/ui/empty-state";
import { PillButton } from "@/app/components/ui/pill-button";
import { TabPillButton } from "@/app/components/ui/tab-pill-button";

Expand Down Expand Up @@ -554,35 +555,30 @@ export function ProjectsOverview() {
</TableBody>
) : loadError ? (
<TableEmptyState>
<OpenProjectSvgIcon className="mb-4 h-8 w-8" />
<p className="text-2xl font-medium font-serif text-gray-900">
Projects
</p>
<p className="mt-1 text-xs text-red-500 max-w-xs">
{loadError}
</p>
<PillButton
tone="black"
size="sm"
onClick={retry}
className="mt-4 px-3"
<EmptyState
icon={OpenProjectSvgIcon}
title="Projects"
description={loadError}
descriptionClassName="text-red-500"
>
Try again
</PillButton>
<PillButton
tone="black"
size="sm"
onClick={retry}
className="mt-4 px-3"
>
Try again
</PillButton>
</EmptyState>
</TableEmptyState>
) : visibleProjects.length === 0 ? (
<TableEmptyState>
{activeFilter === "all" || activeFilter === "mine" ? (
<>
<OpenProjectSvgIcon className="mb-4 h-8 w-8" />
<p className="text-2xl font-medium font-serif text-gray-900">
Projects
</p>
<p className="mt-1 text-xs text-gray-400 max-w-xs">
Upload documents into projects and to
commence chats and tabular reviews with
them.
</p>
<EmptyState
icon={OpenProjectSvgIcon}
title="Projects"
description="Upload documents into projects and to commence chats and tabular reviews with them."
>
<PillButton
tone="black"
size="sm"
Expand All @@ -592,7 +588,7 @@ export function ProjectsOverview() {
<Plus className="h-3.5 w-3.5" />
Create
</PillButton>
</>
</EmptyState>
) : (
<p className="text-sm text-gray-400">
No {activeFilter} projects
Expand Down
10 changes: 3 additions & 7 deletions frontend/src/app/components/shared/DocumentSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { PdfView } from "@/app/components/shared/views/PdfView";
import { DocxView } from "@/app/components/shared/views/DocxView";
import { SpreadsheetView } from "@/app/components/shared/views/SpreadsheetView";
import { GlassIconButton } from "@/app/components/ui/glass-icon-button";
import { PillButton } from "@/app/components/ui/pill-button";
import { WarningPopup } from "@/app/components/popups/WarningPopup";
import type { Document } from "@/app/components/shared/types";
Expand Down Expand Up @@ -152,7 +153,7 @@
if (!doc) return;
setUploadError(null);
void onLoadVersions(doc.id);
}, [doc?.id]);

Check warning on line 156 in frontend/src/app/components/shared/DocumentSidePanel.tsx

View workflow job for this annotation

GitHub Actions / Frontend build and tests

React Hook useEffect has missing dependencies: 'doc' and 'onLoadVersions'. Either include them or remove the dependency array. If 'onLoadVersions' changes too often, find the parent component that defines it and wrap that definition in useCallback

useEffect(() => {
setEditingName(false);
Expand Down Expand Up @@ -507,14 +508,9 @@
Details
</button>
</div>
<button
type="button"
onClick={onClose}
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-white/70 bg-white/55 text-gray-500 shadow-[inset_0_1px_0_rgba(255,255,255,0.75),inset_0_-1px_0_rgba(255,255,255,0.55),0_6px_18px_rgba(15,23,42,0.08)] backdrop-blur-xl transition-colors hover:bg-white/75 hover:text-gray-700"
aria-label="Close"
>
<GlassIconButton onClick={onClose} aria-label="Close">
<X className="h-3.5 w-3.5" />
</button>
</GlassIconButton>
</div>
</div>

Expand Down
57 changes: 21 additions & 36 deletions frontend/src/app/components/shared/FileDirectory.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import { type ReactNode, useEffect, useMemo, useState } from "react";
import { Check, Loader2 } from "lucide-react";
import { Loader2 } from "lucide-react";
import type { Document, LibraryFolder, Project } from "./types";
import { FileTypeIcon } from "./FileTypeIcon";
import { ProjectSvgIcon, SubfolderSvgIcon } from "./FolderSvgIcon";
import { CheckSquare } from "@/app/components/ui/check-square";
import { SearchBar } from "@/app/components/ui/search-bar";
import { TabPillButton } from "@/app/components/ui/tab-pill-button";
import { SkeletonLine } from "./TablePrimitive";
Expand Down Expand Up @@ -495,13 +496,7 @@ export function FileDirectory({
selected ? APP_SURFACE_ACTIVE_CLASS : APP_SURFACE_HOVER_CLASS
}`}
>
<span
className={`shrink-0 h-3.5 w-3.5 rounded border flex items-center justify-center ${
selected ? "bg-gray-900 border-gray-900" : "border-gray-300"
}`}
>
{selected && <Check className="h-2.5 w-2.5 text-white" />}
</span>
<CheckSquare checked={selected} />
<DocFileIcon fileType={doc.file_type} />
<span
className={`min-w-0 truncate ${selected ? "text-gray-900" : "text-gray-700"}`}
Expand Down Expand Up @@ -549,7 +544,7 @@ export function FileDirectory({
style={{ paddingLeft: indentedRowPadding(depth) }}
className={`w-full rounded-md ${DIRECTORY_GRID_CLASS} py-2 pr-2 text-xs transition-all text-left ${APP_SURFACE_HOVER_CLASS}`}
>
<span
<CheckSquare
role="checkbox"
aria-checked={someSelected ? "mixed" : allSelected}
aria-disabled={!folderSelectionReady}
Expand All @@ -564,17 +559,12 @@ export function FileDirectory({
toggleDocuments(docsInFolder);
}
}}
className={`shrink-0 h-3.5 w-3.5 rounded border flex items-center justify-center ${
allSelected || someSelected
? "bg-gray-900 border-gray-900"
: !folderSelectionReady || docsInFolder.length === 0
? "border-gray-200 bg-gray-50"
: "border-gray-300"
}`}
>
{allSelected && <Check className="h-2.5 w-2.5 text-white" />}
{someSelected && <span className="h-px w-2 bg-white" />}
</span>
checked={someSelected ? "mixed" : allSelected}
muted={
!folderSelectionReady ||
docsInFolder.length === 0
}
/>
{(libraryTab && loadingFolderIds[libraryTab].has(folder.id)) ||
(projectId &&
loadingProjectLevels.has(`${projectId}:${folder.id}`)) ||
Expand Down Expand Up @@ -955,7 +945,7 @@ export function FileDirectory({
onClick={() => toggleFolder(project.id)}
className={`w-full rounded-md ${DIRECTORY_GRID_CLASS} px-2 py-2 text-xs transition-all text-left ${APP_SURFACE_HOVER_CLASS}`}
>
<span
<CheckSquare
role="checkbox"
aria-checked={
someProjectDocsSelected
Expand All @@ -974,21 +964,16 @@ export function FileDirectory({
toggleDocuments(docs);
}
}}
className={`shrink-0 h-3.5 w-3.5 rounded border flex items-center justify-center ${
allProjectDocsSelected || someProjectDocsSelected
? "bg-gray-900 border-gray-900"
: !projectSelectionReady || docs.length === 0
? "border-gray-200 bg-gray-50"
: "border-gray-300"
}`}
>
{allProjectDocsSelected && (
<Check className="h-2.5 w-2.5 text-white" />
)}
{someProjectDocsSelected && (
<span className="h-px w-2 bg-white" />
)}
</span>
checked={
someProjectDocsSelected
? "mixed"
: allProjectDocsSelected
}
muted={
!projectSelectionReady ||
docs.length === 0
}
/>
<ProjectSvgIcon
open={isExpanded}
className="h-3.5 w-3.5 shrink-0"
Expand Down
Loading
Loading