Claude Code Plugins

Community-maintained marketplace

Feedback

mydetailarea-components

@rruiz22/my-detail-area
0
0

Professional React component library for MyDetailArea dealership system. Creates enterprise-grade, accessible, and performant components following Notion-style design patterns with shadcn/ui, Tailwind CSS, and TypeScript. Includes data tables, forms, modals, metric cards, charts, and complex UI patterns. Use when building new features, creating reusable components, or implementing professional UI patterns.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name mydetailarea-components
description Professional React component library for MyDetailArea dealership system. Creates enterprise-grade, accessible, and performant components following Notion-style design patterns with shadcn/ui, Tailwind CSS, and TypeScript. Includes data tables, forms, modals, metric cards, charts, and complex UI patterns. Use when building new features, creating reusable components, or implementing professional UI patterns.
license MIT

MyDetailArea Component Architect

Enterprise-grade React component development system for the MyDetailArea platform.

Purpose

This skill provides comprehensive guidance for creating professional, accessible, and performant React components that seamlessly integrate with the MyDetailArea design system. It ensures consistency, maintainability, and enterprise-quality user interfaces.

When to Use

Use this skill when:

  • Creating new feature components
  • Building reusable UI components
  • Implementing data tables with sorting/filtering
  • Designing form components with validation
  • Creating modal dialogs and drawers
  • Building metric cards and dashboards
  • Implementing responsive layouts
  • Adding loading/error/empty states
  • Creating accessible components (WCAG 2.1 AA)
  • Optimizing component performance

Design System (Notion-Style)

Color Palette (HSL Format)

Grays (Foundation) - Primary Use

const colors = {
  gray: {
    50: 'hsl(0, 0%, 100%)',   // White backgrounds
    100: 'hsl(0, 0%, 98%)',   // Subtle backgrounds
    200: 'hsl(0, 0%, 96%)',   // Muted backgrounds
    300: 'hsl(0, 0%, 90%)',   // Borders
    500: 'hsl(0, 0%, 45%)',   // Secondary text
    700: 'hsl(0, 0%, 20%)',   // Primary text
    900: 'hsl(0, 0%, 9%)'     // Headings
  }
};

Muted Accents (ONLY) - Spartan Use

const accents = {
  emerald: {
    50: 'hsl(120, 60%, 97%)',
    500: 'hsl(120, 60%, 45%)'  // Success states
  },
  amber: {
    50: 'hsl(38, 92%, 95%)',
    500: 'hsl(38, 92%, 50%)'   // Warning states
  },
  red: {
    50: 'hsl(0, 84%, 97%)',
    500: 'hsl(0, 84%, 60%)'    // Error/destructive states
  },
  indigo: {
    50: 'hsl(211, 100%, 97%)',
    500: 'hsl(211, 100%, 50%)' // Info/pending states
  }
};

STRICTLY FORBIDDEN:

  • ❌ NO gradients (except subtle 2-color)
  • ❌ NO bright/strong blues (blue-600+)
  • ❌ NO saturated colors
  • ❌ NO neon or vibrant colors

Shadows (Enhanced System)

--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-elegant: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-glow: 0 0 0 3px rgba(59, 130, 246, 0.1);
--shadow-modal: 0 25px 50px -12px rgb(0 0 0 / 0.25);

Typography

const typography = {
  fontFamily: 'system-ui, -apple-system, sans-serif',
  sizes: {
    xs: '0.75rem',    // 12px
    sm: '0.875rem',   // 14px
    base: '1rem',     // 16px
    lg: '1.125rem',   // 18px
    xl: '1.25rem',    // 20px
    '2xl': '1.5rem',  // 24px
    '3xl': '1.875rem' // 30px
  },
  weights: {
    normal: 400,
    medium: 500,
    semibold: 600,
    bold: 700
  }
};

Technology Stack

Core

  • React 18.3.1 - UI framework
  • TypeScript 5.8.3 - Type safety
  • Vite 5.4.19 - Build tool
  • Tailwind CSS 3.4.17 - Styling

UI Components

  • shadcn/ui - Base component library (Radix UI primitives)
  • Radix UI 1.x - Accessible primitives
  • Lucide React 0.462.0 - Icons
  • Framer Motion 12.23.12 - Animations

Forms & Validation

  • React Hook Form 7.x - Form management
  • Zod - Schema validation
  • @hookform/resolvers - Form validation integration

Data Management

  • TanStack Query 5.83.0 - Server state
  • Zustand 5.0.8 - Client state

Charts & Visualization

  • Recharts 2.15.4 - Data visualization
  • date-fns 3.6.0 - Date manipulation

Internationalization

  • react-i18next 15.7.3 - Translations
  • Supported: EN, ES, PT-BR

Component Architecture Patterns

1. Feature Component Structure

components/
├── feature-name/
│   ├── FeatureName.tsx              # Main component
│   ├── FeatureNameForm.tsx          # Form subcomponent
│   ├── FeatureNameCard.tsx          # Card subcomponent
│   ├── FeatureNameDialog.tsx        # Modal subcomponent
│   ├── FeatureNameTable.tsx         # Table subcomponent
│   ├── types.ts                     # TypeScript types
│   ├── hooks.ts                     # Custom hooks
│   ├── utils.ts                     # Helper functions
│   └── index.ts                     # Barrel export

2. Component Template

import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';

interface FeatureComponentProps {
  id?: string;
  onSuccess?: () => void;
  className?: string;
}

export const FeatureComponent: FC<FeatureComponentProps> = ({
  id,
  onSuccess,
  className
}) => {
  const { t } = useTranslation();
  const [isLoading, setIsLoading] = useState(false);

  // Data fetching with TanStack Query
  const { data, isLoading: dataLoading, error } = useQuery({
    queryKey: ['feature', id],
    queryFn: () => fetchFeatureData(id)
  });

  // Loading state
  if (dataLoading) {
    return <FeatureComponentSkeleton />;
  }

  // Error state
  if (error) {
    return (
      <Card className="border-none shadow-sm">
        <CardContent className="p-6">
          <div className="text-center py-8">
            <p className="text-red-600">{t('errors.failed_to_load')}</p>
            <Button onClick={() => refetch()} className="mt-4">
              {t('common.retry')}
            </Button>
          </div>
        </CardContent>
      </Card>
    );
  }

  // Empty state
  if (!data || data.length === 0) {
    return (
      <Card className="border-none shadow-sm">
        <CardContent className="p-6">
          <div className="text-center py-12">
            <p className="text-muted-foreground">{t('feature.no_data')}</p>
          </div>
        </CardContent>
      </Card>
    );
  }

  // Main render
  return (
    <Card className={`border-none shadow-sm hover:shadow-md transition-shadow ${className}`}>
      <CardHeader className="border-b bg-gray-50/50">
        <CardTitle>{t('feature.title')}</CardTitle>
      </CardHeader>
      <CardContent className="p-6">
        {/* Component content */}
      </CardContent>
    </Card>
  );
};

// Loading skeleton
function FeatureComponentSkeleton() {
  return (
    <Card className="border-none shadow-sm">
      <CardContent className="p-6">
        <div className="animate-pulse space-y-4">
          <div className="h-4 bg-gray-200 rounded w-3/4"></div>
          <div className="h-4 bg-gray-200 rounded w-1/2"></div>
        </div>
      </CardContent>
    </Card>
  );
}

3. Form Component Pattern

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';

const formSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number')
});

type FormValues = z.infer<typeof formSchema>;

interface FeatureFormProps {
  initialData?: FormValues;
  onSubmit: (data: FormValues) => Promise<void>;
  onCancel?: () => void;
}

export function FeatureForm({ initialData, onSubmit, onCancel }: FeatureFormProps) {
  const { t } = useTranslation();
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: initialData || {
      name: '',
      email: '',
      phone: ''
    }
  });

  const handleSubmit = async (data: FormValues) => {
    try {
      await onSubmit(data);
      form.reset();
    } catch (error) {
      // Handle error
    }
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>{t('common.name')}</FormLabel>
              <FormControl>
                <Input placeholder={t('common.name_placeholder')} {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>{t('common.email')}</FormLabel>
              <FormControl>
                <Input type="email" placeholder={t('common.email_placeholder')} {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="phone"
          render={({ field }) => (
            <FormItem>
              <FormLabel>{t('common.phone')}</FormLabel>
              <FormControl>
                <Input type="tel" placeholder="+1234567890" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <div className="flex justify-end gap-2">
          {onCancel && (
            <Button type="button" variant="outline" onClick={onCancel}>
              {t('common.cancel')}
            </Button>
          )}
          <Button type="submit" disabled={form.formState.isSubmitting}>
            {form.formState.isSubmitting ? (
              <>
                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                {t('common.saving')}
              </>
            ) : (
              t('common.save')
            )}
          </Button>
        </div>
      </form>
    </Form>
  );
}

4. Data Table Pattern

import { useState, useMemo } from 'react';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { ArrowUpDown, Search } from 'lucide-react';

interface Column<T> {
  key: keyof T;
  label: string;
  sortable?: boolean;
  render?: (value: any, row: T) => React.ReactNode;
}

interface DataTableProps<T> {
  data: T[];
  columns: Column<T>[];
  searchable?: boolean;
  onRowClick?: (row: T) => void;
}

export function DataTable<T extends Record<string, any>>({
  data,
  columns,
  searchable = true,
  onRowClick
}: DataTableProps<T>) {
  const [search, setSearch] = useState('');
  const [sortKey, setSortKey] = useState<keyof T | null>(null);
  const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');

  const filteredData = useMemo(() => {
    if (!search) return data;

    return data.filter(row =>
      Object.values(row).some(value =>
        String(value).toLowerCase().includes(search.toLowerCase())
      )
    );
  }, [data, search]);

  const sortedData = useMemo(() => {
    if (!sortKey) return filteredData;

    return [...filteredData].sort((a, b) => {
      const aVal = a[sortKey];
      const bVal = b[sortKey];

      if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
      if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
      return 0;
    });
  }, [filteredData, sortKey, sortDirection]);

  const handleSort = (key: keyof T) => {
    if (sortKey === key) {
      setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
    } else {
      setSortKey(key);
      setSortDirection('asc');
    }
  };

  return (
    <div className="space-y-4">
      {searchable && (
        <div className="relative">
          <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
          <Input
            placeholder="Search..."
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="pl-9"
          />
        </div>
      )}

      <div className="rounded-md border">
        <Table>
          <TableHeader>
            <TableRow className="bg-gray-50/50">
              {columns.map((column) => (
                <TableHead key={String(column.key)} className="font-semibold">
                  {column.sortable ? (
                    <Button
                      variant="ghost"
                      size="sm"
                      onClick={() => handleSort(column.key)}
                      className="-ml-4 h-8 data-[state=open]:bg-accent"
                    >
                      {column.label}
                      <ArrowUpDown className="ml-2 h-4 w-4" />
                    </Button>
                  ) : (
                    column.label
                  )}
                </TableHead>
              ))}
            </TableRow>
          </TableHeader>
          <TableBody>
            {sortedData.length === 0 ? (
              <TableRow>
                <TableCell
                  colSpan={columns.length}
                  className="h-24 text-center text-muted-foreground"
                >
                  No results found
                </TableCell>
              </TableRow>
            ) : (
              sortedData.map((row, idx) => (
                <TableRow
                  key={idx}
                  className="hover:bg-gray-50/50 cursor-pointer"
                  onClick={() => onRowClick?.(row)}
                >
                  {columns.map((column) => (
                    <TableCell key={String(column.key)}>
                      {column.render
                        ? column.render(row[column.key], row)
                        : row[column.key]}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            )}
          </TableBody>
        </Table>
      </div>
    </div>
  );
}

5. Modal Dialog Pattern (Full-Screen Enterprise)

import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { X } from 'lucide-react';

interface FeatureDialogProps {
  open: boolean;
  onClose: () => void;
  data?: any;
}

export function FeatureDialog({ open, onClose, data }: FeatureDialogProps) {
  return (
    <Dialog open={open} onOpenChange={onClose}>
      <DialogContent className="max-w-none w-screen h-screen m-0 p-0 rounded-none">
        {/* Header */}
        <div className="sticky top-0 z-10 bg-white border-b px-6 py-4 flex items-center justify-between">
          <DialogHeader className="flex-1">
            <DialogTitle className="text-2xl">Feature Title</DialogTitle>
            <DialogDescription>Feature description</DialogDescription>
          </DialogHeader>
          <div className="flex items-center gap-2">
            <Button variant="outline" size="sm">
              Quick Action
            </Button>
            <Button variant="ghost" size="icon" onClick={onClose}>
              <X className="h-4 w-4" />
            </Button>
          </div>
        </div>

        {/* Content */}
        <div className="flex-1 overflow-auto p-6">
          <div className="max-w-7xl mx-auto grid lg:grid-cols-[2fr,1fr] gap-6">
            {/* Main Content */}
            <div className="space-y-6">
              {/* Content here */}
            </div>

            {/* Sidebar */}
            <aside className="space-y-6">
              {/* Sidebar content */}
            </aside>
          </div>
        </div>

        {/* Footer */}
        <div className="sticky bottom-0 bg-white border-t px-6 py-4 flex items-center justify-between">
          <Button variant="outline" onClick={onClose}>
            Close
          </Button>
          <div className="flex gap-2">
            <Button variant="outline">Secondary</Button>
            <Button>Primary Action</Button>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  );
}

6. Metric Card Pattern

import { Card, CardContent } from '@/components/ui/card';
import { TrendingUp, TrendingDown } from 'lucide-react';

interface MetricCardProps {
  label: string;
  value: string | number;
  trend?: {
    value: number;
    direction: 'up' | 'down';
  };
  icon: React.ComponentType<{ className?: string }>;
  iconColor?: 'emerald' | 'amber' | 'red' | 'indigo';
  onClick?: () => void;
}

export function MetricCard({
  label,
  value,
  trend,
  icon: Icon,
  iconColor = 'indigo',
  onClick
}: MetricCardProps) {
  const iconColorClasses = {
    emerald: 'bg-emerald-50 text-emerald-600',
    amber: 'bg-amber-50 text-amber-600',
    red: 'bg-red-50 text-red-600',
    indigo: 'bg-indigo-50 text-indigo-600'
  };

  return (
    <Card
      className={`border-none shadow-sm hover:shadow-md transition-shadow ${onClick ? 'cursor-pointer' : ''}`}
      onClick={onClick}
    >
      <CardContent className="p-6">
        <div className="flex items-start justify-between">
          <div className="space-y-2 flex-1">
            <p className="text-sm font-medium text-muted-foreground">{label}</p>
            <p className="text-2xl font-semibold tracking-tight">{value}</p>
            {trend && (
              <div className="flex items-center gap-1">
                {trend.direction === 'up' ? (
                  <TrendingUp className="h-4 w-4 text-emerald-600" />
                ) : (
                  <TrendingDown className="h-4 w-4 text-red-600" />
                )}
                <span
                  className={`text-sm font-medium ${
                    trend.direction === 'up' ? 'text-emerald-600' : 'text-red-600'
                  }`}
                >
                  {Math.abs(trend.value)}%
                </span>
              </div>
            )}
          </div>
          <div className={`p-3 rounded-lg ${iconColorClasses[iconColor]}`}>
            <Icon className="h-5 w-5" />
          </div>
        </div>
      </CardContent>
    </Card>
  );
}

Performance Optimization

Memoization

import { memo, useMemo, useCallback } from 'react';

// Memoize expensive components
export const ExpensiveComponent = memo(({ data }: Props) => {
  return <div>{/* Render */}</div>;
});

// Memoize expensive calculations
const sortedData = useMemo(() => {
  return data.sort((a, b) => a.value - b.value);
}, [data]);

// Memoize callbacks
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

Lazy Loading

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

export function Parent() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

Virtual Lists (for large datasets)

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: any[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 60
  });

  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <div style={{ height: `${virtualizer.getTotalSize()}px` }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`
            }}
          >
            {items[virtualItem.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

Accessibility (WCAG 2.1 AA)

Keyboard Navigation

<button
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      handleClick();
    }
  }}
  aria-label="Action description"
  tabIndex={0}
>
  Click me
</button>

ARIA Labels

<div role="region" aria-labelledby="section-title">
  <h2 id="section-title">Section Title</h2>
  {/* Content */}
</div>

<button aria-expanded={isOpen} aria-controls="dropdown-menu">
  Toggle Menu
</button>

<div id="dropdown-menu" aria-hidden={!isOpen}>
  {/* Menu items */}
</div>

Focus Management

import { useEffect, useRef } from 'react';

function Dialog({ open }: { open: boolean }) {
  const closeButtonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (open && closeButtonRef.current) {
      closeButtonRef.current.focus();
    }
  }, [open]);

  return (
    <div role="dialog" aria-modal="true">
      <button ref={closeButtonRef}>Close</button>
    </div>
  );
}

Responsive Design Patterns

// Mobile-first grid
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">

// Responsive text
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold">

// Conditional rendering
<div className="hidden sm:block">Desktop only</div>
<div className="block sm:hidden">Mobile only</div>

// Responsive flex direction
<div className="flex flex-col sm:flex-row gap-4">

// Responsive padding/margin
<div className="p-4 sm:p-6 lg:p-8">

Internationalization

import { useTranslation } from 'react-i18next';

function Component() {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('feature.title')}</h1>
      <p>{t('feature.description')}</p>
      <Button>{t('common.save')}</Button>
    </div>
  );
}

// Translation files: public/translations/{en,es,pt-BR}.json
{
  "feature": {
    "title": "Feature Title",
    "description": "Feature description"
  },
  "common": {
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete"
  }
}

Reference Files

Examples

Best Practices

  1. TypeScript strict mode - No any types, proper interfaces
  2. Composition over inheritance - Build with small, reusable components
  3. Single Responsibility - Each component does one thing well
  4. Controlled components - Use controlled inputs with state
  5. Error boundaries - Wrap components to catch errors
  6. Loading states - Always show feedback during async operations
  7. Empty states - Provide guidance when no data exists
  8. Responsive by default - Mobile-first approach
  9. Accessibility first - WCAG 2.1 AA compliance
  10. Internationalization - Support EN/ES/PT-BR

TypeScript Best Practices

// ✅ Good - Explicit types
interface ComponentProps {
  id: string;
  onSuccess: (data: Data) => void;
  optional?: string;
}

// ❌ Bad - Using any
interface ComponentProps {
  data: any;  // Never use any
}

// ✅ Good - Generic types
function DataTable<T extends Record<string, any>>({ data }: { data: T[] }) {}

// ✅ Good - Union types
type Status = 'pending' | 'active' | 'completed';

// ✅ Good - Type guards
function isOrder(data: unknown): data is Order {
  return typeof data === 'object' && data !== null && 'order_number' in data;
}

Common Pitfalls to Avoid

Avoid:

  • Hardcoded strings (use translations)
  • Inline styles (use Tailwind)
  • Bright colors (stick to gray + muted accents)
  • Missing loading states
  • Missing error states
  • Missing empty states
  • Uncontrolled inputs
  • Missing accessibility attributes
  • Non-responsive layouts
  • any types in TypeScript

Do:

  • Use semantic HTML
  • Implement proper TypeScript types
  • Add loading/error/empty states
  • Support keyboard navigation
  • Use ARIA attributes
  • Test on mobile devices
  • Follow Notion design tokens
  • Memoize expensive operations
  • Implement proper error handling
  • Support all 3 languages (EN/ES/PT-BR)