> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Components

> Customizing Sonamu components

Learn how to customize components and views generated by Sonamu to fit your project requirements.

## Customization Overview

<CardGroup cols={2}>
  <Card title="Post-generation Editing" icon="pen-to-square">
    Full control Free modifications
  </Card>

  <Card title="Props Extension" icon="puzzle-piece">
    Add new features Interface extension
  </Card>

  <Card title="Styling" icon="palette">
    Design system Theme application
  </Card>

  <Card title="Logic Addition" icon="code">
    Business logic Validation
  </Card>
</CardGroup>

## Customization Philosophy

### Sonamu's Approach

Sonamu follows the **"80% automatic, 20% customization"** philosophy.

**Generation vs Customization**:

```
Generation phase: Basic CRUD functionality, standard UI patterns
   ↓
Customization phase: Business logic, unique UI/UX
```

**When customization is needed**:

* Special validation rules
* Complex relationship handling
* Unique UI/UX requirements
* Permission-based feature control
* Workflow integration

**Customization vs Regeneration**:

* Generated files are **overwritten when regenerated**
* Therefore, **Git commit immediately** after generation is recommended
* Customization involves **directly modifying generated code**
* If needed, **create new components** to use

## View Customization

### Extending List Page

Add features to generated list page.

**Original (auto-generated)**:

```typescript theme={null}
// pages/users/index.tsx
export default function UsersPage() {
  const [page, setPage] = useState(1);
  const { users, total, isLoading } = useUsers({ page, pageSize: 20 });

  return (
    <div>
      <h1>Users</h1>
      <UserTable users={users} isLoading={isLoading} />
      {/* Pagination */}
    </div>
  );
}
```

**Customized (filters added)**:

```typescript theme={null}
// pages/users/index.tsx (modified)
export default function UsersPage() {
  const [page, setPage] = useState(1);
  const [filters, setFilters] = useState({
    role: null as string | null,
    isActive: true,
    search: "",
  });

  const { users, total, isLoading } = useUsers({
    page,
    pageSize: 20,
    ...filters, // Filters added
  });

  return (
    <div>
      <div className="header">
        <h1>Users</h1>
        <button onClick={() => router.push("/users/new")}>
          Create New User
        </button>
      </div>

      {/* Added: Filter UI */}
      <div className="filters">
        <SearchInput
          value={filters.search}
          onChange={(search) => setFilters({ ...filters, search })}
          placeholder="Search users..."
        />

        <select
          value={filters.role || ""}
          onChange={(e) => setFilters({ ...filters, role: e.target.value || null })}
        >
          <option value="">All Roles</option>
          <option value="admin">Admin</option>
          <option value="user">User</option>
          <option value="guest">Guest</option>
        </select>

        <label>
          <input
            type="checkbox"
            checked={filters.isActive}
            onChange={(e) => setFilters({ ...filters, isActive: e.target.checked })}
          />
          Active Only
        </label>
      </div>

      <UserTable users={users} isLoading={isLoading} />
      {/* Pagination */}
    </div>
  );
}
```

**Added features**:

* Search filter
* Role filter
* Active status filter

### Form Customization

Add business logic to create/edit forms.

**Original (auto-generated)**:

```typescript theme={null}
// components/users/UserForm.tsx
export function UserForm({ initialValues, onSubmit, isSubmitting }: UserFormProps) {
  const { register, handleSubmit, formState: { errors } } = useForm({
    defaultValues: initialValues,
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email", { required: true })} />
      <input {...register("username", { required: true })} />
      <button type="submit">Save</button>
    </form>
  );
}
```

**Customized (complex validation)**:

```typescript theme={null}
// components/users/UserForm.tsx (modified)
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";

// Validation schema
const userSchema = z.object({
  email: z.string()
    .email("Invalid email address")
    .refine(
      async (email) => {
        // Check email duplication
        const { exists } = await userService.checkEmail({ email });
        return !exists;
      },
      { message: "Email already taken" }
    ),
  username: z.string()
    .min(3, "Username must be at least 3 characters")
    .max(20, "Username must be at most 20 characters")
    .regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores"),
  password: z.string()
    .min(8, "Password must be at least 8 characters")
    .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
    .regex(/[a-z]/, "Password must contain at least one lowercase letter")
    .regex(/[0-9]/, "Password must contain at least one number"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords do not match",
  path: ["confirmPassword"],
});

export function UserForm({ initialValues, onSubmit, isSubmitting }: UserFormProps) {
  const { register, handleSubmit, formState: { errors }, watch } = useForm({
    resolver: zodResolver(userSchema),
    defaultValues: initialValues,
  });

  const password = watch("password");

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Email *</label>
        <input {...register("email")} />
        {errors.email && <span className="error">{errors.email.message}</span>}
      </div>

      <div>
        <label>Username *</label>
        <input {...register("username")} />
        {errors.username && <span className="error">{errors.username.message}</span>}
      </div>

      <div>
        <label>Password *</label>
        <input type="password" {...register("password")} />
        {errors.password && <span className="error">{errors.password.message}</span>}

        {/* Added: Password strength indicator */}
        {password && (
          <div className="password-strength">
            <div className="strength-bar" data-strength={getPasswordStrength(password)} />
            <span>{getPasswordStrength(password)}</span>
          </div>
        )}
      </div>

      <div>
        <label>Confirm Password *</label>
        <input type="password" {...register("confirmPassword")} />
        {errors.confirmPassword && <span className="error">{errors.confirmPassword.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Saving..." : "Save"}
      </button>
    </form>
  );
}
```

**Added features**:

* Zod schema-based validation
* Email duplication check (async)
* Password complexity rules
* Password confirmation
* Password strength indicator

### Table Customization

Add features to tables.

**Original (auto-generated)**:

```typescript theme={null}
// components/users/UserTable.tsx
export function UserTable({ users, isLoading }: UserTableProps) {
  return (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Email</th>
          <th>Username</th>
        </tr>
      </thead>
      <tbody>
        {users.map((user) => (
          <tr key={user.id}>
            <td>{user.id}</td>
            <td>{user.email}</td>
            <td>{user.username}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

**Customized (sorting, selection, actions)**:

```typescript theme={null}
// components/users/UserTable.tsx (modified)
interface UserTableProps {
  users: User[];
  isLoading: boolean;
  onRowClick?: (user: User) => void;
  onDelete?: (userId: number) => void;
  selectable?: boolean;
  onSelectionChange?: (selectedIds: number[]) => void;
}

export function UserTable({
  users,
  isLoading,
  onRowClick,
  onDelete,
  selectable,
  onSelectionChange
}: UserTableProps) {
  const [sortField, setSortField] = useState<keyof User>("id");
  const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
  const [selectedIds, setSelectedIds] = useState<number[]>([]);

  // Sort logic
  const sortedUsers = useMemo(() => {
    return [...users].sort((a, b) => {
      const aVal = a[sortField];
      const bVal = b[sortField];
      const order = sortOrder === "asc" ? 1 : -1;
      return aVal > bVal ? order : -order;
    });
  }, [users, sortField, sortOrder]);

  // Sort handler
  function handleSort(field: keyof User) {
    if (sortField === field) {
      setSortOrder(sortOrder === "asc" ? "desc" : "asc");
    } else {
      setSortField(field);
      setSortOrder("asc");
    }
  }

  // Selection handler
  function handleSelect(userId: number) {
    const newSelection = selectedIds.includes(userId)
      ? selectedIds.filter(id => id !== userId)
      : [...selectedIds, userId];
    setSelectedIds(newSelection);
    onSelectionChange?.(newSelection);
  }

  function handleSelectAll() {
    const newSelection = selectedIds.length === users.length
      ? []
      : users.map(u => u.id);
    setSelectedIds(newSelection);
    onSelectionChange?.(newSelection);
  }

  if (isLoading) return <div>Loading...</div>;

  return (
    <table>
      <thead>
        <tr>
          {selectable && (
            <th>
              <input
                type="checkbox"
                checked={selectedIds.length === users.length && users.length > 0}
                onChange={handleSelectAll}
              />
            </th>
          )}

          <th onClick={() => handleSort("id")} className="sortable">
            ID {sortField === "id" && (sortOrder === "asc" ? "↑" : "↓")}
          </th>

          <th onClick={() => handleSort("email")} className="sortable">
            Email {sortField === "email" && (sortOrder === "asc" ? "↑" : "↓")}
          </th>

          <th onClick={() => handleSort("username")} className="sortable">
            Username {sortField === "username" && (sortOrder === "asc" ? "↑" : "↓")}
          </th>

          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
        {sortedUsers.map((user) => (
          <tr key={user.id}>
            {selectable && (
              <td>
                <input
                  type="checkbox"
                  checked={selectedIds.includes(user.id)}
                  onChange={() => handleSelect(user.id)}
                />
              </td>
            )}

            <td onClick={() => onRowClick?.(user)}>{user.id}</td>
            <td onClick={() => onRowClick?.(user)}>{user.email}</td>
            <td onClick={() => onRowClick?.(user)}>{user.username}</td>

            <td>
              <button onClick={() => onRowClick?.(user)}>View</button>
              <button onClick={() => onDelete?.(user.id)}>Delete</button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

**Added features**:

* Sort by column click
* Multiple selection with checkboxes
* Select all/deselect all
* Action buttons per row

## Component Styling

### Tailwind CSS

```typescript theme={null}
// components/users/UserTable.tsx
export function UserTable({ users }: UserTableProps) {
  return (
    <div className="overflow-x-auto">
      <table className="min-w-full divide-y divide-gray-200">
        <thead className="bg-gray-50">
          <tr>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              ID
            </th>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Email
            </th>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Username
            </th>
          </tr>
        </thead>
        <tbody className="bg-white divide-y divide-gray-200">
          {users.map((user) => (
            <tr key={user.id} className="hover:bg-gray-50">
              <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                {user.id}
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                {user.email}
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                {user.username}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

### CSS Modules

```typescript theme={null}
// components/users/UserTable.module.css
.table {
  width: 100%;
  border-collapse: collapse;
}

.table thead {
  background-color: var(--color-gray-100);
}

.table th {
  padding: 12px;
  text-align: left;
  font-weight: 600;
}

.table td {
  padding: 12px;
  border-top: 1px solid var(--color-gray-200);
}

.table tbody tr:hover {
  background-color: var(--color-gray-50);
}
```

```typescript theme={null}
// components/users/UserTable.tsx
import styles from "./UserTable.module.css";

export function UserTable({ users }: UserTableProps) {
  return (
    <table className={styles.table}>
      {/* ... */}
    </table>
  );
}
```

### Styled Components

```typescript theme={null}
// components/users/UserTable.styled.ts
import styled from "styled-components";

export const Table = styled.table`
  width: 100%;
  border-collapse: collapse;
`;

export const TableHead = styled.thead`
  background-color: ${(props) => props.theme.colors.gray[100]};
`;

export const TableHeader = styled.th`
  padding: 12px;
  text-align: left;
  font-weight: 600;
`;

export const TableRow = styled.tr`
  &:hover {
    background-color: ${(props) => props.theme.colors.gray[50]};
  }
`;

export const TableCell = styled.td`
  padding: 12px;
  border-top: 1px solid ${(props) => props.theme.colors.gray[200]};
`;
```

```typescript theme={null}
// components/users/UserTable.tsx
import * as S from "./UserTable.styled";

export function UserTable({ users }: UserTableProps) {
  return (
    <S.Table>
      <S.TableHead>
        <S.TableRow>
          <S.TableHeader>ID</S.TableHeader>
          <S.TableHeader>Email</S.TableHeader>
          <S.TableHeader>Username</S.TableHeader>
        </S.TableRow>
      </S.TableHead>
      <tbody>
        {users.map((user) => (
          <S.TableRow key={user.id}>
            <S.TableCell>{user.id}</S.TableCell>
            <S.TableCell>{user.email}</S.TableCell>
            <S.TableCell>{user.username}</S.TableCell>
          </S.TableRow>
        ))}
      </tbody>
    </S.Table>
  );
}
```

## IdAsyncSelect Customization

### Custom Option Rendering

```typescript theme={null}
<IdAsyncSelect
  entity="User"
  value={userId}
  onChange={setUserId}
  searchField="username"
  // Custom option rendering
  renderOption={(user) => (
    <div className="user-option">
      <img
        src={user.avatar || "/default-avatar.png"}
        alt=""
        className="avatar"
      />
      <div className="user-info">
        <div className="username">{user.username}</div>
        <div className="email">{user.email}</div>
        {user.role && (
          <span className="badge">{user.role}</span>
        )}
      </div>
    </div>
  )}
  // Selected value rendering
  renderValue={(user) => (
    <div className="selected-user">
      <img src={user.avatar || "/default-avatar.png"} alt="" />
      <span>{user.username}</span>
    </div>
  )}
/>
```

### Grouped Options

```typescript theme={null}
<IdAsyncSelect
  entity="User"
  value={userId}
  onChange={setUserId}
  searchField="username"
  // Grouping function
  groupBy={(user) => user.role}
  // Group label
  renderGroup={(role) => (
    <div className="group-header">
      {role.toUpperCase()}
    </div>
  )}
/>
```

## SearchInput Customization

### Custom Suggestion Rendering

```typescript theme={null}
<SearchInput
  value={query}
  onChange={setQuery}
  suggestions={suggestions}
  // Custom suggestion rendering
  renderSuggestion={(suggestion) => (
    <div className="suggestion-item">
      <span className="icon">🔍</span>
      <span className="text">{suggestion.text}</span>
      {suggestion.count && (
        <span className="count">{suggestion.count}</span>
      )}
    </div>
  )}
  onSelectSuggestion={(suggestion) => {
    setQuery(suggestion.text);
    // Execute search
  }}
/>
```

### Category-based Search

```typescript theme={null}
function CategorizedSearch() {
  const [query, setQuery] = useState("");
  const [category, setCategory] = useState("all");

  return (
    <div className="search-container">
      <select
        value={category}
        onChange={(e) => setCategory(e.target.value)}
      >
        <option value="all">All</option>
        <option value="posts">Posts</option>
        <option value="users">Users</option>
        <option value="comments">Comments</option>
      </select>

      <SearchInput
        value={query}
        onChange={setQuery}
        placeholder={`Search ${category}...`}
        onSearch={(q) => {
          // Execute category-based search
          searchService.search({
            query: q,
            category: category === "all" ? undefined : category,
          });
        }}
      />
    </div>
  );
}
```

## Creating New Components

Create new components based on generated ones.

### Composite Component

```typescript theme={null}
// components/users/UserManagement.tsx
import { UserTable } from "./UserTable";
import { UserForm } from "./UserForm";
import { SearchInput } from "@/components/common/SearchInput";

export function UserManagement() {
  const [mode, setMode] = useState<"list" | "create" | "edit">("list");
  const [selectedUser, setSelectedUser] = useState<User | null>(null);
  const [filters, setFilters] = useState({ search: "", role: null });

  const { users, isLoading, refresh } = useUsers(filters);

  async function handleCreate(data: CreateUserInput) {
    await userService.create(data);
    setMode("list");
    refresh();
  }

  async function handleUpdate(data: UpdateUserInput) {
    if (selectedUser) {
      await userService.update(selectedUser.id, data);
      setMode("list");
      setSelectedUser(null);
      refresh();
    }
  }

  return (
    <div className="user-management">
      {mode === "list" && (
        <>
          <div className="header">
            <h1>Users</h1>
            <button onClick={() => setMode("create")}>
              Create New User
            </button>
          </div>

          <SearchInput
            value={filters.search}
            onChange={(search) => setFilters({ ...filters, search })}
            placeholder="Search users..."
          />

          <UserTable
            users={users}
            isLoading={isLoading}
            onRowClick={(user) => {
              setSelectedUser(user);
              setMode("edit");
            }}
          />
        </>
      )}

      {mode === "create" && (
        <div className="form-container">
          <h2>Create New User</h2>
          <UserForm onSubmit={handleCreate} />
          <button onClick={() => setMode("list")}>Cancel</button>
        </div>
      )}

      {mode === "edit" && selectedUser && (
        <div className="form-container">
          <h2>Edit User</h2>
          <UserForm
            initialValues={selectedUser}
            onSubmit={handleUpdate}
          />
          <button onClick={() => setMode("list")}>Cancel</button>
        </div>
      )}
    </div>
  );
}
```

## Best Practices

### 1. Commit Immediately After Generation

```bash theme={null}
pnpm scaffold:view User
git add .
git commit -m "feat: scaffold User views"

# Now safe to customize
```

### 2. Common Logic as Hooks

```typescript theme={null}
// hooks/useEntityCRUD.ts
export function useEntityCRUD<T>(entityName: string, service: any) {
  const router = useRouter();
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function create(data: Partial<T>) {
    setSaving(true);
    setError(null);

    try {
      const result = await service.create(data);
      router.push(`/${entityName}/${result.id}`);
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  return { create, saving, error };
}
```

### 3. Styles Globally

```css theme={null}
/* styles/components.css */
.entity-table {
}
.entity-form {
}
.entity-detail {
}
```

### 4. Extend Types

```typescript theme={null}
// types/user.types.ts (generated file)
export interface User {
  id: number;
  email: string;
  username: string;
}

// types/user.extended.ts (custom file)
import type { User } from "./user.types";

export interface UserWithStats extends User {
  postCount: number;
  commentCount: number;
  lastActiveAt: Date;
}
```

## Cautions

<Warning>
  **Cautions when customizing**: 1. **Regeneration overwrites** - Git commit required 2. Type
  changes should be done in backend first 3. Separate complex logic into Hooks 4. Make Props
  interfaces extensible 5. Maintain style consistency
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="View Scaffolding" icon="wand-magic-sparkles" href="/en/frontend-integration/react-components/scaffolding-views">
    Auto view generation
  </Card>

  <Card title="ID Async Select" icon="list-dropdown" href="/en/frontend-integration/react-components/id-async-select">
    Relational data selection
  </Card>

  <Card title="Search Input" icon="magnifying-glass" href="/en/frontend-integration/react-components/search-input">
    Search component
  </Card>

  <Card title="Using Services" icon="code" href="/en/frontend-integration/generated-services/using-services">
    Service integration
  </Card>
</CardGroup>
