Skip to content

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

PropTypeDescription
columnsColumnDef<TData, TValue>[]TanStack Table column definitions
dataTData[]Array of data rows
searchKeystring (optional)Column key to enable text search filtering
searchPlaceholderstring (optional)Placeholder text for the search input

Usage

tsx
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

PropTypeDescription
openbooleanControls dialog visibility
onOpenChange(open: boolean) => voidCalled when visibility changes
titlestringDialog title
descriptionstringDescriptive text
confirmTextstringText for the confirm button
variant"default" | "destructive"Button style variant
isLoadingbooleanShows loading spinner on confirm button
onConfirm() => voidCalled when user confirms the action

Usage

tsx
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

PropTypeDescription
valuestringHTML content string
onChange(value: string) => voidCalled with updated HTML on change
placeholderstring (optional)Placeholder text
disabledbooleanDisables editing
heightnumberEditor height in pixels (default: 300)
classNamestring (optional)Additional CSS classes

Usage

tsx
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

PropTypeDescription
valueTranslationsObject keyed by language code
onChange(translations: Translations) => voidCalled on any field change
fields("display_name" | "description")[]Which fields to show
disabledbooleanDisables all inputs

Usage

tsx
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

tsx
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

tsx
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 Contents

Clicking 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

tsx
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:

tsx
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:

tsx
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:

tsx
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:

tsx
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.

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
tsx
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:

tsx
// In the admin header
<ThemeToggle />

shadcn/ui Primitives

The following shadcn/ui primitives are included in both apps:

ComponentDescription
ButtonPrimary, secondary, outline, ghost, destructive variants
InputText input with label and error state support
TextareaMulti-line text input
SelectDropdown selection
CheckboxCheckbox with label
SwitchToggle switch
DialogModal dialog
DropdownMenuContext menu / dropdown
TableData table primitives (Header, Body, Row, Cell)
TabsTabbed content panels
CardContainer card with header, content, footer
BadgeStatus badges and labels
SkeletonLoading placeholder animations
TooltipHover tooltip
SeparatorVisual divider
LabelForm field label
AccordionExpandable content sections
AvatarUser avatar with image and fallback
CollapsibleExpandable/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

Released under the MIT License.