Files
myeasycms-v2/packages/ui/CLAUDE.md
Giancarlo Buomprisco 9681e1f90b Chore: force Zod v3, replace deprecated Supabase vars in local dev, updated Dev Tool (#425)
* - Replaced Supabase local environment variables
- Updates packages
- Force Zod v3 if peer deps require Zod v4
- Updated Dev Tool

* Use correct field for pnpm overrides

* Fix inaccuracies in the Agents files; fix type casting in `useForm`

* Updated lock file

* Be more specific when intercepting POST request in flaky e2e test
2025-12-13 12:20:15 +08:00

6.1 KiB

UI Components & Styling Instructions

This file contains instructions for working with UI components, styling, and forms.

Core UI Library

Import from packages/ui/src/:

// Shadcn components
import { Button } from '@kit/ui/button';
import { Card } from '@kit/ui/card';
// Makerkit components
import { If } from '@kit/ui/if';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans';

Styling Guidelines

  • Use Tailwind CSS v4 with semantic classes
  • Prefer Shadcn-ui classes like bg-background, text-muted-foreground
  • Use cn() utility from @kit/ui/utils for class merging
import { cn } from '@kit/ui/utils';

function MyComponent({ className }) {
  return (
    <div className={cn('bg-background text-foreground', className)}>
      Content
    </div>
  );
}

Conditional Rendering

Use the If component from packages/ui/src/makerkit/if.tsx:

import { If } from '@kit/ui/if';

<If condition={isLoading} fallback={<Content />}>
  <Spinner />
</If>

// With type inference
<If condition={error}>
  {(err) => <ErrorMessage error={err} />}
</If>

Testing Attributes

<button data-test="submit-button">Submit</button>
<div data-test="user-profile" data-user-id={user.id}>Profile</div>

Forms with React Hook Form & Zod

// 1. Schema in separate file
export const CreateNoteSchema = z.object({
  title: z.string().min(1),
  content: z.string().min(1),
});

// 2. Client component with form
'use client';
const form = useForm({
  resolver: zodResolver(CreateNoteSchema),
});

const onSubmit = (data) => {
  startTransition(async () => {
    await toast.promise(createNoteAction(data), {
      loading: 'Creating...',
      success: 'Created!',
      error: 'Failed!',
    }).unwrap();
  });
};

Guidelines

  • Place Zod resolver outside so it can be reused with Server Actions
  • Never add generics to useForm, use Zod resolver to infer types instead
  • Never use watch() instead use hook useWatch
  • Add FormDescription (optionally) and always add FormMessage to display errors

Form Examples

  • Contact form: apps/web/app/(marketing)/contact/_components/contact-form.tsx
  • Verify OTP form: packages/otp/src/components/verify-otp-form.tsx

Internationalization

Always use Trans component from packages/ui/src/makerkit/trans.tsx:

import { Trans } from '@kit/ui/trans';

<Trans
  i18nKey="user:welcomeMessage"
  values={{ name: user.name }}
/>

// With HTML elements
<Trans
  i18nKey="terms:agreement"
  components={{
    TermsLink: <a href="/terms" className="underline" />,
  }}
/>

Toast Notifications

Use the toast utility from @kit/ui/sonner:

import { toast } from '@kit/ui/sonner';

// Simple toast
toast.success('Success message');
toast.error('Error message');

// Promise-based toast
await toast.promise(asyncFunction(), {
  loading: 'Processing...',
  success: 'Done!',
  error: 'Failed!',
});

Common Component Patterns

Loading States

import { Spinner } from '@kit/ui/spinner';

<If condition={isLoading} fallback={<Content />}>
  <Spinner className="h-4 w-4" />
</If>

Error Handling

import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';

<If condition={Boolean(error)}>
  <Alert variant="destructive">
    <ExclamationTriangleIcon className="h-4 w-4" />
    <AlertTitle>Error</AlertTitle>
    <AlertDescription>{error}</AlertDescription>
  </Alert>
</If>

Button Patterns

import { Button } from '@kit/ui/button';

// Loading button
<Button disabled={isPending}>
  {isPending ? (
    <>
      <Spinner className="mr-2 h-4 w-4" />
      Loading...
    </>
  ) : (
    'Submit'
  )}
</Button>

// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>

Card Layouts

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kit/ui/card';

<Card>
  <CardHeader>
    <CardTitle>Card Title</CardTitle>
    <CardDescription>Card description</CardDescription>
  </CardHeader>
  <CardContent>
    Card content goes here
  </CardContent>
</Card>

Form Components

Input Fields

import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form';

<FormField
  name="title"
  control={form.control}
  render={({ field }) => (
    <FormItem>
      <FormLabel>Title</FormLabel>
      <FormControl>
        <Input placeholder="Enter title" {...field} />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>

Select Components

import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kit/ui/select';

<FormField
  name="category"
  control={form.control}
  render={({ field }) => (
    <FormItem>
      <FormLabel>Category</FormLabel>
      <Select onValueChange={field.onChange} defaultValue={field.value}>
        <FormControl>
          <SelectTrigger>
            <SelectValue placeholder="Select category" />
          </SelectTrigger>
        </FormControl>
        <SelectContent>
          <SelectItem value="option1">Option 1</SelectItem>
          <SelectItem value="option2">Option 2</SelectItem>
        </SelectContent>
      </Select>
      <FormMessage />
    </FormItem>
  )}
/>

Accessibility Guidelines

  • Always include proper ARIA labels
  • Use semantic HTML elements
  • Ensure proper keyboard navigation
<button
  aria-label="Close modal"
  aria-describedby="modal-description"
  onClick={onClose}
>
  <X className="h-4 w-4" />
</button>

Dark Mode Support

The UI components automatically support dark mode through CSS variables. Use semantic color classes:

// Good - semantic colors
<div className="bg-background text-foreground border-border">
  <p className="text-muted-foreground">Secondary text</p>
</div>

// Avoid - hardcoded colors
<div className="bg-white text-black border-gray-200">
  <p className="text-gray-500">Secondary text</p>
</div>