Files
pos-system/microservices/.agent/skills/react-ui-components/SKILL.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

8.8 KiB

name, description, compatibility, metadata
name description compatibility metadata
react-ui-components UI component patterns với React Aria, Radix UI, và Storybook cho GoodGo. Use for accessible components, composition patterns, animations, và component documentation. react-aria>=3, @radix-ui/*>=1, storybook>=10, framer-motion>=12
author version
Velik Ho 1.0

React UI Components / Components UI React

When to Use This Skill / Khi Nào Sử Dụng

Use this skill when:

  • Creating reusable components / Tạo components tái sử dụng
  • Implementing accessible UI / Triển khai UI accessible
  • Writing Storybook stories / Viết Storybook stories
  • Adding animations / Thêm animations
  • Styling with CVA & Tailwind / Styling với CVA & Tailwind

Core Principles / Nguyên Tắc Cốt Lõi

  1. Accessibility First: Use React Aria/Radix for keyboard & screen reader support
  2. Composition over Props: Build flexible components via composition
  3. Variant-based Styling: Use CVA for maintainable variant logic
  4. Document with Stories: Every component needs Storybook stories

Key Patterns / Mẫu Chính

Component Structure

features/shared/components/
├── Button/
│   ├── Button.tsx           # Main component
│   ├── Button.stories.tsx   # Storybook stories
│   ├── Button.test.tsx      # Unit tests
│   └── index.ts             # Barrel export
└── index.ts                 # Public exports

CVA for Variants (Class Variance Authority)

// components/Button/Button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { clsx } from 'clsx';

/**
 * EN: Button variants using CVA
 * VI: Variants cho Button sử dụng CVA
 */
const buttonVariants = cva(
  // Base styles
  'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        primary: 'bg-accent-primary text-white hover:bg-accent-primary/90',
        secondary: 'bg-bg-secondary text-text-primary hover:bg-bg-tertiary',
        ghost: 'hover:bg-bg-secondary',
        destructive: 'bg-accent-error text-white hover:bg-accent-error/90',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4 text-base',
        lg: 'h-12 px-6 text-lg',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  isLoading?: boolean;
}

export function Button({
  className,
  variant,
  size,
  isLoading,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={clsx(buttonVariants({ variant, size }), className)}
      disabled={isLoading || props.disabled}
      {...props}
    >
      {isLoading ? <Spinner /> : children}
    </button>
  );
}

React Aria Pattern

// components/Switch/Switch.tsx
import { useToggleState } from 'react-stately';
import { useSwitch, useFocusRing, VisuallyHidden } from 'react-aria';
import { useRef } from 'react';

/**
 * EN: Accessible switch using React Aria
 * VI: Switch accessible sử dụng React Aria
 */
export function Switch({ children, ...props }) {
  const state = useToggleState(props);
  const ref = useRef<HTMLInputElement>(null);
  const { inputProps } = useSwitch(props, state, ref);
  const { focusProps, isFocusVisible } = useFocusRing();

  return (
    <label className="flex items-center gap-2 cursor-pointer">
      <VisuallyHidden>
        <input {...inputProps} {...focusProps} ref={ref} />
      </VisuallyHidden>
      <div
        className={clsx(
          'w-10 h-6 rounded-full transition-colors',
          state.isSelected ? 'bg-accent-primary' : 'bg-bg-tertiary',
          isFocusVisible && 'ring-2 ring-border-focus'
        )}
      >
        <div
          className={clsx(
            'w-5 h-5 rounded-full bg-white shadow-md transition-transform',
            state.isSelected && 'translate-x-4'
          )}
        />
      </div>
      {children}
    </label>
  );
}

Radix UI Pattern

// components/Dialog/Dialog.tsx
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { clsx } from 'clsx';

/**
 * EN: Dialog using Radix primitives
 * VI: Dialog sử dụng Radix primitives
 */
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;

export function DialogContent({ className, children, ...props }) {
  return (
    <DialogPrimitive.Portal>
      <DialogPrimitive.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm" />
      <DialogPrimitive.Content
        className={clsx(
          'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
          'bg-bg-elevated rounded-xl shadow-xl p-6 max-w-md w-full',
          'focus:outline-none',
          className
        )}
        {...props}
      >
        {children}
      </DialogPrimitive.Content>
    </DialogPrimitive.Portal>
  );
}

export const DialogTitle = DialogPrimitive.Title;
export const DialogDescription = DialogPrimitive.Description;
export const DialogClose = DialogPrimitive.Close;

Framer Motion Animations

// components/FadeIn/FadeIn.tsx
import { motion } from 'framer-motion';

/**
 * EN: Fade-in animation wrapper
 * VI: Wrapper animation fade-in
 */
export function FadeIn({ children, delay = 0 }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 10 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{
        duration: 0.3,
        delay,
        ease: [0.4, 0, 0.2, 1], // ease-out
      }}
    >
      {children}
    </motion.div>
  );
}

// Stagger animation for lists
export function StaggerList({ children }) {
  return (
    <motion.ul
      initial="hidden"
      animate="visible"
      variants={{
        hidden: {},
        visible: { transition: { staggerChildren: 0.05 } },
      }}
    >
      {children}
    </motion.ul>
  );
}

export function StaggerItem({ children }) {
  return (
    <motion.li
      variants={{
        hidden: { opacity: 0, x: -10 },
        visible: { opacity: 1, x: 0 },
      }}
    >
      {children}
    </motion.li>
  );
}

Storybook Stories

// components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

/**
 * EN: Button component stories
 * VI: Stories cho Button component
 */
const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'ghost', 'destructive'],
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg'],
    },
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    children: 'Primary Button',
    variant: 'primary',
  },
};

export const Secondary: Story = {
  args: {
    children: 'Secondary Button',
    variant: 'secondary',
  },
};

export const Loading: Story = {
  args: {
    children: 'Loading...',
    isLoading: true,
  },
};

// Interactive story with play function
export const Interactive: Story = {
  args: { children: 'Click me' },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');
    await userEvent.click(button);
  },
};

Common Mistakes / Lỗi Thường Gặp

1. Not Using Accessible Primitives

// ❌ BAD: Custom toggle without accessibility
<div onClick={toggle} className="switch" />

// ✅ GOOD: Use React Aria or Radix
<Switch isSelected={on} onChange={toggle} />

2. Hardcoding Styles Instead of Variants

// ❌ BAD: Conditional className logic scattered
<button className={isLarge ? 'h-12 px-6' : 'h-10 px-4'} />

// ✅ GOOD: Use CVA variants
<button className={buttonVariants({ size: isLarge ? 'lg' : 'md' })} />

3. Missing Focus Indicators

// ❌ BAD: Removes focus outline
.button:focus { outline: none; }

// ✅ GOOD: Custom focus ring
.button:focus-visible { 
  outline: none;
  ring: 2px solid var(--border-focus);
}

Quick Reference / Tham Chiếu Nhanh

Library Use Case
CVA Variant-based styling
clsx Conditional classNames
React Aria Low-level accessible hooks
Radix UI High-level accessible primitives
Framer Motion Animations
Storybook Component documentation

Resources / Tài Nguyên