All components

Prompt Library

Prompt Library is a React and Tailwind CSS component from Cult UI, licensed MIT. Copy it and paste it into your project.

Cult UI MIT other

What it is

Prompt Library is a React and Tailwind CSS component from Cult UI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i @hugeicons/core-free-icons @hugeicons/react

Licence

This component comes from Cult UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Prompt Library

apps/www/registry/default/ui/prompt-library.tsx
"use client"

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
  type ComponentProps,
  type PropsWithChildren,
  type ReactNode,
} from "react"
import { LibraryIcon, PlusSignIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  type DialogDescriptionProps,
  type DialogTitleProps,
} from "@/components/ui/dialog"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"
import { Input } from "@/components/ui/input"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Textarea } from "@/components/ui/textarea"
import { useOptionalPromptInputController } from "@/components/ai-elements/prompt-input"

// ============================================================================
// Types
// ============================================================================

export interface Prompt {
  /** Unique identifier for the prompt */
  id: string
  /** Display title for the prompt */
  title: string
  /** Short description shown in the list */
  description: string
  /** The actual prompt content to insert/copy */
  prompt: string
  /** Optional category for grouping */
  category?: string
  /** Whether this is a user-created prompt */
  isCustom?: boolean
}

export interface PromptLibraryContextValue {
  /** All available prompts */
  prompts: Prompt[]
  /** Add a custom prompt */
  addCustom: (prompt: Omit<Prompt, "id" | "isCustom">) => void
  /** Remove a custom prompt */
  removeCustom: (id: string) => void
  /** Select a prompt (insert or copy) */
  selectPrompt: (prompt: Prompt) => void
  /** Last selected prompt ID */
  lastSelectedId: string | null
  /** Open state for the popover */
  open: boolean
  /** Set open state for the popover */
  setOpen: (open: boolean) => void
  /** Open state for the create dialog */
  createDialogOpen: boolean
  /** Set open state for the create dialog */
  setCreateDialogOpen: (open: boolean) => void
}

// ============================================================================
// Context
// ============================================================================

const PromptLibraryContext = createContext<PromptLibraryContextValue | null>(
  null
)

/**
 * Hook to access the PromptLibrary context
 * @throws Error if used outside of PromptLibrary provider
 */
export const usePromptLibrary = () => {
  const ctx = useContext(PromptLibraryContext)
  if (!ctx) {
    throw new Error(
      "usePromptLibrary must be used within a PromptLibrary provider"
    )
  }
  return ctx
}

// ============================================================================
// Controllable State Hook
// ============================================================================

function useControllableState<T>(
  controlledValue: T | undefined,
  defaultValue: T,
  onChange?: (value: T) => void
): [T, (value: T | ((prev: T) => T)) => void] {
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue)
  const isControlled = controlledValue !== undefined
  const value = isControlled ? controlledValue : uncontrolledValue

  const setValue = useCallback(
    (nextValue: T | ((prev: T) => T)) => {
      const resolvedValue =
        typeof nextValue === "function"
          ? (nextValue as (prev: T) => T)(value)
          : nextValue

      if (!isControlled) {
        setUncontrolledValue(resolvedValue)
      }
      onChange?.(resolvedValue)
    },
    [isControlled, onChange, value]
  )

  return [value, setValue]
}

// ============================================================================
// Root Component
// ============================================================================

export type PromptLibraryProps = PropsWithChildren<{
  /** Initial set of prompts */
  prompts?: Prompt[]
  /** Callback when prompts change (for custom prompts) */
  onPromptsChange?: (prompts: Prompt[]) => void
  /** Callback when a prompt is selected */
  onSelect?: (prompt: Prompt) => void
  /** Controlled open state for popover */
  open?: boolean
  /** Callback when open state changes */
  onOpenChange?: (open: boolean) => void
}>

/**
 * Root component for the PromptLibrary widget.
 * Provides context for all child components.
 * Integrates with PromptInput when used inside PromptInputProvider.
 */
export function PromptLibrary({
  prompts: controlledPrompts,
  onPromptsChange,
  onSelect,
  open: controlledOpen,
  onOpenChange,
  children,
}: PromptLibraryProps) {
  const [internalPrompts, setInternalPrompts] = useState<Prompt[]>(
    controlledPrompts ?? []
  )

  const prompts = controlledPrompts ?? internalPrompts

  const [open, setOpen] = useControllableState(
    controlledOpen,
    false,
    onOpenChange
  )
  const [createDialogOpen, setCreateDialogOpen] = useState(false)
  const [lastSelectedId, setLastSelectedId] = useState<string | null>(null)

  // Try to get PromptInput controller for integration
  const promptInputController = useOptionalPromptInputController()

  const selectPrompt = useCallback(
    async (prompt: Prompt) => {
      setLastSelectedId(prompt.id)

      if (promptInputController) {
        // Insert directly into PromptInput textarea
        promptInputController.textInput.setInput(prompt.prompt)
      } else {
        // Fallback: copy to clipboard
        try {
          await navigator.clipboard.writeText(prompt.prompt)
        } catch (err) {
          console.error("Failed to copy to clipboard:", err)
        }
      }

      // Close popover after selection
      setOpen(false)

      // Call onSelect callback
      onSelect?.(prompt)
    },
    [promptInputController, setOpen, onSelect]
  )

  const addCustom = useCallback(
    (prompt: Omit<Prompt, "id" | "isCustom">) => {
      const newPrompt: Prompt = {
        ...prompt,
        id: `custom-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
        isCustom: true,
      }

      if (controlledPrompts) {
        onPromptsChange?.([...controlledPrompts, newPrompt])
      } else {
        setInternalPrompts((prev) => [...prev, newPrompt])
      }
    },
    [controlledPrompts, onPromptsChange]
  )

  const removeCustom = useCallback(
    (id: string) => {
      if (controlledPrompts) {
        onPromptsChange?.(controlledPrompts.filter((p) => p.id !== id))
      } else {
        setInternalPrompts((prev) => prev.filter((p) => p.id !== id))
      }
    },
    [controlledPrompts, onPromptsChange]
  )

  const contextValue = useMemo<PromptLibraryContextValue>(
    () => ({
      prompts,
      addCustom,
      removeCustom,
      selectPrompt,
      lastSelectedId,
      open,
      setOpen,
      createDialogOpen,
      setCreateDialogOpen,
    }),
    [
      prompts,
      addCustom,
      removeCustom,
      selectPrompt,
      lastSelectedId,
      open,
      setOpen,
      createDialogOpen,
    ]
  )

  return (
    <PromptLibraryContext.Provider value={contextValue}>
      <Popover onOpenChange={setOpen} open={open}>
        {children}
      </Popover>
    </PromptLibraryContext.Provider>
  )
}

// ============================================================================
// Trigger Component
// ============================================================================

export type PromptLibraryTriggerProps = ComponentProps<typeof Button> & {
  /** Custom label for the trigger button */
  label?: ReactNode
}

/**
 * Button that opens the prompt library popover.
 */
export function PromptLibraryTrigger({
  className,
  label,
  children,
  ...props
}: PromptLibraryTriggerProps) {
  return (
    <PopoverTrigger asChild>
      <Button
        className={cn("gap-1.5", className)}
        size="sm"
        type="button"
        variant="ghost"
        {...props}
      >
        {children ?? (
          <>
            <HugeiconsIcon
              className="size-3.5"
              icon={LibraryIcon}
              strokeWidth={2}
            />
            {label ?? "Prompts"}
          </>
        )}
      </Button>
    </PopoverTrigger>
  )
}

// ============================================================================
// Content Component
// ============================================================================

export type PromptLibraryContentProps = ComponentProps<typeof PopoverContent>

/**
 * Popover content container for the prompt library list.
 */
export function PromptLibraryContent({
  className,
  children,
  ...props
}: PromptLibraryContentProps) {
  return (
    <PopoverContent
      align="start"
      className={cn("w-80 p-0", className)}
      side="top"
      sideOffset={8}
      {...props}
    >
      <Command className="rounded-lg" data-slot="prompt-library-content">
        {children}
      </Command>
    </PopoverContent>
  )
}

// ============================================================================
// Search Component
// ============================================================================

export type PromptLibrarySearchProps = ComponentProps<typeof CommandInput>

/**
 * Search input for filtering prompts.
 */
export function PromptLibrarySearch({
  placeholder = "Search prompts...",
  className,
  ...props
}: PromptLibrarySearchProps) {
  return (
    <CommandInput
      className={cn(className)}
      data-slot="prompt-library-search"
      placeholder={placeholder}
      {...props}
    />
  )
}

// ============================================================================
// List Component
// ============================================================================

export type PromptLibraryListProps = ComponentProps<typeof CommandList>

/**
 * Scrollable list container for prompts.
 */
export function PromptLibraryList({
  className,
  children,
  ...props
}: PromptLibraryListProps) {
  return (
    <CommandList
      className={cn("max-h-64", className)}
      data-slot="prompt-library-list"
      {...props}
    >
      {children}
    </CommandList>
  )
}

// ============================================================================
// Empty Component
// ============================================================================

export type PromptLibraryEmptyProps = ComponentProps<typeof CommandEmpty>

/**
 * Empty state shown when no prompts match the search.
 */
export function PromptLibraryEmpty({
  children = "No prompts found.",
  className,
  ...props
}: PromptLibraryEmptyProps) {
  return (
    <CommandEmpty
      className={cn(className)}
      data-slot="prompt-library-empty"
      {...props}
    >
      {children}
    </CommandEmpty>
  )
}

// ============================================================================
// Group Component
// ============================================================================

export type PromptLibraryGroupProps = ComponentProps<typeof CommandGroup>

/**
 * Group container for categorizing prompts.
 */
export function PromptLibraryGroup({
  className,
  ...props
}: PromptLibraryGroupProps) {
  return (
    <CommandGroup
      className={cn(
        // Remove default rounding from all items
        "**:[[cmdk-item]]:rounded-none",
        // First child in group gets rounded top
        "[&_[cmdk-group-items]>:first-child_[cmdk-item]]:rounded-t-md",
        // Last child in group gets rounded bottom
        "[&_[cmdk-group-items]>:last-child_[cmdk-item]]:rounded-b-md",
        className
      )}
      data-slot="prompt-library-group"
      {...props}
    />
  )
}

// ============================================================================
// Separator Component
// ============================================================================

export type PromptLibrarySeparatorProps = ComponentProps<
  typeof CommandSeparator
>

/**
 * Visual separator between prompt groups.
 */
export function PromptLibrarySeparator({
  className,
  ...props
}: PromptLibrarySeparatorProps) {
  return (
    <CommandSeparator
      className={cn(className)}
      data-slot="prompt-library-separator"
      {...props}
    />
  )
}

// ============================================================================
// Item Component
// ============================================================================

export type PromptLibraryItemProps = Omit<
  ComponentProps<typeof CommandItem>,
  "value" | "onSelect"
> & {
  /** The prompt data */
  prompt: Prompt
  /** Disable hover card preview */
  disablePreview?: boolean
}

/**
 * Individual prompt item with click-to-select and optional hover preview.
 */
export function PromptLibraryItem({
  prompt,
  disablePreview = false,
  className,
  children,
  ...props
}: PromptLibraryItemProps) {
  const { selectPrompt, lastSelectedId } = usePromptLibrary()
  const isLastSelected = lastSelectedId === prompt.id

  const handleSelect = () => {
    selectPrompt(prompt)
  }

  const itemContent = (
    <CommandItem
      className={cn(
        "group/prompt-item flex cursor-pointer items-start gap-3 py-2",
        isLastSelected && "bg-accent/50",
        className
      )}
      data-slot="prompt-library-item"
      data-state={isLastSelected ? "selected" : "idle"}
      onSelect={handleSelect}
      value={prompt.title}
      {...props}
    >
      <div className="flex min-w-0 flex-1 flex-col gap-0.5">
        {children ?? (
          <>
            <PromptLibraryItemTitle>{prompt.title}</PromptLibraryItemTitle>
            <PromptLibraryItemDescription>
              {prompt.description}
            </PromptLibraryItemDescription>
          </>
        )}
      </div>
    </CommandItem>
  )

  if (disablePreview || !prompt.prompt) {
    return itemContent
  }

  return (
    <PromptLibraryHoverCard prompt={prompt}>
      {itemContent}
    </PromptLibraryHoverCard>
  )
}

// ============================================================================
// Item Title Component
// ============================================================================

export type PromptLibraryItemTitleProps = ComponentProps<"span">

/**
 * Title text for a prompt item.
 */
export function PromptLibraryItemTitle({
  className,
  ...props
}: PromptLibraryItemTitleProps) {
  return (
    <span
      className={cn("font-medium text-foreground text-xs", className)}
      data-slot="prompt-library-item-title"
      {...props}
    />
  )
}

// ============================================================================
// Item Description Component
// ============================================================================

export type PromptLibraryItemDescriptionProps = ComponentProps<"span">

/**
 * Description text for a prompt item.
 */
export function PromptLibraryItemDescription({
  className,
  ...props
}: PromptLibraryItemDescriptionProps) {
  return (
    <span
      className={cn("line-clamp-2 text-muted-foreground text-xs", className)}
      data-slot="prompt-library-item-description"
      {...props}
    />
  )
}

// ============================================================================
// Hover Card Component
// ============================================================================

export interface PromptLibraryHoverCardProps {
  /** The prompt to preview */
  prompt: Prompt
  /** The trigger element (prompt item) */
  children: ReactNode
}

/**
 * Hover card that shows the full prompt content on hover.
 */
export function PromptLibraryHoverCard({
  prompt,
  children,
}: PromptLibraryHoverCardProps) {
  return (
    <HoverCard openDelay={300} closeDelay={100}>
      <HoverCardTrigger>{children}</HoverCardTrigger>
      <HoverCardContent
        align="start"
        className="w-80"
        data-slot="prompt-library-hover-card"
        side="right"
        sideOffset={8}
      >
        <div className="flex flex-col gap-2">
          <div className="flex flex-col gap-1">
            <span className="font-medium text-sm">{prompt.title}</span>
            <span className="text-muted-foreground text-xs">
              {prompt.description}
            </span>
          </div>
          {prompt.prompt && (
            <div className="rounded-md bg-muted/50 p-2">
              <p className="line-clamp-6 whitespace-pre-wrap text-muted-foreground text-xs">
                {prompt.prompt}
              </p>
            </div>
          )}
          {prompt.category && (
            <span className="text-[0.625rem] text-muted-foreground">
              Category: {prompt.category}
            </span>
          )}
          {prompt.isCustom && (
            <span className="text-[0.625rem] text-muted-foreground">
              Custom prompt
            </span>
          )}
        </div>
      </HoverCardContent>
    </HoverCard>
  )
}

// ============================================================================
// Footer Component
// ============================================================================

export type PromptLibraryFooterProps = ComponentProps<"div">

/**
 * Footer container that stays fixed at the bottom of the popover.
 * Use this to place the create trigger outside the scrollable list.
 */
export function PromptLibraryFooter({
  className,
  children,
  ...props
}: PromptLibraryFooterProps) {
  return (
    <div
      className={cn("border-t p-1", className)}
      data-slot="prompt-library-footer"
      {...props}
    >
      {children}
    </div>
  )
}

// ============================================================================
// Create Trigger Component
// ============================================================================

export type PromptLibraryCreateTriggerProps = Omit<
  ComponentProps<typeof CommandItem>,
  "onSelect"
> & {
  /** Custom label for the create button */
  label?: ReactNode
}

/**
 * Button that opens the create prompt dialog.
 */
export function PromptLibraryCreateTrigger({
  label = "New Prompt",
  className,
  children,
  ...props
}: PromptLibraryCreateTriggerProps) {
  const { setCreateDialogOpen, setOpen } = usePromptLibrary()

  const handleSelect = () => {
    setOpen(false)
    setCreateDialogOpen(true)
  }

  return (
    <CommandItem
      className={cn(
        "flex cursor-pointer items-center gap-2 text-muted-foreground",
        className
      )}
      data-slot="prompt-library-create-trigger"
      onSelect={handleSelect}
      value="__new_prompt__"
      {...props}
    >
      {children ?? (
        <>
          <HugeiconsIcon
            className="size-3.5"
            icon={PlusSignIcon}
            strokeWidth={2}
          />
          {label}
        </>
      )}
    </CommandItem>
  )
}

// ============================================================================
// Create Dialog Component
// ============================================================================

export type PromptLibraryCreateDialogProps = Omit<
  ComponentProps<typeof Dialog>,
  "open" | "onOpenChange" | "children"
> & {
  /** Dialog title */
  title?: DialogTitleProps["children"]
  /** Dialog description */
  description?: DialogDescriptionProps["children"]
  /** Custom form content to replace the default form */
  children?: ReactNode
}

/**
 * Modal dialog for creating custom prompts.
 */
export function PromptLibraryCreateDialog({
  title = "Create Prompt",
  description = "Create a custom prompt template for quick access.",
  children,
  ...props
}: PromptLibraryCreateDialogProps) {
  const { createDialogOpen, setCreateDialogOpen, addCustom } =
    usePromptLibrary()
  const [formTitle, setFormTitle] = useState("")
  const [formDescription, setFormDescription] = useState("")
  const [formPrompt, setFormPrompt] = useState("")
  const [formCategory, setFormCategory] = useState("")

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()

    if (!(formTitle.trim() && formDescription.trim() && formPrompt.trim())) {
      return
    }

    addCustom({
      title: formTitle.trim(),
      description: formDescription.trim(),
      prompt: formPrompt.trim(),
      category: formCategory.trim() || undefined,
    })

    // Reset form
    setFormTitle("")
    setFormDescription("")
    setFormPrompt("")
    setFormCategory("")
    setCreateDialogOpen(false)
  }

  const handleOpenChange = (open: boolean) => {
    setCreateDialogOpen(open)
    if (!open) {
      // Reset form when closing
      setFormTitle("")
      setFormDescription("")
      setFormPrompt("")
      setFormCategory("")
    }
  }

  return (
    <Dialog
      data-slot="prompt-library-create-dialog"
      onOpenChange={handleOpenChange}
      open={createDialogOpen}
      {...props}
    >
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          <DialogDescription>{description}</DialogDescription>
        </DialogHeader>
        {children ?? (
          <form className="flex flex-col gap-4" onSubmit={handleSubmit}>
            <div className="flex flex-col gap-2">
              <label className="font-medium text-xs" htmlFor="prompt-title">
                Title
              </label>
              <Input
                id="prompt-title"
                onChange={(e) => setFormTitle(e.target.value)}
                placeholder="e.g., Code Review"
                required
                value={formTitle}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label
                className="font-medium text-xs"
                htmlFor="prompt-description"
              >
                Description
              </label>
              <Input
                id="prompt-description"
                onChange={(e) => setFormDescription(e.target.value)}
                placeholder="e.g., Review code for best practices"
                required
                value={formDescription}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label className="font-medium text-xs" htmlFor="prompt-content">
                Prompt
              </label>
              <Textarea
                className="min-h-24 resize-none"
                id="prompt-content"
                onChange={(e) => setFormPrompt(e.target.value)}
                placeholder="Enter the prompt text..."
                required
                value={formPrompt}
              />
            </div>
            <div className="flex flex-col gap-2">
              <label className="font-medium text-xs" htmlFor="prompt-category">
                Category (optional)
              </label>
              <Input
                id="prompt-category"
                onChange={(e) => setFormCategory(e.target.value)}
                placeholder="e.g., Development"
                value={formCategory}
              />
            </div>
            <DialogFooter>
              <DialogClose asChild>
                <Button type="button" variant="outline">
                  Cancel
                </Button>
              </DialogClose>
              <Button type="submit">Create</Button>
            </DialogFooter>
          </form>
        )}
      </DialogContent>
    </Dialog>
  )
}

// ============================================================================
// Compound Export
// ============================================================================

export const PromptLibraryWidget = Object.assign(PromptLibrary, {
  Trigger: PromptLibraryTrigger,
  Content: PromptLibraryContent,
  Search: PromptLibrarySearch,
  List: PromptLibraryList,
  Empty: PromptLibraryEmpty,
  Group: PromptLibraryGroup,
  Separator: PromptLibrarySeparator,
  Item: PromptLibraryItem,
  ItemTitle: PromptLibraryItemTitle,
  ItemDescription: PromptLibraryItemDescription,
  HoverCard: PromptLibraryHoverCard,
  Footer: PromptLibraryFooter,
  CreateTrigger: PromptLibraryCreateTrigger,
  CreateDialog: PromptLibraryCreateDialog,
})

Similar components