Skip to content

Latest commit

 

History

History
126 lines (106 loc) · 4.56 KB

File metadata and controls

126 lines (106 loc) · 4.56 KB
topic components
last_verified 2026-06-23
sources
components/ui/button.tsx
components/common/container.tsx
components/common/h1.tsx
components/common/h2.tsx
components/common/h3.tsx
components/common/loader.tsx
components/home/hero.tsx
components/home/about.tsx
lib/utils.ts

Component Conventions

Default: Server Components

All components are Server Components unless they have a "use client" directive. Server Components can be async, fetch data directly, and access server-only resources.

When to add "use client"

Only when you need:

  • useState, useReducer, useContext, or other React hooks
  • useEffect or lifecycle behavior
  • Browser APIs (window, document, localStorage)
  • Event handlers (onClick, onChange, etc.) on the component itself

Do not add "use client" to layouts, pages, or wrapper components just because a child needs it — push "use client" down to the smallest possible component.

Directory structure

components/
  ui/       — shadcn primitives (avatar, badge, button, card, dialog, dropdown-menu,
               input, label, separator, skeleton, sonner)
  common/   — cross-feature reusable UI (container, h1, h2, h3, loader)
  home/     — feature-scoped components (hero, about)
  layout/   — layout wrappers (navbar, footer, etc.)
lib/
  utils.ts  — cn() utility and other non-React helpers
types/      — shared TypeScript type definitions

Route files (page.tsx, layout.tsx, loading.tsx, error.tsx) live in app/ only.

cn() utility

All conditional class merging uses cn() from lib/utils.ts:

// lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Import it as import { cn } from "@/lib/utils" in every component that merges classes.

components/ui/ — shadcn primitives

Generated by shadcn/ui. Use class-variance-authority (cva) for variant logic and Slot from radix-ui for the asChild pattern. Do not hand-edit these files directly; re-run the shadcn CLI to update them.

Pattern from button.tsx:

import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"

const buttonVariants = cva("<base-classes>", {
  variants: {
    variant: { default: "...", outline: "...", secondary: "...", ghost: "...", destructive: "...", link: "..." },
    size: { default: "...", xs: "...", sm: "...", lg: "...", icon: "...", "icon-xs": "...", "icon-sm": "...", "icon-lg": "..." },
  },
  defaultVariants: { variant: "default", size: "default" },
})

function Button({
  className,
  variant = "default",
  size = "default",
  asChild = false,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean
  }) {
  const Comp = asChild ? Slot.Root : "button"
  return (
    <Comp
      data-slot="button"
      data-variant={variant}
      data-size={size}
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }

components/common/ — hand-crafted cross-feature components

These are hand-written and use cn() with Tailwind classes directly. They do not use cva.

Container — constrains max width and centers content:

const Container = ({ children, className }: { children: React.ReactNode; className?: string }) => (
  <div className={cn("mx-auto max-w-7xl", className)}>{children}</div>
)

H1, H2, H3 — semantic heading wrappers that forward a className prop through cn(). H2 makes className optional; H1 and H3 require it.

Loader — accepts a required className prop and renders a div with those classes applied via cn().

components/home/ — feature-scoped components

Components specific to the home feature. Scoped to avoid polluting common/.

TypeScript rules

  • No any. Use proper interfaces or unknown.
  • For shadcn primitives, extend React.ComponentProps<"element"> (not ButtonHTMLAttributes) and combine with VariantProps<typeof variantsFn>.
  • For common/feature components, inline prop types in the same file unless the type is shared across multiple files — then move it to types/.
  • Import types with import type to keep runtime bundles clean.
  • React 19: JSX transform is automatic — no import React from "react" needed unless you reference React.* directly.

Imports

Use TypeScript path aliases (@/) from tsconfig.json. Do not write deep relative import chains.