agentleFS
moasqmoasq/ios-dev-agent/.cursor/rules/design-system.mdc

ios-dev-agent / rules

Cursor rule4 starsChanged 6 months agoMITView on GitHub

iOS development rule

The file

Show its rules and text

We split this design-system.mdc into the rules it gives an agent

28 rules. About 0 are common, 10 are distinctive, and 18 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈10

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 18

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every color, font, and spacing value in the app MUST come from AppTheme.

    line 11
  2. Distinctive:

    Before writing ANY view code, verify:

    line 13
  3. Very specific:

    Every app MUST use a centralized theme with nested enums for Colors, Fonts, and Spacing.

    line 62
  4. Very specific:

    NEVER: use raw .font(.title2) or .font(.headline) in views — always AppTheme.Fonts.title2

    line 134
  5. Very specific:

    NEVER: use .font(.system(size: N)) inline — it opts out of Dynamic Type and violates the token rule

    line 135
  6. Very specific:

    When SF Symbol icons need a specific size that doesn't match a text style, create a named token in AppTheme.Fonts (e.g.

    line 136
  7. Distinctive:

    No custom fonts, no downloaded fonts

    line 137
  8. Distinctive:

    Every AppTheme MUST define text color tokens using UIKit's adaptive system colors.

    line 141
  9. Very specific:

    Use .symbolRenderingMode(.hierarchical) or .symbolRenderingMode(.palette) for visual depth

    line 167
  10. Distinctive:

    No custom icon assets unless the app concept specifically requires them

    line 168
  11. Very specific:

    Every app MUST define a Color(hex:) initializer in AppTheme.swift so palette hex values can be used:

    line 171
  12. Very specific:

    When the app has appearance switching (dark/light/system), also define:

    line 189
  13. Very specific:

    When the app does NOT support both light and dark appearances (no dark-mode rule key), the pipeline automatically locks appearance via …

    line 234
  14. Very specific:

    dark-mode rule key: No lock — the app supports adaptive light/dark/system via a user preference picker.

    line 238
  15. Distinctive:

    visionOS: No appearance lock needed — visionOS has no dark mode.

    line 239
  16. Distinctive:

    macOS: No appearance lock. macOS apps always follow the system appearance (dark/light).

    line 240
  17. Distinctive:

    Do NOT add dark mode support, colorScheme checks, or custom dark/light color handling unless the user explicitly requests it

    line 252
  18. Very specific:

    NEVER use hardcoded SwiftUI colors like .blue, .orange, .white, .black directly in views

    line 253
  19. Distinctive:

    Keep brand/surface tokens explicit in AppTheme so appearance changes do not shift core palette identity

    line 254
  20. Very specific:

    Use AppTheme.Spacing constants throughout — never raw numeric values

    line 266
  21. Very specific:

    Every list or collection MUST have an empty state.

    line 269
  22. Very specific:

    Always specify presentationDetents on .sheet:

    line 291
  23. Very specific:

    Use .sheet / .fullScreenCover for creation forms

    line 295
  24. Distinctive:

    Use subtle, purposeful animations for state changes and list mutations:

    line 298
  25. Very specific:

    Always: use withAnimation(.spring) for toggle/complete state changes

    line 311
  26. Very specific:

    Always: add .transition(.opacity.combined(with: .scale)) for list add/remove

    line 312
  27. Distinctive:

    Never: add gratuitous motion that slows down interaction

    line 313
  28. Very specific:

    Keep animations subtle — .spring and .default curves only

    line 314

≈ “Common” is an estimate: at least 1 in 5 of the 6,668 Cursor rule files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
design-system.mdc
---
description: iOS development rule
globs: "**/*.swift"
alwaysApply: true
---

# Design System Rules

## MANDATORY — AppTheme Is the Single Source of Truth

Every color, font, and spacing value in the app MUST come from `AppTheme`. No exceptions.

Before writing ANY view code, verify:
1. Does `AppTheme.Colors` have a token for the color I need? If not, add one.
2. Does `AppTheme.Fonts` have a token for the font I need? If not, add one.
3. Am I using `AppTheme.Spacing` for padding/spacing? If not, switch to it.

## FORBIDDEN — Hardcoded Styling (Zero Tolerance)

These patterns are **BANNED** everywhere in feature views. Violations MUST be caught and fixed.

```swift
// BANNED — hardcoded colors
.foregroundStyle(.white)                    // use AppTheme.Colors.textPrimary
.foregroundStyle(.white.opacity(0.8))       // use AppTheme.Colors.textSecondary
.foregroundStyle(.white.opacity(0.6))       // use AppTheme.Colors.textTertiary
.foregroundStyle(.black)                    // use AppTheme.Colors.textPrimary
.foregroundStyle(Color.red)                 // define AppTheme.Colors.error or semantic token
.foregroundStyle(Color.blue)                // define AppTheme.Colors.accent or semantic token
.background(.blue)                          // use AppTheme.Colors.* token
.background(Color(hex: "FF0000"))           // define in AppTheme.Colors, reference the token
.tint(.white)                               // use AppTheme.Colors.* token

// BANNED — hardcoded fonts
.font(.system(size: 48))                    // use AppTheme.Fonts.* token
.font(.system(size: 64))                    // use AppTheme.Fonts.* token
.font(.system(.largeTitle, design: .rounded, weight: .bold))  // define in AppTheme.Fonts
.font(.title2)                              // use AppTheme.Fonts.title2
.font(.caption)                             // use AppTheme.Fonts.caption
.font(.headline)                            // use AppTheme.Fonts.headline

// BANNED — hardcoded spacing
.padding(20)                                // use AppTheme.Spacing.*
.padding(.horizontal, 12)                   // use AppTheme.Spacing.*
VStack(spacing: 10)                         // use AppTheme.Spacing.*
```

```swift
// CORRECT — always use AppTheme tokens
.foregroundStyle(AppTheme.Colors.textPrimary)   // system adaptive — auto-adapts to appearance
.foregroundStyle(AppTheme.Colors.textSecondary)
.font(AppTheme.Fonts.title2)
.font(AppTheme.Fonts.caption)
.padding(AppTheme.Spacing.md)
.padding(.horizontal, AppTheme.Spacing.sm)
VStack(spacing: AppTheme.Spacing.sm)
.background(AppTheme.Colors.background)         // custom palette hex — brand identity
```

## AppTheme Pattern

Every app **MUST** use a centralized theme with **nested enums** for `Colors`, `Fonts`, and `Spacing`. Do NOT use a flat enum with top-level static properties.

```swift
// REQUIRED — always use nested enums
import SwiftUI

enum AppTheme {
    enum Colors {
        // Brand/theme colors — custom hex from palette
        static let primary = Color(hex: "...")
        static let secondary = Color(hex: "...")
        static let accent = Color(hex: "...")
        static let background = Color(hex: "...")
        static let surface = Color(hex: "...")

        // Text colors — REQUIRED — use UIKit adaptive colors
        // These auto-adapt to the locked appearance (Dark or Light).
        static let textPrimary = Color(.label)
        static let textSecondary = Color(.secondaryLabel)
        static let textTertiary = Color(.tertiaryLabel)
    }

    enum Fonts {
        static let largeTitle = Font.system(.largeTitle, design: .rounded, weight: .bold)
        static let title = Font.system(.title, design: .rounded, weight: .bold)
        static let title2 = Font.system(.title2, design: .rounded, weight: .semibold)
        static let title3 = Font.system(.title3, design: .rounded, weight: .semibold)
        static let headline = Font.system(.headline, design: .rounded)
        static let body = Font.system(.body, design: .rounded)
        static let callout = Font.system(.callout, design: .rounded)
        static let subheadline = Font.system(.subheadline, design: .rounded)
        static let footnote = Font.system(.footnote, design: .rounded)
        static let caption = Font.system(.caption, design: .rounded)
        static let caption2 = Font.system(.caption2, design: .rounded)

        // Icon-specific sizes — use when SF Symbols need a size that doesn't match a text style
        static let heroIcon = Font.system(size: 80, weight: .regular, design: .rounded)
        static let bulletIcon = Font.system(size: 6, weight: .regular, design: .rounded)
    }

    enum Spacing {
        static let xs: CGFloat = 4
        static let sm: CGFloat = 8
        static let md: CGFloat = 16
        static let lg: CGFloat = 24
        static let xl: CGFloat = 40
    }

    enum Style {
        static let cornerRadius: CGFloat = 12
        static let cardCornerRadius: CGFloat = 16
    }
}
```

```swift
// FORBIDDEN — never use flat structure
enum AppTheme {
    static let accentColor = Color.blue   // wrong
    static let spacing: CGFloat = 8       // wrong
}
```

Reference as: `AppTheme.Colors.accent`, `AppTheme.Fonts.headline`, `AppTheme.Spacing.md`

## Fonts — AppTheme.Fonts Required

The `Fonts` enum MUST exist in every AppTheme. It defines the app's typography tokens using the font design from the plan.

Rules:
- **System fonts only** — use SwiftUI font styles: `.largeTitle`, `.title`, `.headline`, `.body`, `.caption`
- Apply the plan's `fontDesign` (rounded, serif, monospaced, default) via `Font.system(.style, design: .rounded)`
- **NEVER** use raw `.font(.title2)` or `.font(.headline)` in views — always `AppTheme.Fonts.title2`
- **NEVER** use `.font(.system(size: N))` inline — it opts out of Dynamic Type and violates the token rule
- When SF Symbol icons need a specific size that doesn't match a text style, create a named token in `AppTheme.Fonts` (e.g. `heroIcon`, `bulletIcon`) — never use `.font(.system(size: N))` inline
- No custom fonts, no downloaded fonts

## Text Colors — System Adaptive via UIKit

Every AppTheme MUST define text color tokens using UIKit's adaptive system colors. These automatically adapt to the locked appearance mode (Dark or Light) set via Info.plist, so text is always legible against the palette background.

```swift
// REQUIRED — UIKit adaptive text colors
static let textPrimary = Color(.label)              // white in Dark, black in Light
static let textSecondary = Color(.secondaryLabel)    // adapts opacity per appearance
static let textTertiary = Color(.tertiaryLabel)      // adapts opacity per appearance
```

**Do NOT use static colors for text** — no `Color.white`, `Color.black`, `Color.primary`, or `.opacity()` on raw colors. The appearance lock ensures UIKit adaptive colors pick the correct variant automatically.

| Instead of | Use |
|---|---|
| `.foregroundStyle(.white)` | `AppTheme.Colors.textPrimary` |
| `.foregroundStyle(.black)` | `AppTheme.Colors.textPrimary` |
| `.foregroundStyle(.white.opacity(0.8))` | `AppTheme.Colors.textSecondary` |
| `.foregroundStyle(.white.opacity(0.6))` | `AppTheme.Colors.textTertiary` |
| `.foregroundStyle(.secondary)` | `AppTheme.Colors.textSecondary` |
| `.foregroundStyle(Color.primary)` | `AppTheme.Colors.textPrimary` |

**macOS note**: On macOS, use `Color(.labelColor)`, `Color(.secondaryLabelColor)`, `Color(.tertiaryLabelColor)` instead — AppKit uses different names than UIKit. For multi-platform shared code, use `#if canImport(UIKit)` / `#if canImport(AppKit)` to select the correct initializer.

## Icons (SF Symbols)
- **SF Symbols only** for all icons — required for every list row, button, empty state, and tab
- Reference via `Image(systemName: "symbol.name")`
- Pick domain-appropriate symbols (e.g. "checkmark.circle.fill" for todos, "note.text" for notes, "heart.fill" for favorites)
- Use `.symbolRenderingMode(.hierarchical)` or `.symbolRenderingMode(.palette)` for visual depth
- No custom icon assets unless the app concept specifically requires them

## Color(hex:) Extension
Every app MUST define a `Color(hex:)` initializer in AppTheme.swift so palette hex values can be used:

```swift
extension Color {
    init(hex: String) {
        let hex = hex.trimmingCharacters(in: .init(charactersIn: "#"))
        let scanner = Scanner(string: hex)
        var rgbValue: UInt64 = 0
        scanner.scanHexInt64(&rgbValue)
        self.init(
            red: Double((rgbValue & 0xFF0000) >> 16) / 255.0,
            green: Double((rgbValue & 0x00FF00) >> 8) / 255.0,
            blue: Double(rgbValue & 0x0000FF) / 255.0
        )
    }
}
```

When the app has appearance switching (dark/light/system), also define:

**iOS / tvOS / visionOS** (UIKit available):
```swift
extension Color {
    init(light: String, dark: String) {
        self.init(uiColor: UIColor { traits in
            traits.userInterfaceStyle == .dark ? UIColor(Color(hex: dark)) : UIColor(Color(hex: light))
        })
    }
}
```

**macOS** (AppKit, no UIKit):
```swift
extension Color {
    init(light: String, dark: String) {
        self.init(nsColor: NSColor(name: nil, dynamicProvider: { appearance in
            let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
            return isDark ? NSColor(Color(hex: dark)) : NSColor(Color(hex: light))
        }))
    }
}
```

**Multi-platform shared code** — use `#if canImport`:
```swift
extension Color {
    init(light: String, dark: String) {
        #if canImport(UIKit)
        self.init(uiColor: UIColor { traits in
            traits.userInterfaceStyle == .dark ? UIColor(Color(hex: dark)) : UIColor(Color(hex: light))
        })
        #elseif canImport(AppKit)
        self.init(nsColor: NSColor(name: nil, dynamicProvider: { appearance in
            let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
            return isDark ? NSColor(Color(hex: dark)) : NSColor(Color(hex: light))
        }))
        #endif
    }
}
```

## Appearance Mode — Palette-Aware Lock via Info.plist

When the app does NOT support both light and dark appearances (no `dark-mode` rule key), the pipeline automatically locks appearance via Info.plist based on palette brightness:

- **Dark palette** (background brightness < 128): `UIUserInterfaceStyle` is set to `Dark`. System chrome (status bar, navigation bars, alerts, sheets, pickers) renders in dark mode to match the app's dark background.
- **Light palette** (background brightness >= 128 or no palette): `UIUserInterfaceStyle` is set to `Light`.
- **`dark-mode` rule key**: No lock — the app supports adaptive light/dark/system via a user preference picker.
- **visionOS**: No appearance lock needed — visionOS has no dark mode. The glass material auto-adapts to the physical environment.
- **macOS**: No appearance lock. macOS apps always follow the system appearance (dark/light). Users expect Mac apps to respect their system preference.

This is handled at the XcodeGen project generation level — **do NOT use `.preferredColorScheme()` for this purpose**. The Info.plist approach ensures the entire app (including system chrome, alerts, and sheets) respects the locked appearance, not just SwiftUI views.

**Text colors auto-adapt**: Because the appearance is locked at the system level, UIKit adaptive colors (`Color(.label)`, `Color(.secondaryLabel)`, `Color(.tertiaryLabel)`) automatically pick the correct variant. Dark-locked apps get white text; light-locked apps get black text. No manual color switching needed.

**When the app supports dark mode** (`dark-mode` in rule_keys with `Color(light:dark:)` adaptive tokens), the pipeline omits these keys and the app follows the system appearance.

## Colors
- **Brand/theme tokens** (primary, secondary, accent, background, surface) use `Color(hex:)` with palette values — these are the app's visual identity
- **Text tokens** (textPrimary, textSecondary, textTertiary) use UIKit adaptive colors (`Color(.label)`, etc.) — these auto-adapt to the appearance lock
- **One accent color** that fits the app's purpose
- Do NOT add dark mode support, colorScheme checks, or custom dark/light color handling unless the user explicitly requests it
- NEVER use hardcoded SwiftUI colors like `.blue`, `.orange`, `.white`, `.black` directly in views
- Keep brand/surface tokens explicit in AppTheme so appearance changes do not shift core palette identity

### Platform-Specific Color Rules
- **visionOS**: AppTheme colors are ONLY for accent buttons, badges, and small decorative elements. NEVER use AppTheme.Colors for backgrounds or body text. Use system glass and vibrancy instead.
- **tvOS**: AppTheme palette must use muted, desaturated colors. Saturation is overwhelming on large TV screens. Dark-first design — light text on dark backgrounds.
- **macOS**: Standard color usage similar to iOS. Sidebar icons should be monochrome (system handles tinting).
- **iOS**: Full AppTheme color palette usage is appropriate.

## Spacing Standards
- **16pt** standard padding (outer margins, section spacing)
- **8pt** compact spacing (between related elements)
- **24pt** large spacing (between major sections)
- Use `AppTheme.Spacing` constants throughout — never raw numeric values

## Empty States
Every list or collection MUST have an empty state. Use `ContentUnavailableView` (iOS 17+) for a polished look:

```swift
if items.isEmpty {
    ContentUnavailableView(
        "No Notes Yet",
        systemImage: "note.text",
        description: Text("Tap + to create your first note")
    )
} else {
    // Show the list
}
```

## Surface Materials
Map the design `surfaces` token to SwiftUI materials:
- **glass** -> `.ultraThinMaterial` (modern/translucent)
- **material** -> `.regularMaterial` (depth/layers)
- **solid** -> opaque `Color` from palette (clean/opaque)
- **flat** -> no shadows, no materials (minimal)

## Sheet Sizing
Always specify `presentationDetents` on `.sheet`:
- Small option pickers -> `.height(N)` (calculate based on content)
- Medium forms -> `.medium`
- Complex multi-section -> `.large`
- Use `.sheet` / `.fullScreenCover` for creation forms

## Animations
Use subtle, purposeful animations for state changes and list mutations:

```swift
withAnimation(.spring) {
    item.isComplete.toggle()
}

.transition(.opacity.combined(with: .scale))
.contentTransition(.numericText())
.animation(.default, value: selectedFilter)
```

Rules:
- **Always** use `withAnimation(.spring)` for toggle/complete state changes
- **Always** add `.transition(.opacity.combined(with: .scale))` for list add/remove
- **Never** add gratuitous motion that slows down interaction
- Keep animations subtle — `.spring` and `.default` curves only

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.