Components
FORGE's frontend applications are built on shadcn/ui, which provides accessible, unstyled primitives powered by Radix UI and styled with Tailwind CSS. On top of these primitives, FORGE generates several higher-level components tailored to the admin dashboard and web application.
Component Architecture
┌─────────────────────────────────────────────────┐
│ Application Components │
│ (Pages, Layouts, Feature-specific components) │
├─────────────────────────────────────────────────┤
│ FORGE Custom Components │
│ (DataTable, RichTextEditor, LanguageSwitcher, │
│ ConfirmDialog, TranslationsInput, etc.) │
├─────────────────────────────────────────────────┤
│ shadcn/ui Primitives │
│ (Button, Input, Dialog, Select, Table, Tabs, │
│ Card, Badge, Skeleton, Tooltip, etc.) │
├─────────────────────────────────────────────────┤
│ Radix UI + Tailwind CSS │
│ (Accessible headless components) │
└─────────────────────────────────────────────────┘DataTable
The DataTable is the primary component for listing resources in the admin dashboard. It wraps TanStack Table with sorting, filtering, column visibility toggling, pagination, and row selection.
Props
| Prop | Type | Description |
|---|---|---|
columns | ColumnDef<TData, TValue>[] | TanStack Table column definitions |
data | TData[] | Array of data rows |
searchKey | string (optional) | Column key to enable text search filtering |
searchPlaceholder | string (optional) | Placeholder text for the search input |
Usage
import { DataTable } from "@/components/ui/data-table";
import { getColumns } from "./columns";
import type { User } from "@/lib/types";
// Define columns in a separate file (columns.tsx)
export function getColumns({
onDelete,
canEdit,
canDelete,
}: {
onDelete: (user: User) => void;
canEdit: boolean;
canDelete: boolean;
}): ColumnDef<User>[] {
return [
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "is_active",
header: "Status",
cell: ({ row }) => (
<Badge variant={row.original.is_active ? "default" : "secondary"}>
{row.original.is_active ? "Active" : "Inactive"}
</Badge>
),
},
{
id: "actions",
cell: ({ row }) => (
<DataTableRowActions
row={row}
onEdit={canEdit ? `/users/${row.original.id}/edit` : undefined}
onDelete={canDelete ? () => onDelete(row.original) : undefined}
/>
),
},
];
}
// Use in a page component
<DataTable
columns={columns}
data={users}
searchKey="name"
searchPlaceholder="Filter users..."
/>Built-in Sub-Components
The DataTable includes several sub-components that work together:
- DataTableColumnHeader -- Sortable column headers with ascending/descending/hide controls
- DataTableRowActions -- Dropdown menu with edit, view, and delete actions
- DataTablePagination -- Page navigation with configurable rows per page (10, 20, 30, 40, 50)
- Column Visibility -- Dropdown to show/hide columns
TIP
Column definitions are kept in a separate columns.tsx file by convention. This keeps page components focused on data fetching and actions while column rendering logic stays isolated and testable.
ConfirmDialog
A confirmation modal used before destructive actions like deletion. Built on Radix UI's AlertDialog.
Props
| Prop | Type | Description |
|---|---|---|
open | boolean | Controls dialog visibility |
onOpenChange | (open: boolean) => void | Called when visibility changes |
title | string | Dialog title |
description | string | Descriptive text |
confirmText | string | Text for the confirm button |
variant | "default" | "destructive" | Button style variant |
isLoading | boolean | Shows loading spinner on confirm button |
onConfirm | () => void | Called when user confirms the action |
Usage
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
const [deleteTarget, setDeleteTarget] = useState<User | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
<ConfirmDialog
open={!!deleteTarget}
onOpenChange={(open) => !open && setDeleteTarget(null)}
title="Delete User"
description="Are you sure? This action cannot be undone."
confirmText="Delete"
variant="destructive"
isLoading={isDeleting}
onConfirm={handleDelete}
/>RichTextEditor
A TinyMCE-based rich text editor used for content page editing in the admin dashboard. It supports RTL content, image insertion, and produces clean HTML.
Props
| Prop | Type | Description |
|---|---|---|
value | string | HTML content string |
onChange | (value: string) => void | Called with updated HTML on change |
placeholder | string (optional) | Placeholder text |
disabled | boolean | Disables editing |
height | number | Editor height in pixels (default: 300) |
className | string (optional) | Additional CSS classes |
Usage
import { RichTextEditor } from "@/components/ui/rich-text-editor";
<RichTextEditor
value={content.details}
onChange={(html) => setContent({ ...content, details: html })}
height={400}
placeholder="Write your content here..."
/>The editor automatically adjusts its text direction based on the current language context, ensuring RTL languages like Arabic render correctly within the editor.
TranslationsInput
A tabbed interface for editing translatable fields across multiple languages. Each tab corresponds to an active language and displays input fields for the translatable properties.
Props
| Prop | Type | Description |
|---|---|---|
value | Translations | Object keyed by language code |
onChange | (translations: Translations) => void | Called on any field change |
fields | ("display_name" | "description")[] | Which fields to show |
disabled | boolean | Disables all inputs |
Usage
import { TranslationsInput } from "@/components/ui/translations-input";
<TranslationsInput
value={role.translations || {}}
onChange={(translations) =>
setRole({ ...role, translations })
}
fields={["display_name", "description"]}
/>This renders a tab bar with one tab per active language (e.g., "English", "Arabic"), and under each tab, input fields for the specified translatable properties.
ContentTranslationsInput
A specialized version of TranslationsInput for content pages. It includes fields for title, summary, details (rich text), button_text, and SEO metadata -- all per language.
Usage
import { ContentTranslationsInput } from "@/components/ui/content-translations-input";
<ContentTranslationsInput
value={content.translations || {}}
onChange={(translations) =>
setContent({ ...content, translations })
}
/>Each language tab renders:
┌─────────────────────────────────────────┐
│ [English] [Arabic] │
├─────────────────────────────────────────┤
│ Title: [________________________] │
│ Summary: [________________________] │
│ Details: ┌────────────────────────┐ │
│ │ Rich Text Editor │ │
│ └────────────────────────┘ │
│ Button Text: [_______________________] │
│ │
│ -- SEO Fields -- │
│ SEO Title: [________________________] │
│ SEO Desc: [________________________] │
│ Keywords: [________________________] │
└─────────────────────────────────────────┘PermissionSelector
A hierarchical checkbox interface for assigning permissions to roles. Permissions are grouped by main_group and group, displaying a tree structure that allows selecting individual permissions or entire groups.
Usage
import { PermissionSelector } from "@/components/ui/permission-selector";
<PermissionSelector
permissions={allPermissions}
selected={selectedPermissionIds}
onChange={setSelectedPermissionIds}
/>The component renders permissions in a tree:
[x] Users (select all)
[x] View Users
[x] Create Users
[x] Edit Users
[ ] Delete Users
[x] Contents (select all)
[x] View Contents
[x] Create Contents
[x] Edit Contents
[x] Delete ContentsClicking a group header toggles all permissions within that group.
LanguageSwitcher
A dropdown that displays available languages and allows the user to switch the active locale. When a language is selected, it updates cookies (locale and direction), updates the <html> element's dir and lang attributes, and refreshes the page.
Usage
import { LanguageSwitcher } from "@/components/ui/language-switcher";
// In header component
<LanguageSwitcher />The component automatically hides itself when only one language is available. It displays each language's native name (e.g., "English", "Arabic") and highlights the active selection.
TIP
The LanguageSwitcher exists in both apps (admin/components/ui/ and web/components/), each tailored to its app's layout. Both use the same useI18n hook from the I18n provider.
Loading States and Skeletons
FORGE uses shadcn/ui's Skeleton component for loading placeholders throughout the admin dashboard:
import { Skeleton } from "@/components/ui/skeleton";
function UserListSkeleton() {
return (
<div className="space-y-4">
<Skeleton className="h-10 w-[250px]" />
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
</div>
);
}Use skeletons in list pages while data is loading:
if (isLoading) return <UserListSkeleton />;
return <DataTable columns={columns} data={users} />;Toast Notifications
FORGE uses Sonner for toast notifications. The DirectionAwareToaster wrapper positions toasts based on the current text direction:
import { toast } from "sonner";
// Success notification
toast.success("User created successfully");
// Error notification
toast.error("Failed to create user");
// Custom with description
toast.success("Settings saved", {
description: "Your changes will take effect immediately.",
});DirectionAwareToaster
A wrapper around Sonner's Toaster that adapts positioning for RTL layouts:
import { DirectionAwareToaster } from "@/components/ui/direction-aware-toaster";
// In the root layout
<DirectionAwareToaster />In LTR mode, toasts appear in the bottom-right corner. In RTL mode, they appear in the bottom-left.
Sidebar
The admin sidebar is a responsive navigation component with the following features:
- Collapsible -- Toggles between full-width (with labels) and icon-only mode on desktop
- Mobile overlay -- Slides in from the left on mobile with a backdrop overlay
- Permission-gated items -- Nav items only render if the user has the required permission
- Collapsible sub-menus -- Parent items expand to reveal child links
- Active state -- Current route is visually highlighted
import { Sidebar } from "@/components/layout/sidebar";
// Used in the dashboard layout
<div className="flex h-screen">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden">
<Header />
<main className="flex-1 overflow-y-auto p-6">
{children}
</main>
</div>
</div>ThemeToggle
A button that cycles between light and dark themes. Uses next-themes under the hood with system preference detection:
// In the admin header
<ThemeToggle />shadcn/ui Primitives
The following shadcn/ui primitives are included in both apps:
| Component | Description |
|---|---|
Button | Primary, secondary, outline, ghost, destructive variants |
Input | Text input with label and error state support |
Textarea | Multi-line text input |
Select | Dropdown selection |
Checkbox | Checkbox with label |
Switch | Toggle switch |
Dialog | Modal dialog |
DropdownMenu | Context menu / dropdown |
Table | Data table primitives (Header, Body, Row, Cell) |
Tabs | Tabbed content panels |
Card | Container card with header, content, footer |
Badge | Status badges and labels |
Skeleton | Loading placeholder animations |
Tooltip | Hover tooltip |
Separator | Visual divider |
Label | Form field label |
Accordion | Expandable content sections |
Avatar | User avatar with image and fallback |
Collapsible | Expandable/collapsible section |
TIP
All shadcn/ui components are generated as source code in components/ui/, not installed as a package. This means you can customize any component directly without ejecting or overriding styles.
What's Next
- Forms & Validation -- How these components integrate with React Hook Form
- Data Fetching -- Loading data into DataTable and other components
- Internationalization -- How components handle multiple languages