frontend-ui-components

Installation
SKILL.md

Frontend UI Components

This skill outlines how to build and use UI components in the frontend applications, leveraging the shared @eridu/ui package and Shadcn patterns.

The @eridu/ui Package

All generic UI components (Buttons, Inputs, Dialogs, etc.) live in packages/ui. Do NOT create local copies of generic components in apps.

Usage

import { Button } from '@eridu/ui/components/button';
import { Input } from '@eridu/ui/components/input';

export function MyForm() {
  return (
    <div className="flex gap-4">
      <Input placeholder="Search..." />
      <Button variant="default">Search</Button>
    </div>
  );
}

Specific Component Guidelines

Date and Time Pickers

Always use the custom DatePicker and DateTimePicker components from @eridu/ui instead of native <input type="date"> or <input type="datetime-local"> unless there is a highly specific and unavoidable requirement to use the native browser inputs. This ensures visual consistency, cross-browser compatibility, and a better user experience across the application.

import { DatePicker, DateTimePicker } from '@eridu/ui/components/date-picker';

// ✅ GOOD
<DatePicker value={dateStr} onChange={setDateStr} />
<DateTimePicker value={dateTimeStr} onChange={setDateTimeStr} />

// ❌ BAD (Avoid native inputs)
<input type="date" value={dateStr} />

Refresh Actions

Use icon-only refresh buttons for data refetch actions to keep toolbar density and interaction patterns consistent.

import { RotateCw } from 'lucide-react';
import { Button } from '@eridu/ui/components/button';

<Button
  type="button"
  variant="outline"
  size="icon"
  className="h-9 w-9"
  onClick={onRefresh}
  disabled={isRefreshing}
  aria-label="Refresh data"
>
  <RotateCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
</Button>

Notes:

  • Always provide an explicit aria-label (for accessibility and stable tests).
  • Keep spinning state on the icon while fetching/refetching.
  • In mobile overflow menus, text labels in dropdown items are still acceptable.

Collapsible Section Toggle Actions

For show/hide controls on collapsible UI sections, use a single shared icon pattern across the app:

  • Expanded state: ChevronUp
  • Collapsed state: ChevronDown
import { ChevronDown, ChevronUp } from 'lucide-react';
import { Button } from '@eridu/ui/components/button';

<Button
  type="button"
  variant="outline"
  size="icon"
  aria-label={isOpen ? 'Collapse section' : 'Expand section'}
  onClick={toggle}
>
  {isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>

Notes:

  • Keep the toggle icon button anchored in a consistent place (prefer top-right of section header) for both desktop and mobile.
  • Do not substitute alternate semantic icons (for example Eye / EyeOff) for section collapse behavior.

Smooth Collapse/Expand Transition (for Toggleable Sections)

When a section is toggled, prefer smooth animated collapse/expand instead of hard unmount/remount.

<div
  className={cn(
    'overflow-hidden transition-all duration-300 ease-in-out',
    isOpen ? 'max-h-[640px] opacity-100' : 'max-h-0 opacity-0 pointer-events-none',
  )}
  aria-hidden={!isOpen}
>
  {content}
</div>

Notes:

  • Keep the content mounted and animate container height/opacity for smoother UX.
  • Use overflow-hidden to prevent clipping artifacts during height transition.
  • Include aria-hidden when collapsed for better accessibility semantics.
  • Keep duration/easing consistent (duration-300, ease-in-out) unless a route has an established alternative.

Styling Pattern (Tailwind CSS v4)

We use Tailwind CSS v4 with clsx and tailwind-merge for conditional styling.

The cn Utility

Use the cn utility from @eridu/ui/lib/utils to merge classes safely.

import { cn } from '@eridu/ui/lib/utils';

interface CardProps {
  className?: string;
  children: React.ReactNode;
}

export function Card({ className, children }: CardProps) {
  return (
    <div className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}>
      {children}
    </div>
  );
}

Component Design Patterns

Composition Over Large Components

Build complex UIs by composing small, focused components instead of creating large monolithic components.

Benefits:

  • Easier to test and maintain
  • Better performance (smaller re-render scope)
  • More reusable components
  • Clearer separation of concerns
// ❌ BAD: Large monolithic component
function UserDashboard() {
  return (
    <div>
      <header>{/* Complex header logic */}</header>
      <nav>{/* Complex navigation */}</nav>
      <main>
        <div>{/* User stats */}</div>
        <div>{/* Activity feed */}</div>
        <div>{/* Recommendations */}</div>
      </main>
      <footer>{/* Footer content */}</footer>
    </div>
  );
}

// ✅ GOOD: Composed from smaller components
function UserDashboard() {
  return (
    <div>
      <DashboardHeader />
      <DashboardNav />
      <main>
        <UserStats />
        <ActivityFeed />
        <Recommendations />
      </main>
      <DashboardFooter />
    </div>
  );
}

Using Children for Composition

Use the children prop to create flexible, composable components.

// Flexible Dialog component using composition
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@eridu/ui/components/dialog';

function ConfirmDeleteDialog({ isOpen, onClose, onConfirm }) {
  return (
    <Dialog open={isOpen} onOpenChange={onClose}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Confirm Deletion</DialogTitle>
        </DialogHeader>
        <p>Are you sure you want to delete this item?</p>
        <div className="flex gap-2">
          <Button variant="outline" onClick={onClose}>Cancel</Button>
          <Button variant="destructive" onClick={onConfirm}>Delete</Button>
        </div>
      </DialogContent>
    </Dialog>
  );
}

Wrapping 3rd Party Components

Wrap 3rd party components to adapt them to your application's needs and make future changes easier.

// Wrap Radix Link to add app-specific behavior
import { Link as RadixLink } from '@radix-ui/react-navigation-menu';
import { cn } from '@eridu/ui/lib/utils';

interface AppLinkProps extends React.ComponentPropsWithoutRef<typeof RadixLink> {
  external?: boolean;
}

export function Link({ external, className, children, ...props }: AppLinkProps) {
  return (
    <RadixLink
      className={cn('text-primary hover:underline', className)}
      target={external ? '_blank' : undefined}
      rel={external ? 'noopener noreferrer' : undefined}
      {...props}
    >
      {children}
      {external && <span className="ml-1"></span>}
    </RadixLink>
  );
}

When to Abstract Components

Abstract components into the shared @eridu/ui package when:

  1. Used in multiple apps - Component is needed across erify_creators, erify_studios, etc.
  2. Generic and reusable - Not tied to specific business logic
  3. Stable API - Interface is unlikely to change frequently
  4. Well-tested - Component has proper tests and documentation

Don't abstract too early:

  • Wait until you have 2-3 use cases before abstracting
  • Avoid premature abstraction (wrong abstractions are worse than duplication)
  • Keep feature-specific components in feature folders

Creating New Components

When creating app-specific features:

  1. Compose them using primitives from @eridu/ui.
  2. Keep them in src/components/{feature-name}/.

When creating new generic primitives:

  1. Add them to packages/ui/src/components/.
  2. Follow the Radix UI + Tailwind pattern (Shadcn style).
  3. Export them via packages/ui/package.json.

Checklist

  • Import generic components from @eridu/ui.
  • Use cn() for class merging.
  • Ensure components are accessible (Radix UI primitives).
  • Use Tailwind text/bg colors that map to the theme (e.g., bg-primary, text-muted-foreground).
Related skills
Installs
1
GitHub Stars
1
First Seen
Mar 31, 2026