autoworkflow 3.1.5 → 3.6.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/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# C# Skill
|
|
2
|
+
|
|
3
|
+
## Records (Immutable Data Types)
|
|
4
|
+
\`\`\`csharp
|
|
5
|
+
// Record with positional syntax
|
|
6
|
+
public record User(string Id, string Email, string Name, bool IsActive = true);
|
|
7
|
+
|
|
8
|
+
// Record with properties
|
|
9
|
+
public record UserResponse
|
|
10
|
+
{
|
|
11
|
+
public required string Id { get; init; }
|
|
12
|
+
public required string Email { get; init; }
|
|
13
|
+
public required string Name { get; init; }
|
|
14
|
+
public bool IsActive { get; init; } = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// With-expressions for copying
|
|
18
|
+
var updatedUser = user with { Name = "New Name", IsActive = false };
|
|
19
|
+
|
|
20
|
+
// Deconstruction
|
|
21
|
+
var (id, email, name, _) = user;
|
|
22
|
+
|
|
23
|
+
// Record struct (value type, no heap allocation)
|
|
24
|
+
public readonly record struct Point(int X, int Y);
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
## Nullable Reference Types
|
|
28
|
+
\`\`\`csharp
|
|
29
|
+
// Enable in .csproj: <Nullable>enable</Nullable>
|
|
30
|
+
|
|
31
|
+
// Non-nullable by default
|
|
32
|
+
public string Name { get; set; } // Cannot be null
|
|
33
|
+
|
|
34
|
+
// Explicitly nullable
|
|
35
|
+
public string? MiddleName { get; set; } // Can be null
|
|
36
|
+
|
|
37
|
+
// Null-conditional and null-coalescing
|
|
38
|
+
var displayName = user?.Name ?? "Anonymous";
|
|
39
|
+
var length = user?.Name?.Length ?? 0;
|
|
40
|
+
|
|
41
|
+
// Null-coalescing assignment
|
|
42
|
+
user.Name ??= "Default Name";
|
|
43
|
+
|
|
44
|
+
// Null-forgiving operator (use sparingly)
|
|
45
|
+
var name = user!.Name; // Tell compiler this won't be null
|
|
46
|
+
|
|
47
|
+
// Pattern matching for null checks
|
|
48
|
+
if (user is { Name: not null } u)
|
|
49
|
+
{
|
|
50
|
+
Console.WriteLine(u.Name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Required members (C# 11+)
|
|
54
|
+
public class User
|
|
55
|
+
{
|
|
56
|
+
public required string Id { get; init; }
|
|
57
|
+
public required string Email { get; init; }
|
|
58
|
+
public string? Name { get; init; }
|
|
59
|
+
}
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
## Pattern Matching
|
|
63
|
+
\`\`\`csharp
|
|
64
|
+
// Type patterns
|
|
65
|
+
object obj = GetValue();
|
|
66
|
+
if (obj is string s)
|
|
67
|
+
{
|
|
68
|
+
Console.WriteLine(s.Length);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Switch expressions
|
|
72
|
+
string GetStatusMessage(UserStatus status) => status switch
|
|
73
|
+
{
|
|
74
|
+
UserStatus.Active => "User is active",
|
|
75
|
+
UserStatus.Pending => "Awaiting verification",
|
|
76
|
+
UserStatus.Suspended => "Account suspended",
|
|
77
|
+
UserStatus.Deleted => "Account deleted",
|
|
78
|
+
_ => throw new ArgumentOutOfRangeException(nameof(status))
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Property patterns
|
|
82
|
+
string Describe(User user) => user switch
|
|
83
|
+
{
|
|
84
|
+
{ IsActive: true, Role: "Admin" } => "Active admin",
|
|
85
|
+
{ IsActive: true } => "Active user",
|
|
86
|
+
{ IsActive: false } => "Inactive user",
|
|
87
|
+
null => throw new ArgumentNullException(nameof(user))
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Relational and logical patterns
|
|
91
|
+
string GetPriceCategory(decimal price) => price switch
|
|
92
|
+
{
|
|
93
|
+
< 10 => "Cheap",
|
|
94
|
+
>= 10 and < 50 => "Moderate",
|
|
95
|
+
>= 50 and < 100 => "Expensive",
|
|
96
|
+
>= 100 => "Premium",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// List patterns (C# 11+)
|
|
100
|
+
int[] numbers = { 1, 2, 3, 4, 5 };
|
|
101
|
+
var result = numbers switch
|
|
102
|
+
{
|
|
103
|
+
[1, 2, ..] => "Starts with 1, 2",
|
|
104
|
+
[.., 4, 5] => "Ends with 4, 5",
|
|
105
|
+
[_, _, _, ..] => "Has at least 3 elements",
|
|
106
|
+
[] => "Empty",
|
|
107
|
+
_ => "Other"
|
|
108
|
+
};
|
|
109
|
+
\`\`\`
|
|
110
|
+
|
|
111
|
+
## LINQ
|
|
112
|
+
\`\`\`csharp
|
|
113
|
+
// Query syntax
|
|
114
|
+
var activeUsers = from user in users
|
|
115
|
+
where user.IsActive
|
|
116
|
+
orderby user.CreatedAt descending
|
|
117
|
+
select new { user.Id, user.Email };
|
|
118
|
+
|
|
119
|
+
// Method syntax (more common)
|
|
120
|
+
var result = users
|
|
121
|
+
.Where(u => u.IsActive)
|
|
122
|
+
.OrderByDescending(u => u.CreatedAt)
|
|
123
|
+
.Select(u => new { u.Id, u.Email })
|
|
124
|
+
.ToList();
|
|
125
|
+
|
|
126
|
+
// Common operations
|
|
127
|
+
var first = users.FirstOrDefault(u => u.Email == email);
|
|
128
|
+
var any = users.Any(u => u.IsActive);
|
|
129
|
+
var all = users.All(u => u.Email.Contains("@"));
|
|
130
|
+
var count = users.Count(u => u.IsActive);
|
|
131
|
+
|
|
132
|
+
// Grouping
|
|
133
|
+
var byDomain = users
|
|
134
|
+
.GroupBy(u => u.Email.Split('@')[1])
|
|
135
|
+
.Select(g => new { Domain = g.Key, Count = g.Count() });
|
|
136
|
+
|
|
137
|
+
// Join
|
|
138
|
+
var usersWithPosts = users.Join(
|
|
139
|
+
posts,
|
|
140
|
+
u => u.Id,
|
|
141
|
+
p => p.AuthorId,
|
|
142
|
+
(user, post) => new { User = user, Post = post }
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Aggregation
|
|
146
|
+
var stats = users
|
|
147
|
+
.GroupBy(u => u.Department)
|
|
148
|
+
.Select(g => new
|
|
149
|
+
{
|
|
150
|
+
Department = g.Key,
|
|
151
|
+
Count = g.Count(),
|
|
152
|
+
AvgSalary = g.Average(u => u.Salary),
|
|
153
|
+
MaxSalary = g.Max(u => u.Salary)
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// SelectMany for flattening
|
|
157
|
+
var allTags = posts
|
|
158
|
+
.SelectMany(p => p.Tags)
|
|
159
|
+
.Distinct()
|
|
160
|
+
.ToList();
|
|
161
|
+
|
|
162
|
+
// Chunk (C# 11+ / .NET 6+)
|
|
163
|
+
var batches = users.Chunk(100);
|
|
164
|
+
foreach (var batch in batches)
|
|
165
|
+
{
|
|
166
|
+
await ProcessBatch(batch);
|
|
167
|
+
}
|
|
168
|
+
\`\`\`
|
|
169
|
+
|
|
170
|
+
## Async/Await
|
|
171
|
+
\`\`\`csharp
|
|
172
|
+
// Async method
|
|
173
|
+
public async Task<User?> GetUserAsync(string id, CancellationToken ct = default)
|
|
174
|
+
{
|
|
175
|
+
return await _context.Users
|
|
176
|
+
.FirstOrDefaultAsync(u => u.Id == id, ct);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Multiple async operations
|
|
180
|
+
public async Task<UserWithPosts> GetUserWithPostsAsync(string userId)
|
|
181
|
+
{
|
|
182
|
+
var userTask = GetUserAsync(userId);
|
|
183
|
+
var postsTask = GetPostsAsync(userId);
|
|
184
|
+
|
|
185
|
+
await Task.WhenAll(userTask, postsTask);
|
|
186
|
+
|
|
187
|
+
return new UserWithPosts(userTask.Result!, postsTask.Result);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Async streams
|
|
191
|
+
public async IAsyncEnumerable<User> GetUsersStreamAsync(
|
|
192
|
+
[EnumeratorCancellation] CancellationToken ct = default)
|
|
193
|
+
{
|
|
194
|
+
await foreach (var user in _context.Users.AsAsyncEnumerable().WithCancellation(ct))
|
|
195
|
+
{
|
|
196
|
+
yield return user;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ValueTask for high-performance scenarios
|
|
201
|
+
public ValueTask<User?> GetCachedUserAsync(string id)
|
|
202
|
+
{
|
|
203
|
+
if (_cache.TryGetValue(id, out var user))
|
|
204
|
+
{
|
|
205
|
+
return ValueTask.FromResult<User?>(user);
|
|
206
|
+
}
|
|
207
|
+
return new ValueTask<User?>(FetchUserAsync(id));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ConfigureAwait for library code
|
|
211
|
+
public async Task<string> LibraryMethodAsync()
|
|
212
|
+
{
|
|
213
|
+
var result = await SomeOperationAsync().ConfigureAwait(false);
|
|
214
|
+
return result.ToString();
|
|
215
|
+
}
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
## Collections
|
|
219
|
+
\`\`\`csharp
|
|
220
|
+
// Immutable collections
|
|
221
|
+
using System.Collections.Immutable;
|
|
222
|
+
|
|
223
|
+
ImmutableList<string> names = ImmutableList.Create("Alice", "Bob");
|
|
224
|
+
var newNames = names.Add("Charlie"); // Returns new list
|
|
225
|
+
|
|
226
|
+
// Collection expressions (C# 12+)
|
|
227
|
+
List<int> numbers = [1, 2, 3, 4, 5];
|
|
228
|
+
int[] array = [1, 2, 3];
|
|
229
|
+
Span<int> span = [1, 2, 3];
|
|
230
|
+
|
|
231
|
+
// Spread operator
|
|
232
|
+
int[] combined = [..array1, ..array2];
|
|
233
|
+
|
|
234
|
+
// Dictionary patterns
|
|
235
|
+
var userById = users.ToDictionary(u => u.Id);
|
|
236
|
+
var count = userById.GetValueOrDefault(id)?.Posts.Count ?? 0;
|
|
237
|
+
|
|
238
|
+
// Concurrent collections
|
|
239
|
+
var cache = new ConcurrentDictionary<string, User>();
|
|
240
|
+
var user = cache.GetOrAdd(id, _ => FetchUser(id));
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
## Exception Handling
|
|
244
|
+
\`\`\`csharp
|
|
245
|
+
// Custom exception
|
|
246
|
+
public class NotFoundException : Exception
|
|
247
|
+
{
|
|
248
|
+
public string ResourceType { get; }
|
|
249
|
+
public string ResourceId { get; }
|
|
250
|
+
|
|
251
|
+
public NotFoundException(string resourceType, string resourceId)
|
|
252
|
+
: base($"{resourceType} with id '{resourceId}' not found")
|
|
253
|
+
{
|
|
254
|
+
ResourceType = resourceType;
|
|
255
|
+
ResourceId = resourceId;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Try pattern
|
|
260
|
+
public bool TryGetUser(string id, out User? user)
|
|
261
|
+
{
|
|
262
|
+
user = _users.FirstOrDefault(u => u.Id == id);
|
|
263
|
+
return user is not null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Exception filters
|
|
267
|
+
try
|
|
268
|
+
{
|
|
269
|
+
await httpClient.GetAsync(url);
|
|
270
|
+
}
|
|
271
|
+
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
|
272
|
+
{
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
|
|
276
|
+
{
|
|
277
|
+
throw new AuthenticationException("Invalid credentials", ex);
|
|
278
|
+
}
|
|
279
|
+
\`\`\`
|
|
280
|
+
|
|
281
|
+
## ✅ DO
|
|
282
|
+
- Use records for immutable data (DTOs, value objects)
|
|
283
|
+
- Enable nullable reference types
|
|
284
|
+
- Use pattern matching for type checks and deconstruction
|
|
285
|
+
- Use \`async/await\` throughout - avoid \`.Result\` and \`.Wait()\`
|
|
286
|
+
- Pass \`CancellationToken\` to async methods
|
|
287
|
+
- Use collection expressions (C# 12+)
|
|
288
|
+
- Use \`required\` for mandatory properties
|
|
289
|
+
|
|
290
|
+
## ❌ DON'T
|
|
291
|
+
- Don't use \`async void\` except for event handlers
|
|
292
|
+
- Don't ignore \`CancellationToken\`
|
|
293
|
+
- Don't use \`?.Result\` on tasks (can deadlock)
|
|
294
|
+
- Don't catch \`Exception\` without rethrowing or logging
|
|
295
|
+
- Don't use \`null!\` to silence nullable warnings
|
|
296
|
+
- Don't use mutable collections when immutable suffice
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# CSS Modules Skill
|
|
2
|
+
|
|
3
|
+
## Basic Usage
|
|
4
|
+
\`\`\`tsx
|
|
5
|
+
// Button.module.css
|
|
6
|
+
.button {
|
|
7
|
+
padding: 0.5rem 1rem;
|
|
8
|
+
border: none;
|
|
9
|
+
border-radius: 0.375rem;
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
transition: background 0.2s;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.primary {
|
|
15
|
+
background: #3b82f6;
|
|
16
|
+
color: white;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.primary:hover {
|
|
20
|
+
background: #2563eb;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.secondary {
|
|
24
|
+
background: #6b7280;
|
|
25
|
+
color: white;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Button.tsx
|
|
29
|
+
import styles from './Button.module.css';
|
|
30
|
+
|
|
31
|
+
interface ButtonProps {
|
|
32
|
+
variant?: 'primary' | 'secondary';
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function Button({ variant = 'primary', children }: ButtonProps) {
|
|
37
|
+
return (
|
|
38
|
+
<button className={\`\${styles.button} \${styles[variant]}\`}>
|
|
39
|
+
{children}
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
## Composition (composes)
|
|
46
|
+
\`\`\`css
|
|
47
|
+
/* shared.module.css */
|
|
48
|
+
.flexCenter {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.truncate {
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Card.module.css */
|
|
61
|
+
.card {
|
|
62
|
+
composes: flexCenter from './shared.module.css';
|
|
63
|
+
padding: 1.5rem;
|
|
64
|
+
border-radius: 0.5rem;
|
|
65
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.title {
|
|
69
|
+
composes: truncate from './shared.module.css';
|
|
70
|
+
font-size: 1.25rem;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Multiple compositions */
|
|
75
|
+
.header {
|
|
76
|
+
composes: flexCenter truncate from './shared.module.css';
|
|
77
|
+
}
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## Global Styles
|
|
81
|
+
\`\`\`css
|
|
82
|
+
/* Component.module.css */
|
|
83
|
+
|
|
84
|
+
/* Local class (default) */
|
|
85
|
+
.container {
|
|
86
|
+
padding: 1rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Global class within module */
|
|
90
|
+
:global(.external-library-class) {
|
|
91
|
+
color: red;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Mix local and global */
|
|
95
|
+
.container :global(.markdown-body) {
|
|
96
|
+
line-height: 1.6;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Global block */
|
|
100
|
+
:global {
|
|
101
|
+
.some-global-class {
|
|
102
|
+
color: blue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
body.dark-mode {
|
|
106
|
+
background: #1a1a1a;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
\`\`\`
|
|
110
|
+
|
|
111
|
+
## Combining Classes
|
|
112
|
+
\`\`\`tsx
|
|
113
|
+
import styles from './Button.module.css';
|
|
114
|
+
import clsx from 'clsx'; // or classnames library
|
|
115
|
+
|
|
116
|
+
interface ButtonProps {
|
|
117
|
+
variant?: 'primary' | 'secondary';
|
|
118
|
+
size?: 'sm' | 'md' | 'lg';
|
|
119
|
+
disabled?: boolean;
|
|
120
|
+
fullWidth?: boolean;
|
|
121
|
+
className?: string;
|
|
122
|
+
children: React.ReactNode;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function Button({
|
|
126
|
+
variant = 'primary',
|
|
127
|
+
size = 'md',
|
|
128
|
+
disabled,
|
|
129
|
+
fullWidth,
|
|
130
|
+
className,
|
|
131
|
+
children,
|
|
132
|
+
}: ButtonProps) {
|
|
133
|
+
return (
|
|
134
|
+
<button
|
|
135
|
+
className={clsx(
|
|
136
|
+
styles.button,
|
|
137
|
+
styles[variant],
|
|
138
|
+
styles[size],
|
|
139
|
+
{
|
|
140
|
+
[styles.disabled]: disabled,
|
|
141
|
+
[styles.fullWidth]: fullWidth,
|
|
142
|
+
},
|
|
143
|
+
className // Allow external classes
|
|
144
|
+
)}
|
|
145
|
+
disabled={disabled}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</button>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Button.module.css
|
|
153
|
+
.button { /* base styles */ }
|
|
154
|
+
.primary { /* primary variant */ }
|
|
155
|
+
.secondary { /* secondary variant */ }
|
|
156
|
+
.sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; }
|
|
157
|
+
.md { padding: 0.5rem 1rem; }
|
|
158
|
+
.lg { padding: 0.75rem 1.5rem; font-size: 1.125rem; }
|
|
159
|
+
.disabled { opacity: 0.5; cursor: not-allowed; }
|
|
160
|
+
.fullWidth { width: 100%; }
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
## CSS Variables Integration
|
|
164
|
+
\`\`\`css
|
|
165
|
+
/* variables.css (global) */
|
|
166
|
+
:root {
|
|
167
|
+
--color-primary: #3b82f6;
|
|
168
|
+
--color-secondary: #6b7280;
|
|
169
|
+
--color-danger: #ef4444;
|
|
170
|
+
--spacing-sm: 0.5rem;
|
|
171
|
+
--spacing-md: 1rem;
|
|
172
|
+
--spacing-lg: 1.5rem;
|
|
173
|
+
--border-radius: 0.375rem;
|
|
174
|
+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
[data-theme='dark'] {
|
|
178
|
+
--color-background: #1a1a1a;
|
|
179
|
+
--color-text: #ffffff;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Button.module.css */
|
|
183
|
+
.button {
|
|
184
|
+
padding: var(--spacing-sm) var(--spacing-md);
|
|
185
|
+
border-radius: var(--border-radius);
|
|
186
|
+
box-shadow: var(--shadow);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.primary {
|
|
190
|
+
background: var(--color-primary);
|
|
191
|
+
}
|
|
192
|
+
\`\`\`
|
|
193
|
+
|
|
194
|
+
## TypeScript Support
|
|
195
|
+
\`\`\`tsx
|
|
196
|
+
// For TypeScript, you need type declarations
|
|
197
|
+
|
|
198
|
+
// Option 1: Global declaration (global.d.ts)
|
|
199
|
+
declare module '*.module.css' {
|
|
200
|
+
const classes: { [key: string]: string };
|
|
201
|
+
export default classes;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
declare module '*.module.scss' {
|
|
205
|
+
const classes: { [key: string]: string };
|
|
206
|
+
export default classes;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Option 2: Use typed-css-modules or css-modules-typescript-loader
|
|
210
|
+
// This generates .d.ts files for each module
|
|
211
|
+
|
|
212
|
+
// Button.module.css.d.ts (auto-generated)
|
|
213
|
+
declare const styles: {
|
|
214
|
+
readonly button: string;
|
|
215
|
+
readonly primary: string;
|
|
216
|
+
readonly secondary: string;
|
|
217
|
+
};
|
|
218
|
+
export default styles;
|
|
219
|
+
|
|
220
|
+
// Now TypeScript knows the available classes
|
|
221
|
+
import styles from './Button.module.css';
|
|
222
|
+
styles.button; // OK
|
|
223
|
+
styles.primary; // OK
|
|
224
|
+
styles.invalid; // TypeScript error!
|
|
225
|
+
\`\`\`
|
|
226
|
+
|
|
227
|
+
## With Sass/SCSS
|
|
228
|
+
\`\`\`scss
|
|
229
|
+
// Button.module.scss
|
|
230
|
+
$primary: #3b82f6;
|
|
231
|
+
$secondary: #6b7280;
|
|
232
|
+
|
|
233
|
+
@mixin button-base {
|
|
234
|
+
padding: 0.5rem 1rem;
|
|
235
|
+
border: none;
|
|
236
|
+
border-radius: 0.375rem;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
transition: all 0.2s;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.button {
|
|
242
|
+
@include button-base;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.primary {
|
|
246
|
+
background: $primary;
|
|
247
|
+
color: white;
|
|
248
|
+
|
|
249
|
+
&:hover {
|
|
250
|
+
background: darken($primary, 10%);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.secondary {
|
|
255
|
+
background: $secondary;
|
|
256
|
+
color: white;
|
|
257
|
+
|
|
258
|
+
&:hover {
|
|
259
|
+
background: darken($secondary, 10%);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Nesting works as expected
|
|
264
|
+
.card {
|
|
265
|
+
padding: 1.5rem;
|
|
266
|
+
|
|
267
|
+
&__title {
|
|
268
|
+
font-size: 1.25rem;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
&__content {
|
|
272
|
+
color: #6b7280;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
\`\`\`
|
|
276
|
+
|
|
277
|
+
## File Structure
|
|
278
|
+
\`\`\`
|
|
279
|
+
src/
|
|
280
|
+
├── components/
|
|
281
|
+
│ ├── Button/
|
|
282
|
+
│ │ ├── Button.tsx
|
|
283
|
+
│ │ ├── Button.module.css
|
|
284
|
+
│ │ └── index.ts
|
|
285
|
+
│ └── Card/
|
|
286
|
+
│ ├── Card.tsx
|
|
287
|
+
│ ├── Card.module.css
|
|
288
|
+
│ └── index.ts
|
|
289
|
+
├── styles/
|
|
290
|
+
│ ├── variables.css # CSS custom properties
|
|
291
|
+
│ ├── shared.module.css # Shared compositions
|
|
292
|
+
│ └── global.css # Global styles
|
|
293
|
+
└── App.tsx
|
|
294
|
+
\`\`\`
|
|
295
|
+
|
|
296
|
+
## Configuration (Vite)
|
|
297
|
+
\`\`\`typescript
|
|
298
|
+
// vite.config.ts
|
|
299
|
+
export default defineConfig({
|
|
300
|
+
css: {
|
|
301
|
+
modules: {
|
|
302
|
+
localsConvention: 'camelCaseOnly', // .my-class → styles.myClass
|
|
303
|
+
generateScopedName: '[name]__[local]___[hash:base64:5]',
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Next.js works out of the box with .module.css
|
|
309
|
+
\`\`\`
|
|
310
|
+
|
|
311
|
+
## ❌ DON'T
|
|
312
|
+
- Use generic class names that don't describe the component
|
|
313
|
+
- Deeply nest selectors (keep flat)
|
|
314
|
+
- Mix global and local styles unnecessarily
|
|
315
|
+
- Forget to handle dynamic class names in TypeScript
|
|
316
|
+
- Create overly long class name strings
|
|
317
|
+
|
|
318
|
+
## ✅ DO
|
|
319
|
+
- Name files with .module.css suffix
|
|
320
|
+
- Use composes for shared styles
|
|
321
|
+
- Use CSS variables for theming
|
|
322
|
+
- Use clsx/classnames for combining classes
|
|
323
|
+
- Set up TypeScript declarations
|
|
324
|
+
- Keep styles colocated with components
|
|
325
|
+
- Use BEM-like naming within modules for nested elements
|