ai-developer-skill-os 8.2.0 → 8.3.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/.agents/LICENSE +21 -21
- package/.agents/README.md +90 -90
- package/.agents/docs/CHI_TIET_SKILLS.md +125 -125
- package/.agents/docs/MIGRATION-CLEANUP-V8.1.3.md +36 -36
- package/.agents/docs/MIGRATION-STATUS.md +35 -35
- package/.agents/docs/MIGRATION-V8.md +10 -10
- package/.agents/docs/V8-CERTIFICATION.md +27 -27
- package/.agents/docs/decisions/ADR-001-v8-migration.md +58 -58
- package/.agents/docs/decisions/ADR-002-workflow-separation.md +50 -50
- package/.agents/docs/decisions/ADR-003-registry-generated.md +54 -54
- package/.agents/docs/decisions/ADR-008-skill-boundary-review.md +27 -27
- package/.agents/registry/graph.json +4 -8
- package/.agents/registry/index.yaml +3 -4
- package/.agents/registry/skills-index.yml +525 -525
- package/.agents/rules/coding.md +10 -0
- package/.agents/rules/command-safety.md +37 -14
- package/.agents/rules/priorities.yml +13 -2
- package/.agents/rules/security.md +47 -7
- package/.agents/rules/skill-quality.md +32 -8
- package/.agents/skills/_template/SKILL.md +133 -132
- package/.agents/skills/_template/capability.yaml +20 -20
- package/.agents/skills/_template/evals/scorecard.yaml +19 -19
- package/.agents/skills/qk-access-policy/SKILL.md +4 -1
- package/.agents/skills/qk-agent-observability/SKILL.md +111 -0
- package/.agents/skills/qk-agent-observability/references/scorecard.yaml +80 -0
- package/.agents/skills/qk-ai-builder/SKILL.md +74 -5
- package/.agents/skills/qk-api-lifecycle/SKILL.md +4 -2
- package/.agents/skills/qk-bug-resolution/SKILL.md +3 -21
- package/.agents/skills/qk-code-review/SKILL.md +188 -189
- package/.agents/skills/qk-context-loader/SKILL.md +3 -47
- package/.agents/skills/qk-data-lifecycle/SKILL.md +7 -2
- package/.agents/skills/qk-db-optimizer/SKILL.md +3 -2
- package/.agents/skills/qk-design-system-engineering/SKILL.md +235 -112
- package/.agents/skills/qk-devops-platform/SKILL.md +241 -117
- package/.agents/skills/qk-docs/SKILL.md +3 -1
- package/.agents/skills/qk-engineering-standard/SKILL.md +4 -75
- package/.agents/skills/qk-engineering-standard/references/anti-patterns.md +121 -0
- package/.agents/skills/qk-engineering-standard/rules/frontend.md +1 -1
- package/.agents/skills/qk-fe-api-integration/SKILL.md +13 -32
- package/.agents/skills/qk-feature-delivery/SKILL.md +54 -202
- package/.agents/skills/qk-frontend-architecture/SKILL.md +258 -134
- package/.agents/skills/qk-help/SKILL.md +21 -159
- package/.agents/skills/qk-orchestrator/SKILL.md +2 -78
- package/.agents/skills/qk-orchestrator/references/routing-table.md +15 -3
- package/.agents/skills/qk-product-specification/SKILL.md +253 -130
- package/.agents/skills/qk-production-release/SKILL.md +32 -67
- package/.agents/skills/qk-project-bootstrap/SKILL.md +59 -8
- package/.agents/skills/qk-project-health/SKILL.md +4 -2
- package/.agents/skills/qk-project-memory/SKILL.md +3 -1
- package/.agents/skills/qk-security-audit/SKILL.md +259 -135
- package/.agents/skills/qk-system-evolution/SKILL.md +18 -68
- package/.agents/skills/qk-test-engineering/SKILL.md +262 -139
- package/.agents/skills/qk-ui-audit/SKILL.md +16 -89
- package/.agents/skills/qk-ui-audit/references/anti-slop-checklist.md +2 -2
- package/.agents/skills/qk-ui-builder/SKILL.md +482 -509
- package/.agents/skills/qk-ui-builder/references/component-cookbook.md +455 -1191
- package/.agents/skills/qk-ui-system-builder/SKILL.md +1 -5
- package/.agents/skills/qk-validation-gate/SKILL.md +0 -74
- package/.agents/skills/qk-web-quality-gate/SKILL.md +232 -114
- package/.agents/workflows/_schema.yml +146 -109
- package/.agents/workflows/bug-resolution.yml +121 -101
- package/.agents/workflows/code-review.yml +93 -77
- package/.agents/workflows/documentation.yml +90 -75
- package/.agents/workflows/feature-delivery.yml +120 -103
- package/.agents/workflows/refactor.yml +99 -81
- package/.agents/workflows/research.yml +75 -60
- package/.agents/workflows/security-audit.yml +115 -72
- package/.agents/workflows/skill-evolution.yml +97 -65
- package/.agents/workflows/spec-driven-development.yml +87 -57
- package/CHANGELOG.md +10 -0
- package/README.md +90 -90
- package/bin/install.js +330 -180
- package/package.json +2 -2
- package/.agents/CHANGELOG.md +0 -131
- package/.agents/learnings/draft/README.md +0 -37
- package/.agents/reports/RELEASE-CHECKLIST.md +0 -29
- package/.agents/reports/architecture-audit.md +0 -13
- package/.agents/reports/graph-health.md +0 -20
- package/.agents/reports/skill-audit.md +0 -215
|
@@ -1,1194 +1,458 @@
|
|
|
1
|
-
# Component Cookbook
|
|
1
|
+
# 📘 Component Cookbook — Blueprint chuẩn
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</
|
|
235
|
-
</
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
**When to use:** Show navigation hierarchy for pages with parent/child relationships.
|
|
373
|
-
|
|
374
|
-
**Structure:**
|
|
375
|
-
```html
|
|
376
|
-
<nav class="breadcrumb" aria-label="Breadcrumb">
|
|
377
|
-
<ol class="breadcrumb__list">
|
|
378
|
-
<li class="breadcrumb__item">
|
|
379
|
-
<a class="breadcrumb__link" href="/">Home</a>
|
|
380
|
-
</li>
|
|
381
|
-
<li class="breadcrumb__item" aria-current="page">
|
|
382
|
-
<span class="breadcrumb__current">Products</span>
|
|
383
|
-
</li>
|
|
384
|
-
</ol>
|
|
385
|
-
</nav>
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
**States required:**
|
|
389
|
-
- Default (linked items)
|
|
390
|
-
- Current page (non-linked, `aria-current="page"`)
|
|
391
|
-
- Hover (link underline or color change)
|
|
392
|
-
|
|
393
|
-
**Common mistakes:**
|
|
394
|
-
- Using `>` or `/` as separators without aria-hidden
|
|
395
|
-
- Not marking current page with `aria-current`
|
|
396
|
-
- Using breadcrumbs for flat site structures
|
|
397
|
-
- Forgetting the `<nav>` wrapper with `aria-label`
|
|
398
|
-
|
|
399
|
-
---
|
|
400
|
-
|
|
401
|
-
## Pagination
|
|
402
|
-
|
|
403
|
-
**When to use:** Split large datasets or search results into discrete pages.
|
|
404
|
-
|
|
405
|
-
**Structure:**
|
|
406
|
-
```html
|
|
407
|
-
<nav class="pagination" aria-label="Pagination">
|
|
408
|
-
<button class="pagination__btn" aria-label="Previous page">←</button>
|
|
409
|
-
<ol class="pagination__list">
|
|
410
|
-
<li><a class="pagination__link" href="?page=1">1</a></li>
|
|
411
|
-
<li><span class="pagination__current" aria-current="page">2</span></li>
|
|
412
|
-
<li><a class="pagination__link" href="?page=3">3</a></li>
|
|
413
|
-
</ol>
|
|
414
|
-
<button class="pagination__btn" aria-label="Next page">→</button>
|
|
415
|
-
</nav>
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
**States required:**
|
|
419
|
-
- Current page (highlighted, `aria-current="page"`)
|
|
420
|
-
- Available page (linked)
|
|
421
|
-
- Disabled prev/next (grayed out, `disabled`)
|
|
422
|
-
|
|
423
|
-
**Common mistakes:**
|
|
424
|
-
- Not disabling prev/next at boundaries
|
|
425
|
-
- Showing all pages instead of ellipsis for large sets
|
|
426
|
-
- Missing `aria-label` on prev/next buttons
|
|
427
|
-
- Not providing skip links for screen readers
|
|
428
|
-
|
|
429
|
-
---
|
|
430
|
-
|
|
431
|
-
## Sidebar
|
|
432
|
-
|
|
433
|
-
**When to use:** Secondary navigation, filters, or contextual information alongside main content.
|
|
434
|
-
|
|
435
|
-
**Structure:**
|
|
436
|
-
```html
|
|
437
|
-
<aside class="sidebar" aria-label="Sidebar">
|
|
438
|
-
<nav class="sidebar__nav">
|
|
439
|
-
<a class="sidebar__link" href="#dashboard">Dashboard</a>
|
|
440
|
-
<a class="sidebar__link sidebar__link--active" href="#settings">Settings</a>
|
|
441
|
-
</nav>
|
|
442
|
-
</aside>
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
**States required:**
|
|
446
|
-
- Default
|
|
447
|
-
- Active (highlighted link)
|
|
448
|
-
- Collapsed (icons only)
|
|
449
|
-
- Mobile overlay (off-canvas)
|
|
450
|
-
|
|
451
|
-
**Common mistakes:**
|
|
452
|
-
- Not collapsing on mobile
|
|
453
|
-
- Using `<div>` for navigation instead of `<nav>`
|
|
454
|
-
- Not indicating active page/section
|
|
455
|
-
- Forgetting to trap focus in mobile drawer
|
|
456
|
-
|
|
457
|
-
---
|
|
458
|
-
|
|
459
|
-
## Navbar
|
|
460
|
-
|
|
461
|
-
**When to use:** Primary site navigation, brand identity, and user actions at the top of every page.
|
|
462
|
-
|
|
463
|
-
**Structure:**
|
|
464
|
-
```html
|
|
465
|
-
<header class="navbar">
|
|
466
|
-
<a class="navbar__brand" href="/">Brand</a>
|
|
467
|
-
<button class="navbar__toggle" aria-label="Toggle menu" aria-expanded="false">
|
|
468
|
-
<span class="navbar__toggle-icon"></span>
|
|
469
|
-
</button>
|
|
470
|
-
<nav class="navbar__nav" aria-label="Main">
|
|
471
|
-
<a class="navbar__link" href="#features">Features</a>
|
|
472
|
-
<a class="navbar__link" href="#pricing">Pricing</a>
|
|
473
|
-
<a class="navbar__link navbar__link--cta" href="#signup">Sign Up</a>
|
|
474
|
-
</nav>
|
|
475
|
-
</header>
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
**States required:**
|
|
479
|
-
- Default
|
|
480
|
-
- Scrolled (background change, shadow)
|
|
481
|
-
- Mobile open (hamburger menu expanded)
|
|
482
|
-
- Active link (underline or color)
|
|
483
|
-
|
|
484
|
-
**Common mistakes:**
|
|
485
|
-
- Logo not linking to homepage
|
|
486
|
-
- Mobile menu without proper ARIA states
|
|
487
|
-
- Too many nav items (5-7 max)
|
|
488
|
-
- Not hiding scrollbar in mobile menu
|
|
489
|
-
|
|
490
|
-
---
|
|
491
|
-
|
|
492
|
-
## Footer
|
|
493
|
-
|
|
494
|
-
**When to use:** Site-wide links, legal information, social links, and secondary navigation.
|
|
495
|
-
|
|
496
|
-
**Structure:**
|
|
497
|
-
```html
|
|
498
|
-
<footer class="footer">
|
|
499
|
-
<div class="footer__brand">
|
|
500
|
-
<a class="footer__logo" href="/">Brand</a>
|
|
501
|
-
<p class="footer__tagline">Building better software.</p>
|
|
502
|
-
</div>
|
|
503
|
-
<nav class="footer__nav" aria-label="Footer">
|
|
504
|
-
<div class="footer__section">
|
|
505
|
-
<h3 class="footer__heading">Product</h3>
|
|
506
|
-
<a class="footer__link" href="#features">Features</a>
|
|
507
|
-
<a class="footer__link" href="#pricing">Pricing</a>
|
|
508
|
-
</div>
|
|
509
|
-
</nav>
|
|
510
|
-
<div class="footer__bottom">
|
|
511
|
-
<p>© 2024 Brand. All rights reserved.</p>
|
|
512
|
-
</div>
|
|
513
|
-
</footer>
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
**States required:**
|
|
517
|
-
- Default
|
|
518
|
-
- Link hover (underline or color shift)
|
|
519
|
-
|
|
520
|
-
**Common mistakes:**
|
|
521
|
-
- Four-column generic structure (AI tell)
|
|
522
|
-
- Newsletter signup without clear value proposition
|
|
523
|
-
- Missing legal links (privacy, terms)
|
|
524
|
-
- Social icons without accessible labels
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
## Hero
|
|
529
|
-
|
|
530
|
-
**When to use:** Primary value proposition and call-to-action on landing or product pages.
|
|
531
|
-
|
|
532
|
-
**Structure:**
|
|
533
|
-
```html
|
|
534
|
-
<section class="hero">
|
|
535
|
-
<div class="hero__content">
|
|
536
|
-
<h1 class="hero__title">Build faster with our platform</h1>
|
|
537
|
-
<p class="hero__subtitle">
|
|
538
|
-
The complete toolkit for modern development teams.
|
|
539
|
-
</p>
|
|
540
|
-
<div class="hero__actions">
|
|
541
|
-
<a class="btn btn--primary btn--lg" href="#signup">Start free trial</a>
|
|
542
|
-
<a class="btn btn--secondary btn--lg" href="#demo">Watch demo</a>
|
|
543
|
-
</div>
|
|
544
|
-
</div>
|
|
545
|
-
<div class="hero__media">
|
|
546
|
-
<img class="hero__image" src="dashboard.png" alt="Dashboard preview" />
|
|
547
|
-
</div>
|
|
548
|
-
</section>
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
**States required:**
|
|
552
|
-
- Default
|
|
553
|
-
- Animated entrance (fade up, staggered)
|
|
554
|
-
|
|
555
|
-
**Common mistakes:**
|
|
556
|
-
- Carousel instead of static hero (low engagement)
|
|
557
|
-
- Too many CTAs (decision paralysis)
|
|
558
|
-
- Generic headlines ("Welcome to our website")
|
|
559
|
-
- Missing clear value proposition
|
|
560
|
-
|
|
561
|
-
---
|
|
562
|
-
|
|
563
|
-
## FeatureGrid
|
|
564
|
-
|
|
565
|
-
**When to use:** Display product features, services, or benefits in a scannable layout.
|
|
566
|
-
|
|
567
|
-
**Structure:**
|
|
568
|
-
```html
|
|
569
|
-
<section class="feature-grid">
|
|
570
|
-
<h2 class="feature-grid__title">Everything you need</h2>
|
|
571
|
-
<div class="feature-grid__items">
|
|
572
|
-
<div class="feature-card">
|
|
573
|
-
<div class="feature-card__icon">
|
|
574
|
-
<svg>...</svg>
|
|
3
|
+
> Stack mặc định: React + TypeScript + Next.js (App Router) + Tailwind (Design Token qua CSS variable) + **TanStack Query (React Query)** cho toàn bộ server-state.
|
|
4
|
+
> Nếu dự án dùng stack khác (Vue, Redux, styled-components...), điều chỉnh cú pháp nhưng giữ nguyên **cấu trúc giải phẫu** bên dưới.
|
|
5
|
+
>
|
|
6
|
+
> **Quy tắc phân định state (quan trọng khi có React Query):**
|
|
7
|
+
> - **Server state** (dữ liệu đến từ API/DB: user, list sản phẩm, kết quả search...) → luôn qua React Query. KHÔNG copy vào `useState`/Redux/Zustand.
|
|
8
|
+
> - **Client state** (UI thuần: modal đang mở, tab đang chọn, giá trị input chưa submit) → `useState`/Context/Zustand như bình thường.
|
|
9
|
+
> - Hai loại state này không được trộn vào cùng một chỗ quản lý.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Giải phẫu Component chuẩn
|
|
14
|
+
|
|
15
|
+
Một component "sạch" luôn tách làm 3 lớp theo đúng thứ tự:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
1. Types/Props → định nghĩa hợp đồng dữ liệu (interface)
|
|
19
|
+
2. Custom Hook → toàn bộ logic, state, side-effect, gọi service
|
|
20
|
+
3. JSX thuần → chỉ render, KHÔNG chứa logic nghiệp vụ
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Quy tắc vàng: **nếu bạn phải "đọc" logic mới hiểu component render cái gì, tức là logic và UI đang bị trộn lẫn.**
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
components/
|
|
27
|
+
Button/
|
|
28
|
+
Button.tsx ← JSX thuần + import hook
|
|
29
|
+
useButton.ts ← logic (nếu có state/behavior phức tạp)
|
|
30
|
+
Button.types.ts ← Props interface
|
|
31
|
+
Button.stories.tsx ← (tuỳ chọn) Storybook
|
|
32
|
+
index.ts ← re-export
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Ví dụ mẫu: Button chuẩn mực
|
|
38
|
+
|
|
39
|
+
### `Button.types.ts`
|
|
40
|
+
```ts
|
|
41
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
42
|
+
|
|
43
|
+
export type ButtonVariant = "primary" | "secondary" | "danger" | "ghost";
|
|
44
|
+
export type ButtonSize = "sm" | "md" | "lg";
|
|
45
|
+
|
|
46
|
+
export interface ButtonProps
|
|
47
|
+
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
|
48
|
+
/** Nội dung hiển thị bên trong button */
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
/** Kiểu giao diện, mặc định 'primary' */
|
|
51
|
+
variant?: ButtonVariant;
|
|
52
|
+
/** Kích thước, mặc định 'md' */
|
|
53
|
+
size?: ButtonSize;
|
|
54
|
+
/** Trạng thái đang xử lý async */
|
|
55
|
+
isLoading?: boolean;
|
|
56
|
+
/** Icon hiển thị trước label (tuỳ chọn) */
|
|
57
|
+
leadingIcon?: ReactNode;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `useButton.ts` (logic tách riêng — chỉ cần khi có behavior phức tạp)
|
|
62
|
+
```ts
|
|
63
|
+
import { useCallback } from "react";
|
|
64
|
+
import type { ButtonProps } from "./Button.types";
|
|
65
|
+
|
|
66
|
+
export function useButton({
|
|
67
|
+
isLoading,
|
|
68
|
+
disabled,
|
|
69
|
+
onClick,
|
|
70
|
+
}: Pick<ButtonProps, "isLoading" | "disabled" | "onClick">) {
|
|
71
|
+
const isDisabled = Boolean(isLoading || disabled);
|
|
72
|
+
|
|
73
|
+
const handleClick = useCallback<NonNullable<ButtonProps["onClick"]>>(
|
|
74
|
+
(event) => {
|
|
75
|
+
if (isDisabled) return;
|
|
76
|
+
onClick?.(event);
|
|
77
|
+
},
|
|
78
|
+
[isDisabled, onClick]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return { isDisabled, handleClick };
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `Button.tsx` (JSX thuần)
|
|
86
|
+
```tsx
|
|
87
|
+
import { forwardRef } from "react";
|
|
88
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
89
|
+
import { cn } from "@/lib/utils";
|
|
90
|
+
import { useButton } from "./useButton";
|
|
91
|
+
import type { ButtonProps } from "./Button.types";
|
|
92
|
+
|
|
93
|
+
// Design Token được ánh xạ qua Tailwind class (không hardcode hex color)
|
|
94
|
+
const buttonStyles = cva(
|
|
95
|
+
"inline-flex items-center justify-center gap-2 rounded-md font-medium " +
|
|
96
|
+
"transition-colors focus-visible:outline-none focus-visible:ring-2 " +
|
|
97
|
+
"focus-visible:ring-offset-2 focus-visible:ring-ring " +
|
|
98
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
99
|
+
{
|
|
100
|
+
variants: {
|
|
101
|
+
variant: {
|
|
102
|
+
primary: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80",
|
|
103
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/90",
|
|
104
|
+
danger: "bg-danger text-danger-foreground hover:bg-danger/90",
|
|
105
|
+
ghost: "bg-transparent text-foreground hover:bg-muted",
|
|
106
|
+
},
|
|
107
|
+
size: {
|
|
108
|
+
sm: "h-8 px-3 text-sm",
|
|
109
|
+
md: "h-10 px-4 text-sm",
|
|
110
|
+
lg: "h-12 px-6 text-base",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
defaultVariants: { variant: "primary", size: "md" },
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
118
|
+
(
|
|
119
|
+
{ children, variant, size, isLoading, leadingIcon, className, onClick, disabled, ...rest },
|
|
120
|
+
ref
|
|
121
|
+
) => {
|
|
122
|
+
const { isDisabled, handleClick } = useButton({ isLoading, disabled, onClick });
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<button
|
|
126
|
+
ref={ref}
|
|
127
|
+
type="button"
|
|
128
|
+
className={cn(buttonStyles({ variant, size }), className)}
|
|
129
|
+
disabled={isDisabled}
|
|
130
|
+
aria-busy={isLoading || undefined}
|
|
131
|
+
aria-disabled={isDisabled || undefined}
|
|
132
|
+
onClick={handleClick}
|
|
133
|
+
{...rest}
|
|
134
|
+
>
|
|
135
|
+
{isLoading ? (
|
|
136
|
+
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" aria-hidden="true" />
|
|
137
|
+
) : (
|
|
138
|
+
leadingIcon
|
|
139
|
+
)}
|
|
140
|
+
<span>{children}</span>
|
|
141
|
+
</button>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
Button.displayName = "Button";
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `index.ts`
|
|
150
|
+
```ts
|
|
151
|
+
export { Button } from "./Button";
|
|
152
|
+
export type { ButtonProps, ButtonVariant, ButtonSize } from "./Button.types";
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Vì sao mẫu này đạt chuẩn:**
|
|
156
|
+
- ✅ Props có type rõ ràng, kế thừa đúng HTML attributes gốc thay vì định nghĩa lại từ đầu.
|
|
157
|
+
- ✅ Logic (`isDisabled`, `handleClick`) tách khỏi JSX qua `useButton`.
|
|
158
|
+
- ✅ Style dùng Design Token (`bg-primary`, `text-danger-foreground`...) — không hardcode màu.
|
|
159
|
+
- ✅ Có đủ `variant`/`size` mở rộng qua `cva`, dễ thêm token mới.
|
|
160
|
+
- ✅ Có `aria-busy`, `aria-disabled`, dùng đúng thẻ `<button>` — hỗ trợ người dùng khuyết tật.
|
|
161
|
+
- ✅ `forwardRef` để component cha có thể focus/measure DOM node khi cần.
|
|
162
|
+
- ✅ Xử lý trạng thái loading (spinner) mà không phá layout.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 3. Mẫu component có gọi dữ liệu async — dùng React Query (ví dụ `UserCard`)
|
|
167
|
+
|
|
168
|
+
Đây là ví dụ minh hoạ nguyên tắc "không gọi API trực tiếp trong component trình bày", với React Query đảm nhiệm toàn bộ cache/loading/error thay vì tự viết `useState` + `useEffect`:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
features/user/
|
|
172
|
+
components/UserCard.tsx ← JSX thuần, dùng hook query
|
|
173
|
+
hooks/useUserProfile.ts ← wrap useQuery, khai báo queryKey chuẩn
|
|
174
|
+
services/userService.ts ← fetch logic thuần, không biết gì về React
|
|
175
|
+
queryKeys.ts ← nơi tập trung mọi queryKey của feature
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// services/userService.ts — fetch logic thuần, không import React/React Query
|
|
180
|
+
export async function fetchUserProfile(userId: string): Promise<UserProfile> {
|
|
181
|
+
const res = await fetch(`/api/users/${userId}`);
|
|
182
|
+
if (!res.ok) throw new Error("Failed to fetch user profile");
|
|
183
|
+
return res.json();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function updateUserProfile(userId: string, patch: Partial<UserProfile>): Promise<UserProfile> {
|
|
187
|
+
const res = await fetch(`/api/users/${userId}`, {
|
|
188
|
+
method: "PATCH",
|
|
189
|
+
headers: { "Content-Type": "application/json" },
|
|
190
|
+
body: JSON.stringify(patch),
|
|
191
|
+
});
|
|
192
|
+
if (!res.ok) throw new Error("Failed to update user profile");
|
|
193
|
+
return res.json();
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// queryKeys.ts — tập trung key, tránh string rải rác gây khó invalidate
|
|
199
|
+
export const userKeys = {
|
|
200
|
+
all: ["users"] as const,
|
|
201
|
+
detail: (userId: string) => [...userKeys.all, "detail", userId] as const,
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// hooks/useUserProfile.ts
|
|
207
|
+
import { useQuery } from "@tanstack/react-query";
|
|
208
|
+
import { fetchUserProfile } from "../services/userService";
|
|
209
|
+
import { userKeys } from "../queryKeys";
|
|
210
|
+
|
|
211
|
+
export function useUserProfile(userId: string) {
|
|
212
|
+
return useQuery({
|
|
213
|
+
queryKey: userKeys.detail(userId),
|
|
214
|
+
queryFn: () => fetchUserProfile(userId),
|
|
215
|
+
enabled: Boolean(userId), // tránh gọi khi userId rỗng
|
|
216
|
+
staleTime: 60_000, // dữ liệu ít đổi → giảm refetch thừa
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
// components/UserCard.tsx
|
|
223
|
+
import { useUserProfile } from "../hooks/useUserProfile";
|
|
224
|
+
|
|
225
|
+
export function UserCard({ userId }: { userId: string }) {
|
|
226
|
+
const { data, isPending, isError } = useUserProfile(userId);
|
|
227
|
+
|
|
228
|
+
if (isPending) return <UserCardSkeleton />;
|
|
229
|
+
if (isError) return <p role="alert">Không tải được thông tin người dùng.</p>;
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<article className="rounded-lg border p-4">
|
|
233
|
+
<h3 className="font-semibold">{data.name}</h3>
|
|
234
|
+
<p className="text-muted-foreground text-sm">{data.email}</p>
|
|
235
|
+
</article>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Mutation chuẩn (ví dụ cập nhật hồ sơ)
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// hooks/useUpdateUserProfile.ts
|
|
244
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
245
|
+
import { updateUserProfile } from "../services/userService";
|
|
246
|
+
import { userKeys } from "../queryKeys";
|
|
247
|
+
import type { UserProfile } from "../types";
|
|
248
|
+
|
|
249
|
+
export function useUpdateUserProfile(userId: string) {
|
|
250
|
+
const queryClient = useQueryClient();
|
|
251
|
+
|
|
252
|
+
return useMutation({
|
|
253
|
+
mutationFn: (patch: Partial<UserProfile>) => updateUserProfile(userId, patch),
|
|
254
|
+
onSuccess: (updated) => {
|
|
255
|
+
// Cập nhật cache ngay, không cần chờ refetch
|
|
256
|
+
queryClient.setQueryData(userKeys.detail(userId), updated);
|
|
257
|
+
},
|
|
258
|
+
// onError: hiển thị toast lỗi ở nơi gọi mutate, không nuốt lỗi im lặng
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// components/EditProfileForm.tsx
|
|
265
|
+
import { useUpdateUserProfile } from "../hooks/useUpdateUserProfile";
|
|
266
|
+
|
|
267
|
+
export function EditProfileForm({ userId }: { userId: string }) {
|
|
268
|
+
const { mutate, isPending, error } = useUpdateUserProfile(userId);
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<form
|
|
272
|
+
onSubmit={(e) => {
|
|
273
|
+
e.preventDefault();
|
|
274
|
+
const form = new FormData(e.currentTarget);
|
|
275
|
+
mutate({ name: String(form.get("name")) });
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
<input name="name" defaultValue="" />
|
|
279
|
+
<Button type="submit" isLoading={isPending}>Lưu</Button>
|
|
280
|
+
{error && <p role="alert">Cập nhật thất bại, thử lại.</p>}
|
|
281
|
+
</form>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Vì sao mẫu này đạt chuẩn:**
|
|
287
|
+
- ✅ `queryKey` được khai báo tập trung ở `queryKeys.ts`, không rải string tay ở nhiều nơi.
|
|
288
|
+
- ✅ `service` layer không biết gì về React/React Query — có thể tái dùng ở server, test độc lập.
|
|
289
|
+
- ✅ Dùng `isPending`/`isError` do React Query cung cấp thay vì tự quản lý state loading.
|
|
290
|
+
- ✅ Mutation cập nhật cache qua `setQueryData` thay vì `invalidateQueries` tràn lan gây refetch thừa (chỉ dùng `invalidateQueries` khi thực sự cần đồng bộ lại từ server).
|
|
291
|
+
- ✅ Component KHÔNG tự gọi `fetch` — chỉ gọi hook.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 4. Blueprint nhanh: Table / Form / Virtual / Router
|
|
296
|
+
|
|
297
|
+
### 4.1 TanStack Table (headless — bảng có sort/filter/pagination)
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
// components/UserTable.tsx
|
|
301
|
+
import { useReactTable, getCoreRowModel, getSortedRowModel, type ColumnDef } from "@tanstack/react-table";
|
|
302
|
+
|
|
303
|
+
const columns: ColumnDef<UserProfile>[] = [
|
|
304
|
+
{ accessorKey: "name", header: "Tên", enableSorting: true },
|
|
305
|
+
{ accessorKey: "email", header: "Email" },
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
export function UserTable({ data }: { data: UserProfile[] }) {
|
|
309
|
+
const table = useReactTable({
|
|
310
|
+
data,
|
|
311
|
+
columns,
|
|
312
|
+
getCoreRowModel: getCoreRowModel(),
|
|
313
|
+
getSortedRowModel: getSortedRowModel(),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<table className="w-full text-sm">
|
|
318
|
+
<thead>
|
|
319
|
+
{table.getHeaderGroups().map((hg) => (
|
|
320
|
+
<tr key={hg.id}>
|
|
321
|
+
{hg.headers.map((h) => (
|
|
322
|
+
<th key={h.id} onClick={h.column.getToggleSortingHandler()} className="cursor-pointer text-left p-2">
|
|
323
|
+
{h.isPlaceholder ? null : h.column.columnDef.header as string}
|
|
324
|
+
</th>
|
|
325
|
+
))}
|
|
326
|
+
</tr>
|
|
327
|
+
))}
|
|
328
|
+
</thead>
|
|
329
|
+
<tbody>
|
|
330
|
+
{table.getRowModel().rows.map((row) => (
|
|
331
|
+
<tr key={row.id} className="border-t">
|
|
332
|
+
{row.getVisibleCells().map((cell) => (
|
|
333
|
+
<td key={cell.id} className="p-2">{String(cell.getValue())}</td>
|
|
334
|
+
))}
|
|
335
|
+
</tr>
|
|
336
|
+
))}
|
|
337
|
+
</tbody>
|
|
338
|
+
</table>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
> Nguyên tắc: `columns` định nghĩa tách biệt khỏi JSX, JSX chỉ `.map()` để render — không tự viết sort/filter tay.
|
|
343
|
+
|
|
344
|
+
### 4.2 TanStack Virtual (danh sách lớn)
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
// components/VirtualUserList.tsx
|
|
348
|
+
import { useRef } from "react";
|
|
349
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
350
|
+
|
|
351
|
+
export function VirtualUserList({ items }: { items: UserProfile[] }) {
|
|
352
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
353
|
+
const virtualizer = useVirtualizer({
|
|
354
|
+
count: items.length,
|
|
355
|
+
getScrollElement: () => parentRef.current,
|
|
356
|
+
estimateSize: () => 56, // chiều cao ước tính mỗi row (px)
|
|
357
|
+
overscan: 8,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div ref={parentRef} className="h-[480px] overflow-auto">
|
|
362
|
+
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
|
|
363
|
+
{virtualizer.getVirtualItems().map((row) => (
|
|
364
|
+
<div
|
|
365
|
+
key={row.key}
|
|
366
|
+
style={{ position: "absolute", top: 0, left: 0, width: "100%", transform: `translateY(${row.start}px)`, height: row.size }}
|
|
367
|
+
>
|
|
368
|
+
{items[row.index].name}
|
|
369
|
+
</div>
|
|
370
|
+
))}
|
|
575
371
|
</div>
|
|
576
|
-
<h3 class="feature-card__title">Analytics</h3>
|
|
577
|
-
<p class="feature-card__desc">Track performance with real-time dashboards.</p>
|
|
578
372
|
</div>
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
-
|
|
656
|
-
-
|
|
657
|
-
-
|
|
658
|
-
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
**Structure:**
|
|
667
|
-
```html
|
|
668
|
-
<details class="faq-item">
|
|
669
|
-
<summary class="faq-item__question">How do I reset my password?</summary>
|
|
670
|
-
<div class="faq-item__answer">
|
|
671
|
-
<p>Go to Settings > Security and click "Reset Password."</p>
|
|
672
|
-
</div>
|
|
673
|
-
</details>
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
**States required:**
|
|
677
|
-
- Closed (default)
|
|
678
|
-
- Open (expanded)
|
|
679
|
-
- Hover (summary highlight)
|
|
680
|
-
|
|
681
|
-
**Common mistakes:**
|
|
682
|
-
- Building custom accordions instead of using native `<details>`
|
|
683
|
-
- Missing `open` attribute management for animations
|
|
684
|
-
- Long answers without clear structure
|
|
685
|
-
- Duplicate questions across the page
|
|
686
|
-
|
|
687
|
-
---
|
|
688
|
-
|
|
689
|
-
## Stats
|
|
690
|
-
|
|
691
|
-
**When to use:** Display key metrics, social proof numbers, or performance indicators.
|
|
692
|
-
|
|
693
|
-
**Structure:**
|
|
694
|
-
```html
|
|
695
|
-
<div class="stats">
|
|
696
|
-
<div class="stat">
|
|
697
|
-
<p class="stat__value">99.9%</p>
|
|
698
|
-
<p class="stat__label">Uptime</p>
|
|
699
|
-
</div>
|
|
700
|
-
<div class="stat">
|
|
701
|
-
<p class="stat__value">10M+</p>
|
|
702
|
-
<p class="stat__label">Users</p>
|
|
703
|
-
</div>
|
|
704
|
-
</div>
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
**States required:**
|
|
708
|
-
- Default
|
|
709
|
-
- Animated count-up (optional)
|
|
710
|
-
|
|
711
|
-
**Common mistakes:**
|
|
712
|
-
- Invented metrics without source data
|
|
713
|
-
- Using too many stats (3-5 max)
|
|
714
|
-
- Small text with low contrast
|
|
715
|
-
- Animating numbers that don't add value
|
|
716
|
-
|
|
717
|
-
---
|
|
718
|
-
|
|
719
|
-
## Timeline
|
|
720
|
-
|
|
721
|
-
**When to use:** Display chronological events, project milestones, or history.
|
|
722
|
-
|
|
723
|
-
**Structure:**
|
|
724
|
-
```html
|
|
725
|
-
<ul class="timeline">
|
|
726
|
-
<li class="timeline__item">
|
|
727
|
-
<div class="timeline__marker"></div>
|
|
728
|
-
<div class="timeline__content">
|
|
729
|
-
<h3 class="timeline__title">Project Launch</h3>
|
|
730
|
-
<time class="timeline__date" datetime="2024-01-15">Jan 15, 2024</time>
|
|
731
|
-
<p>Successfully launched to 10,000 users.</p>
|
|
732
|
-
</div>
|
|
733
|
-
</li>
|
|
734
|
-
</ul>
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
**States required:**
|
|
738
|
-
- Default
|
|
739
|
-
- Active (current milestone)
|
|
740
|
-
- Hover (card highlight)
|
|
741
|
-
|
|
742
|
-
**Common mistakes:**
|
|
743
|
-
- Vertical line broken by card backgrounds
|
|
744
|
-
- Using cards for every item (too heavy)
|
|
745
|
-
- Missing time elements for SEO
|
|
746
|
-
- Reverse chronological order without clear label
|
|
747
|
-
|
|
748
|
-
---
|
|
749
|
-
|
|
750
|
-
## Stepper
|
|
751
|
-
|
|
752
|
-
**When to use:** Guide users through multi-step processes like checkout, onboarding, or forms.
|
|
753
|
-
|
|
754
|
-
**Structure:**
|
|
755
|
-
```html
|
|
756
|
-
<ol class="stepper">
|
|
757
|
-
<li class="stepper__step stepper__step--completed">
|
|
758
|
-
<span class="stepper__marker">1</span>
|
|
759
|
-
<span class="stepper__label">Account</span>
|
|
760
|
-
</li>
|
|
761
|
-
<li class="stepper__step stepper__step--active">
|
|
762
|
-
<span class="stepper__marker">2</span>
|
|
763
|
-
<span class="stepper__label">Details</span>
|
|
764
|
-
</li>
|
|
765
|
-
<li class="stepper__step">
|
|
766
|
-
<span class="stepper__marker">3</span>
|
|
767
|
-
<span class="stepper__label">Confirm</span>
|
|
768
|
-
</li>
|
|
769
|
-
</ol>
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
**States required:**
|
|
773
|
-
- Completed (checkmark, muted)
|
|
774
|
-
- Active (highlighted, connected line)
|
|
775
|
-
- Pending (muted, no connection)
|
|
776
|
-
- Disabled (grayed out)
|
|
777
|
-
|
|
778
|
-
**Common mistakes:**
|
|
779
|
-
- Using icons without text labels
|
|
780
|
-
- Not indicating progress visually
|
|
781
|
-
- Allowing users to skip steps
|
|
782
|
-
- Not persisting step state across navigation
|
|
783
|
-
|
|
784
|
-
---
|
|
785
|
-
|
|
786
|
-
## FileUpload
|
|
787
|
-
|
|
788
|
-
**When to use:** Allow users to upload documents, images, or other files.
|
|
789
|
-
|
|
790
|
-
**Structure:**
|
|
791
|
-
```html
|
|
792
|
-
<div class="file-upload">
|
|
793
|
-
<input type="file" id="file" class="file-upload__input" />
|
|
794
|
-
<label class="file-upload__dropzone" for="file">
|
|
795
|
-
<svg>...</svg>
|
|
796
|
-
<p>Drag files here or <span class="file-upload__link">browse</span></p>
|
|
797
|
-
</label>
|
|
798
|
-
<ul class="file-upload__list">
|
|
799
|
-
<li class="file-upload__item">
|
|
800
|
-
<span>document.pdf</span>
|
|
801
|
-
<button class="file-upload__remove" aria-label="Remove document.pdf">×</button>
|
|
802
|
-
</li>
|
|
803
|
-
</ul>
|
|
804
|
-
</div>
|
|
805
|
-
```
|
|
806
|
-
|
|
807
|
-
**States required:**
|
|
808
|
-
- Default (dropzone)
|
|
809
|
-
- Drag over (highlighted border)
|
|
810
|
-
- Uploading (progress bar)
|
|
811
|
-
- Success (file listed)
|
|
812
|
-
- Error (message, retry option)
|
|
813
|
-
|
|
814
|
-
**Common mistakes:**
|
|
815
|
-
- Using `<div>` instead of `<input type="file">`
|
|
816
|
-
- Not supporting drag and drop
|
|
817
|
-
- Not showing file names after selection
|
|
818
|
-
- Missing file size/type validation feedback
|
|
819
|
-
|
|
820
|
-
---
|
|
821
|
-
|
|
822
|
-
## DatePicker
|
|
823
|
-
|
|
824
|
-
**When to use:** Select dates for appointments, bookings, filters, or scheduling.
|
|
825
|
-
|
|
826
|
-
**Structure:**
|
|
827
|
-
```html
|
|
828
|
-
<div class="date-picker">
|
|
829
|
-
<label class="date-picker__label" for="date">Select date</label>
|
|
830
|
-
<div class="date-picker__input-group">
|
|
831
|
-
<input
|
|
832
|
-
class="date-picker__input"
|
|
833
|
-
type="text"
|
|
834
|
-
id="date"
|
|
835
|
-
placeholder="MM/DD/YYYY"
|
|
836
|
-
readonly
|
|
837
|
-
/>
|
|
838
|
-
<button class="date-picker__toggle" aria-label="Open calendar">
|
|
839
|
-
<svg>...</svg>
|
|
840
|
-
</button>
|
|
841
|
-
</div>
|
|
842
|
-
<div class="date-picker__calendar" role="dialog" aria-label="Calendar">
|
|
843
|
-
<div class="date-picker__header">
|
|
844
|
-
<button aria-label="Previous month">←</button>
|
|
845
|
-
<span>January 2024</span>
|
|
846
|
-
<button aria-label="Next month">→</button>
|
|
847
|
-
</div>
|
|
848
|
-
<div class="date-picker__grid" role="grid">
|
|
849
|
-
<button role="gridcell">1</button>
|
|
850
|
-
<button role="gridcell" aria-selected="true">2</button>
|
|
851
|
-
</div>
|
|
852
|
-
</div>
|
|
853
|
-
</div>
|
|
854
|
-
```
|
|
855
|
-
|
|
856
|
-
**States required:**
|
|
857
|
-
- Default (input with icon)
|
|
858
|
-
- Open (calendar visible)
|
|
859
|
-
- Hover (date cells)
|
|
860
|
-
- Selected (highlighted date)
|
|
861
|
-
- Disabled (past dates, grayed out)
|
|
862
|
-
|
|
863
|
-
**Common mistakes:**
|
|
864
|
-
- Not supporting keyboard navigation
|
|
865
|
-
- Missing month/year navigation ARIA
|
|
866
|
-
- Using native `<input type="date">` without custom styling fallback
|
|
867
|
-
- Not validating date ranges
|
|
868
|
-
|
|
869
|
-
---
|
|
870
|
-
|
|
871
|
-
## ColorPicker
|
|
872
|
-
|
|
873
|
-
**When to use:** Select colors for themes, branding, design tools, or customization.
|
|
874
|
-
|
|
875
|
-
**Structure:**
|
|
876
|
-
```html
|
|
877
|
-
<div class="color-picker">
|
|
878
|
-
<label class="color-picker__label" for="color">Choose color</label>
|
|
879
|
-
<div class="color-picker__swatches">
|
|
880
|
-
<button class="color-picker__swatch" style="background: #6366f1" aria-label="Indigo"></button>
|
|
881
|
-
<button class="color-picker__swatch" style="background: #8b5cf6" aria-label="Violet"></button>
|
|
882
|
-
</div>
|
|
883
|
-
<input class="color-picker__input" type="color" id="color" value="#6366f1" />
|
|
884
|
-
</div>
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
**States required:**
|
|
888
|
-
- Default (swatches + input)
|
|
889
|
-
- Hover (swatch ring)
|
|
890
|
-
- Selected (checkmark or border)
|
|
891
|
-
- Focus (input ring)
|
|
892
|
-
|
|
893
|
-
**Common mistakes:**
|
|
894
|
-
- Using `<input type="color">` without custom swatches
|
|
895
|
-
- Not providing hex/RGB text input
|
|
896
|
-
- Poor color contrast for selected state
|
|
897
|
-
- Not supporting keyboard navigation between swatches
|
|
898
|
-
|
|
899
|
-
---
|
|
900
|
-
|
|
901
|
-
## Tooltip
|
|
902
|
-
|
|
903
|
-
**When to use:** Provide brief, contextual information on hover or focus.
|
|
904
|
-
|
|
905
|
-
**Structure:**
|
|
906
|
-
```html
|
|
907
|
-
<div class="tooltip" data-tooltip="Save your changes">
|
|
908
|
-
<button class="tooltip__trigger">Save</button>
|
|
909
|
-
<div class="tooltip__content" role="tooltip" aria-hidden="true">
|
|
910
|
-
Save your changes
|
|
911
|
-
</div>
|
|
912
|
-
</div>
|
|
913
|
-
```
|
|
914
|
-
|
|
915
|
-
**States required:**
|
|
916
|
-
- Hidden (default)
|
|
917
|
-
- Visible (on hover/focus)
|
|
918
|
-
- Hover (tooltip stays visible)
|
|
919
|
-
|
|
920
|
-
**Common mistakes:**
|
|
921
|
-
- Using tooltips for critical information (use Alert instead)
|
|
922
|
-
- Not managing `aria-describedby` linkage
|
|
923
|
-
- Tooltips covering the triggering element
|
|
924
|
-
- Long tooltip text without line breaks
|
|
925
|
-
|
|
926
|
-
---
|
|
927
|
-
|
|
928
|
-
## Popover
|
|
929
|
-
|
|
930
|
-
**When to use:** Display contextual menus, forms, or detailed content anchored to a trigger element.
|
|
931
|
-
|
|
932
|
-
**Structure:**
|
|
933
|
-
```html
|
|
934
|
-
<div class="popover">
|
|
935
|
-
<button class="popover__trigger" aria-expanded="false">Options</button>
|
|
936
|
-
<div class="popover__content" role="dialog" aria-label="Options menu">
|
|
937
|
-
<button class="popover__item">Edit</button>
|
|
938
|
-
<button class="popover__item">Delete</button>
|
|
939
|
-
</div>
|
|
940
|
-
</div>
|
|
941
|
-
```
|
|
942
|
-
|
|
943
|
-
**States required:**
|
|
944
|
-
- Closed
|
|
945
|
-
- Open
|
|
946
|
-
- Focus trap (when open)
|
|
947
|
-
- Hover (items)
|
|
948
|
-
|
|
949
|
-
**Common mistakes:**
|
|
950
|
-
- Not managing focus trap
|
|
951
|
-
- Closing on outside click without proper event handling
|
|
952
|
-
- Using popover for navigation (use Dropdown instead)
|
|
953
|
-
- Missing `aria-expanded` on trigger
|
|
954
|
-
|
|
955
|
-
---
|
|
956
|
-
|
|
957
|
-
## Skeleton
|
|
958
|
-
|
|
959
|
-
**When to use:** Show placeholder structure while content loads.
|
|
960
|
-
|
|
961
|
-
**Structure:**
|
|
962
|
-
```html
|
|
963
|
-
<div class="skeleton">
|
|
964
|
-
<div class="skeleton__avatar"></div>
|
|
965
|
-
<div class="skeleton__content">
|
|
966
|
-
<div class="skeleton__title"></div>
|
|
967
|
-
<div class="skeleton__text"></div>
|
|
968
|
-
</div>
|
|
969
|
-
</div>
|
|
970
|
-
```
|
|
971
|
-
|
|
972
|
-
**States required:**
|
|
973
|
-
- Loading (shimmer animation)
|
|
974
|
-
- Loaded (content replaces skeleton)
|
|
975
|
-
|
|
976
|
-
**Common mistakes:**
|
|
977
|
-
- Using spinner instead of skeleton for content areas
|
|
978
|
-
- Incorrect skeleton dimensions (doesn't match actual content)
|
|
979
|
-
- Not removing skeleton from DOM after load
|
|
980
|
-
- Animating skeleton too fast (distracting)
|
|
981
|
-
|
|
982
|
-
---
|
|
983
|
-
|
|
984
|
-
## FileUpload (Alternative: DropZone)
|
|
985
|
-
|
|
986
|
-
**When to use:** Drag-and-drop file uploads with visual feedback.
|
|
987
|
-
|
|
988
|
-
**Structure:**
|
|
989
|
-
```html
|
|
990
|
-
<div class="dropzone" role="button" tabindex="0" aria-label="Upload files">
|
|
991
|
-
<svg class="dropzone__icon">...</svg>
|
|
992
|
-
<p class="dropzone__text">Drop files here or click to browse</p>
|
|
993
|
-
<p class="dropzone__hint">PNG, JPG up to 5MB</p>
|
|
994
|
-
</div>
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
**States required:**
|
|
998
|
-
- Default
|
|
999
|
-
- Drag over (border highlight, background change)
|
|
1000
|
-
- Disabled (grayed out, no interaction)
|
|
1001
|
-
- Uploading (progress indicator)
|
|
1002
|
-
|
|
1003
|
-
**Common mistakes:**
|
|
1004
|
-
- Not supporting keyboard activation (Enter/Space)
|
|
1005
|
-
- Missing file type and size validation
|
|
1006
|
-
- No visual feedback during drag
|
|
1007
|
-
- Uploading multiple files without queue indication
|
|
1008
|
-
|
|
1009
|
-
---
|
|
1010
|
-
|
|
1011
|
-
## DatePicker (Alternative: DateRange)
|
|
1012
|
-
|
|
1013
|
-
**When to use:** Select start and end dates for bookings, reports, or filters.
|
|
1014
|
-
|
|
1015
|
-
**Structure:**
|
|
1016
|
-
```html
|
|
1017
|
-
<div class="date-range">
|
|
1018
|
-
<div class="date-range__field">
|
|
1019
|
-
<label for="start-date">Start</label>
|
|
1020
|
-
<input type="date" id="start-date" />
|
|
1021
|
-
</div>
|
|
1022
|
-
<span class="date-range__separator">to</span>
|
|
1023
|
-
<div class="date-range__field">
|
|
1024
|
-
<label for="end-date">End</label>
|
|
1025
|
-
<input type="date" id="end-date" />
|
|
1026
|
-
</div>
|
|
1027
|
-
</div>
|
|
1028
|
-
```
|
|
1029
|
-
|
|
1030
|
-
**States required:**
|
|
1031
|
-
- Default
|
|
1032
|
-
- Invalid range (end date before start date)
|
|
1033
|
-
- Disabled dates (past dates, weekends)
|
|
1034
|
-
|
|
1035
|
-
**Common mistakes:**
|
|
1036
|
-
- Not validating start < end relationship
|
|
1037
|
-
- Using two separate inputs without clear relationship
|
|
1038
|
-
- Not providing preset ranges (Last 7 days, This month)
|
|
1039
|
-
- Poor mobile date input experience
|
|
1040
|
-
|
|
1041
|
-
---
|
|
1042
|
-
|
|
1043
|
-
## ColorPicker (Alternative: ThemePicker)
|
|
1044
|
-
|
|
1045
|
-
**When to use:** Select from predefined theme colors or create custom themes.
|
|
1046
|
-
|
|
1047
|
-
**Structure:**
|
|
1048
|
-
```html
|
|
1049
|
-
<div class="theme-picker">
|
|
1050
|
-
<p class="theme-picker__label">Choose theme</p>
|
|
1051
|
-
<div class="theme-picker__options">
|
|
1052
|
-
<button class="theme-picker__option theme-picker__option--active" data-theme="light">
|
|
1053
|
-
<span class="theme-picker__preview"></span>
|
|
1054
|
-
Light
|
|
1055
|
-
</button>
|
|
1056
|
-
<button class="theme-picker__option" data-theme="dark">
|
|
1057
|
-
<span class="theme-picker__preview"></span>
|
|
1058
|
-
Dark
|
|
1059
|
-
</button>
|
|
1060
|
-
</div>
|
|
1061
|
-
</div>
|
|
1062
|
-
```
|
|
1063
|
-
|
|
1064
|
-
**States required:**
|
|
1065
|
-
- Default
|
|
1066
|
-
- Selected (border, checkmark)
|
|
1067
|
-
- Hover (subtle ring)
|
|
1068
|
-
|
|
1069
|
-
**Common mistakes:**
|
|
1070
|
-
- Not applying theme immediately on selection
|
|
1071
|
-
- Using radio buttons without proper ARIA
|
|
1072
|
-
- Missing preview of theme colors
|
|
1073
|
-
- Not persisting theme choice
|
|
1074
|
-
|
|
1075
|
-
---
|
|
1076
|
-
|
|
1077
|
-
## Tooltip (Alternative: InfoPopover)
|
|
1078
|
-
|
|
1079
|
-
**When to use:** Display rich content or forms anchored to a trigger, dismissible by user.
|
|
1080
|
-
|
|
1081
|
-
**Structure:**
|
|
1082
|
-
```html
|
|
1083
|
-
<div class="info-popover">
|
|
1084
|
-
<button class="info-popover__trigger" aria-expanded="false">?</button>
|
|
1085
|
-
<div class="info-popover__content">
|
|
1086
|
-
<h4>Need help?</h4>
|
|
1087
|
-
<p>Contact support for assistance.</p>
|
|
1088
|
-
<a href="#contact">Contact us</a>
|
|
1089
|
-
</div>
|
|
1090
|
-
</div>
|
|
1091
|
-
```
|
|
1092
|
-
|
|
1093
|
-
**States required:**
|
|
1094
|
-
- Closed
|
|
1095
|
-
- Open
|
|
1096
|
-
- Focus trap (when open)
|
|
1097
|
-
|
|
1098
|
-
**Common mistakes:**
|
|
1099
|
-
- Triggering on hover only (no keyboard support)
|
|
1100
|
-
- Not dismissing on Escape
|
|
1101
|
-
- Popover positioning causing viewport overflow
|
|
1102
|
-
- Missing close button
|
|
1103
|
-
|
|
1104
|
-
---
|
|
1105
|
-
|
|
1106
|
-
## Skeleton (Alternative: ContentSkeleton)
|
|
1107
|
-
|
|
1108
|
-
**When to use:** Complex content placeholders for cards, lists, or article pages.
|
|
1109
|
-
|
|
1110
|
-
**Structure:**
|
|
1111
|
-
```html
|
|
1112
|
-
<article class="skeleton-card">
|
|
1113
|
-
<div class="skeleton-card__media"></div>
|
|
1114
|
-
<div class="skeleton-card__body">
|
|
1115
|
-
<div class="skeleton skeleton--title"></div>
|
|
1116
|
-
<div class="skeleton skeleton--text"></div>
|
|
1117
|
-
<div class="skeleton skeleton--text skeleton--short"></div>
|
|
1118
|
-
</div>
|
|
1119
|
-
</article>
|
|
1120
|
-
```
|
|
1121
|
-
|
|
1122
|
-
**States required:**
|
|
1123
|
-
- Loading (animated shimmer)
|
|
1124
|
-
- Loaded (content visible)
|
|
1125
|
-
|
|
1126
|
-
**Common mistakes:**
|
|
1127
|
-
- Skeleton dimensions not matching actual content
|
|
1128
|
-
- Too many skeleton elements (overkill)
|
|
1129
|
-
- Shimmer animation too fast or slow
|
|
1130
|
-
- Not transitioning smoothly to loaded state
|
|
1131
|
-
|
|
1132
|
-
---
|
|
1133
|
-
|
|
1134
|
-
## Additional Archetypes
|
|
1135
|
-
|
|
1136
|
-
### Toast
|
|
1137
|
-
**When to use:** Temporary notifications that auto-dismiss.
|
|
1138
|
-
**Structure:** Fixed positioned bar with icon, message, and close button.
|
|
1139
|
-
**States:** Success, error, warning, info, dismissing.
|
|
1140
|
-
**Mistakes:** Stacking too many, not auto-dismissing, missing close button.
|
|
1141
|
-
|
|
1142
|
-
### Toggle
|
|
1143
|
-
**When to use:** Binary on/off states for settings.
|
|
1144
|
-
**Structure:** Checkbox input with sliding thumb visual.
|
|
1145
|
-
**States:** On, off, disabled, loading.
|
|
1146
|
-
**Mistakes:** Using checkbox without visual toggle, confusing on/off labels, missing focus state.
|
|
1147
|
-
|
|
1148
|
-
### Slider
|
|
1149
|
-
**When to use:** Select values within a range.
|
|
1150
|
-
**Structure:** Track with draggable thumb, optional min/max labels.
|
|
1151
|
-
**States:** Default, dragging, min, max, disabled.
|
|
1152
|
-
**Mistakes:** No keyboard support, missing value display, breakpoints not aligned to values.
|
|
1153
|
-
|
|
1154
|
-
### Accordion
|
|
1155
|
-
**When to use:** Expandable sections for FAQs or grouped content.
|
|
1156
|
-
**Structure:** `<details>` or div with header and collapsible panel.
|
|
1157
|
-
**States:** Closed, open, hover, disabled.
|
|
1158
|
-
**Mistakes:** Multiple panels open at once (confusing), no animation, missing ARIA.
|
|
1159
|
-
|
|
1160
|
-
### Search
|
|
1161
|
-
**When to use:** Filter or find content within a page or dataset.
|
|
1162
|
-
**Structure:** Input with search icon, optional clear button, results dropdown.
|
|
1163
|
-
**States:** Default, typing, results shown, empty state, loading.
|
|
1164
|
-
**Mistakes:** No debounce, no empty state, missing search icon, results not keyboard navigable.
|
|
1165
|
-
|
|
1166
|
-
### Select
|
|
1167
|
-
**When to use:** Single or multiple selection from predefined options.
|
|
1168
|
-
**Structure:** Native `<select>` or custom dropdown with checkboxes.
|
|
1169
|
-
**States:** Default, open, selected, disabled.
|
|
1170
|
-
**Mistakes:** Custom select without keyboard support, missing label, options too long.
|
|
1171
|
-
|
|
1172
|
-
### ProgressBar
|
|
1173
|
-
**When to use:** Indicate completion status for uploads, forms, or tasks.
|
|
1174
|
-
**Structure:** Track with filled portion, percentage label.
|
|
1175
|
-
**States:** Indeterminate, determinate, complete, error.
|
|
1176
|
-
**Mistakes:** No percentage label, wrong color for complete state, not accessible to screen readers.
|
|
1177
|
-
|
|
1178
|
-
### Spinner
|
|
1179
|
-
**When to use:** Indicate loading for async operations.
|
|
1180
|
-
**Structure:** Animated circle or dots, optional label.
|
|
1181
|
-
**States:** Spinning, complete, error.
|
|
1182
|
-
**Mistakes:** No accessible label, animating too fast, using for short operations.
|
|
1183
|
-
|
|
1184
|
-
### Toolbar
|
|
1185
|
-
**When to use:** Group related actions for content editing or viewing.
|
|
1186
|
-
**Structure:** Row of icon buttons with separators, optional overflow menu.
|
|
1187
|
-
**States:** Default, hover, active, disabled, overflow.
|
|
1188
|
-
**Mistakes:** Too many buttons without overflow, no tooltips on icons, missing keyboard navigation.
|
|
1189
|
-
|
|
1190
|
-
### EmptyState
|
|
1191
|
-
**When to use:** Communicate no data scenarios with guidance.
|
|
1192
|
-
**Structure:** Illustration/icon, headline, description, CTA.
|
|
1193
|
-
**States:** Default.
|
|
1194
|
-
**Mistakes:** Generic copy ("No data"), no CTA, poor illustration choice, not actionable.
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
> Nguyên tắc: không render toàn bộ `items.map()` trực tiếp khi danh sách có thể vượt vài trăm phần tử.
|
|
377
|
+
|
|
378
|
+
### 4.3 TanStack Form (form nhiều field, validate type-safe)
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
// components/EditProfileForm.tsx
|
|
382
|
+
import { useForm } from "@tanstack/react-form";
|
|
383
|
+
import { z } from "zod";
|
|
384
|
+
|
|
385
|
+
const schema = z.object({
|
|
386
|
+
name: z.string().min(1, "Tên không được để trống"),
|
|
387
|
+
email: z.string().email("Email không hợp lệ"),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
export function EditProfileForm({ onSubmit }: { onSubmit: (v: z.infer<typeof schema>) => void }) {
|
|
391
|
+
const form = useForm({
|
|
392
|
+
defaultValues: { name: "", email: "" },
|
|
393
|
+
onSubmit: async ({ value }) => onSubmit(schema.parse(value)),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
|
|
398
|
+
<form.Field
|
|
399
|
+
name="name"
|
|
400
|
+
validators={{ onChange: ({ value }) => (value.length ? undefined : "Tên không được để trống") }}
|
|
401
|
+
>
|
|
402
|
+
{(field) => (
|
|
403
|
+
<>
|
|
404
|
+
<input
|
|
405
|
+
value={field.state.value}
|
|
406
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
407
|
+
onBlur={field.handleBlur}
|
|
408
|
+
/>
|
|
409
|
+
{field.state.meta.errors.length > 0 && <p role="alert">{field.state.meta.errors[0]}</p>}
|
|
410
|
+
</>
|
|
411
|
+
)}
|
|
412
|
+
</form.Field>
|
|
413
|
+
<Button type="submit">Lưu</Button>
|
|
414
|
+
</form>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
> Nguyên tắc: validate schema dùng chung (Zod) giữa client và server; mỗi field tự quản lý re-render qua `form.Field`, tránh re-render toàn form mỗi keystroke.
|
|
419
|
+
|
|
420
|
+
### 4.4 TanStack Router (route + loader prefetch)
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
// routes/users.$userId.tsx
|
|
424
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
425
|
+
import { userKeys } from "../queryKeys";
|
|
426
|
+
import { fetchUserProfile } from "../services/userService";
|
|
427
|
+
|
|
428
|
+
export const Route = createFileRoute("/users/$userId")({
|
|
429
|
+
loader: ({ context: { queryClient }, params }) =>
|
|
430
|
+
queryClient.ensureQueryData({
|
|
431
|
+
queryKey: userKeys.detail(params.userId),
|
|
432
|
+
queryFn: () => fetchUserProfile(params.userId),
|
|
433
|
+
}),
|
|
434
|
+
component: UserPage,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
function UserPage() {
|
|
438
|
+
const { userId } = Route.useParams();
|
|
439
|
+
// Data đã được prefetch trong loader — useQuery ở đây chỉ đọc cache, không fetch lại
|
|
440
|
+
return <UserCard userId={userId} />;
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
> Nguyên tắc: loader prefetch qua `queryClient`, component tiêu thụ lại qua `useQuery`/hook cùng `queryKey` — tránh fetch 2 lần.
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## 5. Checklist khi sinh component mới
|
|
448
|
+
|
|
449
|
+
- [ ] Props có `interface` riêng, đặt tên `<Component>Props`
|
|
450
|
+
- [ ] Logic phức tạp (>1 state hoặc có side-effect) được tách vào custom hook
|
|
451
|
+
- [ ] Không gọi `fetch`/API trực tiếp trong file `.tsx` trình bày
|
|
452
|
+
- [ ] Style dùng Design Token/Tailwind class, không hardcode màu/spacing
|
|
453
|
+
- [ ] Có xử lý đủ trạng thái loading/error/empty nếu có dữ liệu async
|
|
454
|
+
- [ ] Có ARIA attributes và dùng đúng thẻ semantic
|
|
455
|
+
- [ ] Export qua `index.ts` để import path gọn (`@/components/Button`)
|
|
456
|
+
- [ ] Bảng dữ liệu dùng TanStack Table (`columns` tách khỏi JSX), danh sách lớn dùng TanStack Virtual
|
|
457
|
+
- [ ] Form nhiều field dùng TanStack Form với schema Zod dùng chung client/server
|
|
458
|
+
- [ ] Route có params/search dùng TanStack Router type-safe, dữ liệu prefetch qua `loader` + `ensureQueryData`
|