# 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 (

Dashboard

); } ``` ### 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 ( ); } ``` ### 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 (
{/* Client component for interactivity */} {/* Can be server or client */}
); } ``` ### Passing Server Data to Client Components ```typescript // Server Component export default async function Page() { const initialData = await fetchData(); return ; } // 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 // 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. - Browser validation should assert the panel center is within 2px of the viewport center on desktop and mobile. ### Static Module Data Contract Static or reserved module pages must not fabricate operational data. If a module does not have a Drizzle-backed query/API and real persisted records, render a clear empty or not-connected state instead of generated counters, fake workflows, sample records, or mock priority/status rows. `modules/shared/components/ModulePage.tsx` is the shared placeholder for these modules. Its props should stay descriptive only: ```typescript type ModulePageProps = { title: string; eyebrow: string; description: string; icon: LucideIcon; phase: "待接入" | "二期预留" | "三期预留"; }; ``` ```typescript // Bad: static pages pretending to have live business data. // Good: no fabricated records until a real data source exists. ``` When a module becomes real, replace the placeholder with a Server Component that loads data from the server boundary and pass 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. ### 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
Click me
Item 1
Item 2
// Good: semantic elements
  • Item 1
  • Item 2
``` ### Button vs Div Always use ` ``` ### Form Elements ```typescript // Bad: Missing labels, wrong elements
Email
// Good: Proper form structure
{error && }
``` ### Navigation ```typescript // Bad
router.push('/about')}>About
// Good About // For programmatic navigation with button appearance About ``` ## Next.js Image Component ### Always Use next/image ```typescript // Bad: Raw img tag Hero // Good: Optimized Image component import Image from 'next/image'; Hero image ``` ### Responsive Images ```typescript // Fill container
Banner
``` ### 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 ( No results found. router.push('/dashboard')}> Go to Dashboard router.push('/settings')}> Go to Settings Create New Order ); } ``` ### With Search Results ```typescript export function SearchCommandPalette() { const [search, setSearch] = useState(''); const { data: results, isLoading } = useSearch(search); return ( {isLoading && Searching...} No results found. {results?.map((item) => ( handleSelect(item)} > {item.title} ))} ); } ``` ## Styling with Tailwind ### Component Styling Pattern ```typescript // Use className for styling export function Card({ children, className, }: { children: ReactNode; className?: string; }) { return (
{children}
); } ``` ### Conditional Styles ```typescript import { cn } from '@/lib/utils'; export function Button({ variant = 'primary', size = 'md', className, ...props }: ButtonProps) { return ( ); } ``` ### ARIA Labels ```typescript ``` ## 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