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.
Files changed (79) hide show
  1. package/.agents/LICENSE +21 -21
  2. package/.agents/README.md +90 -90
  3. package/.agents/docs/CHI_TIET_SKILLS.md +125 -125
  4. package/.agents/docs/MIGRATION-CLEANUP-V8.1.3.md +36 -36
  5. package/.agents/docs/MIGRATION-STATUS.md +35 -35
  6. package/.agents/docs/MIGRATION-V8.md +10 -10
  7. package/.agents/docs/V8-CERTIFICATION.md +27 -27
  8. package/.agents/docs/decisions/ADR-001-v8-migration.md +58 -58
  9. package/.agents/docs/decisions/ADR-002-workflow-separation.md +50 -50
  10. package/.agents/docs/decisions/ADR-003-registry-generated.md +54 -54
  11. package/.agents/docs/decisions/ADR-008-skill-boundary-review.md +27 -27
  12. package/.agents/registry/graph.json +4 -8
  13. package/.agents/registry/index.yaml +3 -4
  14. package/.agents/registry/skills-index.yml +525 -525
  15. package/.agents/rules/coding.md +10 -0
  16. package/.agents/rules/command-safety.md +37 -14
  17. package/.agents/rules/priorities.yml +13 -2
  18. package/.agents/rules/security.md +47 -7
  19. package/.agents/rules/skill-quality.md +32 -8
  20. package/.agents/skills/_template/SKILL.md +133 -132
  21. package/.agents/skills/_template/capability.yaml +20 -20
  22. package/.agents/skills/_template/evals/scorecard.yaml +19 -19
  23. package/.agents/skills/qk-access-policy/SKILL.md +4 -1
  24. package/.agents/skills/qk-agent-observability/SKILL.md +111 -0
  25. package/.agents/skills/qk-agent-observability/references/scorecard.yaml +80 -0
  26. package/.agents/skills/qk-ai-builder/SKILL.md +74 -5
  27. package/.agents/skills/qk-api-lifecycle/SKILL.md +4 -2
  28. package/.agents/skills/qk-bug-resolution/SKILL.md +3 -21
  29. package/.agents/skills/qk-code-review/SKILL.md +188 -189
  30. package/.agents/skills/qk-context-loader/SKILL.md +3 -47
  31. package/.agents/skills/qk-data-lifecycle/SKILL.md +7 -2
  32. package/.agents/skills/qk-db-optimizer/SKILL.md +3 -2
  33. package/.agents/skills/qk-design-system-engineering/SKILL.md +235 -112
  34. package/.agents/skills/qk-devops-platform/SKILL.md +241 -117
  35. package/.agents/skills/qk-docs/SKILL.md +3 -1
  36. package/.agents/skills/qk-engineering-standard/SKILL.md +4 -75
  37. package/.agents/skills/qk-engineering-standard/references/anti-patterns.md +121 -0
  38. package/.agents/skills/qk-engineering-standard/rules/frontend.md +1 -1
  39. package/.agents/skills/qk-fe-api-integration/SKILL.md +13 -32
  40. package/.agents/skills/qk-feature-delivery/SKILL.md +54 -202
  41. package/.agents/skills/qk-frontend-architecture/SKILL.md +258 -134
  42. package/.agents/skills/qk-help/SKILL.md +21 -159
  43. package/.agents/skills/qk-orchestrator/SKILL.md +2 -78
  44. package/.agents/skills/qk-orchestrator/references/routing-table.md +15 -3
  45. package/.agents/skills/qk-product-specification/SKILL.md +253 -130
  46. package/.agents/skills/qk-production-release/SKILL.md +32 -67
  47. package/.agents/skills/qk-project-bootstrap/SKILL.md +59 -8
  48. package/.agents/skills/qk-project-health/SKILL.md +4 -2
  49. package/.agents/skills/qk-project-memory/SKILL.md +3 -1
  50. package/.agents/skills/qk-security-audit/SKILL.md +259 -135
  51. package/.agents/skills/qk-system-evolution/SKILL.md +18 -68
  52. package/.agents/skills/qk-test-engineering/SKILL.md +262 -139
  53. package/.agents/skills/qk-ui-audit/SKILL.md +16 -89
  54. package/.agents/skills/qk-ui-audit/references/anti-slop-checklist.md +2 -2
  55. package/.agents/skills/qk-ui-builder/SKILL.md +482 -509
  56. package/.agents/skills/qk-ui-builder/references/component-cookbook.md +455 -1191
  57. package/.agents/skills/qk-ui-system-builder/SKILL.md +1 -5
  58. package/.agents/skills/qk-validation-gate/SKILL.md +0 -74
  59. package/.agents/skills/qk-web-quality-gate/SKILL.md +232 -114
  60. package/.agents/workflows/_schema.yml +146 -109
  61. package/.agents/workflows/bug-resolution.yml +121 -101
  62. package/.agents/workflows/code-review.yml +93 -77
  63. package/.agents/workflows/documentation.yml +90 -75
  64. package/.agents/workflows/feature-delivery.yml +120 -103
  65. package/.agents/workflows/refactor.yml +99 -81
  66. package/.agents/workflows/research.yml +75 -60
  67. package/.agents/workflows/security-audit.yml +115 -72
  68. package/.agents/workflows/skill-evolution.yml +97 -65
  69. package/.agents/workflows/spec-driven-development.yml +87 -57
  70. package/CHANGELOG.md +10 -0
  71. package/README.md +90 -90
  72. package/bin/install.js +330 -180
  73. package/package.json +2 -2
  74. package/.agents/CHANGELOG.md +0 -131
  75. package/.agents/learnings/draft/README.md +0 -37
  76. package/.agents/reports/RELEASE-CHECKLIST.md +0 -29
  77. package/.agents/reports/architecture-audit.md +0 -13
  78. package/.agents/reports/graph-health.md +0 -20
  79. package/.agents/reports/skill-audit.md +0 -215
@@ -1,1194 +1,458 @@
1
- # Component Cookbook: 30 UI Component Archetypes
1
+ # 📘 Component Cookbook Blueprint chuẩn
2
2
 
3
- ## Button
4
-
5
- **When to use:** Trigger actions, submit forms, navigate, or toggle states. Primary user interaction point.
6
-
7
- **Structure:**
8
- ```html
9
- <button type="button" class="btn btn--primary">
10
- <span class="btn__text">Save Changes</span>
11
- </button>
12
- ```
13
-
14
- **States required:**
15
- - Default
16
- - Hover (color shift, subtle shadow)
17
- - Active (slight scale down or darker shade)
18
- - Focus (visible focus ring)
19
- - Disabled (reduced opacity, `cursor: not-allowed`, `aria-disabled="true"`)
20
- - Loading (spinner inside button, text changes to "Saving...")
21
-
22
- **Common mistakes:**
23
- - Using `<div>` or `<a>` instead of `<button>`
24
- - Applying `transition: all` instead of specific properties
25
- - Forgetting `type="button"` in forms (causes unintended submit)
26
- - Making disabled buttons fully transparent (fails contrast)
27
-
28
- ---
29
-
30
- ## Input
31
-
32
- **When to use:** Collect single-line text, numbers, or short responses from users.
33
-
34
- **Structure:**
35
- ```html
36
- <div class="form-field">
37
- <label class="form-field__label" for="email">Email address</label>
38
- <input
39
- class="form-field__input"
40
- type="email"
41
- id="email"
42
- name="email"
43
- placeholder="you@example.com"
44
- autocomplete="email"
45
- />
46
- <p class="form-field__error" id="email-error">Please enter a valid email</p>
47
- </div>
48
- ```
49
-
50
- **States required:**
51
- - Default
52
- - Focus (border color change, subtle ring)
53
- - Filled (background or border indicates value present)
54
- - Error (red border, error message visible, `aria-invalid="true"`)
55
- - Disabled (grayed out, `disabled` attribute)
56
- - Readonly (visual distinction from disabled, `readonly` attribute)
57
-
58
- **Common mistakes:**
59
- - Missing associated `<label>` (accessibility violation)
60
- - Using placeholder as the only label
61
- - Not providing `autocomplete` attributes
62
- - Forgetting `id`/`for` linkage between label and input
63
-
64
- ---
65
-
66
- ## Card
67
-
68
- **When to use:** Group related content, display summaries, or create visual containers for actions.
69
-
70
- **Structure:**
71
- ```html
72
- <article class="card">
73
- <div class="card__header">
74
- <h3 class="card__title">Project Alpha</h3>
75
- <span class="card__badge">Active</span>
76
- </div>
77
- <p class="card__body">Last updated 2 hours ago</p>
78
- <div class="card__actions">
79
- <button class="btn btn--secondary">View Details</button>
80
- </div>
81
- </article>
82
- ```
83
-
84
- **States required:**
85
- - Default (subtle shadow, border)
86
- - Hover (elevated shadow, slight lift)
87
- - Selected (border highlight, checkmark)
88
- - Disabled (reduced opacity, no hover)
89
-
90
- **Common mistakes:**
91
- - Nesting cards inside cards (card-in-card syndrome)
92
- - Making the entire card clickable without clear affordance
93
- - Using cards for unrelated content
94
- - Ignoring card height consistency when in grids
95
-
96
- ---
97
-
98
- ## Modal
99
-
100
- **When to use:** Focused tasks requiring user attention, confirmations, or forms that block main workflow.
101
-
102
- **Structure:**
103
- ```html
104
- <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
105
- <div class="modal__backdrop"></div>
106
- <div class="modal__content">
107
- <header class="modal__header">
108
- <h2 id="modal-title">Confirm Action</h2>
109
- <button class="modal__close" aria-label="Close modal">&times;</button>
110
- </header>
111
- <div class="modal__body">
112
- <p>Are you sure you want to delete this item?</p>
113
- </div>
114
- <footer class="modal__footer">
115
- <button class="btn btn--secondary">Cancel</button>
116
- <button class="btn btn--danger">Delete</button>
117
- </footer>
118
- </div>
119
- </div>
120
- ```
121
-
122
- **States required:**
123
- - Closed (hidden from DOM or `display: none`)
124
- - Opening (fade/scale animation)
125
- - Open (visible, backdrop active)
126
- - Closing (exit animation)
127
-
128
- **Common mistakes:**
129
- - Trapping focus incorrectly or not at all
130
- - Not closing on Escape key
131
- - Not preventing body scroll when open
132
- - Nesting modals inside modals
133
- - Missing `aria-modal` and `role="dialog"`
134
-
135
- ---
136
-
137
- ## Dropdown
138
-
139
- **When to use:** Select one option from a list, toggle menus, or filter content.
140
-
141
- **Structure:**
142
- ```html
143
- <div class="dropdown">
144
- <button
145
- class="dropdown__trigger"
146
- aria-haspopup="listbox"
147
- aria-expanded="false"
148
- aria-labelledby="dropdown-label"
149
- >
150
- Select option
151
- <svg class="dropdown__icon">...</svg>
152
- </button>
153
- <ul class="dropdown__menu" role="listbox" aria-labelledby="dropdown-label">
154
- <li role="option" aria-selected="true">Option 1</li>
155
- <li role="option" aria-selected="false">Option 2</li>
156
- <li role="option" aria-selected="false">Option 3</li>
157
- </ul>
158
- </div>
159
- ```
160
-
161
- **States required:**
162
- - Collapsed
163
- - Expanded
164
- - Hover (menu items)
165
- - Selected (highlighted option)
166
- - Disabled (grayed out, not clickable)
167
-
168
- **Common mistakes:**
169
- - Using native `<select>` for complex dropdowns
170
- - Not managing `aria-expanded` state
171
- - Forgetting keyboard navigation (Arrow keys, Enter, Escape)
172
- - Closing dropdown on trigger click without toggle logic
173
-
174
- ---
175
-
176
- ## Tabs
177
-
178
- **When to use:** Organize related content into switchable panels within the same viewport.
179
-
180
- **Structure:**
181
- ```html
182
- <div class="tabs">
183
- <div class="tabs__list" role="tablist" aria-label="Content sections">
184
- <button class="tabs__tab" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
185
- Overview
186
- </button>
187
- <button class="tabs__tab" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
188
- Details
189
- </button>
190
- </div>
191
- <div class="tabs__panel" role="tabpanel" id="panel-1" aria-labelledby="tab-1">
192
- <p>Overview content...</p>
193
- </div>
194
- <div class="tabs__panel" role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
195
- <p>Details content...</p>
196
- </div>
197
- </div>
198
- ```
199
-
200
- **States required:**
201
- - Active tab (visual highlight, `aria-selected="true"`)
202
- - Inactive tab (muted appearance)
203
- - Hover (subtle background change)
204
- - Disabled tab (reduced opacity, `aria-disabled="true"`)
205
-
206
- **Common mistakes:**
207
- - Using `<div>` for tabs instead of buttons with ARIA
208
- - Not linking tabs to panels with `aria-controls`/`aria-labelledby`
209
- - Forgetting `hidden` attribute on inactive panels
210
- - Animating height changes without fixed or measured heights
211
-
212
- ---
213
-
214
- ## Table
215
-
216
- **When to use:** Display structured data with rows and columns, especially for comparison or detailed lists.
217
-
218
- **Structure:**
219
- ```html
220
- <div class="table-container">
221
- <table class="table">
222
- <thead>
223
- <tr>
224
- <th scope="col">Name</th>
225
- <th scope="col">Status</th>
226
- <th scope="col">Date</th>
227
- </tr>
228
- </thead>
229
- <tbody>
230
- <tr>
231
- <td>Project Alpha</td>
232
- <td><span class="badge badge--success">Active</span></td>
233
- <td>2024-01-15</td>
234
- </tr>
235
- </tbody>
236
- </table>
237
- </div>
238
- ```
239
-
240
- **States required:**
241
- - Default (zebra striping optional)
242
- - Hover (row highlight)
243
- - Sorted (arrow indicator on header)
244
- - Selected (row highlight, checkbox)
245
- - Loading (skeleton rows)
246
-
247
- **Common mistakes:**
248
- - Missing `scope` attributes on `<th>`
249
- - No responsive wrapper for overflow
250
- - Using tables for layout (semantic violation)
251
- - Not providing sort indicators or accessibility labels
252
-
253
- ---
254
-
255
- ## Form
256
-
257
- **When to use:** Collect multiple inputs for registration, checkout, settings, or data entry.
258
-
259
- **Structure:**
260
- ```html
261
- <form class="form" novalidate>
262
- <div class="form__group">
263
- <label class="form__label" for="name">Full name</label>
264
- <input class="form__input" type="text" id="name" name="name" required />
265
- <p class="form__error">Name is required</p>
266
- </div>
267
- <div class="form__group">
268
- <label class="form__label" for="email">Email</label>
269
- <input class="form__input" type="email" id="email" name="email" required />
270
- </div>
271
- <button class="btn btn--primary" type="submit">Submit</button>
272
- </form>
273
- ```
274
-
275
- **States required:**
276
- - Default
277
- - Focus (per-field)
278
- - Valid (green border or checkmark)
279
- - Invalid (red border, error message)
280
- - Submitting (disable inputs, show spinner)
281
- - Success (confirmation message, clear form)
282
-
283
- **Common mistakes:**
284
- - Not using `novalidate` if custom validation is implemented
285
- - Forgetting `name` attributes for data binding
286
- - Not grouping related fields with `<fieldset>`
287
- - Showing all errors at once instead of on blur/submit
288
-
289
- ---
290
-
291
- ## Alert
292
-
293
- **When to use:** Communicate important information, warnings, errors, or success states.
294
-
295
- **Structure:**
296
- ```html
297
- <div class="alert alert--success" role="alert" aria-live="polite">
298
- <svg class="alert__icon" aria-hidden="true">...</svg>
299
- <div class="alert__content">
300
- <p class="alert__title">Success</p>
301
- <p class="alert__message">Your changes have been saved.</p>
302
- </div>
303
- <button class="alert__close" aria-label="Dismiss">&times;</button>
304
- </div>
305
- ```
306
-
307
- **States required:**
308
- - Info (blue)
309
- - Success (green)
310
- - Warning (yellow/orange)
311
- - Error (red)
312
- - Dismissed (removed from DOM or hidden)
313
-
314
- **Common mistakes:**
315
- - Using `alert` ARIA role for non-critical notifications
316
- - Not providing a close button when appropriate
317
- - Using color alone to convey meaning (add icon)
318
- - Stacking multiple alerts without clear hierarchy
319
-
320
- ---
321
-
322
- ## Badge
323
-
324
- **When to use:** Display status, counts, or short labels inline with other content.
325
-
326
- **Structure:**
327
- ```html
328
- <span class="badge badge--success">Active</span>
329
- <span class="badge badge--count">3</span>
330
- <span class="badge badge--warning">Pending</span>
331
- ```
332
-
333
- **States required:**
334
- - Default (solid background)
335
- - Subtle (outline or light background)
336
- - Dot (small indicator without text)
337
-
338
- **Common mistakes:**
339
- - Using badges for long text
340
- - Not providing sufficient contrast for small text
341
- - Using too many badge colors (limit to 3-4 semantic colors)
342
-
343
- ---
344
-
345
- ## Avatar
346
-
347
- **When to use:** Represent users, authors, or entities in lists, comments, and profiles.
348
-
349
- **Structure:**
350
- ```html
351
- <img class="avatar" src="user.jpg" alt="Jane Doe" width="40" height="40" />
352
- <span class="avatar avatar--fallback" aria-label="Jane Doe">JD</span>
353
- ```
354
-
355
- **States required:**
356
- - Image loaded
357
- - Fallback (initials or icon)
358
- - Online indicator (green dot)
359
- - Offline indicator (gray dot)
360
- - Status indicator (busy, away)
361
-
362
- **Common mistakes:**
363
- - Missing `alt` text or using `alt=""` when content is meaningful
364
- - Not providing fallback for broken images
365
- - Using gradient placeholders with initials (AI tell)
366
- - Making clickable avatars without clear affordance
367
-
368
- ---
369
-
370
- ## Breadcrumb
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">&larr;</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">&rarr;</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>&copy; 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. 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ử 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 ( 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
- </div>
580
- </section>
581
- ```
582
-
583
- **States required:**
584
- - Default
585
- - Hover (card lift, icon color change)
586
- - Icon hover (subtle scale or glow)
587
-
588
- **Common mistakes:**
589
- - Exactly three cards every time (AI tell)
590
- - Icon-heavy without text explanation
591
- - Equal height cards with excessive whitespace
592
- - Generic descriptions ("Powerful analytics")
593
-
594
- ---
595
-
596
- ## PricingTable
597
-
598
- **When to use:** Display pricing plans, feature comparisons, or subscription options.
599
-
600
- **Structure:**
601
- ```html
602
- <section class="pricing-table">
603
- <div class="pricing-card">
604
- <h3 class="pricing-card__name">Starter</h3>
605
- <p class="pricing-card__price">$9<span>/mo</span></p>
606
- <ul class="pricing-card__features">
607
- <li>5 projects</li>
608
- <li>10GB storage</li>
609
- <li>Email support</li>
610
- </ul>
611
- <a class="btn btn--secondary" href="#signup">Get started</a>
612
- </div>
613
- </section>
614
- ```
615
-
616
- **States required:**
617
- - Default
618
- - Hover (card lift)
619
- - Popular/Recommended (border highlight, badge)
620
- - Selected (checkmark or border)
621
-
622
- **Common mistakes:**
623
- - Three-column cookie-cutter layout regardless of actual plans
624
- - "Most Popular" badge on middle tier by default
625
- - Hiding pricing behind "Contact us" without explanation
626
- - Feature lists with checkmarks for non-inclusive features
627
-
628
- ---
629
-
630
- ## Testimonial
631
-
632
- **When to use:** Social proof from customers, users, or stakeholders.
633
-
634
- **Structure:**
635
- ```html
636
- <blockquote class="testimonial">
637
- <p class="testimonial__quote">
638
- "This product transformed our workflow. We shipped 2x faster."
639
- </p>
640
- <footer class="testimonial__author">
641
- <img class="testimonial__avatar" src="avatar.jpg" alt="Jane Doe" />
642
- <div>
643
- <cite class="testimonial__name">Jane Doe</cite>
644
- <p class="testimonial__role">CTO, Acme Inc.</p>
645
- </div>
646
- </footer>
647
- </blockquote>
648
- ```
649
-
650
- **States required:**
651
- - Default
652
- - Hover (subtle card lift)
653
-
654
- **Common mistakes:**
655
- - Using "John Doe" or generic names
656
- - Missing attribution (name, role, company)
657
- - Using gradient avatars with initials
658
- - Carousel instead of static grid (low engagement)
659
-
660
- ---
661
-
662
- ## FAQ
663
-
664
- **When to use:** Address common questions, reduce support burden, and improve SEO.
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">&times;</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">&larr;</button>
845
- <span>January 2024</span>
846
- <button aria-label="Next month">&rarr;</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 `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 params/search dùng TanStack Router type-safe, dữ liệu prefetch qua `loader` + `ensureQueryData`