Skip to content

Latest commit

 

History

History
435 lines (331 loc) · 9.6 KB

File metadata and controls

435 lines (331 loc) · 9.6 KB

Component Patterns & Architecture Guide

This guide documents the component patterns and architectural decisions made during the frontend modernization.

File Organization

src/
├── app/                    # Next.js 15 App Router
│   ├── projects/
│   │   ├── page.tsx       # Route component
│   │   ├── loading.tsx    # Loading state
│   │   ├── error.tsx      # Error boundary
│   │   └── [name]/        # Dynamic routes
├── components/            # Reusable components
│   ├── ui/               # Shadcn base components
│   ├── layouts/          # Layout components
│   └── *.tsx             # Custom components
├── services/             # API layer
│   ├── api/             # HTTP clients
│   └── queries/         # React Query hooks
├── hooks/               # Custom hooks
├── types/               # TypeScript types
└── lib/                 # Utilities

Naming Conventions

  • Files: kebab-case (e.g., empty-state.tsx)
  • Components: PascalCase (e.g., EmptyState)
  • Hooks: camelCase with use prefix (e.g., useAsyncAction)
  • Types: PascalCase (e.g., ProjectSummary)

Component Patterns

1. Type Over Interface

Guideline: Always use type instead of interface

// ✅ Good
type ButtonProps = {
  label: string;
  onClick: () => void;
};

// ❌ Bad
interface ButtonProps {
  label: string;
  onClick: () => void;
}

2. Component Props

Pattern: Destructure props with typed parameters

type EmptyStateProps = {
  icon?: React.ComponentType<{ className?: string }>;
  title: string;
  description?: string;
  action?: React.ReactNode;
};

export function EmptyState({
  icon: Icon,
  title,
  description,
  action
}: EmptyStateProps) {
  // Implementation
}

3. Children Props

Pattern: Use React.ReactNode for children

type PageContainerProps = {
  children: React.ReactNode;
  maxWidth?: 'sm' | 'md' | 'lg';
};

4. Loading States

Pattern: Use skeleton components, not spinners

// ✅ Good - loading.tsx
import { TableSkeleton } from '@/components/skeletons';

export default function SessionsLoading() {
  return <TableSkeleton rows={10} columns={5} />;
}

// ❌ Bad - inline spinner
if (loading) return <Spinner />;

5. Error Handling

Pattern: Use error boundaries, not inline error states

// ✅ Good - error.tsx
'use client';

export default function SessionsError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Failed to load sessions</CardTitle>
        <CardDescription>{error.message}</CardDescription>
      </CardHeader>
      <CardContent>
        <Button onClick={reset}>Try again</Button>
      </CardContent>
    </Card>
  );
}

6. Empty States

Pattern: Use EmptyState component consistently

{sessions.length === 0 ? (
  <EmptyState
    icon={Inbox}
    title="No sessions yet"
    description="Create your first session to get started"
    action={
      <Button onClick={handleCreate}>
        <Plus className="w-4 h-4 mr-2" />
        New Session
      </Button>
    }
  />
) : (
  // Render list
)}

React Query Patterns

1. Query Hooks

Pattern: Create typed query hooks in services/queries/

export function useProjects() {
  return useQuery({
    queryKey: ['projects'],
    queryFn: () => projectsApi.listProjects(),
    staleTime: 30000, // 30 seconds
  });
}

2. Mutation Hooks

Pattern: Include optimistic updates and cache invalidation

export function useDeleteProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (name: string) => projectsApi.deleteProject(name),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['projects'] });
    },
  });
}

3. Page Usage

Pattern: Destructure query results

export default function ProjectsPage() {
  const { data: projects, isLoading, error } = useProjects();
  const deleteMutation = useDeleteProject();

  // Use loading.tsx for isLoading
  // Use error.tsx for error
  // Render data
}

Layout Patterns

1. Page Structure

<PageContainer maxWidth="xl">
  <PageHeader
    title="Projects"
    description="Manage your projects"
    actions={<Button>New Project</Button>}
  />

  <PageSection title="Active Projects">
    {/* Content */}
  </PageSection>
</PageContainer>

2. Sidebar Layout

<SidebarLayout
  sidebar={<ProjectNav />}
  sidebarWidth="16rem"
>
  {children}
</SidebarLayout>

Form Patterns

1. Form Fields

Pattern: Use FormFieldWrapper for consistency

<FormFieldsGrid>
  <FormFieldWrapper
    label="Project Name"
    description="Unique identifier"
    error={errors.name}
  >
    <Input {...register('name')} />
  </FormFieldWrapper>
</FormFieldsGrid>

2. Submit Buttons

Pattern: Use LoadingButton for mutations

<LoadingButton
  type="submit"
  loading={mutation.isPending}
  disabled={!isValid}
>
  Create Project
</LoadingButton>

Custom Hooks

1. Async Actions

const { execute, isLoading, error } = useAsyncAction(
  async (data) => {
    return await api.createProject(data);
  }
);

await execute(formData);

2. Local Storage

const [theme, setTheme] = useLocalStorage('theme', 'light');

3. Clipboard

const { copy, copied } = useClipboard();

<Button onClick={() => copy(text)}>
  {copied ? 'Copied!' : 'Copy'}
</Button>

TypeScript Patterns

1. No Any Types

// ✅ Good
type MessageHandler = (msg: SessionMessage) => void;

// ❌ Bad
type MessageHandler = (msg: any) => void;

2. Optional Chaining

// ✅ Good
const name = project?.displayName ?? project.name;

// ❌ Bad
const name = project ? project.displayName || project.name : '';

3. Type Guards

function isErrorResponse(data: unknown): data is ErrorResponse {
  return typeof data === 'object' &&
         data !== null &&
         'error' in data;
}

Performance Patterns

1. Code Splitting

Pattern: Use dynamic imports for heavy components

const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <Skeleton />,
});

2. React Query Caching

Pattern: Set appropriate staleTime

// Fast-changing data
staleTime: 0

// Slow-changing data
staleTime: 300000 // 5 minutes

// Static data
staleTime: Infinity

Accessibility Patterns

1. ARIA Labels

<Button aria-label="Delete project">
  <Trash className="w-4 h-4" />
</Button>

2. Keyboard Navigation

<div
  role="button"
  tabIndex={0}
  onKeyDown={(e) => e.key === 'Enter' && handleClick()}
>
  {content}
</div>

Error Message Patterns

// ✅ User-friendly
"Failed to load projects. Please try again."

// ❌ Technical
"Error: ECONNREFUSED 127.0.0.1:3000"

Onboarding Wizard

The FTUE (first-time user experience) wizard lives in src/components/onboarding/. It is a dedicated shared component (deliberate exception to colocation) because it may be triggered from multiple entry points.

Architecture

components/onboarding/
  welcome-wizard.tsx              # Thin state machine shell (<100 lines)
  integration-registry.ts         # INTEGRATION_REGISTRY + IntegrationEntry type
  use-should-show-onboarding.ts   # Trigger hook (zero projects + localStorage)
  use-app-config.ts               # Reads server config from <meta> tags
  steps/
    welcome-step.tsx              # Step 1: intro
    create-workspace-step.tsx     # Step 2: workspace creation
    integrations-step.tsx         # Step 3: connect integrations
    completion-step.tsx           # Step 4: redirect CTAs

WelcomeWizard composition

The wizard shell is a thin state machine driven by a STEPS array. Each step receives WizardStepProps (onNext, onSkip, wizardState) and is self-contained. To add a new step, create the component and append it to the STEPS array.

Integration Registry (INTEGRATION_REGISTRY)

A typed array of IntegrationEntry objects. Each entry provides id, name, description, isConnected(status), and renderCard(props). The integrations step iterates the registry; it never imports individual *ConnectionCard components directly.

A compile-time guard ensures every key of IntegrationsStatus (excluding mcpServers) has a registry entry. Adding a new integration to the API type without a registry entry causes a build error.

WorkspaceForm callback interface

WorkspaceForm (in src/components/workspace-form.tsx) does NOT own any mutation. It exposes onSubmit(data), onError(err), and isSubmitting callbacks so consumers control what happens on success. Both CreateWorkspaceDialog and the onboarding wizard use the same form.

Invariants (agent review checklist)

  • welcome-wizard.tsx must stay under 100 lines
  • WorkspaceForm must not import useCreateProject
  • integrations-step.tsx must not import individual connection cards
  • INTEGRATION_REGISTRY must cover all non-MCP keys of IntegrationsStatus
  • Wizard state persisted to sessionStorage for GitHub OAuth redirect must be cleared on completion/dismissal

Summary

Key patterns:

  • Use type over interface
  • Skeleton components for loading
  • Error boundaries for errors
  • EmptyState for empty lists
  • React Query for data fetching
  • TypeScript strict mode
  • No any types
  • Proper error messages