Files
gh-hirefrank-hirefrank-mark…/commands/es-component.md
2025-11-29 18:45:50 +08:00

24 KiB

description
description
Scaffold shadcn/ui components with distinctive design, accessibility, and animation best practices built-in. Prevents generic aesthetics from the start.

Component Generator Command

<command_purpose> Generate shadcn/ui components with distinctive design patterns, deep customization, accessibility features, and engaging animations built-in. Prevents generic "AI aesthetic" by providing branded templates from the start. </command_purpose>

Introduction

Senior Component Architect with expertise in shadcn/ui, React 19 with hooks, Tailwind CSS, accessibility, and distinctive design patterns

Design Philosophy: Start with distinctive, accessible, engaging components rather than fixing generic patterns later.

Prerequisites

- Tanstack Start project with Vue 3 - shadcn/ui component library installed - Tailwind 4 CSS configured with custom theme (or will be created) - (Optional) shadcn/ui MCP server for component API validation - (Optional) Existing `composables/useDesignSystem.ts` for consistent patterns

Command Usage

/es-component <type> <name> [options]

Arguments:

  • <type>: Component type (button, card, form, modal, hero, navigation, etc.)
  • <name>: Component name in PascalCase (e.g., PrimaryButton, FeatureCard)
  • [options]: Optional flags:
    • --theme <dark|light|custom>: Theme variant
    • --animations <minimal|standard|rich>: Animation complexity
    • --accessible: Include enhanced accessibility features (default: true)
    • --output <path>: Custom output path (default: components/)

Examples:

# Generate primary button component
/es-component button PrimaryButton

# Generate feature card with rich animations
/es-component card FeatureCard --animations rich

# Generate hero section with custom theme
/es-component hero LandingHero --theme custom

# Generate modal with custom output path
/es-component modal ConfirmDialog --output components/dialogs/

Main Tasks

1. Project Context Analysis

First, I need to understand existing design system, theme configuration, and component patterns. This ensures generated components match existing project aesthetics.

Immediate Actions:

<task_list>

  • Check for tailwind.config.ts and extract custom theme (fonts, colors, animations)
  • Check for composables/useDesignSystem.ts and extract existing variants
  • Check for app.config.ts and extract shadcn/ui global customization
  • Scan existing components for naming conventions and structure patterns
  • Determine if design system is established or needs creation

</task_list>

Output Summary:

<summary_format> 📦 Project Context:

  • Custom fonts: Found/Not Found (Inter or Custom )
  • Brand colors: Found/Not Found (Purple or Custom )
  • Design system composable: Exists/Missing
  • Component count: X components found
  • Naming convention: Detected pattern </summary_format>

2. Validate Component Type with MCP (if available)

If shadcn/ui MCP is available, validate that the requested component type exists and get accurate props/slots before generating.

MCP Validation:

<mcp_workflow>

If shadcn/ui MCP available:

  1. Query shadcn-ui.list_components() to get available components
  2. Map component type to shadcn/ui component:
    • buttonButton
    • cardCard
    • modalDialog
    • formUForm + Input/UTextarea/etc.
    • hero → Custom layout with Button, Card
    • navigationUTabs or custom
  3. Query shadcn-ui.get_component("Button") for accurate props
  4. Use real props in generated component (prevent hallucination)

If MCP not available:

  • Use documented shadcn/ui API
  • Include comment: "// TODO: Verify props with shadcn/ui docs"

</mcp_workflow>

3. Generate Component with Design Best Practices

Generate React component with: 1. Distinctive typography (custom fonts, not Inter) 2. Brand colors (custom palette, not purple) 3. Rich animations (transitions, micro-interactions) 4. Deep shadcn/ui customization (ui prop + utilities) 5. Accessibility features (ARIA, keyboard, focus states) 6. Responsive design

Component Templates by Type:

Button Component

<button_template>

<!-- app/components/PrimaryButton.tsx -->
<script setup lang="ts">
import { computed } from 'react';

interface Props {
  /** Button label */
  label?: string;
  /** Icon name (Iconify format) */
  icon?: string;
  /** Loading state */
  loading?: boolean;
  /** Disabled state */
  disabled?: boolean;
  /** Button size */
  size?: 'sm' | 'md' | 'lg' | 'xl';
  /** Full width */
  fullWidth?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  label: '',
  icon: '',
  loading: false,
  disabled: false,
  size: 'lg',
  fullWidth: false
});

const emit = defineEmits<{
  click: [event: MouseEvent];
}>();

const buttonClasses = computed(() => ({
  'w-full': props.fullWidth
}));

  <Button
    :color="primary"
    :size="size"
    loading={loading"
    disabled={disabled || loading"
    :icon="icon"
    :ui="{
      font: 'font-heading tracking-wide',
      rounded: 'rounded-full',
      padding: {
        sm: 'px-4 py-2',
        md: 'px-6 py-3',
        lg: 'px-8 py-4',
        xl: 'px-10 py-5'
      },
      shadow: 'shadow-lg hover:shadow-xl'
    }"
    :class="[
      'transition-all duration-300 ease-out',
      'hover:scale-105 hover:-rotate-1',
      'active:scale-95 active:rotate-0',
      'focus:outline-none',
      'focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2',
      'motion-safe:hover:scale-105',
      'motion-reduce:hover:bg-primary-700',
      buttonClasses
    ]"
    onClick={emit('click', $event)"
  >
    <span class="inline-flex items-center gap-2">
      <slot>{ label}</slot>

      <!-- Animated icon on hover -->
      <Icon
        {&& "icon && !loading"
        :name="icon"
        class="
          transition-transform duration-300
          group-hover:translate-x-1 group-hover:-translate-y-0.5
        "
      />
    </span>
  </Button>

Usage Example:

const handleClick = () => {
  console.log('Clicked!');
};

  <PrimaryButton
    label="Get Started"
    icon="i-heroicons-arrow-right"
    size="lg"
    onClick={handleClick"
  />

</button_template>

Card Component

<card_template>

<!-- app/components/FeatureCard.tsx -->
<script setup lang="ts">
import { ref } from 'react';

interface Props {
  /** Card title */
  title: string;
  /** Card description */
  description?: string;
  /** Icon name */
  icon?: string;
  /** Enable hover effects */
  hoverable?: boolean;
  /** Card variant */
  variant?: 'default' | 'elevated' | 'outlined';
}

const props = withDefaults(defineProps<Props>(), {
  description: '',
  icon: '',
  hoverable: true,
  variant: 'elevated'
});

const isHovered = ref(false);

const cardUi = computed(() => ({
  background: 'bg-white dark:bg-brand-midnight',
  ring: props.variant === 'outlined' ? 'ring-1 ring-brand-coral/20' : '',
  rounded: 'rounded-2xl',
  shadow: props.variant === 'elevated' ? 'shadow-xl hover:shadow-2xl' : 'shadow-md',
  body: {
    padding: 'p-8',
    background: 'bg-gradient-to-br from-white to-gray-50 dark:from-brand-midnight dark:to-gray-900'
  }
}));

  <Card
    :ui="cardUi"
    :class="[
      'transition-all duration-300',
      hoverable && 'hover:-translate-y-2 hover:rotate-1 cursor-pointer',
      'motion-safe:hover:-translate-y-2',
      'motion-reduce:hover:shadow-xl'
    ]"
    onMouseEnter="isHovered = true"
    onMouseLeave="isHovered = false"
  >
    <div class="space-y-4">
      <!-- Icon -->
      <div
        {&& "icon"
        :class="[
          'inline-flex items-center justify-center',
          'w-16 h-16 rounded-2xl',
          'bg-gradient-to-br from-brand-coral to-brand-ocean',
          'transition-transform duration-300',
          isHovered && 'scale-110 rotate-3'
        ]"
      >
        <Icon
          :name="icon"
          class="w-8 h-8 text-white"
        />
      </div>

      <!-- Title -->
      <h3
        :class="[
          'font-heading text-2xl',
          'text-brand-midnight dark:text-white',
          'transition-colors duration-300',
          isHovered && 'text-brand-coral'
        ]"
      >
        { title}
      </h3>

      <!-- Description -->
      <p
        {&& "description"
        class="text-gray-700 dark:text-gray-300 leading-relaxed"
      >
        { description}
      </p>

      <!-- Default slot for custom content -->
      <div {&& "$slots.default">
        <slot />
      </div>

      <!-- Footer slot -->
      <div {&& "$slots.footer" class="pt-4 border-t border-gray-200 dark:border-gray-700">
        <slot name="footer" />
      </div>
    </div>
  </Card>

Usage Example:

  <FeatureCard
    title="Fast Deployment"
    description="Deploy to the edge in seconds with Cloudflare Workers"
    icon="i-heroicons-rocket-launch"
    hoverable
  >
    <template #footer>
      <PrimaryButton label="Learn More" size="sm" />
  </FeatureCard>

</card_template>

Form Component

<form_template>

<!-- app/components/ContactForm.tsx -->
<script setup lang="ts">
import { ref, reactive } from 'react';
import { z } from 'zod';
import type { FormSubmitEvent } from '#ui/types';

// Validation schema
const schema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters')
});

type Schema = z.output<typeof schema>;

const formData = reactive<Schema>({
  name: '',
  email: '',
  message: ''
});

const isSubmitting = ref(false);
const showSuccess = ref(false);
const showError = ref(false);
const errorMessage = ref('');

const onSubmit = async (event: FormSubmitEvent<Schema>) => {
  isSubmitting.value = true;
  showSuccess.value = false;
  showError.value = false;

  try {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 2000));

    console.log('Form submitted:', event.data);

    showSuccess.value = true;

    // Reset form
    formData.name = '';
    formData.email = '';
    formData.message = '';

    // Hide success message after 5 seconds
    setTimeout(() => {
      showSuccess.value = false;
    }, 5000);
  } catch (error) {
    showError.value = true;
    errorMessage.value = 'Failed to submit form. Please try again.';
  } finally {
    isSubmitting.value = false;
  }
};

  <div class="space-y-6">
    <!-- Success Alert -->
    <Transition
      enter-active-class="transition-all duration-300 ease-out"
      enter-from-class="opacity-0 translate-y-2 scale-95"
      enter-to-class="opacity-100 translate-y-0 scale-100"
      leave-active-class="transition-all duration-200 ease-in"
      leave-from-class="opacity-100"
      leave-to-class="opacity-0"
    >
      <Alert
        {&& "showSuccess"
        color="green"
        icon="i-heroicons-check-circle"
        title="Success!"
        description="Your message has been sent successfully."
        :closable="true"
        :ui="{ rounded: 'rounded-xl', padding: 'p-4' }"
        onClose="showSuccess = false"
      />
    </Transition>

    <!-- Error Alert -->
    <Transition
      enter-active-class="transition-all duration-300 ease-out"
      enter-from-class="opacity-0 translate-y-2"
      enter-to-class="opacity-100 translate-y-0"
    >
      <Alert
        {&& "showError"
        color="red"
        icon="i-heroicons-x-circle"
        title="Error"
        :description="errorMessage"
        :closable="true"
        onClose="showError = false"
      />
    </Transition>

    <!-- Form -->
    <UForm
      :schema="schema"
      :state="formData"
      class="space-y-6"
      onSubmit="onSubmit"
    >
      <!-- Name Field -->
      <UFormGroup
        label="Name"
        name="name"
        required
        :ui="{ label: { base: 'font-medium text-sm' } }"
      >
        <Input
          value="formData.name"
          placeholder="Your name"
          icon="i-heroicons-user"
          :ui="{
            rounded: 'rounded-lg',
            padding: { sm: 'px-4 py-3' },
            icon: { leading: { padding: { sm: 'ps-11' } } }
          }"
          class="transition-all duration-200 focus-within:ring-2 focus-within:ring-brand-coral"
        />
      </UFormGroup>

      <!-- Email Field -->
      <UFormGroup
        label="Email"
        name="email"
        required
      >
        <Input
          value="formData.email"
          type="email"
          placeholder="your@email.com"
          icon="i-heroicons-envelope"
          :ui="{
            rounded: 'rounded-lg',
            padding: { sm: 'px-4 py-3' }
          }"
          class="transition-all duration-200 focus-within:ring-2 focus-within:ring-brand-coral"
        />
      </UFormGroup>

      <!-- Message Field -->
      <UFormGroup
        label="Message"
        name="message"
        required
      >
        <UTextarea
          value="formData.message"
          placeholder="Your message..."
          :rows="5"
          :ui="{
            rounded: 'rounded-lg',
            padding: { sm: 'px-4 py-3' }
          }"
          class="transition-all duration-200 focus-within:ring-2 focus-within:ring-brand-coral"
        />
      </UFormGroup>

      <!-- Submit Button -->
      <Button
        type="submit"
        loading={isSubmitting"
        disabled={isSubmitting"
        color="primary"
        size="lg"
        :ui="{
          font: 'font-heading',
          rounded: 'rounded-full',
          padding: { lg: 'px-8 py-4' }
        }"
        class="
          w-full
          transition-all duration-300
          hover:scale-105 hover:shadow-xl
          active:scale-95
          motion-safe:hover:scale-105
          motion-reduce:hover:bg-primary-700
        "
      >
        <span class="inline-flex items-center gap-2">
          <Icon
            {&& "!isSubmitting"
            name="i-heroicons-paper-airplane"
            class="transition-transform duration-300 group-hover:translate-x-1 group-hover:-translate-y-0.5"
          />
          { isSubmitting ? 'Sending...' : 'Send Message'}
        </span>
      </Button>
    </UForm>
  </div>

</form_template>

Hero Component

<hero_template>

<!-- app/components/LandingHero.tsx -->
<script setup lang="ts">
interface Props {
  /** Hero title */
  title: string;
  /** Hero subtitle */
  subtitle?: string;
  /** Primary CTA label */
  primaryCta?: string;
  /** Secondary CTA label */
  secondaryCta?: string;
}

const props = withDefaults(defineProps<Props>(), {
  subtitle: '',
  primaryCta: 'Get Started',
  secondaryCta: 'Learn More'
});

const emit = defineEmits<{
  primaryClick: [];
  secondaryClick: [];
}>();

  <div class="relative min-h-screen flex items-center justify-center overflow-hidden">
    <!-- Atmospheric Background -->
    <div class="absolute inset-0 bg-gradient-to-br from-brand-cream via-white to-brand-ocean/10" />

    <!-- Animated Gradient Orbs -->
    <div
      class="absolute top-20 left-20 w-96 h-96 bg-brand-coral/20 rounded-full blur-3xl animate-pulse"
      aria-hidden="true"
    />
    <div
      class="absolute bottom-20 right-20 w-96 h-96 bg-brand-ocean/20 rounded-full blur-3xl animate-pulse"
      style="animation-delay: 1s;"
      aria-hidden="true"
    />

    <!-- Subtle Pattern Overlay -->
    <div
      class="absolute inset-0 opacity-5"
      style="background-image: radial-gradient(circle, #000 1px, transparent 1px); background-size: 20px 20px;"
      aria-hidden="true"
    />

    <!-- Content -->
    <div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
      <div class="text-center space-y-8">
        <!-- Animated Badge -->
        <div
          class="
            inline-flex items-center gap-2
            px-4 py-2 rounded-full
            bg-brand-coral/10 border border-brand-coral/20
            text-brand-coral font-medium
            animate-in slide-in-from-top duration-500
          "
        >
          <Icon name="i-heroicons-sparkles" class="w-4 h-4 animate-pulse" />
          <span class="text-sm">New: Now on Cloudflare Workers</span>
        </div>

        <!-- Title -->
        <h1
          class="
            font-heading text-6xl sm:text-7xl lg:text-8xl
            tracking-tighter leading-none
            text-brand-midnight dark:text-white
            animate-in slide-in-from-top duration-700
          "
          style="animation-delay: 100ms;"
        >
          { title}
        </h1>

        <!-- Subtitle -->
        <p
          {&& "subtitle"
          class="
            max-w-2xl mx-auto
            text-xl sm:text-2xl leading-relaxed
            text-gray-700 dark:text-gray-300
            animate-in slide-in-from-top duration-700
          "
          style="animation-delay: 200ms;"
        >
          { subtitle}
        </p>

        <!-- CTAs -->
        <div
          class="
            flex flex-col sm:flex-row items-center justify-center gap-4
            animate-in slide-in-from-top duration-700
          "
          style="animation-delay: 300ms;"
        >
          <Button
            color="primary"
            size="xl"
            :ui="{
              font: 'font-heading tracking-wide',
              rounded: 'rounded-full',
              padding: { xl: 'px-10 py-5' }
            }"
            class="
              transition-all duration-300
              hover:scale-110 hover:-rotate-2 hover:shadow-2xl
              active:scale-95 active:rotate-0
              motion-safe:hover:scale-110
            "
            onClick={emit('primaryClick')"
          >
            <span class="inline-flex items-center gap-2">
              { primaryCta}
              <Icon
                name="i-heroicons-arrow-right"
                class="transition-transform duration-300 group-hover:translate-x-2"
              />
            </span>
          </Button>

          <Button
            color="gray"
            variant="outline"
            size="xl"
            :ui="{
              font: 'font-sans',
              rounded: 'rounded-full'
            }"
            class="
              transition-all duration-300
              hover:scale-105 hover:shadow-lg
              active:scale-95
            "
            onClick={emit('secondaryClick')"
          >
            { secondaryCta}
          </Button>
        </div>

        <!-- Slot for additional content -->
        <div {&& "$slots.default" class="mt-12">
          <slot />
        </div>
      </div>
    </div>
  </div>

</hero_template>

4. Create Design System Composable (if missing)

If design system composable doesn't exist, generate it to ensure consistency across all components.

<design_system_composable>

// composables/useDesignSystem.ts
import type { ButtonProps } from '#ui/types';

export const useDesignSystem = () => {
  /**
   * Button Variants
   */
  const button = {
    primary: {
      color: 'primary',
      size: 'lg',
      ui: {
        font: 'font-heading tracking-wide',
        rounded: 'rounded-full',
        padding: { lg: 'px-8 py-4' },
        shadow: 'shadow-lg hover:shadow-xl'
      },
      class: 'transition-all duration-300 hover:scale-105 hover:-rotate-1 active:scale-95 active:rotate-0'
    } as ButtonProps,

    secondary: {
      color: 'gray',
      variant: 'outline',
      size: 'md',
      ui: {
        font: 'font-sans',
        rounded: 'rounded-lg'
      },
      class: 'transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800'
    } as ButtonProps,

    ghost: {
      variant: 'ghost',
      size: 'md',
      ui: {
        font: 'font-sans'
      },
      class: 'transition-colors duration-200'
    } as ButtonProps
  };

  /**
   * Card Variants
   */
  const card = {
    elevated: {
      ui: {
        background: 'bg-white dark:bg-brand-midnight',
        rounded: 'rounded-2xl',
        shadow: 'shadow-xl hover:shadow-2xl',
        body: { padding: 'p-8' }
      },
      class: 'transition-all duration-300 hover:-translate-y-1'
    },

    outlined: {
      ui: {
        background: 'bg-white dark:bg-brand-midnight',
        ring: 'ring-1 ring-brand-coral/20',
        rounded: 'rounded-2xl',
        body: { padding: 'p-8' }
      },
      class: 'transition-all duration-300 hover:ring-brand-coral/40'
    }
  };

  /**
   * Animation Presets
   */
  const animations = {
    fadeIn: 'animate-in fade-in duration-500',
    slideUp: 'animate-in slide-in-from-bottom duration-500',
    slideDown: 'animate-in slide-in-from-top duration-500',
    scaleIn: 'animate-in zoom-in duration-300',
    hover: {
      scale: 'transition-transform duration-300 hover:scale-105',
      lift: 'transition-all duration-300 hover:-translate-y-1',
      shadow: 'transition-shadow duration-300 hover:shadow-xl'
    }
  };

  return {
    button,
    card,
    animations
  };
};

</design_system_composable>

5. Generate Component Files

Create the actual files in the filesystem with proper naming and structure.

File Creation:

<file_creation_steps>

  1. Determine output path:

    • Default: components/<ComponentName>.react
    • Custom: User-specified --output path
  2. Generate component file:

    • Use template for component type
    • Replace placeholders with actual names
    • Include TypeScript types
    • Include JSDoc comments
    • Include usage examples in comments
  3. Update or create design system composable (if needed):

    • Path: composables/useDesignSystem.ts
    • Add new variants if applicable
  4. Generate Storybook story (optional, if Storybook detected):

    • Path: components/<ComponentName>.stories.ts
  5. Generate test file (optional):

    • Path: components/<ComponentName>.spec.ts

</file_creation_steps>

6. Validate Generated Component

Run validation to ensure generated component follows best practices.

Validation Checks:

<validation_checklist>

  • Uses custom fonts (not Inter/Roboto)
  • Uses brand colors (not default purple)
  • Includes animations/transitions
  • Has hover states on interactive elements
  • Has focus states (focus-visible rings)
  • Respects reduced motion (motion-safe/motion-reduce)
  • Includes ARIA labels where needed
  • Uses shadcn/ui components (not reinventing)
  • Deep customization with ui prop
  • TypeScript props interface
  • JSDoc comments
  • Accessible (keyboard navigation, screen readers)
  • Responsive design
  • Dark mode support

</validation_checklist>

Output Format

<output_format>

✅ Component Generated: <ComponentName>

📁 Files Created:
- app/components/<ComponentName>.tsx (primary component)
- composables/useDesignSystem.ts (updated/created)

🎨 Design Features:
✅ Custom typography (font-heading)
✅ Brand colors (brand-coral, brand-ocean)
✅ Rich animations (hover:scale-105, transitions)
✅ Deep shadcn/ui customization (ui prop)
✅ Accessibility features (ARIA, focus states)
✅ Reduced motion support (motion-safe)
✅ Responsive design
✅ Dark mode support

📖 Usage Example:

```tsx
import { <ComponentName> } from '#components';

// Your component logic

  <<ComponentName>
    prop1="value1"
    prop2="value2"
    onEvent="handleEvent"
  />

🔍 Next Steps:

  1. Review generated component in components/<ComponentName>.react
  2. Customize props/styles as needed
  3. Test accessibility with keyboard navigation
  4. Test animations with reduced motion preference
  5. Run /es-design-review to validate design patterns

</output_format>

## Success Criteria

✅ Component generated successfully when:
- File created at correct path
- Uses distinctive design patterns (not generic)
- Includes all accessibility features
- Includes rich animations
- TypeScript types included
- Usage examples in comments
- Follows project conventions

## Post-Generation Actions

After generating component:
1. **Review code**: Open generated file and review
2. **Test component**: Add to a page and test interactions
3. **Validate design**: Run `/es-design-review` if needed
4. **Document**: Add to component library docs/Storybook

## Notes

- This command generates **starting templates** with best practices built-in
- Components are **fully customizable** after generation
- **Design system composable** ensures consistency across all generated components
- Use **shadcn/ui MCP** (if available) to prevent prop hallucination
- All generated components follow **WCAG 2.1 AA** accessibility standards
- Generated components respect **user's reduced motion preference**