# Component Development Guidelines

This document covers component development patterns including Server vs Client components, semantic HTML, and UI best practices.

## Server vs Client Components

### Default to Server Components

Next.js App Router defaults to Server Components. Use them for:

- Data fetching
- Accessing backend resources directly
- Keeping sensitive data on the server
- Reducing client-side JavaScript

```typescript
// app/(app)/dashboard/page.tsx (Server Component)
import { DashboardStats } from '@/modules/dashboard/components';

export default async function DashboardPage() {
  // Can fetch data directly
  const stats = await fetchDashboardStats();

  return (
    <main>
      <h1>Dashboard</h1>
      <DashboardStats data={stats} />
    </main>
  );
}
```

### When to Use Client Components

Add `'use client'` directive only when you need:

- Event handlers (onClick, onChange, etc.)
- useState, useEffect, or other React hooks
- Browser-only APIs (localStorage, window)
- Class components with lifecycle methods

```typescript
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
```

### Composition Pattern

Keep Server Components at the top, push Client Components down:

```typescript
// Server Component (page.tsx)
import { ProductList } from './ProductList';
import { FilterSidebar } from './FilterSidebar'; // Client

export default async function ProductsPage() {
  const products = await fetchProducts();

  return (
    <div className="flex">
      <FilterSidebar /> {/* Client component for interactivity */}
      <ProductList products={products} /> {/* Can be server or client */}
    </div>
  );
}
```

### Passing Server Data to Client Components

```typescript
// Server Component
export default async function Page() {
  const initialData = await fetchData();

  return <InteractiveWidget initialData={initialData} />;
}

// Client Component
'use client';

export function InteractiveWidget({ initialData }: { initialData: Data }) {
  const [data, setData] = useState(initialData);
  // Interactive logic...
}
```

## Semantic HTML

## Kumo UI Convention

Use `components/ui/*` as the project-owned adapter layer for Cloudflare Kumo components.
Feature modules and route files should import `Button`, `Input`, `Select`, `Textarea`, `Table`,
`Checkbox`, `Badge`, `Card`, and `Dialog` from `@/components/ui/...` instead of importing
`@cloudflare/kumo` directly.

```typescript
// Good: project adapter keeps Kumo API differences local
import { Select } from "@/components/ui/select";
import { Table } from "@/components/ui/table";

// Avoid: feature code should not hand-style native controls
<select name="roleId" />
<table />
```

Visible form controls and data tables should use the Kumo-backed adapters. Native hidden
inputs are acceptable only inside adapters when needed to preserve FormData semantics.

### Select Adapter Positioning Contract

`components/ui/select.tsx` is intentionally project-owned instead of directly wrapping
`@cloudflare/kumo/components/select`. Kumo Select 2.6 can inherit Base UI
`alignItemWithTrigger=true`, which may render `data-side="none"` and overlap the trigger in
production.

The project Select adapter must:

- Preserve the local API: `options`, `value`, `defaultValue`, `name`, `placeholder`,
  `disabled`, `required`, `aria-*`, and `onValueChange`.
- Render a hidden form value only inside the adapter when `name` is provided.
- Use portal/fixed positioning for the popup and keep a positive gap between trigger and
  panel. Browser validation should assert `verticalOverlap === 0` on desktop and mobile.
- Keep keyboard behavior for ArrowUp/ArrowDown, Home/End, Enter/Space, Escape, and Tab.

```typescript
// Good: feature code stays on the project adapter.
<Select name="organizationId" options={organizationOptions} />

// Avoid: this can reintroduce production overlay regressions.
import { Select } from "@cloudflare/kumo/components/select";
```

### Dialog Adapter Centering Contract

`components/ui/dialog.tsx` may use Kumo Dialog primitives for accessibility, but the
project adapter owns panel geometry. Kumo Dialog includes fixed `left-1/2 top-1/2`
and translate classes, and those transforms can interact with project animations or
production CSS ordering.

The project Dialog adapter must:

- Center the panel without relying on translate transforms. Use `fixed inset-0 m-auto`
  plus explicit width, max-height, and `h-fit`.
- Override Kumo minimum width so mobile dialogs stay inside `100vw`.
- Keep `transform: none` and `translate: 0 0` on `.teatea-dialog`; use
  opacity/scale-only motion if animation is needed.
- Kumo may inject `-translate-x-1/2 -translate-y-1/2` before project classes in the
  runtime class list. Do not rely only on Tailwind class order or a plain CSS rule;
  the Dialog adapter should enforce `style={{ transform: "none", translate: "0 0" }}`
  or an equally strong adapter-owned override.
- Browser validation should assert the panel center is within 2px of the viewport center
  on desktop and mobile and should inspect computed `translate` when a dialog is visibly
  offset.

### Static Module Data Contract

Operational module pages must not fabricate data. If a module does not have a
Drizzle-backed query/API and real persisted records, render a clear empty or
not-connected state inside that module's own route/component instead of generated
counters, fake workflows, sample records, or mock priority/status rows.

The old shared reserved-module placeholder components were removed after devices,
notices, alerts, family, health, care, and emergency became real persisted workspaces.
Do not reintroduce a generic `ReservedModulePages` layer for operational modules.

When a module becomes real, implement the route as a Server Component that loads data
from the server boundary and passes only persisted, permission-filtered data into
client components.

### App Shell Tenant and Account Menu Contract

The desktop app shell footer is the tenant/account workspace control, not only a sign-out
surface.

- Show a compact organization switcher above the account card.
- Display both organization name and non-empty `slug`; do not hide or fabricate missing
  tenant identifiers.
- Switch organizations by calling `POST /api/auth/organization` and then `router.refresh()`;
  do not store the active organization in localStorage.
- The account card menu opens a user settings dialog for the current account. Profile edits
  call `PATCH /api/account/profile`.
- OIDC binding UI must reflect real backend capability. If binding records/callbacks are not
  implemented, show an honest not-connected state instead of fake provider accounts.
- The sidebar nav selected state belongs in a small client component using `usePathname()`;
  keep the rest of `AppShell` server-rendered.
- Authenticated workspace links should preserve the active organization slug by using
  `modules/shared/lib/workspace-routing.ts`. Sidebar links, breadcrumbs, permission
  redirects, table search/pagination paths, login redirects, and organization switch
  redirects should call `getWorkspaceHref(activeSlug, workspacePath)` instead of hard-coding
  `/app/...` paths.
- The legacy unscoped `/app/...` paths remain valid fallback paths for accounts without an
  active organization. When a session has an active organization, redirect into
  `/app/{organizationSlug}/...` so operators can see which workspace they are using.
- Top-level workspace route names such as `dashboard`, `settings`, `elders`, and `beds` are
  reserved path segments, not tenant identifiers. Keep the frontend reserved list in
  `workspace-routing.ts` aligned with backend organization slug validation.

### Business Form Defaults Contract

Create forms for persisted business records must not prefill required domain fields with
assumed values. Defaults like age `75`, gender `male`, care level `self-care`, status
`active`, or "first available role" can turn into real PostgreSQL records when a user
submits without noticing.

Use explicit empty selection states for required business choices, then validate before
calling the API:

```typescript
// Bad: saves fabricated domain decisions if the user only fills name/email.
const emptyInput = {
  gender: "male",
  age: 75,
  careLevel: "self-care",
  status: "active",
};

// Good: the user must provide the domain values that will be persisted.
const emptyInput = {
  gender: "",
  age: "",
  careLevel: "",
  status: "",
};

const genderOptions = [
  { value: "", label: "Select gender", disabled: true },
  ...realGenderOptions,
];
```

Edit forms may preload values that already exist on the persisted record. Create,
invite, approve, and assignment forms should require an explicit selection for role,
organization, status, and other authorization or workflow fields unless the default is
server-owned and documented as a product rule.

### Use Proper Elements

```typescript
// Bad: div for everything
<div onClick={handleClick}>Click me</div>
<div>
  <div>Item 1</div>
  <div>Item 2</div>
</div>

// Good: semantic elements
<button onClick={handleClick}>Click me</button>
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
</ul>
```

### Button vs Div

Always use `<button>` for clickable actions:

```typescript
// Bad: Non-semantic, no keyboard support, no accessibility
<div
  className="cursor-pointer"
  onClick={handleClick}
>
  Save
</div>

// Good: Semantic, keyboard accessible, proper focus
<button
  type="button"
  onClick={handleClick}
  className="..."
>
  Save
</button>
```

### Form Elements

```typescript
// Bad: Missing labels, wrong elements
<div>
  <span>Email</span>
  <input type="text" />
</div>

// Good: Proper form structure
<div>
  <label htmlFor="email">Email</label>
  <input
    id="email"
    type="email"
    aria-describedby="email-error"
  />
  {error && <p id="email-error" role="alert">{error}</p>}
</div>
```

### Navigation

```typescript
// Bad
<div onClick={() => router.push('/about')}>About</div>

// Good
<Link href="/about">About</Link>

// For programmatic navigation with button appearance
<Link href="/about" className="btn btn-primary">
  About
</Link>
```

## Next.js Image Component

### Always Use next/image

```typescript
// Bad: Raw img tag
<img src="/hero.jpg" alt="Hero" />

// Good: Optimized Image component
import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={600}
  priority // For above-the-fold images
/>
```

### Responsive Images

```typescript
// Fill container
<div className="relative h-64 w-full">
  <Image
    src="/banner.jpg"
    alt="Banner"
    fill
    className="object-cover"
    sizes="(max-width: 768px) 100vw, 50vw"
  />
</div>
```

### Remote Images

Configure domains in `next.config.js`:

```javascript
// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
      },
    ],
  },
};
```

## Command Palette (cmdk)

### Basic Implementation

```typescript
'use client';

import { Command } from 'cmdk';
import { useState, useEffect } from 'react';

export function CommandPalette() {
  const [open, setOpen] = useState(false);

  // Toggle with keyboard shortcut
  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        setOpen((open) => !open);
      }
    };

    document.addEventListener('keydown', down);
    return () => document.removeEventListener('keydown', down);
  }, []);

  return (
    <Command.Dialog
      open={open}
      onOpenChange={setOpen}
      label="Global Command Menu"
    >
      <Command.Input placeholder="Type a command or search..." />
      <Command.List>
        <Command.Empty>No results found.</Command.Empty>

        <Command.Group heading="Navigation">
          <Command.Item onSelect={() => router.push('/dashboard')}>
            Go to Dashboard
          </Command.Item>
          <Command.Item onSelect={() => router.push('/settings')}>
            Go to Settings
          </Command.Item>
        </Command.Group>

        <Command.Group heading="Actions">
          <Command.Item onSelect={handleNewOrder}>
            Create New Order
          </Command.Item>
        </Command.Group>
      </Command.List>
    </Command.Dialog>
  );
}
```

### With Search Results

```typescript
export function SearchCommandPalette() {
  const [search, setSearch] = useState('');
  const { data: results, isLoading } = useSearch(search);

  return (
    <Command.Dialog open={open} onOpenChange={setOpen}>
      <Command.Input
        value={search}
        onValueChange={setSearch}
        placeholder="Search..."
      />
      <Command.List>
        {isLoading && <Command.Loading>Searching...</Command.Loading>}

        <Command.Empty>No results found.</Command.Empty>

        {results?.map((item) => (
          <Command.Item
            key={item.id}
            value={item.title}
            onSelect={() => handleSelect(item)}
          >
            {item.title}
          </Command.Item>
        ))}
      </Command.List>
    </Command.Dialog>
  );
}
```

## Styling with Tailwind

### Component Styling Pattern

```typescript
// Use className for styling
export function Card({
  children,
  className,
}: {
  children: ReactNode;
  className?: string;
}) {
  return (
    <div
      className={cn(
        'rounded-lg border bg-card p-4 shadow-sm',
        className
      )}
    >
      {children}
    </div>
  );
}
```

### Conditional Styles

```typescript
import { cn } from '@/lib/utils';

export function Button({
  variant = 'primary',
  size = 'md',
  className,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(
        'inline-flex items-center justify-center rounded-md font-medium',
        // Variants
        {
          'bg-primary text-primary-foreground': variant === 'primary',
          'bg-secondary text-secondary-foreground': variant === 'secondary',
          'border bg-transparent': variant === 'outline',
        },
        // Sizes
        {
          'h-8 px-3 text-sm': size === 'sm',
          'h-10 px-4': size === 'md',
          'h-12 px-6 text-lg': size === 'lg',
        },
        className
      )}
      {...props}
    />
  );
}
```

### Responsive Design

```typescript
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  {items.map((item) => (
    <Card key={item.id}>{item.content}</Card>
  ))}
</div>
```

## Accessibility

### Focus Management

```typescript
export function Modal({ open, onClose, children }: ModalProps) {
  const closeButtonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (open) {
      closeButtonRef.current?.focus();
    }
  }, [open]);

  return (
    <Dialog open={open} onOpenChange={onClose}>
      <DialogContent>
        {children}
        <button ref={closeButtonRef} onClick={onClose}>
          Close
        </button>
      </DialogContent>
    </Dialog>
  );
}
```

### ARIA Labels

```typescript
<button
  aria-label="Close dialog"
  aria-expanded={isOpen}
  aria-controls="dropdown-menu"
>
  <CloseIcon />
</button>

<div
  id="dropdown-menu"
  role="menu"
  aria-hidden={!isOpen}
>
  {/* Menu items */}
</div>
```

## Best Practices

1. **Server First**: Default to Server Components
2. **Semantic HTML**: Use the right element for the job
3. **Optimize Images**: Always use next/image
4. **Accessibility**: Include ARIA labels and keyboard support
5. **Type Props**: Define TypeScript interfaces for all props
6. **Composition**: Break large components into smaller pieces

## Anti-Patterns

- Using `div` for buttons and links
- Using `img` instead of `next/image`
- Adding `'use client'` at the top of every file
- Inline styles instead of Tailwind classes
- Missing accessibility attributes
- Components with too many responsibilities
