@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: swift-patterns
|
|
3
|
+
description: Swift patterns for protocols, async/await, Combine, SwiftUI state, property wrappers, and Codable.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Swift Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when writing Swift 5.9+ code for iOS, macOS, or server-side
|
|
11
|
+
Swift. Use this skill for protocol-oriented design, structured concurrency with
|
|
12
|
+
async/await, managing SwiftUI state, creating property wrappers, and encoding/
|
|
13
|
+
decoding data with Codable.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### Protocol-Oriented Design
|
|
18
|
+
|
|
19
|
+
Define protocols for behavior contracts. Use protocol extensions for default
|
|
20
|
+
implementations. Prefer protocols over class inheritance.
|
|
21
|
+
|
|
22
|
+
```swift
|
|
23
|
+
protocol Repository {
|
|
24
|
+
associatedtype Entity: Identifiable
|
|
25
|
+
func fetch(id: Entity.ID) async throws -> Entity
|
|
26
|
+
func save(_ entity: Entity) async throws
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
extension Repository {
|
|
30
|
+
func fetchOrCreate(id: Entity.ID, default: Entity) async throws -> Entity {
|
|
31
|
+
do {
|
|
32
|
+
return try await fetch(id: id)
|
|
33
|
+
} catch {
|
|
34
|
+
try await save(default)
|
|
35
|
+
return default
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
struct UserRepository: Repository {
|
|
41
|
+
func fetch(id: UUID) async throws -> User { ... }
|
|
42
|
+
func save(_ entity: User) async throws { ... }
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Async/Await and Structured Concurrency
|
|
47
|
+
|
|
48
|
+
Use `async let` for parallel work. Use `TaskGroup` for dynamic fan-out. Use
|
|
49
|
+
`Task` for fire-and-forget from synchronous contexts.
|
|
50
|
+
|
|
51
|
+
```swift
|
|
52
|
+
func loadDashboard() async throws -> Dashboard {
|
|
53
|
+
async let profile = fetchProfile()
|
|
54
|
+
async let notifications = fetchNotifications()
|
|
55
|
+
async let analytics = fetchAnalytics()
|
|
56
|
+
|
|
57
|
+
return try await Dashboard(
|
|
58
|
+
profile: profile,
|
|
59
|
+
notifications: notifications,
|
|
60
|
+
analytics: analytics
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func fetchAll(urls: [URL]) async throws -> [Data] {
|
|
65
|
+
try await withThrowingTaskGroup(of: (Int, Data).self) { group in
|
|
66
|
+
for (index, url) in urls.enumerated() {
|
|
67
|
+
group.addTask {
|
|
68
|
+
let (data, _) = try await URLSession.shared.data(from: url)
|
|
69
|
+
return (index, data)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var results = [Data](repeating: Data(), count: urls.count)
|
|
73
|
+
for try await (index, data) in group {
|
|
74
|
+
results[index] = data
|
|
75
|
+
}
|
|
76
|
+
return results
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Combine for Reactive Pipelines
|
|
82
|
+
|
|
83
|
+
Use Combine for event streams, timers, and publisher chains. Prefer async/await
|
|
84
|
+
for one-shot async work; use Combine when you need ongoing observation.
|
|
85
|
+
|
|
86
|
+
```swift
|
|
87
|
+
class SearchModel: ObservableObject {
|
|
88
|
+
@Published var query = ""
|
|
89
|
+
@Published private(set) var results: [SearchResult] = []
|
|
90
|
+
|
|
91
|
+
private var cancellables = Set<AnyCancellable>()
|
|
92
|
+
|
|
93
|
+
init(service: SearchService) {
|
|
94
|
+
$query
|
|
95
|
+
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
|
|
96
|
+
.removeDuplicates()
|
|
97
|
+
.filter { $0.count >= 2 }
|
|
98
|
+
.flatMap { query in
|
|
99
|
+
service.search(query: query)
|
|
100
|
+
.catch { _ in Just([]) }
|
|
101
|
+
}
|
|
102
|
+
.assign(to: &$results)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### SwiftUI State Management
|
|
108
|
+
|
|
109
|
+
Use `@State` for local view state, `@Binding` for child views, `@StateObject`
|
|
110
|
+
for owned observable objects, `@EnvironmentObject` for shared dependencies.
|
|
111
|
+
|
|
112
|
+
```swift
|
|
113
|
+
struct ItemListView: View {
|
|
114
|
+
@StateObject private var viewModel = ItemListViewModel()
|
|
115
|
+
@State private var showingAddSheet = false
|
|
116
|
+
|
|
117
|
+
var body: some View {
|
|
118
|
+
List(viewModel.items) { item in
|
|
119
|
+
ItemRow(item: item, onToggle: { viewModel.toggle(item) })
|
|
120
|
+
}
|
|
121
|
+
.searchable(text: $viewModel.searchText)
|
|
122
|
+
.sheet(isPresented: $showingAddSheet) {
|
|
123
|
+
AddItemView(onSave: { item in
|
|
124
|
+
viewModel.add(item)
|
|
125
|
+
showingAddSheet = false
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Observable
|
|
132
|
+
class ItemListViewModel {
|
|
133
|
+
var items: [Item] = []
|
|
134
|
+
var searchText = ""
|
|
135
|
+
|
|
136
|
+
var filteredItems: [Item] {
|
|
137
|
+
guard !searchText.isEmpty else { return items }
|
|
138
|
+
return items.filter { $0.title.localizedCaseInsensitiveContains(searchText) }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Property Wrappers
|
|
144
|
+
|
|
145
|
+
Create property wrappers for cross-cutting concerns: clamping values, UserDefaults
|
|
146
|
+
persistence, or validation.
|
|
147
|
+
|
|
148
|
+
```swift
|
|
149
|
+
@propertyWrapper
|
|
150
|
+
struct Clamped<Value: Comparable> {
|
|
151
|
+
private var value: Value
|
|
152
|
+
let range: ClosedRange<Value>
|
|
153
|
+
|
|
154
|
+
var wrappedValue: Value {
|
|
155
|
+
get { value }
|
|
156
|
+
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
|
|
160
|
+
self.range = range
|
|
161
|
+
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
struct AudioSettings {
|
|
166
|
+
@Clamped(0...100) var volume: Int = 50
|
|
167
|
+
@Clamped(0.5...2.0) var playbackSpeed: Double = 1.0
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Codable
|
|
172
|
+
|
|
173
|
+
Use `CodingKeys` for key mapping. Implement custom `init(from:)` for complex
|
|
174
|
+
decoding. Use `@propertyWrapper` for date formats.
|
|
175
|
+
|
|
176
|
+
```swift
|
|
177
|
+
struct APIResponse: Codable {
|
|
178
|
+
let id: Int
|
|
179
|
+
let displayName: String
|
|
180
|
+
let createdAt: Date
|
|
181
|
+
let tags: [String]
|
|
182
|
+
|
|
183
|
+
enum CodingKeys: String, CodingKey {
|
|
184
|
+
case id
|
|
185
|
+
case displayName = "display_name"
|
|
186
|
+
case createdAt = "created_at"
|
|
187
|
+
case tags
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Decoder configuration
|
|
192
|
+
let decoder = JSONDecoder()
|
|
193
|
+
decoder.dateDecodingStrategy = .iso8601
|
|
194
|
+
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Examples
|
|
198
|
+
|
|
199
|
+
**Pattern: Result type with typed errors**
|
|
200
|
+
```swift
|
|
201
|
+
enum NetworkError: Error, LocalizedError {
|
|
202
|
+
case notFound
|
|
203
|
+
case unauthorized
|
|
204
|
+
case serverError(statusCode: Int)
|
|
205
|
+
|
|
206
|
+
var errorDescription: String? {
|
|
207
|
+
switch self {
|
|
208
|
+
case .notFound: "Resource not found"
|
|
209
|
+
case .unauthorized: "Authentication required"
|
|
210
|
+
case .serverError(let code): "Server error (\(code))"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Checklist
|
|
217
|
+
|
|
218
|
+
- [ ] Protocols over class inheritance; protocol extensions for defaults
|
|
219
|
+
- [ ] `async let` for parallel work, `TaskGroup` for dynamic concurrency
|
|
220
|
+
- [ ] `@State` for local, `@StateObject` for owned objects, `@EnvironmentObject` for shared
|
|
221
|
+
- [ ] `@Observable` macro (iOS 17+) over `ObservableObject` where possible
|
|
222
|
+
- [ ] Property wrappers for cross-cutting validation and persistence
|
|
223
|
+
- [ ] `CodingKeys` for JSON key mapping; snake_case strategy on decoder
|
|
224
|
+
- [ ] Errors conform to `LocalizedError` with `errorDescription`
|
|
225
|
+
- [ ] `guard let` / `guard else` for early returns, not nested `if let`
|
|
226
|
+
- [ ] `Task` cancellation handled (check `Task.isCancelled` in loops)
|
|
227
|
+
- [ ] SwiftUI previews for every view with representative data
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tailwind-patterns
|
|
3
|
+
description: Write effective Tailwind CSS — responsive design, dark mode, animations, component extraction, and custom utilities.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tailwind CSS Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Use these patterns when building UIs with Tailwind CSS. Tailwind's utility-first
|
|
11
|
+
approach is fast but can produce unreadable class strings and duplicated styles
|
|
12
|
+
without discipline. These patterns keep your Tailwind code maintainable as the
|
|
13
|
+
project grows.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### 1. Responsive Design
|
|
18
|
+
|
|
19
|
+
Mobile-first breakpoints. Unprefixed utilities apply to all screens, prefixed
|
|
20
|
+
utilities apply at that breakpoint and above.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<div className="
|
|
24
|
+
grid grid-cols-1 gap-4 p-4
|
|
25
|
+
md:grid-cols-2 md:gap-6 md:p-6
|
|
26
|
+
lg:grid-cols-3 lg:gap-8
|
|
27
|
+
xl:grid-cols-4
|
|
28
|
+
">
|
|
29
|
+
{items.map(item => <Card key={item.id} item={item} />)}
|
|
30
|
+
</div>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Common breakpoints: `sm` (640px), `md` (768px), `lg` (1024px), `xl` (1280px), `2xl` (1536px).
|
|
34
|
+
|
|
35
|
+
Container pattern for consistent page width:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
39
|
+
{children}
|
|
40
|
+
</main>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Dark Mode
|
|
44
|
+
|
|
45
|
+
Use the `dark:` variant. Configure with `class` strategy for user-controlled
|
|
46
|
+
toggling.
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// tailwind.config.js
|
|
50
|
+
module.exports = {
|
|
51
|
+
darkMode: 'class', // toggled via class on <html>
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
|
57
|
+
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
|
58
|
+
Dashboard
|
|
59
|
+
</h1>
|
|
60
|
+
<p className="text-gray-600 dark:text-gray-400">
|
|
61
|
+
Welcome back
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Toggle implementation:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
function toggleDarkMode() {
|
|
70
|
+
document.documentElement.classList.toggle('dark');
|
|
71
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
72
|
+
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Initialize on page load
|
|
76
|
+
const theme = localStorage.getItem('theme') ??
|
|
77
|
+
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
78
|
+
document.documentElement.classList.toggle('dark', theme === 'dark');
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. Animations and Transitions
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// Hover transition
|
|
85
|
+
<button className="
|
|
86
|
+
rounded-lg bg-blue-600 px-4 py-2 text-white
|
|
87
|
+
transition-colors duration-200
|
|
88
|
+
hover:bg-blue-700
|
|
89
|
+
active:bg-blue-800
|
|
90
|
+
">
|
|
91
|
+
Save
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
// Smooth expand/collapse
|
|
95
|
+
<div className={`
|
|
96
|
+
overflow-hidden transition-all duration-300 ease-in-out
|
|
97
|
+
${isOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'}
|
|
98
|
+
`}>
|
|
99
|
+
{content}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
// Keyframe animation (built-in)
|
|
103
|
+
<div className="animate-pulse rounded-lg bg-gray-200 h-48" /> {/* skeleton */}
|
|
104
|
+
<div className="animate-spin h-5 w-5 border-2 border-white border-t-transparent rounded-full" /> {/* spinner */}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Custom animations in config:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// tailwind.config.js
|
|
111
|
+
module.exports = {
|
|
112
|
+
theme: {
|
|
113
|
+
extend: {
|
|
114
|
+
keyframes: {
|
|
115
|
+
'slide-in': {
|
|
116
|
+
'0%': { transform: 'translateX(100%)' },
|
|
117
|
+
'100%': { transform: 'translateX(0)' },
|
|
118
|
+
},
|
|
119
|
+
'fade-in': {
|
|
120
|
+
'0%': { opacity: '0' },
|
|
121
|
+
'100%': { opacity: '1' },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
animation: {
|
|
125
|
+
'slide-in': 'slide-in 0.3s ease-out',
|
|
126
|
+
'fade-in': 'fade-in 0.2s ease-in',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 4. Component Extraction with CVA
|
|
134
|
+
|
|
135
|
+
Use `class-variance-authority` to build variant-driven components.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
139
|
+
import { cn } from '@/lib/utils';
|
|
140
|
+
|
|
141
|
+
const buttonVariants = cva(
|
|
142
|
+
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
|
|
143
|
+
{
|
|
144
|
+
variants: {
|
|
145
|
+
variant: {
|
|
146
|
+
primary: 'bg-blue-600 text-white hover:bg-blue-700',
|
|
147
|
+
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-100',
|
|
148
|
+
danger: 'bg-red-600 text-white hover:bg-red-700',
|
|
149
|
+
ghost: 'hover:bg-gray-100 dark:hover:bg-gray-800',
|
|
150
|
+
},
|
|
151
|
+
size: {
|
|
152
|
+
sm: 'h-8 px-3 text-sm',
|
|
153
|
+
md: 'h-10 px-4 text-sm',
|
|
154
|
+
lg: 'h-12 px-6 text-base',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
defaultVariants: {
|
|
158
|
+
variant: 'primary',
|
|
159
|
+
size: 'md',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
interface ButtonProps
|
|
165
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
166
|
+
VariantProps<typeof buttonVariants> {}
|
|
167
|
+
|
|
168
|
+
export function Button({ className, variant, size, ...props }: ButtonProps) {
|
|
169
|
+
return (
|
|
170
|
+
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The `cn` utility merges Tailwind classes without conflicts:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
179
|
+
import { twMerge } from 'tailwind-merge';
|
|
180
|
+
|
|
181
|
+
export function cn(...inputs: ClassValue[]) {
|
|
182
|
+
return twMerge(clsx(inputs));
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 5. Custom Utilities
|
|
187
|
+
|
|
188
|
+
Extend Tailwind for project-specific needs.
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
// tailwind.config.js
|
|
192
|
+
module.exports = {
|
|
193
|
+
theme: {
|
|
194
|
+
extend: {
|
|
195
|
+
colors: {
|
|
196
|
+
brand: {
|
|
197
|
+
50: '#f0f4ff',
|
|
198
|
+
500: '#3b6cf5',
|
|
199
|
+
600: '#2a5ce0',
|
|
200
|
+
700: '#1a4cc0',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
spacing: {
|
|
204
|
+
'18': '4.5rem',
|
|
205
|
+
'88': '22rem',
|
|
206
|
+
},
|
|
207
|
+
fontSize: {
|
|
208
|
+
'display': ['3.5rem', { lineHeight: '1.1', letterSpacing: '-0.02em' }],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 6. Prose for Long-Form Content
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
<article className="prose prose-lg dark:prose-invert max-w-none">
|
|
219
|
+
<h1>Getting Started</h1>
|
|
220
|
+
<p>This content is styled automatically by the typography plugin.</p>
|
|
221
|
+
<pre><code>npm install @bootspring/cli</code></pre>
|
|
222
|
+
</article>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Install: `@tailwindcss/typography` plugin.
|
|
226
|
+
|
|
227
|
+
### 7. Layout Patterns
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
// Sticky header
|
|
231
|
+
<header className="sticky top-0 z-50 border-b bg-white/80 backdrop-blur dark:bg-gray-900/80">
|
|
232
|
+
<nav className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4">
|
|
233
|
+
...
|
|
234
|
+
</nav>
|
|
235
|
+
</header>
|
|
236
|
+
|
|
237
|
+
// Sidebar layout
|
|
238
|
+
<div className="flex h-screen">
|
|
239
|
+
<aside className="hidden w-64 shrink-0 border-r md:block">...</aside>
|
|
240
|
+
<main className="flex-1 overflow-y-auto p-6">...</main>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
// Card grid with consistent height
|
|
244
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
245
|
+
<div className="flex flex-col rounded-lg border p-6">
|
|
246
|
+
<h3 className="text-lg font-semibold">Title</h3>
|
|
247
|
+
<p className="mt-2 flex-1 text-gray-600">Description fills space</p>
|
|
248
|
+
<button className="mt-4">Action</button>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Examples
|
|
254
|
+
|
|
255
|
+
| Pattern | Classes | Purpose |
|
|
256
|
+
|---------|---------|---------|
|
|
257
|
+
| Responsive grid | `grid-cols-1 md:grid-cols-2 lg:grid-cols-3` | Adapts to screen size |
|
|
258
|
+
| Dark mode text | `text-gray-900 dark:text-gray-100` | Theme-aware content |
|
|
259
|
+
| Smooth hover | `transition-colors duration-200 hover:bg-blue-700` | Polished interactions |
|
|
260
|
+
| Loading skeleton | `animate-pulse bg-gray-200 rounded h-4` | Content placeholder |
|
|
261
|
+
| Truncated text | `truncate` or `line-clamp-2` | Overflow prevention |
|
|
262
|
+
|
|
263
|
+
## Checklist
|
|
264
|
+
|
|
265
|
+
- [ ] All responsive styles are mobile-first (unprefixed = mobile, `md:` = tablet, `lg:` = desktop)
|
|
266
|
+
- [ ] Dark mode uses `dark:` variants with `darkMode: 'class'` strategy
|
|
267
|
+
- [ ] Repeated class patterns are extracted into components with CVA
|
|
268
|
+
- [ ] `cn()` utility (clsx + tailwind-merge) is used for conditional and merged classes
|
|
269
|
+
- [ ] Custom colors use the brand scale defined in `tailwind.config.js`
|
|
270
|
+
- [ ] Transitions use `duration-200` or `duration-300` for polished feel
|
|
271
|
+
- [ ] Long class strings are broken across multiple lines for readability
|
|
272
|
+
- [ ] The `@tailwindcss/typography` plugin is used for prose content
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tdd-workflow
|
|
3
|
+
description: Test-driven development workflow with red-green-refactor, test doubles, outside-in TDD, and property-based testing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Test-Driven Development
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when building new features, fixing bugs, or refactoring existing code. TDD catches design problems early, produces naturally testable code, and creates a safety net for future changes. Skip TDD only for throwaway spikes or exploratory prototypes that will be rewritten.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Red-Green-Refactor Cycle
|
|
14
|
+
|
|
15
|
+
The core loop. Never skip a step.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
RED -> Write a failing test for the next behavior you need
|
|
19
|
+
GREEN -> Write the minimum code to make it pass
|
|
20
|
+
REFACTOR -> Clean up without changing behavior (tests stay green)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// RED -- test for a function that doesn't exist yet
|
|
25
|
+
import { describe, it, expect } from "vitest";
|
|
26
|
+
import { calculateDiscount } from "./pricing";
|
|
27
|
+
|
|
28
|
+
describe("calculateDiscount", () => {
|
|
29
|
+
it("applies 10% discount for orders over $100", () => {
|
|
30
|
+
expect(calculateDiscount(150)).toBe(15);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns 0 for orders at or below $100", () => {
|
|
34
|
+
expect(calculateDiscount(100)).toBe(0);
|
|
35
|
+
expect(calculateDiscount(50)).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// GREEN -- simplest implementation
|
|
40
|
+
export function calculateDiscount(orderTotal: number): number {
|
|
41
|
+
if (orderTotal > 100) return orderTotal * 0.1;
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// REFACTOR -- extract magic numbers
|
|
46
|
+
const DISCOUNT_THRESHOLD = 100;
|
|
47
|
+
const DISCOUNT_RATE = 0.1;
|
|
48
|
+
|
|
49
|
+
export function calculateDiscount(orderTotal: number): number {
|
|
50
|
+
return orderTotal > DISCOUNT_THRESHOLD ? orderTotal * DISCOUNT_RATE : 0;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Each cycle should take 1-5 minutes. If you spend longer, the step is too big.
|
|
55
|
+
|
|
56
|
+
### Test Doubles
|
|
57
|
+
|
|
58
|
+
Use the right kind of double for the job:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { vi, describe, it, expect } from "vitest";
|
|
62
|
+
|
|
63
|
+
// STUB -- returns canned data, no assertions on calls
|
|
64
|
+
const userRepo = {
|
|
65
|
+
findById: vi.fn().mockResolvedValue({ id: "1", name: "Alice" }),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// MOCK -- asserts specific interactions happened
|
|
69
|
+
const emailService = { send: vi.fn() };
|
|
70
|
+
await resetPassword(userRepo, emailService, "alice@test.com");
|
|
71
|
+
expect(emailService.send).toHaveBeenCalledWith(
|
|
72
|
+
expect.objectContaining({
|
|
73
|
+
to: "alice@test.com",
|
|
74
|
+
subject: expect.stringContaining("Reset"),
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// SPY -- wraps real implementation, records calls
|
|
79
|
+
const logSpy = vi.spyOn(console, "warn");
|
|
80
|
+
processInput(invalidData);
|
|
81
|
+
expect(logSpy).toHaveBeenCalledOnce();
|
|
82
|
+
logSpy.mockRestore();
|
|
83
|
+
|
|
84
|
+
// FAKE -- working in-memory implementation
|
|
85
|
+
class InMemoryUserRepo implements UserRepository {
|
|
86
|
+
private users = new Map<string, User>();
|
|
87
|
+
async save(user: User) { this.users.set(user.id, user); }
|
|
88
|
+
async findById(id: string) { return this.users.get(id) ?? null; }
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Prefer fakes for repositories and stubs for external services. Only use mocks when verifying that a side effect actually happened.
|
|
93
|
+
|
|
94
|
+
### Outside-In TDD (London School)
|
|
95
|
+
|
|
96
|
+
Start from the outermost boundary and work inward:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Step 1: API test (outermost layer)
|
|
100
|
+
describe("POST /api/orders", () => {
|
|
101
|
+
it("creates an order and returns 201", async () => {
|
|
102
|
+
const res = await request(app)
|
|
103
|
+
.post("/api/orders")
|
|
104
|
+
.send({ items: [{ sku: "WIDGET-1", qty: 2 }] });
|
|
105
|
+
expect(res.status).toBe(201);
|
|
106
|
+
expect(res.body.order.total).toBe(49.98);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Step 2: Discover the service interface from the route handler
|
|
111
|
+
// -> orderService.create(items) => Order
|
|
112
|
+
|
|
113
|
+
// Step 3: Discover the repository interface from the service
|
|
114
|
+
// -> productRepo.findBySku(sku) => Product
|
|
115
|
+
|
|
116
|
+
// Step 4: Implement the repository (innermost layer)
|
|
117
|
+
// -> Use a fake for unit tests, real DB for integration tests
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Each layer defines the interface it needs from the next layer down.
|
|
121
|
+
|
|
122
|
+
### Property-Based Testing
|
|
123
|
+
|
|
124
|
+
Instead of testing individual examples, declare properties that must always hold:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { fc } from "@fast-check/vitest";
|
|
128
|
+
import { describe, it, expect } from "vitest";
|
|
129
|
+
|
|
130
|
+
describe("sort", () => {
|
|
131
|
+
it.prop([fc.array(fc.integer())])("output length equals input length", (arr) => {
|
|
132
|
+
expect(sort(arr)).toHaveLength(arr.length);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it.prop([fc.array(fc.integer())])("output is ordered", (arr) => {
|
|
136
|
+
const sorted = sort(arr);
|
|
137
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
138
|
+
expect(sorted[i]).toBeGreaterThanOrEqual(sorted[i - 1]);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it.prop([fc.array(fc.integer())])("output contains same elements", (arr) => {
|
|
143
|
+
expect(sort(arr).sort()).toEqual([...arr].sort());
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Property-based testing excels at finding edge cases: empty arrays, negative numbers, duplicates, and boundary values that hand-written examples miss.
|
|
149
|
+
|
|
150
|
+
### Mutation Testing
|
|
151
|
+
|
|
152
|
+
Tests pass, but are they actually checking the right things?
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx stryker run
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
// stryker.config.mjs
|
|
160
|
+
export default {
|
|
161
|
+
mutator: "typescript",
|
|
162
|
+
testRunner: "vitest",
|
|
163
|
+
reporters: ["html", "clear-text"],
|
|
164
|
+
coverageAnalysis: "perTest",
|
|
165
|
+
thresholds: { high: 80, low: 60, break: 50 },
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Common mutations Stryker introduces: replace `>` with `>=`, remove function calls, swap `true`/`false`, replace `+` with `-`. A mutation score below 70% means tests are passing but not actually verifying behavior.
|
|
170
|
+
|
|
171
|
+
### When to Write Tests After
|
|
172
|
+
|
|
173
|
+
TDD is the default, but some situations call for test-after:
|
|
174
|
+
|
|
175
|
+
- **Exploratory spikes** -- learning how an API works, throw away the code
|
|
176
|
+
- **UI layout** -- visual correctness verified by eye or screenshot tests
|
|
177
|
+
- **Generated code** -- test the generator, not every generated file
|
|
178
|
+
|
|
179
|
+
Even in test-after mode, write tests before considering the work done.
|
|
180
|
+
|
|
181
|
+
## Examples
|
|
182
|
+
|
|
183
|
+
| Situation | TDD Approach | Outcome |
|
|
184
|
+
|-----------|-------------|---------|
|
|
185
|
+
| New pricing engine | Red-green-refactor, one rule at a time | Each rule tested in isolation |
|
|
186
|
+
| Integrating Stripe webhooks | Outside-in from route handler | Clean service boundary |
|
|
187
|
+
| Bug: discount applied twice | Write failing test reproducing the bug | Regression test prevents reoccurrence |
|
|
188
|
+
| Refactoring a 500-line function | Characterization tests, then extract | Safe refactoring with full coverage |
|
|
189
|
+
| Serializer edge cases | Property-based testing | Finds empty/null/unicode edge cases |
|
|
190
|
+
|
|
191
|
+
## Checklist
|
|
192
|
+
- [ ] Every new feature starts with a failing test
|
|
193
|
+
- [ ] Each red-green-refactor cycle takes under 5 minutes
|
|
194
|
+
- [ ] Test doubles chosen intentionally (fake > stub > mock)
|
|
195
|
+
- [ ] Tests verify behavior, not implementation details
|
|
196
|
+
- [ ] Outside-in tests define interfaces from the caller's perspective
|
|
197
|
+
- [ ] Property-based tests cover invariants for pure functions
|
|
198
|
+
- [ ] Mutation testing runs on critical business logic (score > 70%)
|
|
199
|
+
- [ ] Bug fixes include a regression test written before the fix
|