| name | worldcrafter-feature-builder |
| description | Build complete features with Server Actions, forms, Zod validation, database CRUD operations, and comprehensive tests. Use when user requests "add a feature", "build a [feature]", "create [feature] with forms", or needs end-to-end implementation with validation and testing. Scaffolds pages, actions, schemas, loading/error states, and unit/integration/E2E tests. Supports multi-step wizards, image uploads, markdown editing, custom JSON attributes, and relationship management. Do NOT use for simple static pages (use worldcrafter-route-creator), database-only changes (use worldcrafter-database-setup), testing existing code (use worldcrafter-test-generator), or auth-only additions (use worldcrafter-auth-guard). |
WorldCrafter Feature Builder
Version: 2.0.0 Last Updated: 2025-01-15
This skill provides a systematic approach to implementing new features in the WorldCrafter codebase following established architectural patterns, best practices, and testing standards.
Skill Metadata
Related Skills:
worldcrafter-database-setup- Use first if feature needs new database tablesworldcrafter-auth-guard- Use to add authentication to generated featuresworldcrafter-test-generator- Alternative for adding tests to existing featuresworldcrafter-route-creator- Alternative for simple pages without forms
Example Use Cases:
- "I want to add a blog post feature with authentication" → Creates BlogPost model, form with validation, Server Actions, auth checks, and full test suite
- "Build a user settings page where users can update their profile" → Creates settings route, update form with Zod validation, Server Action, and tests
- "Create a commenting system for posts" → Creates Comment model, comment form, CRUD operations, and E2E tests
When to Use This Skill
Use this skill when the user wants to:
- Add a new feature to the application
- Build a new form with validation and Server Actions
- Create CRUD operations for a new resource
- Implement authenticated workflows
- Add features that require database operations
- Build features with comprehensive testing (unit + integration + E2E)
Feature Implementation Process
Phase 1: Requirements Gathering
Understand the feature requirements
- What is the feature's purpose?
- What user interactions are needed?
- Does it require authentication?
- What data needs to be stored/retrieved?
- What validations are required?
Plan the database changes (if needed)
- What new tables or columns are needed?
- What relationships exist with other tables?
- What RLS policies should be applied?
- Reference
references/feature-patterns.mdfor database patterns
Phase 2: Scaffold Feature Structure
Use the scripts/scaffold_feature.py script to generate the complete feature structure:
python .claude/skills/worldcrafter-feature-builder/scripts/scaffold_feature.py <feature-name>
This creates:
src/app/<feature-name>/page.tsx- Client component with formsrc/app/<feature-name>/actions.ts- Server Actionssrc/app/<feature-name>/loading.tsx- Loading statesrc/app/<feature-name>/error.tsx- Error boundarysrc/lib/schemas/<feature-name>.ts- Zod validation schemasrc/app/__tests__/<feature-name>.integration.test.ts- Integration teste2e/<feature-name>.spec.ts- E2E test
Alternatively, manually copy templates from assets/templates/ to customize the structure.
Phase 3: Implement Database Layer (if needed)
Update Prisma schema (
prisma/schema.prisma):- Add new models with proper field types
- Define relationships with
@relation - Use snake_case with
@@mapfor table names
Create migration:
npx prisma migrate dev --name add_<feature_name>Apply RLS policies:
- Create SQL migration in
prisma/migrations/<timestamp>_add_<feature_name>/ - Reference
references/feature-patterns.mdfor RLS policy patterns - Run
npm run db:rlsto apply policies
- Create SQL migration in
Update test database:
npm run db:test:sync
Phase 4: Implement Validation Layer
Create Zod schema in
src/lib/schemas/<feature-name>.ts:- Define validation rules for all form fields
- Export TypeScript type:
export type FeatureFormValues = z.infer<typeof featureSchema> - Reference
assets/templates/schema.tsfor examples
Common validation patterns:
- Email:
z.string().email() - Required string:
z.string().min(1, "Field is required") - Optional:
z.string().optional() - Enum:
z.enum(["option1", "option2"]) - Number:
z.number().min(0).max(100) - Date:
z.date()orz.string().datetime()
- Email:
Phase 5: Implement Server Actions
Create Server Actions in
src/app/<feature-name>/actions.ts:- Mark file with
"use server"directive - Import Zod schema for validation
- Import Prisma client and Supabase server client
- Reference
assets/templates/actions.tsfor the standard pattern
- Mark file with
Server Action pattern:
"use server" import { revalidatePath } from "next/cache" import { prisma } from "@/lib/prisma" import { createClient } from "@/lib/supabase/server" import { featureSchema, type FeatureFormValues } from "@/lib/schemas/feature" export async function submitFeature(values: FeatureFormValues) { try { // 1. Validate const validated = featureSchema.parse(values) // 2. Authenticate (if needed) const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) return { success: false, error: "Unauthorized" } // 3. Database operation const result = await prisma.feature.create({ data: { ...validated, user_id: user.id } }) // 4. Revalidate revalidatePath("/feature") // 5. Return return { success: true, data: result } } catch (error) { return { success: false, error: "Operation failed" } } }Security checklist:
- Always validate input with Zod schema
- Check authentication for protected operations
- Verify user owns resource before update/delete
- Use parameterized queries (Prisma handles this)
- Never expose sensitive data in responses
Phase 6: Implement Client Components
Create form component in
src/app/<feature-name>/page.tsx:- Use React Hook Form with
zodResolver - Import shadcn/ui components from
@/components/ui - Call Server Action on submit
- Handle loading and error states
- Reference
assets/templates/page.tsxfor examples
- Use React Hook Form with
Form handling pattern:
"use client" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { featureSchema, type FeatureFormValues } from "@/lib/schemas/feature" import { submitFeature } from "./actions" export default function FeaturePage() { const form = useForm<FeatureFormValues>({ resolver: zodResolver(featureSchema), defaultValues: { /* ... */ } }) async function onSubmit(values: FeatureFormValues) { const result = await submitFeature(values) if (result.success) { // Handle success } else { // Handle error } } return <form onSubmit={form.handleSubmit(onSubmit)}>...</form> }Add shadcn/ui components if needed:
npx shadcn@latest add [component-name]
Phase 7: Implement Loading and Error States
Create loading state (
loading.tsx):- Use skeleton loaders matching the page layout
- Reference
assets/templates/loading.tsx
Create error boundary (
error.tsx):- Must be a client component (
"use client") - Include reset functionality
- Reference
assets/templates/error.tsx
- Must be a client component (
Phase 8: Write Tests
Reference references/testing-guide.md for detailed testing patterns.
Integration test (
src/app/__tests__/<feature-name>.integration.test.ts):- Test Server Actions with real test database
- Use test data factories from
src/test/factories/ - Clean up data in
afterAllhooks - Reference
assets/templates/integration.test.ts
E2E test (
e2e/<feature-name>.spec.ts):- Test complete user flows
- Use Page Object Model pattern
- Test across different browsers/viewports
- Reference
assets/templates/e2e.spec.ts
Run tests:
npm test # Unit tests npm run test:coverage # With coverage npm run test:e2e # E2E tests npm run test:all # All tests
Phase 9: Final Checks
Type checking:
npm run buildLinting and formatting:
npm run lint npm run formatTest database sync (if schema changed):
npm run db:test:syncVerify tests pass:
npm run test:all
Common Patterns
Authentication-Required Features
For features requiring authentication:
Check auth in Server Action:
const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) return { success: false, error: "Unauthorized" }Optionally protect the route in middleware or layout
Data Fetching Patterns
In Server Components (preferred):
- Fetch data directly with Prisma or Supabase
- No API route needed
In Client Components:
- Use TanStack Query (
useQuery,useMutation) - Call Server Actions or API routes
Form with Multiple Steps
- Use state management (useState) for current step
- Validate each step independently
- Combine all data in final submission
- Show progress indicator
Advanced Entity Features (v2.0)
WorldCrafter applications often need sophisticated entity creation and management workflows for worldbuilding. This section covers advanced patterns for character creators, location builders, item editors, and other complex entity forms.
Multi-Step Form Wizard Pattern
For complex entity creation (e.g., character creator with Basics → Appearance → Personality → Backstory → Custom Attributes):
State Management:
"use client"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
type WizardStep = "basics" | "appearance" | "personality" | "backstory" | "attributes"
export default function CharacterWizard() {
const [currentStep, setCurrentStep] = useState<WizardStep>("basics")
const [formData, setFormData] = useState({})
const form = useForm({
resolver: zodResolver(stepSchemas[currentStep]),
defaultValues: formData,
})
const steps: WizardStep[] = ["basics", "appearance", "personality", "backstory", "attributes"]
const currentIndex = steps.indexOf(currentStep)
const progress = ((currentIndex + 1) / steps.length) * 100
async function handleNext(values: any) {
// Merge values into accumulated form data
setFormData(prev => ({ ...prev, ...values }))
if (currentIndex < steps.length - 1) {
setCurrentStep(steps[currentIndex + 1])
form.reset() // Reset for next step
} else {
// Final step - submit all data
const result = await submitCharacter({ ...formData, ...values })
if (result.success) {
// Handle success
}
}
}
function handleBack() {
if (currentIndex > 0) {
setCurrentStep(steps[currentIndex - 1])
}
}
return (
<div>
{/* Progress indicator */}
<div className="mb-8">
<div className="flex justify-between mb-2">
{steps.map((step, i) => (
<div
key={step}
className={i <= currentIndex ? "text-primary" : "text-muted-foreground"}
>
{step}
</div>
))}
</div>
<div className="h-2 bg-secondary rounded-full">
<div
className="h-full bg-primary rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
{/* Step content */}
<form onSubmit={form.handleSubmit(handleNext)}>
{currentStep === "basics" && <BasicsStep form={form} />}
{currentStep === "appearance" && <AppearanceStep form={form} />}
{/* ... other steps ... */}
<div className="flex justify-between mt-6">
<Button
type="button"
variant="outline"
onClick={handleBack}
disabled={currentIndex === 0}
>
Back
</Button>
<Button type="submit">
{currentIndex === steps.length - 1 ? "Save Character" : "Next"}
</Button>
</div>
</form>
</div>
)
}
Key Implementation Details:
- Each step has its own Zod schema for incremental validation
- Form data accumulates in state across steps
- Progress bar shows completion percentage
- Navigation buttons enable Back/Next/Save
- Form resets between steps but preserves accumulated data
- Final step triggers Server Action with all combined data
See template: assets/templates/multi-step-wizard.tsx
Image Upload with Supabase Storage
For entity images (character portraits, location maps, item icons):
Component Pattern:
"use client"
import { useState } from "react"
import { createClient } from "@/lib/supabase/client"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import Image from "next/image"
export function ImageUpload({
value,
onChange,
bucket = "entity-images",
folder = "characters"
}: {
value?: string
onChange: (url: string) => void
bucket?: string
folder?: string
}) {
const [uploading, setUploading] = useState(false)
const [preview, setPreview] = useState<string | null>(value || null)
async function handleImageUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
try {
const supabase = createClient()
// Generate unique filename
const fileExt = file.name.split('.').pop()
const fileName = `${folder}/${Date.now()}-${Math.random().toString(36).substring(7)}.${fileExt}`
// Upload to Supabase Storage
const { data, error } = await supabase.storage
.from(bucket)
.upload(fileName, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from(bucket)
.getPublicUrl(fileName)
setPreview(publicUrl)
onChange(publicUrl) // Update form field
} catch (error) {
console.error('Upload error:', error)
alert('Failed to upload image')
} finally {
setUploading(false)
}
}
return (
<div className="space-y-4">
<Label htmlFor="image-upload">Image</Label>
{preview && (
<div className="relative w-48 h-48 border rounded-lg overflow-hidden">
<Image
src={preview}
alt="Preview"
fill
className="object-cover"
/>
</div>
)}
<Input
id="image-upload"
type="file"
accept="image/*"
onChange={handleImageUpload}
disabled={uploading}
/>
{uploading && <p className="text-sm text-muted-foreground">Uploading...</p>}
</div>
)
}
Integration with Form:
<FormField
control={form.control}
name="imageUrl"
render={({ field }) => (
<FormItem>
<ImageUpload
value={field.value}
onChange={field.onChange}
folder="characters"
/>
<FormMessage />
</FormItem>
)}
/>
Schema:
export const characterSchema = z.object({
name: z.string().min(1),
imageUrl: z.string().url().optional(),
// ... other fields
})
Storage Setup:
- Create bucket in Supabase Dashboard: Storage → New Bucket → "entity-images"
- Set bucket to public or configure RLS policies
- Configure CORS if needed for direct uploads
See template: assets/templates/image-upload.tsx
Custom JSON Attributes Pattern
For genre-specific or dynamic fields stored in a JSON column (e.g., Fantasy characters have "Mana Points", Sci-Fi characters have "Tech Level"):
Database Schema:
model Character {
id String @id @default(uuid())
name String
worldId String
world World @relation(fields: [worldId], references: [id])
attributes Json? // Custom fields based on world genre
@@map("characters")
}
model World {
id String @id @default(uuid())
name String
genre String // "fantasy", "scifi", "modern", etc.
@@map("worlds")
}
Zod Schema:
export const characterSchema = z.object({
name: z.string().min(1),
worldId: z.string().uuid(),
// Flexible attributes - validated at runtime based on genre
attributes: z.record(z.any()).optional(),
})
// Genre-specific schemas
export const fantasyAttributesSchema = z.object({
manaPoints: z.number().min(0).max(100),
magicSchool: z.enum(["fire", "water", "earth", "air"]),
spellSlots: z.number().min(0),
})
export const scifiAttributesSchema = z.object({
techLevel: z.number().min(1).max(10),
cybernetics: z.array(z.string()),
faction: z.string(),
})
Dynamic Form Component:
"use client"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
export default function CharacterForm({ worldId }: { worldId: string }) {
const [world, setWorld] = useState<any>(null)
const form = useForm()
useEffect(() => {
// Fetch world to get genre
fetch(`/api/worlds/${worldId}`)
.then(res => res.json())
.then(data => setWorld(data))
}, [worldId])
function renderAttributeFields() {
if (!world) return null
switch (world.genre) {
case "fantasy":
return (
<>
<FormField
control={form.control}
name="attributes.manaPoints"
render={({ field }) => (
<FormItem>
<FormLabel>Mana Points</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="attributes.magicSchool"
render={({ field }) => (
<FormItem>
<FormLabel>Magic School</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="fire">Fire</SelectItem>
<SelectItem value="water">Water</SelectItem>
<SelectItem value="earth">Earth</SelectItem>
<SelectItem value="air">Air</SelectItem>
</SelectContent>
</Select>
</FormItem>
)}
/>
</>
)
case "scifi":
return (
<>
<FormField
control={form.control}
name="attributes.techLevel"
render={({ field }) => (
<FormItem>
<FormLabel>Tech Level (1-10)</FormLabel>
<FormControl>
<Input type="number" min="1" max="10" {...field} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="attributes.faction"
render={({ field }) => (
<FormItem>
<FormLabel>Faction</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
</>
)
default:
return null
}
}
return (
<form>
{/* Standard fields */}
<FormField name="name" ... />
{/* Dynamic genre-specific fields */}
{renderAttributeFields()}
</form>
)
}
Server Action Validation:
export async function createCharacter(values: any) {
// Validate base schema
const validated = characterSchema.parse(values)
// Fetch world to validate attributes
const world = await prisma.world.findUnique({ where: { id: validated.worldId } })
if (!world) throw new Error("World not found")
// Validate genre-specific attributes
if (world.genre === "fantasy" && validated.attributes) {
fantasyAttributesSchema.parse(validated.attributes)
} else if (world.genre === "scifi" && validated.attributes) {
scifiAttributesSchema.parse(validated.attributes)
}
// Create character
const character = await prisma.character.create({ data: validated })
return { success: true, data: character }
}
See template: assets/templates/custom-attributes.tsx
Markdown Editor Integration
For long-form text fields (backstories, descriptions, lore):
Installation:
npm install @uiw/react-md-editor
Component:
"use client"
import dynamic from "next/dynamic"
import { useState } from "react"
import "@uiw/react-md-editor/markdown-editor.css"
import "@uiw/react-markdown-preview/markdown.css"
// Import dynamically to avoid SSR issues
const MDEditor = dynamic(
() => import("@uiw/react-md-editor"),
{ ssr: false }
)
export function MarkdownField({
value,
onChange,
label,
height = 300,
}: {
value?: string
onChange: (value: string) => void
label: string
height?: number
}) {
return (
<div className="space-y-2">
<label className="text-sm font-medium">{label}</label>
<MDEditor
value={value}
onChange={(val) => onChange(val || "")}
height={height}
preview="edit" // "edit" | "live" | "preview"
/>
</div>
)
}
Integration with React Hook Form:
<FormField
control={form.control}
name="backstory"
render={({ field }) => (
<FormItem>
<MarkdownField
value={field.value}
onChange={field.onChange}
label="Backstory"
height={400}
/>
<FormDescription>
Use Markdown formatting for rich text
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
Schema:
export const characterSchema = z.object({
name: z.string().min(1),
backstory: z.string().optional(),
// Markdown is stored as plain text in database
})
Display Markdown:
import ReactMarkdown from "react-markdown"
export function CharacterDetail({ character }) {
return (
<div>
<h1>{character.name}</h1>
<div className="prose dark:prose-invert">
<ReactMarkdown>{character.backstory}</ReactMarkdown>
</div>
</div>
)
}
See template: assets/templates/markdown-editor.tsx
Relationship Management
For managing connections between entities (character-to-character relationships, location hierarchies):
Database Schema (from worldcrafter-database-setup):
model CharacterRelationship {
id String @id @default(uuid())
fromCharacterId String
toCharacterId String
relationshipType String // "friend", "enemy", "family", "ally", etc.
description String?
fromCharacter Character @relation("RelationshipsFrom", fields: [fromCharacterId], references: [id], onDelete: Cascade)
toCharacter Character @relation("RelationshipsTo", fields: [toCharacterId], references: [id], onDelete: Cascade)
@@map("character_relationships")
}
model Character {
id String @id @default(uuid())
name String
relationshipsFrom CharacterRelationship[] @relation("RelationshipsFrom")
relationshipsTo CharacterRelationship[] @relation("RelationshipsTo")
@@map("characters")
}
Relationships Panel Component:
"use client"
import { useState } from "react"
import { Plus, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Select } from "@/components/ui/select"
import { addRelationship, removeRelationship } from "./actions"
export function RelationshipsPanel({
characterId,
relationships
}: {
characterId: string
relationships: any[]
}) {
const [isAddingRelationship, setIsAddingRelationship] = useState(false)
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Relationships</h3>
<Button
size="sm"
onClick={() => setIsAddingRelationship(true)}
>
<Plus className="w-4 h-4 mr-1" />
Add Relationship
</Button>
</div>
{/* List existing relationships */}
<div className="space-y-2">
{relationships.map((rel) => (
<div
key={rel.id}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div>
<p className="font-medium">{rel.toCharacter.name}</p>
<p className="text-sm text-muted-foreground">
{rel.relationshipType}
</p>
{rel.description && (
<p className="text-sm mt-1">{rel.description}</p>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeRelationship(rel.id)}
>
<X className="w-4 h-4" />
</Button>
</div>
))}
{relationships.length === 0 && (
<p className="text-muted-foreground text-sm">
No relationships yet
</p>
)}
</div>
{/* Add relationship modal */}
<AddRelationshipModal
open={isAddingRelationship}
onClose={() => setIsAddingRelationship(false)}
characterId={characterId}
/>
</div>
)
}
Add Relationship Modal:
function AddRelationshipModal({
open,
onClose,
characterId
}: {
open: boolean
onClose: () => void
characterId: string
}) {
const form = useForm({
resolver: zodResolver(relationshipSchema),
defaultValues: {
fromCharacterId: characterId,
toCharacterId: "",
relationshipType: "",
description: "",
}
})
async function onSubmit(values: any) {
const result = await addRelationship(values)
if (result.success) {
onClose()
form.reset()
}
}
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Relationship</DialogTitle>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="toCharacterId"
render={({ field }) => (
<FormItem>
<FormLabel>Character</FormLabel>
<CharacterSelect
value={field.value}
onChange={field.onChange}
excludeId={characterId}
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="relationshipType"
render={({ field }) => (
<FormItem>
<FormLabel>Relationship Type</FormLabel>
<Select onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="friend">Friend</SelectItem>
<SelectItem value="enemy">Enemy</SelectItem>
<SelectItem value="family">Family</SelectItem>
<SelectItem value="ally">Ally</SelectItem>
<SelectItem value="rival">Rival</SelectItem>
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description (Optional)</FormLabel>
<Textarea {...field} />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit">Add Relationship</Button>
</div>
</form>
</DialogContent>
</Dialog>
)
}
Server Actions:
"use server"
export async function addRelationship(values: any) {
const validated = relationshipSchema.parse(values)
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { success: false, error: "Unauthorized" }
// Verify user owns the character
const character = await prisma.character.findUnique({
where: { id: validated.fromCharacterId },
select: { userId: true }
})
if (!character || character.userId !== user.id) {
return { success: false, error: "Forbidden" }
}
const relationship = await prisma.characterRelationship.create({
data: validated
})
revalidatePath(`/characters/${validated.fromCharacterId}`)
return { success: true, data: relationship }
}
export async function removeRelationship(id: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { success: false, error: "Unauthorized" }
// Verify ownership
const relationship = await prisma.characterRelationship.findUnique({
where: { id },
include: { fromCharacter: { select: { userId: true } } }
})
if (!relationship || relationship.fromCharacter.userId !== user.id) {
return { success: false, error: "Forbidden" }
}
await prisma.characterRelationship.delete({ where: { id } })
revalidatePath(`/characters/${relationship.fromCharacterId}`)
return { success: true }
}
See template: assets/templates/relationships-panel.tsx
Reference Files
references/feature-patterns.md- Detailed architectural patternsreferences/testing-guide.md- Testing conventions and examplesreferences/advanced-features-guide.md- NEW v2.0: Multi-step wizards, image uploads, custom attributes, markdown, relationshipsreferences/related-skills.md- How this skill works with other WorldCrafter skillsassets/templates/- Complete template files for quick scaffoldingmulti-step-wizard.tsx- NEW v2.0: Character creation wizard exampleimage-upload.tsx- NEW v2.0: Supabase Storage image upload componentcustom-attributes.tsx- NEW v2.0: Dynamic genre-specific attributes formmarkdown-editor.tsx- NEW v2.0: Rich text editing with @uiw/react-md-editorrelationships-panel.tsx- NEW v2.0: Entity relationship management UI
Skill Orchestration
This skill works seamlessly with other WorldCrafter skills to provide complete feature implementation.
Common Workflows
Complete Feature with Authentication:
- worldcrafter-database-setup - Create database tables and RLS policies first
- worldcrafter-feature-builder (this skill) - Build feature UI, forms, and Server Actions
- worldcrafter-auth-guard - Add authentication checks to routes and actions
- worldcrafter-test-generator - Add additional test coverage if needed (basic tests included)
Quick Feature (No Database):
- worldcrafter-feature-builder (this skill) - Creates feature with form and validation
- worldcrafter-auth-guard - Protect feature if needed
Database-First Approach:
- worldcrafter-database-setup - Design and create database schema
- worldcrafter-feature-builder (this skill) - Build UI and forms for the data model
When Claude Should Use Multiple Skills
Claude will naturally orchestrate multiple skills when:
- User request spans multiple capabilities (e.g., "add blog with auth and database")
- One skill creates prerequisites for another (database before UI)
- Task requires complementary expertise (feature + security)
Example orchestration:
User: "Add a comments feature for blog posts with user authentication"
Claude's workflow:
1. worldcrafter-database-setup: Create Comment model with RLS policies
2. worldcrafter-feature-builder: Create comment form, Server Actions, UI
3. worldcrafter-auth-guard: Add auth checks to ensure only logged-in users can comment
4. Tests are automatically included by feature-builder
Skill Selection Guidance
Choose this skill when:
- Building a complete feature from scratch
- Need forms, validation, Server Actions, and tests together
- User wants end-to-end implementation
Choose worldcrafter-route-creator when:
- User only needs a simple page without forms
- Static content or read-only pages
- No validation or Server Actions needed
Choose worldcrafter-database-setup when:
- User only wants to modify database schema
- Adding tables without immediate UI
- Setting up data models for later
Success Criteria
A complete feature implementation includes:
- ✅ Database schema with RLS policies (if applicable)
- ✅ Zod validation schema
- ✅ Server Actions with proper error handling
- ✅ Client components with forms and UI
- ✅ Loading and error states
- ✅ Integration tests with test database
- ✅ E2E tests for critical user flows
- ✅ Type checking passes (
npm run build) - ✅ All tests pass (
npm run test:all)