@dynokostya/just-works 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/csharp-code-writer.md +32 -0
- package/.claude/agents/diagrammer.md +49 -0
- package/.claude/agents/frontend-code-writer.md +36 -0
- package/.claude/agents/prompt-writer.md +38 -0
- package/.claude/agents/python-code-writer.md +32 -0
- package/.claude/agents/swift-code-writer.md +32 -0
- package/.claude/agents/typescript-code-writer.md +32 -0
- package/.claude/commands/git-sync.md +96 -0
- package/.claude/commands/project-docs.md +287 -0
- package/.claude/settings.json +112 -0
- package/.claude/settings.json.default +15 -0
- package/.claude/skills/csharp-coding/SKILL.md +368 -0
- package/.claude/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.claude/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.claude/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.claude/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.claude/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.claude/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.claude/skills/python-coding/SKILL.md +293 -0
- package/.claude/skills/react-coding/SKILL.md +264 -0
- package/.claude/skills/rest-api/SKILL.md +421 -0
- package/.claude/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.claude/skills/swift-coding/SKILL.md +401 -0
- package/.claude/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.claude/skills/typescript-coding/SKILL.md +464 -0
- package/.claude/statusline-command.sh +34 -0
- package/.codex/prompts/plan-reviewer.md +162 -0
- package/.codex/prompts/project-docs.md +287 -0
- package/.codex/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.codex/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.codex/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.codex/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.codex/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.codex/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.codex/skills/python-coding/SKILL.md +293 -0
- package/.codex/skills/react-coding/SKILL.md +264 -0
- package/.codex/skills/rest-api/SKILL.md +421 -0
- package/.codex/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.codex/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.codex/skills/typescript-coding/SKILL.md +464 -0
- package/AGENTS.md +57 -0
- package/CLAUDE.md +98 -0
- package/LICENSE +201 -0
- package/README.md +114 -0
- package/bin/cli.mjs +291 -0
- package/package.json +39 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: swift-coding
|
|
3
|
+
description: Apply when writing or editing Swift (.swift) files. Behavioral corrections for error handling, concurrency, memory management, type safety, protocol-oriented design, security defaults, and common antipatterns. Project conventions always override these defaults.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Swift Coding
|
|
7
|
+
|
|
8
|
+
Match the project's existing conventions. When uncertain, read 2-3 existing files to infer the local style. Check `Package.swift` or `.xcodeproj`/`.xcworkspace` for Swift version, platform targets, and dependencies. These defaults apply only when the project has no established convention.
|
|
9
|
+
|
|
10
|
+
## Never rules
|
|
11
|
+
|
|
12
|
+
These are unconditional. They prevent bugs and vulnerabilities regardless of project style.
|
|
13
|
+
|
|
14
|
+
- **Never force unwrap (`!`) outside tests and controlled contexts** — crashes at runtime with no recovery path. Use `guard let`, `if let`, `??`, or optional chaining. Force unwrap is acceptable in tests, `@IBOutlet`, and when failure is provably a programmer error with an explaining comment.
|
|
15
|
+
- **Never `try!` outside tests** — crashes on any error. Use `do`-`catch`, `try?`, or propagate with `throws`. A network timeout, a missing file, a malformed response — any of these kill your process silently with no chance to recover or report.
|
|
16
|
+
- **Never bare `catch { }` without handling** — silently swallows errors. Catch specific error types, or log and rethrow. A decode failure looks the same as a permission error, and you'll never know which one is happening in production.
|
|
17
|
+
- **Never `[weak self]` without checking for nil** — accessing self after capture without `guard let self` leads to silent no-ops or partial execution. Half-completed operations are worse than failures because they corrupt state without raising errors.
|
|
18
|
+
- **Never mutable global or static state** — shared mutable state causes data races. Use actors, `@MainActor`, or dependency injection. The Swift 6 concurrency model will flag these as compile errors, so fix them now.
|
|
19
|
+
- **Never blocking calls on MainActor** — no `Thread.sleep()`, synchronous network calls, or heavy computation on the main thread. Use `Task`, `async`/`await`, or dispatch to a background context. Blocking main freezes the UI and triggers watchdog kills on iOS.
|
|
20
|
+
- **Never `+` string concatenation in loops** — use string interpolation or array `joined(separator:)`. Swift strings are value types; repeated concatenation copies the entire buffer each time — O(n^2) at scale.
|
|
21
|
+
- **Never `Random.default` for security** — not cryptographically secure. Use `SecRandomCopyBytes` or `CryptoKit` for tokens, keys, session IDs. An attacker who observes outputs can predict future ones.
|
|
22
|
+
- **Never `print()` in production code** — use `os.Logger` or `OSLog`. `print` is not filterable, not structured, and persists in release builds. It cannot be searched in Console.app and adds noise to device logs.
|
|
23
|
+
- **Never `class` when `struct` suffices** — default to value types. Use `class` only when you need reference semantics, inheritance, or identity. Structs are stack-allocated, thread-safe by default, and avoid retain/release overhead.
|
|
24
|
+
- **Never retain cycles in closures** — escaping closures capturing `self` in classes must use `[weak self]` or `[unowned self]`. Retain cycles cause silent memory leaks that accumulate over app lifetime, eventually triggering OOM kills.
|
|
25
|
+
- **Never `Any` or `AnyObject` when a protocol or generic suffices** — type erasure disables compile-time checking. Use generics, `some`, or `any` with specific protocols. A runtime cast failure is always worse than a compile error.
|
|
26
|
+
|
|
27
|
+
## Error handling
|
|
28
|
+
|
|
29
|
+
Use `throws` and `do`-`catch` at system boundaries. Propagate errors with `throws` through internal layers and handle them at the outermost boundary where you can take meaningful action.
|
|
30
|
+
|
|
31
|
+
```swift
|
|
32
|
+
enum NetworkError: Error {
|
|
33
|
+
case invalidURL(String)
|
|
34
|
+
case requestFailed(statusCode: Int)
|
|
35
|
+
case decodingFailed(underlying: Error)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func fetchUser(id: Int) async throws -> User {
|
|
39
|
+
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
|
|
40
|
+
throw NetworkError.invalidURL("/users/\(id)")
|
|
41
|
+
}
|
|
42
|
+
let (data, response) = try await URLSession.shared.data(from: url)
|
|
43
|
+
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
|
|
44
|
+
throw NetworkError.requestFailed(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0)
|
|
45
|
+
}
|
|
46
|
+
do {
|
|
47
|
+
return try JSONDecoder().decode(User.self, from: data)
|
|
48
|
+
} catch {
|
|
49
|
+
throw NetworkError.decodingFailed(underlying: error)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use `guard` for early exits. It keeps the happy path unindented and makes preconditions explicit:
|
|
55
|
+
|
|
56
|
+
```swift
|
|
57
|
+
func process(order: Order?) throws -> Receipt {
|
|
58
|
+
guard let order else { throw AppError.missingOrder }
|
|
59
|
+
guard order.items.isEmpty == false else { throw AppError.emptyOrder }
|
|
60
|
+
guard order.total > 0 else { throw AppError.invalidTotal(order.total) }
|
|
61
|
+
return Receipt(order: order, timestamp: .now)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
When to use `Result` vs `throws`: prefer `throws` for most code. Use `Result` when you need to store an outcome for later processing, pass it across non-throwing boundaries, or work with callback-based APIs that cannot be made async.
|
|
66
|
+
|
|
67
|
+
Swift 6 typed throws allow callers to handle specific error types without type erasure:
|
|
68
|
+
|
|
69
|
+
```swift
|
|
70
|
+
func validate(input: String) throws(ValidationError) -> Validated {
|
|
71
|
+
guard input.count >= 3 else { throw .tooShort(minimum: 3) }
|
|
72
|
+
return Validated(value: input)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Resource cleanup
|
|
77
|
+
|
|
78
|
+
Use `defer` for cleanup — file handles, locks, temporary state restoration. `defer` executes when the scope exits regardless of how (return, throw, break):
|
|
79
|
+
|
|
80
|
+
```swift
|
|
81
|
+
func writeData(_ data: Data, to path: String) throws {
|
|
82
|
+
let handle = try FileHandle(forWritingTo: URL(filePath: path))
|
|
83
|
+
defer { try? handle.close() }
|
|
84
|
+
handle.write(data)
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
With structured concurrency, child tasks are automatically cancelled when their parent scope exits. Prefer structured concurrency (`async let`, `TaskGroup`) over unstructured `Task { }` to get automatic cleanup for free.
|
|
89
|
+
|
|
90
|
+
## Async patterns
|
|
91
|
+
|
|
92
|
+
Use `async`/`await` for all asynchronous work. Prefer structured concurrency over unstructured tasks.
|
|
93
|
+
|
|
94
|
+
`TaskGroup` for concurrent work with dynamic fan-out:
|
|
95
|
+
|
|
96
|
+
```swift
|
|
97
|
+
func fetchAllUsers(ids: [Int]) async throws -> [User] {
|
|
98
|
+
try await withThrowingTaskGroup(of: User.self) { group in
|
|
99
|
+
for id in ids {
|
|
100
|
+
group.addTask { try await fetchUser(id: id) }
|
|
101
|
+
}
|
|
102
|
+
var users: [User] = []
|
|
103
|
+
for try await user in group {
|
|
104
|
+
users.append(user)
|
|
105
|
+
}
|
|
106
|
+
return users
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Actors for shared mutable state — they serialize access automatically:
|
|
112
|
+
|
|
113
|
+
```swift
|
|
114
|
+
actor ImageCache {
|
|
115
|
+
private var cache: [URL: Data] = [:]
|
|
116
|
+
|
|
117
|
+
func image(for url: URL) -> Data? { cache[url] }
|
|
118
|
+
|
|
119
|
+
func store(_ data: Data, for url: URL) { cache[url] = data }
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Use `@MainActor` for UI work. Apply it to the type when all members need main-thread access, or to individual methods when only some do:
|
|
124
|
+
|
|
125
|
+
```swift
|
|
126
|
+
@MainActor
|
|
127
|
+
final class ViewModel: Observable {
|
|
128
|
+
var items: [Item] = []
|
|
129
|
+
|
|
130
|
+
func refresh() async throws {
|
|
131
|
+
let fetched = try await service.fetchItems()
|
|
132
|
+
items = fetched
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`Sendable` conformance is required for values crossing actor boundaries. Structs and enums with all-Sendable stored properties conform automatically. For classes, mark as `final class: Sendable` with only immutable (`let`) properties or use `@unchecked Sendable` with internal synchronization.
|
|
138
|
+
|
|
139
|
+
Check cancellation in long-running work:
|
|
140
|
+
|
|
141
|
+
```swift
|
|
142
|
+
for item in largeCollection {
|
|
143
|
+
try Task.checkCancellation()
|
|
144
|
+
await process(item)
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
When to use structured vs unstructured concurrency: use `async let` and `TaskGroup` (structured) for work scoped to a function. Use `Task { }` (unstructured) only for fire-and-forget work or bridging from synchronous contexts. Structured concurrency handles cancellation and error propagation automatically.
|
|
149
|
+
|
|
150
|
+
## Type system
|
|
151
|
+
|
|
152
|
+
`some` (opaque types) returns a specific concrete type hidden from the caller — use it when you want to hide implementation details while preserving type identity. `any` (existentials) is a type-erased box — use it when you need heterogeneous collections or dynamic dispatch:
|
|
153
|
+
|
|
154
|
+
```swift
|
|
155
|
+
// Opaque: caller gets one concrete type, compiler optimizes
|
|
156
|
+
func makeSequence() -> some Sequence<Int> {
|
|
157
|
+
[1, 2, 3].lazy.filter { $0 > 1 }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Existential: heterogeneous collection of different types
|
|
161
|
+
func allValidators() -> [any Validator] {
|
|
162
|
+
[EmailValidator(), LengthValidator(min: 3)]
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Protocols with associated types:
|
|
167
|
+
|
|
168
|
+
```swift
|
|
169
|
+
protocol Repository {
|
|
170
|
+
associatedtype Entity: Identifiable
|
|
171
|
+
func find(by id: Entity.ID) async throws -> Entity?
|
|
172
|
+
func save(_ entity: Entity) async throws
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
struct UserRepository: Repository {
|
|
176
|
+
typealias Entity = User
|
|
177
|
+
func find(by id: User.ID) async throws -> User? { /* ... */ }
|
|
178
|
+
func save(_ entity: User) async throws { /* ... */ }
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Generic functions with constraints:
|
|
183
|
+
|
|
184
|
+
```swift
|
|
185
|
+
func merge<T: Decodable & Sendable>(_ items: [T], with others: [T]) -> [T]
|
|
186
|
+
where T: Equatable
|
|
187
|
+
{
|
|
188
|
+
var result = items
|
|
189
|
+
for item in others where !result.contains(item) {
|
|
190
|
+
result.append(item)
|
|
191
|
+
}
|
|
192
|
+
return result
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Value vs reference types
|
|
197
|
+
|
|
198
|
+
Default to structs. Use classes when you need reference semantics (shared mutable state), inheritance, or Objective-C interop. Use enum as a namespace (caseless enum) for grouping constants and static functions:
|
|
199
|
+
|
|
200
|
+
```swift
|
|
201
|
+
enum API {
|
|
202
|
+
static let baseURL = URL(string: "https://api.example.com")!
|
|
203
|
+
static let timeout: TimeInterval = 30
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Collections use copy-on-write — passing an array to a function does not copy until mutation. This means value semantics are cheap in practice for standard library types.
|
|
208
|
+
|
|
209
|
+
## Data modeling
|
|
210
|
+
|
|
211
|
+
| Use Case | Choice | Reason |
|
|
212
|
+
|----------|--------|--------|
|
|
213
|
+
| API response/request | `Codable` struct | Serialization, immutable |
|
|
214
|
+
| Configuration | struct with defaults | Value semantics |
|
|
215
|
+
| Domain entity with identity | class or actor | Reference semantics, shared state |
|
|
216
|
+
| Simple value objects | struct | Stack-allocated, value equality |
|
|
217
|
+
| Fixed set of states | enum with associated values | Exhaustive switch, type-safe |
|
|
218
|
+
| UI state (SwiftUI) | `@Observable` class | Observation framework |
|
|
219
|
+
|
|
220
|
+
Codable at system boundaries:
|
|
221
|
+
|
|
222
|
+
```swift
|
|
223
|
+
struct UserResponse: Codable, Sendable {
|
|
224
|
+
let id: Int
|
|
225
|
+
let email: String
|
|
226
|
+
let name: String
|
|
227
|
+
let createdAt: Date
|
|
228
|
+
|
|
229
|
+
enum CodingKeys: String, CodingKey {
|
|
230
|
+
case id, email, name
|
|
231
|
+
case createdAt = "created_at"
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Enums with associated values for domain states:
|
|
237
|
+
|
|
238
|
+
```swift
|
|
239
|
+
enum PaymentResult {
|
|
240
|
+
case success(transactionId: String, amount: Decimal)
|
|
241
|
+
case declined(reason: String)
|
|
242
|
+
case requiresVerification(url: URL)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
func handle(_ result: PaymentResult) {
|
|
246
|
+
switch result {
|
|
247
|
+
case .success(let id, let amount):
|
|
248
|
+
log.info("Payment \(id) completed: \(amount)")
|
|
249
|
+
case .declined(let reason):
|
|
250
|
+
showError("Payment declined: \(reason)")
|
|
251
|
+
case .requiresVerification(let url):
|
|
252
|
+
openVerification(url)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Memory management
|
|
258
|
+
|
|
259
|
+
ARC manages memory automatically, but you must handle reference cycles. Strong references (the default) keep objects alive. Use `weak` for optional back-references and delegates. Use `unowned` only when the referenced object is guaranteed to outlive the reference.
|
|
260
|
+
|
|
261
|
+
Delegate pattern with `weak`:
|
|
262
|
+
|
|
263
|
+
```swift
|
|
264
|
+
protocol DownloadDelegate: AnyObject {
|
|
265
|
+
func downloadDidFinish(_ data: Data)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
class Downloader {
|
|
269
|
+
weak var delegate: DownloadDelegate?
|
|
270
|
+
|
|
271
|
+
func start() async {
|
|
272
|
+
let data = try? await fetchData()
|
|
273
|
+
delegate?.downloadDidFinish(data ?? Data())
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Closure capture lists — always unwrap `weak self` before doing work:
|
|
279
|
+
|
|
280
|
+
```swift
|
|
281
|
+
service.onComplete = { [weak self] result in
|
|
282
|
+
guard let self else { return }
|
|
283
|
+
self.items = result.items
|
|
284
|
+
self.tableView.reloadData()
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Use value captures to snapshot a value at closure creation time:
|
|
289
|
+
|
|
290
|
+
```swift
|
|
291
|
+
let currentCount = items.count
|
|
292
|
+
Task { [currentCount] in
|
|
293
|
+
await reportAnalytics(itemCount: currentCount)
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Pattern matching
|
|
298
|
+
|
|
299
|
+
`switch` must be exhaustive — the compiler enforces this for enums. Use `@unknown default` when switching on enums from external modules to catch future cases:
|
|
300
|
+
|
|
301
|
+
```swift
|
|
302
|
+
switch status {
|
|
303
|
+
case .pending:
|
|
304
|
+
showSpinner()
|
|
305
|
+
case .completed(let result):
|
|
306
|
+
display(result)
|
|
307
|
+
case .failed(let error) where error is NetworkError:
|
|
308
|
+
retryButton.isHidden = false
|
|
309
|
+
case .failed:
|
|
310
|
+
showGenericError()
|
|
311
|
+
@unknown default:
|
|
312
|
+
showGenericError()
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`if case let` and `guard case let` for single-pattern extraction:
|
|
317
|
+
|
|
318
|
+
```swift
|
|
319
|
+
if case .success(let user) = result {
|
|
320
|
+
greet(user)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
guard case .authenticated(let token) = session else {
|
|
324
|
+
throw AuthError.notAuthenticated
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Tuple patterns for combining conditions:
|
|
329
|
+
|
|
330
|
+
```swift
|
|
331
|
+
switch (isLoggedIn, hasPermission) {
|
|
332
|
+
case (true, true): proceed()
|
|
333
|
+
case (true, false): requestPermission()
|
|
334
|
+
case (false, _): showLogin()
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Naming conventions
|
|
339
|
+
|
|
340
|
+
Follow the [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/). Types and protocols use UpperCamelCase. Properties, methods, variables, and arguments use lowerCamelCase.
|
|
341
|
+
|
|
342
|
+
Booleans read as assertions: `isEmpty`, `hasPermission`, `canFly`, `shouldRefresh`. Mutating/nonmutating pairs: `sort()`/`sorted()`, `append()`/`appending()`, `formUnion()`/`union()`.
|
|
343
|
+
|
|
344
|
+
Argument labels form prepositional phrases with the method name: `move(to: position)`, `remove(at: index)`, `fade(from: color, to: color)`. Omit the label when the argument is the direct object of the verb: `print(value)`, `contains(element)`.
|
|
345
|
+
|
|
346
|
+
Protocols that describe what something is use nouns: `Collection`, `Sequence`, `Error`. Protocols that describe a capability use `-able`/`-ible`: `Codable`, `Sendable`, `Identifiable`.
|
|
347
|
+
|
|
348
|
+
## Testing
|
|
349
|
+
|
|
350
|
+
Use Swift Testing (`@Test`, `#expect`, `#require`, `@Suite`) for new tests. Use XCTest for UI tests and when the project is already standardized on it.
|
|
351
|
+
|
|
352
|
+
```swift
|
|
353
|
+
@Suite("UserService")
|
|
354
|
+
struct UserServiceTests {
|
|
355
|
+
let service = UserService(repository: MockRepository())
|
|
356
|
+
|
|
357
|
+
@Test("Returns user when found")
|
|
358
|
+
func userFound() async throws {
|
|
359
|
+
let user = try await service.getUser(id: 42)
|
|
360
|
+
#expect(user.name == "Alice")
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@Test("Throws not found for missing user")
|
|
364
|
+
func userNotFound() async {
|
|
365
|
+
await #expect(throws: AppError.notFound) {
|
|
366
|
+
try await service.getUser(id: 999)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
@Test("Validates email formats", arguments: [
|
|
371
|
+
("alice@example.com", true),
|
|
372
|
+
("not-an-email", false),
|
|
373
|
+
("", false),
|
|
374
|
+
])
|
|
375
|
+
func emailValidation(email: String, isValid: Bool) {
|
|
376
|
+
#expect(EmailValidator.isValid(email) == isValid)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Swift Testing provides parameterized tests via `arguments`, tags for organizing, and traits for configuring behavior. `#expect` replaces `XCTAssertEqual` with a single macro that captures the expression. `#require` unwraps optionals or throws on failure, replacing `XCTUnwrap`.
|
|
382
|
+
|
|
383
|
+
When to mock: external APIs with rate limits or costs, network-dependent behavior, error paths. When to use real instances: pure logic, value types, in-memory implementations. Test behavior, not implementation — test what a function returns or what state it changes, not how it works internally.
|
|
384
|
+
|
|
385
|
+
## Concurrency migration (Swift 6)
|
|
386
|
+
|
|
387
|
+
Enable strict concurrency checking incrementally. Start with warnings (`-strict-concurrency=targeted`), then move to `complete`, then enable the Swift 6 language mode.
|
|
388
|
+
|
|
389
|
+
Fix bottom-up: start with leaf modules that have no dependencies, make their types `Sendable`, then work up the dependency graph. Mark callbacks and closures as `@Sendable` when they cross isolation boundaries.
|
|
390
|
+
|
|
391
|
+
Use `nonisolated` to opt methods out of an actor's isolation when they only access immutable or `Sendable` state:
|
|
392
|
+
|
|
393
|
+
```swift
|
|
394
|
+
actor SessionManager {
|
|
395
|
+
let configuration: AppConfiguration // immutable
|
|
396
|
+
|
|
397
|
+
nonisolated var appName: String {
|
|
398
|
+
configuration.name
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tailwind-css-coding
|
|
3
|
+
description: Apply when writing or editing Tailwind CSS classes in any template or component file. Behavioral corrections for dynamic styling, class composition, responsive design, dark mode, interaction states, accessibility, and common antipatterns. Project conventions always override these defaults.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tailwind CSS Coding
|
|
7
|
+
|
|
8
|
+
Match the project's existing conventions. When uncertain, read 2-3 existing components to infer the local style. Check `package.json` for the `tailwindcss` version. v4 signals: `@tailwindcss/postcss` or `@tailwindcss/vite` in deps, `@import "tailwindcss"` in CSS, `@theme {}` blocks. v3 signals: `tailwind.config.js` with `module.exports`, `@tailwind base;` directives, `autoprefixer` as separate dep. These defaults apply only when the project has no established convention.
|
|
9
|
+
|
|
10
|
+
## Never rules
|
|
11
|
+
|
|
12
|
+
These are unconditional. They prevent broken builds, invisible bugs, and inaccessible UI regardless of project style.
|
|
13
|
+
|
|
14
|
+
- **Never construct class names dynamically** -- Tailwind's compiler scans source files as plain text with regex. It never executes code. Template literals, string concatenation, and interpolation produce classes the compiler cannot find. Use lookup maps of complete static strings.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
// Wrong: compiler cannot extract "bg-red-500" from this
|
|
18
|
+
const cls = `bg-${color}-500`;
|
|
19
|
+
|
|
20
|
+
// Correct: every class is a complete static string
|
|
21
|
+
const bgMap = {
|
|
22
|
+
red: "bg-red-500",
|
|
23
|
+
blue: "bg-blue-500",
|
|
24
|
+
} as const;
|
|
25
|
+
const cls = bgMap[color];
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **Never use template literal concatenation for class composition** -- CSS source order determines which class wins when two utilities target the same property, not HTML attribute order. `p-4 p-6` is unpredictable. Use `cn()` (clsx + tailwind-merge) to merge classes safely.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
// Wrong: conflicting padding, last-in-source wins (not last-in-string)
|
|
32
|
+
className={`p-4 ${isLarge ? "p-6" : ""}`}
|
|
33
|
+
|
|
34
|
+
// Correct: tailwind-merge resolves conflicts deterministically
|
|
35
|
+
import { cn } from "@/lib/utils";
|
|
36
|
+
className={cn("p-4", isLarge && "p-6")}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **Never use arbitrary values when a design token exists** -- `p-[16px]` is `p-4`. `bg-[#3b82f6]` is `bg-blue-500`. `w-[100%]` is `w-full`. `text-[14px]` is `text-sm`. Arbitrary values bypass the design system and create inconsistency.
|
|
40
|
+
|
|
41
|
+
- **Never omit interaction states on interactive elements** -- every button and link needs `hover:`, `focus-visible:`, and `disabled:` states. Add `transition-colors` for smooth feedback. In v4: add `cursor-pointer` explicitly -- Preflight no longer sets it on buttons.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// Wrong: no interaction feedback
|
|
45
|
+
<button className="bg-blue-500 text-white px-4 py-2 rounded">Save</button>
|
|
46
|
+
|
|
47
|
+
// Correct: full interaction states (v4: include cursor-pointer)
|
|
48
|
+
<button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors cursor-pointer">
|
|
49
|
+
Save
|
|
50
|
+
</button>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- **Never use `@apply` for patterns extractable to components** -- extract a React/Vue/Svelte component instead. `@apply` is only for third-party library overrides, CMS/Markdown HTML, and non-component template languages.
|
|
54
|
+
|
|
55
|
+
- **Never forget dark mode counterparts** -- every `bg-`, `text-`, and `border-` color needs a `dark:` variant, or use CSS variable theming to handle both modes in one declaration.
|
|
56
|
+
|
|
57
|
+
- **Never use `sm:` thinking it means "small screens"** -- `sm:` means 640px AND ABOVE. Unprefixed utilities apply to all screens (mobile-first). Write base styles for mobile, then layer breakpoints upward.
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<!-- Wrong mental model: "sm means small phones" -->
|
|
61
|
+
<div class="hidden sm:block">Only on small screens</div>
|
|
62
|
+
|
|
63
|
+
<!-- Correct: sm:block means "show at 640px and above" -->
|
|
64
|
+
<div class="block sm:hidden">Mobile only</div>
|
|
65
|
+
<div class="hidden sm:block">Desktop only</div>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **Never hallucinate class names** -- common fakes: `flex-center` (use `flex items-center justify-center`), `text-bold` (use `font-bold`), `bg-grey-500` (American spelling: `bg-gray-500`), `d-flex` (Bootstrap, not Tailwind).
|
|
69
|
+
|
|
70
|
+
- **Never output conflicting utilities** -- `p-4 p-6` is unpredictable. One value per CSS property. Don't redundantly set defaults (`flex flex-row`, `flex flex-nowrap`).
|
|
71
|
+
|
|
72
|
+
- **Never forget accessibility** -- use `sr-only` for icon-only button labels, `focus:not-sr-only` for skip links. Every interactive element needs visible focus indication via `focus-visible:`.
|
|
73
|
+
|
|
74
|
+
## Dynamic styling
|
|
75
|
+
|
|
76
|
+
For conditional classes, use `cn()` (clsx + tailwind-merge). For truly dynamic values that cannot be enumerated (user-set colors, computed positions), use inline `style` props -- these bypass the compiler entirely and always work.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// Conditional classes: cn()
|
|
80
|
+
<div className={cn("rounded p-4", isActive && "ring-2 ring-blue-500")} />
|
|
81
|
+
|
|
82
|
+
// Truly dynamic: inline style
|
|
83
|
+
<div className="rounded p-4" style={{ backgroundColor: user.brandColor }} />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For variant-driven styling, use a lookup map with complete static strings:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
const sizeClasses = {
|
|
90
|
+
sm: "px-2 py-1 text-sm",
|
|
91
|
+
md: "px-4 py-2 text-base",
|
|
92
|
+
lg: "px-6 py-3 text-lg",
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
function Button({ size = "md", className, ...props }: ButtonProps) {
|
|
96
|
+
return <button className={cn(sizeClasses[size], className)} {...props} />;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For components with 2+ variant dimensions, consider `cva` from class-variance-authority.
|
|
101
|
+
|
|
102
|
+
When the compiler must see classes that only appear in dynamic data (CMS content, database values), safelist them. In v4: `@source inline("bg-red-500 bg-blue-500")`. In v3: `safelist` array in `tailwind.config.js`.
|
|
103
|
+
|
|
104
|
+
## Class composition
|
|
105
|
+
|
|
106
|
+
The `cn()` helper combines `clsx` (conditional joining) with `tailwind-merge` (conflict resolution). Standard setup:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { clsx, type ClassValue } from "clsx";
|
|
110
|
+
import { twMerge } from "tailwind-merge";
|
|
111
|
+
|
|
112
|
+
export function cn(...inputs: ClassValue[]) {
|
|
113
|
+
return twMerge(clsx(inputs));
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use `cn()` whenever merging external `className` props with internal defaults -- raw concatenation silently breaks when both sides set the same property.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// Wrong: parent's p-6 may or may not override internal p-4
|
|
121
|
+
function Card({ className }: { className?: string }) {
|
|
122
|
+
return <div className={`rounded bg-white p-4 ${className}`} />;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Correct: tailwind-merge ensures parent overrides win
|
|
126
|
+
function Card({ className }: { className?: string }) {
|
|
127
|
+
return <div className={cn("rounded bg-white p-4", className)} />;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Responsive design
|
|
132
|
+
|
|
133
|
+
Mobile-first: write base styles for the smallest screen, then add breakpoints upward. Always order breakpoints `sm:` -> `md:` -> `lg:` -> `xl:` -> `2xl:`. Never skip to `lg:` without considering the gap.
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<!-- Single column on mobile, two on tablet, three on desktop -->
|
|
137
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
In v4: container queries are built-in (no plugin). Use `@container` for component-scoped responsive design:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
// Parent declares a container
|
|
144
|
+
<div className="@container">
|
|
145
|
+
{/* Child responds to container width, not viewport */}
|
|
146
|
+
<div className="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3 gap-4">
|
|
147
|
+
{children}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Dark mode
|
|
153
|
+
|
|
154
|
+
Cover every visible color. A component with `bg-white text-gray-900` needs `dark:bg-gray-900 dark:text-white`. Missing a single `dark:` variant causes unreadable text or invisible elements.
|
|
155
|
+
|
|
156
|
+
Better approach -- CSS variable theming. Define colors once, switch palettes:
|
|
157
|
+
|
|
158
|
+
```css
|
|
159
|
+
/* v4: @theme block */
|
|
160
|
+
@theme {
|
|
161
|
+
--color-surface: #ffffff;
|
|
162
|
+
--color-on-surface: #111827;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
166
|
+
|
|
167
|
+
.dark {
|
|
168
|
+
--color-surface: #111827;
|
|
169
|
+
--color-on-surface: #f9fafb;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Then use `bg-surface text-on-surface` everywhere -- no `dark:` variants needed per component.
|
|
174
|
+
|
|
175
|
+
## Interaction states
|
|
176
|
+
|
|
177
|
+
Minimum states for buttons: `hover:`, `focus-visible:`, `disabled:`, `transition-colors`. Minimum for inputs: `focus:`, `disabled:`, `placeholder:`. Minimum for links: `hover:`, `focus-visible:`.
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
// Complete button pattern
|
|
181
|
+
<button className={cn(
|
|
182
|
+
"px-4 py-2 rounded font-medium transition-colors",
|
|
183
|
+
"bg-blue-600 text-white",
|
|
184
|
+
"hover:bg-blue-700",
|
|
185
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600",
|
|
186
|
+
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
187
|
+
"cursor-pointer" // v4: Preflight no longer sets cursor:pointer on buttons
|
|
188
|
+
)}>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Complete input pattern:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<input className={cn(
|
|
195
|
+
"w-full rounded border px-3 py-2 transition-colors",
|
|
196
|
+
"border-gray-300 bg-white text-gray-900",
|
|
197
|
+
"dark:border-gray-600 dark:bg-gray-800 dark:text-white",
|
|
198
|
+
"placeholder:text-gray-400 dark:placeholder:text-gray-500",
|
|
199
|
+
"focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500",
|
|
200
|
+
"disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-50",
|
|
201
|
+
)} />
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
In v4: `hover:` only fires on hover-capable devices (`@media (hover: hover)`). Touch-only devices skip hover styles entirely -- don't rely on hover for critical information.
|
|
205
|
+
|
|
206
|
+
## Accessibility
|
|
207
|
+
|
|
208
|
+
Icon-only buttons need screen reader text:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
<button className="p-2 rounded hover:bg-gray-100">
|
|
212
|
+
<SearchIcon className="h-5 w-5" aria-hidden="true" />
|
|
213
|
+
<span className="sr-only">Search</span>
|
|
214
|
+
</button>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Skip links use `sr-only` that becomes visible on focus:
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
<a href="#main" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-white focus:text-black focus:rounded">
|
|
221
|
+
Skip to main content
|
|
222
|
+
</a>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Every focusable element needs a visible focus indicator. `focus-visible:` is preferred over `focus:` -- it only shows for keyboard navigation, not mouse clicks.
|
|
226
|
+
|
|
227
|
+
Ensure sufficient color contrast on interactive states. A `hover:bg-blue-700` on `bg-blue-600` is barely visible -- test both light and dark modes.
|
|
228
|
+
|
|
229
|
+
## v4 configuration
|
|
230
|
+
|
|
231
|
+
In v4, configuration lives in CSS, not JavaScript. Entry point is `@import "tailwindcss"`. Autoprefixer is built-in (don't add it separately).
|
|
232
|
+
|
|
233
|
+
```css
|
|
234
|
+
@import "tailwindcss";
|
|
235
|
+
|
|
236
|
+
@theme {
|
|
237
|
+
--font-display: "Inter", sans-serif;
|
|
238
|
+
--color-brand: #4f46e5;
|
|
239
|
+
--breakpoint-3xl: 1920px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@utility card {
|
|
243
|
+
background: var(--color-surface);
|
|
244
|
+
border-radius: var(--radius-lg);
|
|
245
|
+
padding: var(--spacing-6);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Key v4 renames (old names still work via compat but generate warnings):
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
v3 v4
|
|
253
|
+
shadow -> shadow-sm ring -> ring-3
|
|
254
|
+
shadow-sm -> shadow-xs outline-none -> outline-hidden
|
|
255
|
+
rounded -> rounded-sm bg-gradient-to-r -> bg-linear-to-r
|
|
256
|
+
rounded-sm -> rounded-xs blur -> blur-sm
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Other v4 changes inline:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
// v3: bg-opacity-50 -> v4: bg-black/50 (opacity modifier)
|
|
263
|
+
// v3: bg-[--brand-color] -> v4: bg-(--brand-color) (CSS variable syntax)
|
|
264
|
+
// v3: !bg-red-500 -> v4: bg-red-500! (important modifier at end)
|
|
265
|
+
// v3: first:*:pt-0 -> v4: *:first:pt-0 (variant stacking left-to-right)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Borders default to `currentColor` in v4, not `gray-200`. Add explicit border colors if the v3 default was relied upon. Custom utilities via `@utility name {}` not `@layer components {}`. Safelist via `@source inline("...")` not `safelist` array.
|