Cursor AI Integration
Enable AI assistants to use PrettyUI effectively with our comprehensive Cursor rule. This guide covers the complete component library, theming system, and best practices.
Setup
Add the PrettyUI rule to your Cursor IDE for AI-assisted development:
1. Create the Rules Directory
mkdir -p .cursor/rules
2. Add the PrettyUI Rule
Create .cursor/rules/PrettyUI.mdc in your project with the content below, or copy it from the PrettyUI repository.
Complete Cursor Rule
Copy the complete rule below into your project to enable AI-assisted PrettyUI development.
---
description: Comprehensive guide for using the PrettyUI SwiftUI component library effectively
globs: "**/*.swift"
alwaysApply: false
---
# PrettyUI - Complete Component & Theming Guide
PrettyUI is a comprehensive SwiftUI component library inspired by Family.co's design language. It provides 20+ customizable components with a powerful theming system supporting design tokens for colors, spacing, radius, shadows, and typography.
---
## Quick Start
### Installation
Add to `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/mecurylabs/PrettyUI.git", from: "1.0.0")
]
```
### Basic Usage
```swift
import PrettyUI
struct ContentView: View {
var body: some View {
VStack(spacing: 16) {
PButton("Get Started", variant: .primary) {
print("Tapped!")
}
PCard {
PText("Welcome to PrettyUI", style: .headline)
}
}
.prettyTheme(.sky) // Apply theme at root
}
}
```
---
## Theme System
### Applying Themes
Apply themes at your app's root or any view hierarchy:
```swift
// Use built-in presets
.prettyTheme(.sky) // Cyan-blue theme
.prettyTheme(.indigo) // Deep purple theme
.prettyTheme(.emerald) // Green theme
.prettyTheme(.amber) // Orange-yellow theme
.prettyTheme(.default) // Neutral gray theme
// Or create custom themes
.prettyTheme(PrettyTheme(
lightColors: ColorTokens(
primary: Color(hex: "#FF6B6B"),
primaryForeground: .white,
// ... other colors
),
darkColors: ColorTokens(/* dark mode colors */),
spacing: SpacingTokens.default,
radius: RadiusTokens.default,
typography: TypographyTokens.default,
shadows: ShadowTokens.default
))
```
### Accessing Theme Values
```swift
struct MyView: View {
@Environment(\.prettyTheme) var theme
@Environment(\.colorScheme) var colorScheme
var body: some View {
let colors = theme.colors(for: colorScheme)
VStack(spacing: theme.spacing.md) {
Text("Hello")
.foregroundColor(colors.primary)
.padding(theme.spacing.lg)
}
.background(colors.background)
.cornerRadius(theme.radius.xl)
}
}
// Using ThemeReader for cleaner access
ThemeReader { theme, colors in
Text("Primary color")
.foregroundColor(colors.primary)
.padding(theme.spacing.md)
}
```
---
## Design Tokens
### Color Tokens
```swift
// Access colors via theme.colors(for: colorScheme)
colors.primary // Main brand color (#1DA1F2 in Sky)
colors.primaryForeground // Text on primary color
colors.secondary // Secondary actions
colors.background // Page background
colors.foreground // Primary text color
colors.card // Card surfaces
colors.cardForeground // Text on cards
colors.muted // Muted backgrounds
colors.mutedForeground // Secondary text
colors.border // Border color
colors.input // Input border
colors.ring // Focus ring
colors.destructive // Error/danger color
colors.success // Success color
colors.warning // Warning color
```
### Spacing Tokens (8pt Grid)
```swift
theme.spacing.xxs // 2pt
theme.spacing.xs // 4pt
theme.spacing.sm // 8pt
theme.spacing.md // 16pt (standard padding)
theme.spacing.lg // 24pt
theme.spacing.xl // 32pt
theme.spacing.xxl // 48pt
theme.spacing.xxxl // 64pt
// Usage
.padding(theme.spacing.md)
VStack(spacing: theme.spacing.sm) { ... }
```
### Radius Tokens
```swift
theme.radius.none // 0pt
theme.radius.sm // 6pt - chips, tags
theme.radius.md // 10pt - inputs
theme.radius.lg // 14pt - standard cards
theme.radius.xl // 20pt - large cards
theme.radius.xxl // 28pt - modals, sheets
theme.radius.full // 9999pt - pills, avatars
// Or use enum
theme.radius[.sm]
theme.radius[.lg]
```
### Shadow Tokens
```swift
theme.shadows.none
theme.shadows.sm // Subtle: 4px blur, 2px y-offset
theme.shadows.md // Medium: 12px blur, 4px y-offset
theme.shadows.lg // Large: 20px blur, 8px y-offset
theme.shadows.xl // Extra large: 32px blur
theme.shadows.xxl // Maximum: 48px blur
// Apply shadow
.prettyShadow(theme.shadows.md)
// Or directly
.prettyShadow(theme.shadows[.lg])
```
### Typography Tokens
```swift
theme.typography.fontFamily // "SF Pro Display"
theme.typography.monoFontFamily // "SF Mono"
theme.typography.sizes.xxs // 10pt
theme.typography.sizes.xs // 12pt
theme.typography.sizes.sm // 14pt
theme.typography.sizes.base // 16pt
theme.typography.sizes.lg // 18pt
theme.typography.sizes.xl // 20pt
theme.typography.sizes.xxl // 24pt
theme.typography.sizes.xxxl // 30pt
theme.typography.sizes.display // 36pt
```
---
## Components Reference
### PButton
Customizable button with variants, sizes, loading states, and icons.
```swift
// Basic usage
PButton("Get Started") { action() }
// Variants
PButton("Primary", variant: .primary) { }
PButton("Secondary", variant: .secondary) { }
PButton("Outline", variant: .outline) { }
PButton("Ghost", variant: .ghost) { }
PButton("Destructive", variant: .destructive) { }
PButton("Link", variant: .link) { }
// Sizes
PButton("Small", size: .sm) { }
PButton("Medium", size: .md) { } // default
PButton("Large", size: .lg) { }
// Full width
PButton("Full Width", fullWidth: true) { }
// Custom radius
PButton("Pill Button", radius: .full) { }
PButton("Rounded", radius: .lg) { }
// With icons
PButton("Continue", icon: "arrow.right", iconPosition: .trailing) { }
PButton("Add Item", icon: "plus", iconPosition: .leading) { }
// Loading state
@State var isLoading = false
PButton("Submit", isLoading: isLoading, loadingPosition: .replace) { }
// Disabled
PButton("Disabled", isDisabled: true) { }
// Haptic feedback
PButton("Tap Me")
.hapticFeedback(true)
.hapticStyle(.medium)
.action { }
// Fluent API
PButton("Custom")
.variant(.primary)
.size(.lg)
.radius(.full)
.fullWidth(true)
.icon("star.fill", position: .leading)
.loading(isLoading, position: .trailing)
.disabled(false)
.haptics(enabled: true, style: .light)
.action { print("Tapped") }
```
**Variants:** `.primary`, `.secondary`, `.outline`, `.ghost`, `.destructive`, `.link`
**Sizes:** `.sm` (36pt), `.md` (44pt), `.lg` (52pt)
**Icon Positions:** `.leading`, `.trailing`
**Loading Positions:** `.leading`, `.trailing`, `.replace`
---
### PCard
Content container with variants and interactive states.
```swift
// Basic card
PCard {
Text("Card content")
}
// Variants
PCard(variant: .elevated) { content } // With shadow
PCard(variant: .outlined) { content } // With border
PCard(variant: .filled) { content } // Muted background
PCard(variant: .ghost) { content } // Transparent
// Custom styling
PCard(radius: .xl, shadow: .md, padding: .lg) {
VStack { ... }
}
// Interactive (pressable)
PCard(pressable: true) {
Text("Tap me")
}
.onTap { print("Card tapped") }
// Selectable
@State var isSelected = false
PCard(selected: isSelected) {
Text("Selectable card")
}
.onTap { isSelected.toggle() }
// With header and footer
VStack(spacing: 0) {
PCardHeader {
PCardTitle("Settings", subtitle: "Manage your preferences")
}
// Card content
VStack { ... }
PCardFooter {
PButton("Save") { }
}
}
```
**Variants:** `.elevated`, `.outlined`, `.filled`, `.ghost`
---
### PTextField
Text input with floating labels, validation, and icons.
```swift
// Basic usage
@State var text = ""
PTextField("Email", text: $text)
// With placeholder
PTextField("Username", text: $text, placeholder: "Enter username")
// Variants
PTextField("Default", text: $text, variant: .default)
PTextField("Filled", text: $text, variant: .filled)
PTextField("Outlined", text: $text, variant: .outlined)
PTextField("Underlined", text: $text, variant: .underlined)
// Secure input (password)
PTextField("Password", text: $password, isSecure: true)
// With icons
PTextField("Search", text: $search, leadingIcon: "magnifyingglass")
PTextField("Email", text: $email, trailingIcon: "envelope")
// Validation states
PTextField("Email", text: $email)
.error("Invalid email address")
PTextField("Username", text: $username)
.success("Username available!")
PTextField("Bio", text: $bio)
.helper("Max 150 characters")
// Character limit (display counter only)
PTextField("Tweet", text: $tweet)
.maxCharacters(280)
// Enforce max length (restricts input)
PTextField("PIN", text: $pin)
.maxLength(4) // User cannot type more than 4 characters
// Character limit with enforcement
PTextField("Username", text: $username)
.maxCharacters(20, enforced: true) // Shows counter AND restricts input
// Disabled & required
PTextField("Locked", text: $locked)
.disabled(true)
.required(true)
// Keyboard types
PTextField("Phone", text: $phone)
.keyboardType(.phonePad)
// Text alignment
PTextField("Amount", text: $amount)
.textAlignment(.center) // Center aligned text
PTextField("Price", text: $price)
.textAlignment(.trailing) // Right aligned text
// Convenience components
PLabeledTextField(label: "Full Name", text: $name)
PSearchField(text: $searchQuery, placeholder: "Search...")
```
**Variants:** `.default`, `.filled`, `.outlined`, `.underlined`
**Sizes:** `.sm`, `.md`, `.lg`
**Text Alignment:** `.leading` (default), `.center`, `.trailing`
---
### PText
Typography component with preset styles.
```swift
// Preset styles
PText("Display", style: .display)
PText("Headline", style: .headline)
PText("Title", style: .title)
PText("Body Large", style: .bodyLarge)
PText("Body", style: .body)
PText("Caption", style: .caption)
PText("Small", style: .small)
// Colors
PText("Primary", color: .primary)
PText("Muted", color: .muted)
PText("Success", color: .success)
PText("Warning", color: .warning)
PText("Destructive", color: .destructive)
PText("Custom", color: .custom(Color.purple))
// Weight & design
PText("Bold Text")
.weight(.bold)
.design(.rounded)
// Decorations
PText("Underlined")
.underline(style: .single, color: .primary)
PText("Strikethrough")
.strikethrough(style: .single, color: .destructive)
// Custom font
PText("Custom")
.customFont("Helvetica Neue", size: 18)
```
**Styles:** `.display`, `.headline`, `.title`, `.bodyLarge`, `.body`, `.caption`, `.small`
**Colors:** `.primary`, `.foreground`, `.muted`, `.success`, `.warning`, `.destructive`, `.custom(Color)`
---
### PAvatar
User avatar with images, initials, and status indicators.
```swift
// From URL
PAvatar(url: URL(string: "https://example.com/avatar.jpg"))
// From local image
PAvatar(image: Image("profile"))
// From name (generates initials)
PAvatar(name: "John Doe") // Shows "JD"
// Sizes
PAvatar(name: "Jane").size(.xs) // 24pt
PAvatar(name: "Jane").size(.sm) // 32pt
PAvatar(name: "Jane").size(.md) // 40pt (default)
PAvatar(name: "Jane").size(.lg) // 56pt
PAvatar(name: "Jane").size(.xl) // 80pt
PAvatar(name: "Jane").size(.xxl) // 120pt
// Shapes
PAvatar(name: "User").shape(.circle) // default
PAvatar(name: "User").shape(.rounded)
PAvatar(name: "User").shape(.square)
// Status indicator
PAvatar(name: "Online User")
.status(.online) // Green dot
.status(.offline) // Gray dot
.status(.busy) // Red dot
.status(.away) // Yellow dot
// Badge
PAvatar(name: "User")
.badge(5) // Shows "5"
.badge(100, maxCount: 99) // Shows "99+"
.badgePosition(.topTrailing) // Position
// Icon overlay
PAvatar(name: "Admin")
.iconBadge(.checkmark)
.iconBadge(.plus)
.iconBadge(.edit)
.iconBadge(.custom("star.fill", Color.yellow))
// Avatar group
PAvatarGroup(overlap: 12) {
PAvatar(name: "Alice")
PAvatar(name: "Bob")
PAvatar(name: "Charlie")
}
.moreIndicator("+3")
```
**Sizes:** `.xs`, `.sm`, `.md`, `.lg`, `.xl`, `.xxl`
**Shapes:** `.circle`, `.rounded`, `.square`
**Status:** `.online`, `.offline`, `.busy`, `.away`
---
### PAlert
Alert/notification banners with variants and actions.
```swift
// Basic usage
PAlert(variant: .info, title: "Information")
// Variants
PAlert(variant: .info, title: "Info message")
PAlert(variant: .success, title: "Success!", description: "Your changes were saved.")
PAlert(variant: .warning, title: "Warning", description: "Proceed with caution.")
PAlert(variant: .destructive, title: "Error", description: "Something went wrong.")
// Styles
PAlert(variant: .info, style: .filled, title: "Filled style")
PAlert(variant: .success, style: .outlined, title: "Outlined style")
PAlert(variant: .warning, style: .soft, title: "Soft style")
// Dismissible
@State var showAlert = true
PAlert(variant: .info, title: "Dismissible")
.dismissible(true, action: { showAlert = false })
// With action button
PAlert(variant: .warning, title: "Session expiring")
.action("Extend", action: { extendSession() })
// Banner presentation
.alertBanner(
isPresented: $showAlert,
variant: .success,
title: "Saved!",
position: .top
)
```
**Variants:** `.info`, `.success`, `.warning`, `.destructive`
**Styles:** `.filled`, `.outlined`, `.soft`
---
### PModal
Modal dialog with fluid animations.
```swift
// Basic modal
@State var showModal = false
Button("Open Modal") { showModal = true }
.pModal(isPresented: $showModal) {
PModalContent {
Text("Modal content here")
}
}
// With configuration
.pModal(
isPresented: $showModal,
position: .center, // .center, .bottom, .top
overlayStyle: .dimmed, // .dimmed, .blur, .none
dismissOnBackgroundTap: true,
enableSwipeToDismiss: true
) {
VStack(spacing: 16) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 48))
.foregroundColor(.green)
Text("Success!")
.font(.title2.bold())
Text("Your action was completed.")
PButton("Continue", fullWidth: true) {
showModal = false
}
}
.padding(24)
}
// Alert-style modal
.pModalAlert(
isPresented: $showDelete,
icon: "trash.fill",
iconColor: .red,
title: "Delete Item?",
message: "This action cannot be undone.",
primaryButton: PModalButton(title: "Delete", role: .destructive) {
deleteItem()
},
secondaryButton: PModalButton(title: "Cancel", role: .cancel) {
showDelete = false
}
)
```
**Positions:** `.center`, `.bottom`, `.top`
**Overlay Styles:** `.dimmed`, `.blur`, `.none`
---
### PToast
Ephemeral notifications with queue management.
```swift
// Setup container at root
YourView()
.pToastContainer()
// Using ToastManager (recommended for multiple toasts)
@StateObject var toastManager = PToastManager()
Button("Show Toast") {
toastManager.show(
title: "Success!",
description: "Your file was uploaded.",
variant: .success,
duration: 3.0
)
}
.environmentObject(toastManager)
// Simple binding-based toast
@State var showToast = false
Button("Show") { showToast = true }
.pToast(
isPresented: $showToast,
title: "Saved",
variant: .success,
position: .top
)
// Toast variants
toastManager.show(title: "Info", variant: .info)
toastManager.show(title: "Success", variant: .success)
toastManager.show(title: "Warning", variant: .warning)
toastManager.show(title: "Error", variant: .error)
// With action
toastManager.show(
title: "Deleted",
description: "Item removed",
variant: .info,
action: PToastItem.ToastAction(title: "Undo") {
undoDelete()
}
)
// Positions
toastManager.show(title: "Top", position: .top)
toastManager.show(title: "Bottom", position: .bottom)
```
**Variants:** `.info`, `.success`, `.warning`, `.error`
**Positions:** `.top`, `.bottom`
---
### PSpinner
Loading spinner with multiple styles.
```swift
// Basic spinner
PSpinner()
// Sizes
PSpinner().size(.sm) // 16pt
PSpinner().size(.md) // 24pt (default)
PSpinner().size(.lg) // 32pt
PSpinner().size(.xl) // 48pt
// Styles
PSpinner().style(.circular) // Default rotating arc
PSpinner().style(.dots) // Animated dots
PSpinner().style(.track) // Circular with track
PSpinner().style(.minimal) // Simple thin spinner
PSpinner().style(.orbit) // Orbiting dots
// Custom color
PSpinner().color(.purple)
// With label
PSpinner()
.label("Loading...")
.labelPlacement(.bottom) // .bottom, .trailing
// Overlay on view
ContentView()
.spinnerOverlay(isLoading: isLoading, style: .circular, label: "Please wait...")
```
**Sizes:** `.sm`, `.md`, `.lg`, `.xl`
**Styles:** `.circular`, `.dots`, `.track`, `.minimal`, `.orbit`
---
### PTooltip
Contextual information popups with arrows.
```swift
// Basic tooltip
Text("Hover me")
.pTooltip(isPresented: $showTooltip, text: "This is a tooltip")
// On hover/long-press (automatic)
Button("Help") { }
.pTooltipOnHover(text: "Click for more information")
// Positions
.pTooltip(isPresented: $show, text: "Top", position: .top)
.pTooltip(isPresented: $show, text: "Bottom", position: .bottom)
.pTooltip(isPresented: $show, text: "Leading", position: .leading)
.pTooltip(isPresented: $show, text: "Trailing", position: .trailing)
// Styles
.pTooltip(isPresented: $show, text: "Dark", style: .dark)
.pTooltip(isPresented: $show, text: "Light", style: .light)
.pTooltip(isPresented: $show, text: "Custom", style: .custom(TooltipColor(
background: .purple,
foreground: .white
)))
// Auto-dismiss
.pTooltip(
isPresented: $show,
text: "Auto-dismiss",
autoDismiss: true,
displayDuration: 2.0
)
// Rich content
.pTooltip(isPresented: $show, position: .bottom) {
PTooltipRichContent(
title: "Pro Tip",
description: "You can customize this tooltip!",
icon: "lightbulb.fill"
)
}
// Suggestions list
.pTooltip(isPresented: $show) {
PTooltipSuggestions(
title: "Quick Actions",
items: ["Copy", "Paste", "Delete"]
) { item in
performAction(item)
}
}
```
**Positions:** `.top`, `.bottom`, `.leading`, `.trailing`
**Styles:** `.dark`, `.light`, `.custom(TooltipColor)`
---
### PAccordion
Expandable content sections with animations.
```swift
// Basic accordion
PAccordion {
PAccordionItem(title: "Section 1") {
Text("Content for section 1")
}
PAccordionItem(title: "Section 2") {
Text("Content for section 2")
}
}
// Variants
PAccordion {
// items...
}
.variant(.standard) // No container styling
.variant(.bordered) // With border
.variant(.separated) // Cards for each item
// Expansion modes
.expansionMode(.single) // Only one open at a time
.expansionMode(.multiple) // Multiple can be open
// Item customization
PAccordionItem(
title: "FAQ Item",
subtitle: "Click to expand",
icon: "questionmark.circle"
) {
Text("Answer goes here")
}
.initiallyExpanded(true)
.showDivider(false)
// Standalone accordion item
@State var isExpanded = false
PStandaloneAccordionItem(
title: "Manual Control",
isExpanded: $isExpanded
) {
Text("Controlled externally")
}
```
**Variants:** `.standard`, `.bordered`, `.separated`
**Expansion Modes:** `.single`, `.multiple`
---
### PPopover
Interactive content anchored to triggers.
```swift
// Setup root (required)
YourApp()
.pPopoverRoot()
// Basic popover
@State var showPopover = false
Button("Show Menu") { showPopover = true }
.pPopover(isPresented: $showPopover) { dismiss in
VStack {
Button("Option 1") { dismiss() }
Button("Option 2") { dismiss() }
}
.padding()
}
// Positions
.pPopover(isPresented: $show, position: .bottom) { ... }
.pPopover(isPresented: $show, position: .top) { ... }
.pPopover(isPresented: $show, position: .leading) { ... }
.pPopover(isPresented: $show, position: .trailing) { ... }
// Styles
.pPopover(isPresented: $show, style: .light) { ... }
.pPopover(isPresented: $show, style: .dark) { ... }
// Menu helper
Button("Actions") { showMenu = true }
.pPopover(isPresented: $showMenu, position: .bottom) { dismiss in
PPopoverMenu {
PPopoverMenuItem(icon: "doc.on.doc", title: "Copy") {
copyItem()
dismiss()
}
PPopoverMenuDivider()
PPopoverMenuItem(icon: "trash", title: "Delete", variant: .destructive) {
deleteItem()
dismiss()
}
}
}
```
**Positions:** `.top`, `.bottom`, `.leading`, `.trailing`
**Styles:** `.light`, `.dark`, `.custom(PopoverColor)`
---
### PIconButton
Circular icon-only buttons.
```swift
// Basic usage
PIconButton(icon: "plus") { action() }
// Variants
PIconButton(icon: "heart", variant: .primary) { }
PIconButton(icon: "share", variant: .secondary) { }
PIconButton(icon: "bookmark", variant: .outline) { }
PIconButton(icon: "ellipsis", variant: .ghost) { }
// Sizes
PIconButton(icon: "gear").size(.sm) // 32pt
PIconButton(icon: "gear").size(.md) // 40pt
PIconButton(icon: "gear").size(.lg) // 48pt
PIconButton(icon: "gear").size(.xl) // 56pt
// Custom colors
PIconButton(icon: "star.fill")
.foreground(.yellow)
.background(.yellow.opacity(0.2))
// Disabled
PIconButton(icon: "trash")
.disabled(true)
// Haptics
PIconButton(icon: "bell")
.haptics(true)
```
**Variants:** `.primary`, `.secondary`, `.outline`, `.ghost`
**Sizes:** `.sm`, `.md`, `.lg`, `.xl`, `.custom(CGFloat)`
---
### PSidebar
Animated sidebar navigation panel.
```swift
@State var showSidebar = false
Button("Menu") { showSidebar = true }
.pSidebar(isPresented: $showSidebar) { dismiss in
PSidebarContent {
PSidebarHeader(title: "Menu", subtitle: "Navigation")
.avatar(url: URL(string: "..."))
PSidebarSection("Main") {
PSidebarItem(icon: "house", title: "Home") {
navigateTo(.home)
}
.badge(3)
PSidebarItem(icon: "gear", title: "Settings") {
navigateTo(.settings)
}
PSidebarItem(icon: "trash", title: "Delete", variant: .destructive) {
showDeleteConfirm = true
}
}
PSidebarDivider()
PSidebarFooter {
PButton("Logout", variant: .ghost) { logout() }
}
}
}
// Configuration options
.pSidebar(
isPresented: $show,
position: .leading, // .leading, .trailing
style: .floating, // .fullHeight, .floating
overlayStyle: .dimmedBlur, // .dimmed, .blurred, .dimmedBlur, .none
width: 280,
enableDragToDismiss: true
) { ... }
```
**Positions:** `.leading`, `.trailing`
**Styles:** `.fullHeight`, `.floating`
**Overlay Styles:** `.dimmed`, `.blurred`, `.dimmedBlur`, `.none`
---
### PTapGesture
Fluid press animations for any view.
```swift
// Basic tap gesture
Text("Tap me")
.pTapGesture { print("Tapped") }
// Presets
View().pTapGestureBouncy { } // More scale effect
View().pTapGestureSubtle { } // Minimal feedback
View().pTapGestureProminent { } // Strong feedback
View().pTapGestureSnappy { } // Quick response
// Custom configuration
View()
.modifier(
PTapGesture { print("Tapped") }
.scaleEffect(0.95)
.opacityEffect(0.8)
.brightnessEffect(-0.05)
.haptics(true)
.hapticStyle(.medium)
.pressAnimation(response: 0.2, damping: 0.7)
.releaseAnimation(response: 0.3, damping: 0.8)
)
```
---
### PList
Settings-style grouped lists.
```swift
PList("Account") {
PListItem("Profile", subtitle: "John Doe", icon: "person.fill")
.accessory(.chevron)
.onTap { navigateToProfile() }
PListItem("Email", subtitle: "john@example.com", icon: "envelope.fill")
.accessory(.chevron)
PListItem("Verified", icon: "checkmark.seal.fill")
.accessory(.checkmark)
.hideDivider()
}
// Toggle items
PList("Preferences") {
PListToggleItem("Notifications", icon: "bell.fill", isOn: $notifications)
PListToggleItem("Dark Mode", icon: "moon.fill", isOn: $darkMode)
}
// Styles
PList("Items", style: .grouped) { ... } // Card background
PList("Items", style: .insetGrouped) { ... } // Rounded with margins
PList("Items", style: .plain) { ... } // No styling
// With custom leading content
PListItem("User Name", subtitle: "Online") {
PAvatar(name: "User")
.size(.sm)
.status(.online)
}
.accessory(.chevron)
// Destructive action
PListItem("Delete Account", icon: "trash.fill", iconColor: .red)
.destructive()
.onTap { deleteAccount() }
// With badges
PListItem("Messages")
.accessory(.badge("5"))
// Sections
PList {
PListSection("General") {
PListItem("Language")
PListItem("Region")
}
PListSection("Privacy", footer: "Manage your data") {
PListItem("Clear History")
}
}
```
**Styles:** `.grouped`, `.insetGrouped`, `.plain`
**Accessories:** `.chevron`, `.checkmark`, `.icon(String)`, `.badge(String)`, `.none`
---
### PExpandableFAB (Morphing FAB)
Floating action button that expands into a menu. (iOS 17+ only)
```swift
@State var isExpanded = false
PExpandableFAB(isExpanded: $isExpanded) {
Image(systemName: "plus")
.font(.title3.bold())
} menu: {
VStack(spacing: 4) {
PFABMenuItem(icon: "paperplane", title: "Send", description: "Transfer crypto") {
// action
isExpanded = true // Expand to detail
}
PFABMenuItem(icon: "arrow.down", title: "Receive") {
// action
}
}
.padding(.vertical, 16)
} detail: {
// Full-screen detail view when isExpanded = true
DetailView()
}
// Menu only (no detail)
PExpandableFAB {
Image(systemName: "plus")
} menu: {
// Menu items
}
// Customization
PExpandableFAB(...)
.size(60)
.cornerRadius(30)
.tint(.purple)
.iconColor(.white)
.shadow(.md)
.haptics(true)
```
---
### PSkeleton
Loading placeholder with shimmer animation.
```swift
// Basic shapes
PSkeleton(shape: .rectangle(.md))
.frame(height: 20)
PSkeleton(shape: .circle)
.frame(width: 48, height: 48)
PSkeleton(shape: .capsule)
.frame(width: 100, height: 32)
// Text skeleton
PSkeletonText(lines: 3)
PSkeletonText(lines: 2, lastLineWidth: 0.6)
// Avatar skeleton
PSkeletonAvatar(size: .lg)
PSkeletonAvatar(size: 48)
// Card skeleton
PSkeletonCard(showAvatar: true, lines: 2)
PSkeletonCard(showAvatar: false, lines: 3, showImage: true)
// Custom colors
PSkeleton()
.baseColor(.gray.opacity(0.2))
.highlightColor(.gray.opacity(0.4))
.duration(1.5)
// Conditional skeleton
Text("Actual content")
.skeleton(isLoading)
// With custom placeholder
View()
.skeleton(isLoading) {
PSkeletonCard(showAvatar: true)
}
```
---
### PBottomTab
Bottom navigation bar with standard and floating styles.
```swift
@State var selected = "Home"
PBottomTab(selection: $selected) {
VStack(spacing: 0) {
// Content panes
PBottomTabPane {
PBottomTabContent("Home") { HomeView() }
PBottomTabContent("Search") { SearchView() }
PBottomTabContent("Profile") { ProfileView() }
}
// Tab bar
PBottomTabList {
PBottomTabTrigger("Home", icon: "house")
PBottomTabTrigger("Search", icon: "magnifyingglass")
PBottomTabTrigger("Profile", icon: "person", badge: 3)
}
}
}
.style(.standard) // iOS-like tab bar
.style(.floating) // Floating pill bar
.transition(.slide) // Content transition
// Custom trigger with separate active icon
PBottomTabTrigger("Home", icon: "house", activeIcon: "house.fill")
// Customization
.bottomTabListBackground(.black.opacity(0.85))
.bottomTabIndicatorColor(.white)
.bottomTabActiveColor(.blue)
.bottomTabInactiveColor(.gray)
.bottomTabIconSize(24)
.bottomTabShowLabels(false) // Icon-only
```
**Styles:** `.standard`, `.floating`
**Transitions:** `.none`, `.fade`, `.slide`, `.scale`, `.lift`, `.zoom`
---
### PTopTab
Top navigation tabs with segmented, pills, and underline styles.
```swift
@State var selected = "Account"
PTopTab(selection: $selected) {
VStack(spacing: 0) {
PTopTabList {
PTopTabTrigger("Account")
PTopTabTrigger("Password")
PTopTabTrigger("Settings")
}
.padding(.horizontal)
PTopTabPane {
PTopTabContent("Account") { AccountView() }
PTopTabContent("Password") { PasswordView() }
PTopTabContent("Settings") { SettingsView() }
}
}
}
.style(.segmented) // iOS segmented control
.style(.pills) // Pill indicator
.style(.underline) // Underline indicator
// With icons
PTopTabTrigger("Tokens", icon: "bitcoinsign.circle")
PTopTabTrigger("NFTs", icon: "photo.stack", activeIcon: "photo.stack.fill")
// Customization
.topTabFontSize(16)
.topTabFontWeight(.semibold)
.topTabActiveColor(.white)
.topTabInactiveColor(.gray)
.topTabIndicatorColor(.blue)
.topTabListBackground(.gray.opacity(0.2))
.topTabIconPosition(.leading) // .leading, .trailing, .top
.topTabShowLabels(false) // Icon-only mode
```
**Styles:** `.segmented`, `.pills`, `.underline`
**Sizes:** `.sm`, `.md`, `.lg`
**Transitions:** `.none`, `.fade`, `.slide`, `.scale`, `.lift`, `.zoom`
---
## Animation Guidelines
### Spring Animations (Recommended)
```swift
// Standard spring for most interactions
.animation(.spring(response: 0.35, dampingFraction: 0.7), value: state)
// Quick spring for micro-interactions
.animation(.spring(response: 0.25, dampingFraction: 0.8), value: state)
// Gentle spring for large transitions
.animation(.spring(response: 0.5, dampingFraction: 0.75), value: state)
```
### Press States
```swift
// Standard press effect
.scaleEffect(isPressed ? 0.97 : 1.0)
.animation(.spring(response: 0.25, dampingFraction: 0.7), value: isPressed)
```
### Respect Reduced Motion
```swift
@Environment(\.accessibilityReduceMotion) var reduceMotion
.animation(reduceMotion ? nil : .spring(response: 0.35, dampingFraction: 0.7), value: state)
```
---
## Accessibility
- All components support VoiceOver with proper labels
- Touch targets are minimum 44x44pt
- Color contrast meets WCAG AA standards
- Reduced motion is respected across all animations
- Haptic feedback can be disabled
---
## Platform Support
- **iOS 16.0+** - Full support
- **macOS 13.0+** - Full support (except PMorphingFAB)
- **tvOS 16.0+** - Full support (except PMorphingFAB)
- **watchOS 9.0+** - Full support (except PMorphingFAB)
---
## Best Practices
1. **Apply theme at root** - Use `.prettyTheme()` at your app's root view
2. **Use design tokens** - Access `theme.spacing`, `theme.radius`, etc. instead of hardcoded values
3. **Leverage fluent API** - Chain modifiers for readable configuration
4. **Respect dark mode** - Use `theme.colors(for: colorScheme)` for automatic color adaptation
5. **Consider accessibility** - Test with VoiceOver and reduced motion enabled
6. **Use spring animations** - They feel more natural than linear/ease animations
7. **Keep press scales subtle** - 0.97-0.98 scale feels responsive without being jarring
Benefits of Using the Cursor Rule
AI assistants can generate correct PrettyUI code instantly, with proper modifiers and theming.
The rule ensures AI follows PrettyUI conventions, design tokens, and best practices.
All 20+ components are documented with their full API, variants, and modifiers.
AI understands the theming system and generates properly themed components.
Example Prompts
Once you've added the Cursor rule, try these prompts:
"Create a login form with PrettyUI using email and password fields, a primary submit button, and show a success toast on submit""Build a settings screen with PList showing profile info, notification toggles, and a destructive logout option""Create a card grid showing user avatars with online status and notification badges""Add a confirmation modal that appears when deleting an item, with Cancel and Delete buttons"