Skip to main content

UI Abstraction Guidelines

This document outlines the three-level abstraction hierarchy for UI components in the Core package, designed to maximize code reusability while maintaining flexibility and developer efficiency.

Overview

The UI abstraction follows a three-tier architecture:
  1. Level 1: UI Components - Primitive building blocks (shadcn/ui or custom detail components)
  2. Level 2: Frontend Components - Reusable UI patterns with client-side logic
  3. Level 3: Backend-Controlled Components - Full-stack components with server-side integration
Each level builds upon the previous one, creating a coherent system that balances abstraction with pragmatism.

Level 1: UI Components (Primitive Building Blocks)

Purpose: Provide low-level, atomic UI elements that serve as the foundation for all higher-level components. Location: resources/js/core/components/ui/ Characteristics:
  • Typically based on shadcn/ui
  • Focus on visual presentation and basic interaction
Examples: Button, Input, Dialog, LoadingIcon, Card, Select

When to Create Level 1 Components

  • New visual primitive not provided by shadcn/ui
  • Small utility components (custom icons, loading states)

Level 2: Frontend Components (UI Pattern Wrappers)

Purpose: Encapsulate recurring UI patterns and frontend logic to reduce code duplication. Location: resources/js/core/components/ Characteristics:
  • Compose multiple Level 1 components
  • Contain client-side logic (state, validation, formatting)
  • No direct backend communication (use callbacks)

Example: CoreConfirmDialog

import { CoreConfirmDialog } from '@/core/components/core-confirm-dialog';

<CoreConfirmDialog
  open={isOpen}
  onOpenChange={setIsOpen}
  title={__('Delete account')}
  description={__('Are you sure?')}
  confirmText={__('Delete')}
  variant="destructive"
  onConfirm={handleDelete}
/>;
Other Examples: CoreAvatar, DetailedLink, CoreSheet, CoreDialog

Implementation Pattern

// Level 2 components compose Level 1 primitives
import { Button } from '@/core/components/ui/button';
import { Dialog, DialogContent } from '@/core/components/ui/dialog';

interface CoreConfirmDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  onConfirm: () => void | Promise<void>;
  variant?: 'default' | 'destructive';
}

export function CoreConfirmDialog({
  open,
  onOpenChange,
  title,
  onConfirm,
  variant = 'default',
}: CoreConfirmDialogProps) {
  const [loading, setLoading] = useState(false);

  const handleConfirm = async () => {
    setLoading(true);
    try {
      await onConfirm();
      onOpenChange(false);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
        </DialogHeader>
        <DialogFooter>
          <Button variant="outline" onClick={() => onOpenChange(false)}>
            {__('Cancel')}
          </Button>
          <Button variant={variant} onClick={handleConfirm} disabled={loading}>
            {loading ? __('Processing...') : __('Confirm')}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Guidelines

  1. Compose Level 1 components
  2. Use callbacks for actions (no API calls)
  3. Handle loading/disabled states
  4. Strict TypeScript typing
  5. Accept className for customization

When to Create Level 2 Components

  • Pattern appears multiple times across the application
  • Standardizing a specific interaction

Level 3: Backend-Controlled Components (Full-Stack Components)

Purpose: Provide integrated frontend-backend solutions for complex features requiring server-side data management. Location:
  • Backend: src/Tables/, src/Forms/
  • Frontend: resources/js/core/components/
Characteristics:
  • Consistent backend-frontend contract for data and actions
  • Efficient server-side data preparation and validation
  • Standardized UI patterns for complex features

Example: CoreTable (InertiaTable)

Backend:
<?php

namespace App\Tables;

use Inly\Core\Tables\CoreTable;
use Inly\Core\Tables\Columns\ObjectColumn;

class ProductTable extends CoreTable
{
    protected ?string $resource = Product::class;

    public function columns(): array
    {
        return [
            ObjectColumn::make('name', __('Product'))
                ->sortable()
                ->searchable(),

            TextColumn::make('price', __('Price'))
                ->sortable(),
        ];
    }

    protected function stats(): array
    {
        return [
            'total' => [
                'label' => __('Total Products'),
                'value' => $this->queryWithRequestApplied()->count(),
            ],
        ];
    }
}
Controller:
<?php

use Inly\Core\Pages\CoreIndexPage;

class ProductController extends Controller
{
    public function index()
    {
      CoreIndexPage::make('products/product-index')->with([
          'productTable' => ProductTable::defer(), // Use defer() for performance
      ]);
    }
}
Frontend:
import { InertiaTable } from '@/core/components/inertia-table';
import { CoreIndexPage } from '@/core/components/core-index-page';

export default function ProductIndex({ productTable }: Props) {
  return (
    <CoreIndexPage>
      <InertiaTable resource={productTable} />
    </CoreIndexPage>
  );
}

Guidelines

  1. Backend handles data fetching, validation, authorization
  2. Use TypeScript generation for backend types
  3. Validation in Form Requests (never inline)
  4. Use deferred props for expensive operations

When to Create Level 3 Components

  • Server-side filtering, sorting, or pagination needed
  • Data requires authorization or validation
  • Multiple round-trips between client and server

Decision Tree

Need a new UI element?

├─ Is it a basic visual primitive?
│  └─ Level 1: UI Component (Button, Input, Dialog)

├─ Is it a recurring frontend pattern with no backend needs?
│  └─ Level 2: Frontend Component (CoreConfirmDialog, DetailedLink)

└─ Does it require server-side data/logic?
   └─ Level 3: Backend-Controlled Component (CoreTable, CoreForm)

Best Practices

Naming Conventions

  • Level 1: shadcn/ui names or single-word names (Button, LoadingIcon)
  • Level 2: Core prefix (CoreAvatar, CoreConfirmDialog)
  • Level 3: Domain-specific or Core prefix (CoreTable, CoreForm)

Component Composition

Always prefer composition over duplication:
// ❌ Bad: Duplicating UI patterns
function UserCard({ user }: Props) {
  return (
    <div className="rounded-lg border p-4">
      <img src={user.avatar} className="h-10 w-10 rounded-full" />
      <div className="font-semibold">{user.name}</div>
    </div>
  );
}

// ✅ Good: Using existing components
function UserCard({ user }: Props) {
  return (
    <Card>
      <CardContent className="p-4">
        <DetailedLink
          title={user.name}
          subtitle={user.email}
          avatar={user.avatar_url}
          href={route('users.show', user.id)}
        />
      </CardContent>
    </Card>
  );
}

Progressive Enhancement

  1. First use: Write inline code
  2. Second use: Extract to local component
  3. Third use: Consider Level 2 abstraction
  4. Frequent use + backend: Consider Level 3 abstraction

Summary

  • Level 1: Building blocks (Button, Input, Dialog)
  • Level 2: UI patterns (CoreConfirmDialog, DetailedLink)
  • Level 3: Full-stack features (CoreTable, CoreForm)