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 patternsCommand 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.tsand extract custom theme (fonts, colors, animations) - Check for
composables/useDesignSystem.tsand extract existing variants - Check for
app.config.tsand 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:
- Query
shadcn-ui.list_components()to get available components - Map component type to shadcn/ui component:
button→Buttoncard→Cardmodal→Dialogform→UForm+Input/UTextarea/etc.hero→ Custom layout withButton,Cardnavigation→UTabsor custom
- Query
shadcn-ui.get_component("Button")for accurate props - 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 designComponent 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>
-
Determine output path:
- Default:
components/<ComponentName>.react - Custom: User-specified
--outputpath
- Default:
-
Generate component file:
- Use template for component type
- Replace placeholders with actual names
- Include TypeScript types
- Include JSDoc comments
- Include usage examples in comments
-
Update or create design system composable (if needed):
- Path:
composables/useDesignSystem.ts - Add new variants if applicable
- Path:
-
Generate Storybook story (optional, if Storybook detected):
- Path:
components/<ComponentName>.stories.ts
- Path:
-
Generate test file (optional):
- Path:
components/<ComponentName>.spec.ts
- Path:
</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
uiprop - 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:
- Review generated component in
components/<ComponentName>.react - Customize props/styles as needed
- Test accessibility with keyboard navigation
- Test animations with reduced motion preference
- Run
/es-design-reviewto 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**