@comitor/ui 0.1.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 (307) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +458 -0
  3. package/dist/chart/chart.d.ts +88 -0
  4. package/dist/chart/chart.js +199 -0
  5. package/dist/chart/chart.js.map +1 -0
  6. package/dist/chart/comitor-chart.d.ts +109 -0
  7. package/dist/chart/comitor-chart.js +189 -0
  8. package/dist/chart/comitor-chart.js.map +1 -0
  9. package/dist/chart/index.d.ts +4 -0
  10. package/dist/chart/index.js +3 -0
  11. package/dist/chart/index.js.map +1 -0
  12. package/dist/components/composite/avatar-group.d.ts +33 -0
  13. package/dist/components/composite/avatar-group.js +86 -0
  14. package/dist/components/composite/avatar-group.js.map +1 -0
  15. package/dist/components/composite/combobox.d.ts +70 -0
  16. package/dist/components/composite/combobox.js +291 -0
  17. package/dist/components/composite/combobox.js.map +1 -0
  18. package/dist/components/composite/command-palette.d.ts +56 -0
  19. package/dist/components/composite/command-palette.js +131 -0
  20. package/dist/components/composite/command-palette.js.map +1 -0
  21. package/dist/components/composite/confirm-dialog.d.ts +40 -0
  22. package/dist/components/composite/confirm-dialog.js +96 -0
  23. package/dist/components/composite/confirm-dialog.js.map +1 -0
  24. package/dist/components/composite/copy-button.d.ts +27 -0
  25. package/dist/components/composite/copy-button.js +80 -0
  26. package/dist/components/composite/copy-button.js.map +1 -0
  27. package/dist/components/composite/data-table.d.ts +136 -0
  28. package/dist/components/composite/data-table.js +300 -0
  29. package/dist/components/composite/data-table.js.map +1 -0
  30. package/dist/components/composite/date-picker.d.ts +77 -0
  31. package/dist/components/composite/date-picker.js +268 -0
  32. package/dist/components/composite/date-picker.js.map +1 -0
  33. package/dist/components/composite/empty-state.d.ts +38 -0
  34. package/dist/components/composite/empty-state.js +35 -0
  35. package/dist/components/composite/empty-state.js.map +1 -0
  36. package/dist/components/composite/filter-chips.d.ts +39 -0
  37. package/dist/components/composite/filter-chips.js +79 -0
  38. package/dist/components/composite/filter-chips.js.map +1 -0
  39. package/dist/components/composite/form-field.d.ts +58 -0
  40. package/dist/components/composite/form-field.js +81 -0
  41. package/dist/components/composite/form-field.js.map +1 -0
  42. package/dist/components/composite/form-section.d.ts +33 -0
  43. package/dist/components/composite/form-section.js +85 -0
  44. package/dist/components/composite/form-section.js.map +1 -0
  45. package/dist/components/composite/inline-edit.d.ts +38 -0
  46. package/dist/components/composite/inline-edit.js +165 -0
  47. package/dist/components/composite/inline-edit.js.map +1 -0
  48. package/dist/components/composite/keyboard-hint.d.ts +31 -0
  49. package/dist/components/composite/keyboard-hint.js +72 -0
  50. package/dist/components/composite/keyboard-hint.js.map +1 -0
  51. package/dist/components/composite/letter-avatar.d.ts +47 -0
  52. package/dist/components/composite/letter-avatar.js +66 -0
  53. package/dist/components/composite/letter-avatar.js.map +1 -0
  54. package/dist/components/composite/page-container.d.ts +25 -0
  55. package/dist/components/composite/page-container.js +23 -0
  56. package/dist/components/composite/page-container.js.map +1 -0
  57. package/dist/components/composite/page-header.d.ts +49 -0
  58. package/dist/components/composite/page-header.js +75 -0
  59. package/dist/components/composite/page-header.js.map +1 -0
  60. package/dist/components/composite/relative-time.d.ts +28 -0
  61. package/dist/components/composite/relative-time.js +44 -0
  62. package/dist/components/composite/relative-time.js.map +1 -0
  63. package/dist/components/composite/search-filter-bar.d.ts +38 -0
  64. package/dist/components/composite/search-filter-bar.js +135 -0
  65. package/dist/components/composite/search-filter-bar.js.map +1 -0
  66. package/dist/components/composite/stat-card.d.ts +48 -0
  67. package/dist/components/composite/stat-card.js +76 -0
  68. package/dist/components/composite/stat-card.js.map +1 -0
  69. package/dist/components/composite/status-config.d.ts +85 -0
  70. package/dist/components/composite/status-config.js +41 -0
  71. package/dist/components/composite/status-config.js.map +1 -0
  72. package/dist/components/composite/status-pill.d.ts +26 -0
  73. package/dist/components/composite/status-pill.js +59 -0
  74. package/dist/components/composite/status-pill.js.map +1 -0
  75. package/dist/components/composite/table-pagination.d.ts +31 -0
  76. package/dist/components/composite/table-pagination.js +103 -0
  77. package/dist/components/composite/table-pagination.js.map +1 -0
  78. package/dist/components/composite/text-utils.d.ts +38 -0
  79. package/dist/components/composite/text-utils.js +40 -0
  80. package/dist/components/composite/text-utils.js.map +1 -0
  81. package/dist/components/composite/toast.d.ts +56 -0
  82. package/dist/components/composite/toast.js +63 -0
  83. package/dist/components/composite/toast.js.map +1 -0
  84. package/dist/components/ui/accordion.d.ts +17 -0
  85. package/dist/components/ui/accordion.js +53 -0
  86. package/dist/components/ui/accordion.js.map +1 -0
  87. package/dist/components/ui/alert-dialog.d.ts +34 -0
  88. package/dist/components/ui/alert-dialog.js +103 -0
  89. package/dist/components/ui/alert-dialog.js.map +1 -0
  90. package/dist/components/ui/alert.d.ts +16 -0
  91. package/dist/components/ui/alert.js +50 -0
  92. package/dist/components/ui/alert.js.map +1 -0
  93. package/dist/components/ui/aspect-ratio.d.ts +7 -0
  94. package/dist/components/ui/aspect-ratio.js +10 -0
  95. package/dist/components/ui/aspect-ratio.js.map +1 -0
  96. package/dist/components/ui/avatar.d.ts +11 -0
  97. package/dist/components/ui/avatar.js +33 -0
  98. package/dist/components/ui/avatar.js.map +1 -0
  99. package/dist/components/ui/badge.d.ts +15 -0
  100. package/dist/components/ui/badge.js +37 -0
  101. package/dist/components/ui/badge.js.map +1 -0
  102. package/dist/components/ui/breadcrumb.d.ts +30 -0
  103. package/dist/components/ui/breadcrumb.js +89 -0
  104. package/dist/components/ui/breadcrumb.js.map +1 -0
  105. package/dist/components/ui/button-group.d.ts +21 -0
  106. package/dist/components/ui/button-group.js +62 -0
  107. package/dist/components/ui/button-group.js.map +1 -0
  108. package/dist/components/ui/button.d.ts +21 -0
  109. package/dist/components/ui/button.js +70 -0
  110. package/dist/components/ui/button.js.map +1 -0
  111. package/dist/components/ui/calendar.d.ts +26 -0
  112. package/dist/components/ui/calendar.js +148 -0
  113. package/dist/components/ui/calendar.js.map +1 -0
  114. package/dist/components/ui/card.d.ts +33 -0
  115. package/dist/components/ui/card.js +70 -0
  116. package/dist/components/ui/card.js.map +1 -0
  117. package/dist/components/ui/carousel.d.ts +46 -0
  118. package/dist/components/ui/carousel.js +173 -0
  119. package/dist/components/ui/carousel.js.map +1 -0
  120. package/dist/components/ui/checkbox.d.ts +17 -0
  121. package/dist/components/ui/checkbox.js +67 -0
  122. package/dist/components/ui/checkbox.js.map +1 -0
  123. package/dist/components/ui/collapsible.d.ts +17 -0
  124. package/dist/components/ui/collapsible.js +18 -0
  125. package/dist/components/ui/collapsible.js.map +1 -0
  126. package/dist/components/ui/command.d.ts +41 -0
  127. package/dist/components/ui/command.js +122 -0
  128. package/dist/components/ui/command.js.map +1 -0
  129. package/dist/components/ui/context-menu.d.ts +51 -0
  130. package/dist/components/ui/context-menu.js +166 -0
  131. package/dist/components/ui/context-menu.js.map +1 -0
  132. package/dist/components/ui/dialog.d.ts +48 -0
  133. package/dist/components/ui/dialog.js +131 -0
  134. package/dist/components/ui/dialog.js.map +1 -0
  135. package/dist/components/ui/drawer.d.ts +32 -0
  136. package/dist/components/ui/drawer.js +102 -0
  137. package/dist/components/ui/drawer.js.map +1 -0
  138. package/dist/components/ui/dropdown-menu.d.ts +52 -0
  139. package/dist/components/ui/dropdown-menu.js +167 -0
  140. package/dist/components/ui/dropdown-menu.js.map +1 -0
  141. package/dist/components/ui/empty.d.ts +22 -0
  142. package/dist/components/ui/empty.js +87 -0
  143. package/dist/components/ui/empty.js.map +1 -0
  144. package/dist/components/ui/field.d.ts +46 -0
  145. package/dist/components/ui/field.js +197 -0
  146. package/dist/components/ui/field.js.map +1 -0
  147. package/dist/components/ui/form.d.ts +50 -0
  148. package/dist/components/ui/form.js +100 -0
  149. package/dist/components/ui/form.js.map +1 -0
  150. package/dist/components/ui/hover-card.d.ts +17 -0
  151. package/dist/components/ui/hover-card.js +31 -0
  152. package/dist/components/ui/hover-card.js.map +1 -0
  153. package/dist/components/ui/input-group.d.ts +26 -0
  154. package/dist/components/ui/input-group.js +148 -0
  155. package/dist/components/ui/input-group.js.map +1 -0
  156. package/dist/components/ui/input-otp.d.ts +23 -0
  157. package/dist/components/ui/input-otp.js +54 -0
  158. package/dist/components/ui/input-otp.js.map +1 -0
  159. package/dist/components/ui/input.d.ts +21 -0
  160. package/dist/components/ui/input.js +62 -0
  161. package/dist/components/ui/input.js.map +1 -0
  162. package/dist/components/ui/item.d.ts +39 -0
  163. package/dist/components/ui/item.js +142 -0
  164. package/dist/components/ui/item.js.map +1 -0
  165. package/dist/components/ui/kbd.d.ts +8 -0
  166. package/dist/components/ui/kbd.js +25 -0
  167. package/dist/components/ui/kbd.js.map +1 -0
  168. package/dist/components/ui/label.d.ts +7 -0
  169. package/dist/components/ui/label.js +21 -0
  170. package/dist/components/ui/label.js.map +1 -0
  171. package/dist/components/ui/menubar.d.ts +51 -0
  172. package/dist/components/ui/menubar.js +196 -0
  173. package/dist/components/ui/menubar.js.map +1 -0
  174. package/dist/components/ui/navigation-menu.d.ts +39 -0
  175. package/dist/components/ui/navigation-menu.js +123 -0
  176. package/dist/components/ui/navigation-menu.js.map +1 -0
  177. package/dist/components/ui/pagination.d.ts +40 -0
  178. package/dist/components/ui/pagination.js +95 -0
  179. package/dist/components/ui/pagination.js.map +1 -0
  180. package/dist/components/ui/popover.d.ts +19 -0
  181. package/dist/components/ui/popover.js +35 -0
  182. package/dist/components/ui/popover.js.map +1 -0
  183. package/dist/components/ui/progress.d.ts +12 -0
  184. package/dist/components/ui/progress.js +27 -0
  185. package/dist/components/ui/progress.js.map +1 -0
  186. package/dist/components/ui/radio-group.d.ts +9 -0
  187. package/dist/components/ui/radio-group.js +34 -0
  188. package/dist/components/ui/radio-group.js.map +1 -0
  189. package/dist/components/ui/resizable.d.ts +19 -0
  190. package/dist/components/ui/resizable.js +38 -0
  191. package/dist/components/ui/resizable.js.map +1 -0
  192. package/dist/components/ui/scroll-area.d.ts +13 -0
  193. package/dist/components/ui/scroll-area.js +46 -0
  194. package/dist/components/ui/scroll-area.js.map +1 -0
  195. package/dist/components/ui/select.d.ts +33 -0
  196. package/dist/components/ui/select.js +133 -0
  197. package/dist/components/ui/select.js.map +1 -0
  198. package/dist/components/ui/separator.d.ts +7 -0
  199. package/dist/components/ui/separator.js +23 -0
  200. package/dist/components/ui/separator.js.map +1 -0
  201. package/dist/components/ui/sheet.d.ts +38 -0
  202. package/dist/components/ui/sheet.js +96 -0
  203. package/dist/components/ui/sheet.js.map +1 -0
  204. package/dist/components/ui/skeleton.d.ts +6 -0
  205. package/dist/components/ui/skeleton.js +9 -0
  206. package/dist/components/ui/skeleton.js.map +1 -0
  207. package/dist/components/ui/slider.d.ts +12 -0
  208. package/dist/components/ui/slider.js +54 -0
  209. package/dist/components/ui/slider.js.map +1 -0
  210. package/dist/components/ui/sonner.d.ts +9 -0
  211. package/dist/components/ui/sonner.js +25 -0
  212. package/dist/components/ui/sonner.js.map +1 -0
  213. package/dist/components/ui/spinner.d.ts +6 -0
  214. package/dist/components/ui/spinner.js +10 -0
  215. package/dist/components/ui/spinner.js.map +1 -0
  216. package/dist/components/ui/switch.d.ts +14 -0
  217. package/dist/components/ui/switch.js +65 -0
  218. package/dist/components/ui/switch.js.map +1 -0
  219. package/dist/components/ui/table.d.ts +30 -0
  220. package/dist/components/ui/table.js +87 -0
  221. package/dist/components/ui/table.js.map +1 -0
  222. package/dist/components/ui/tabs.d.ts +35 -0
  223. package/dist/components/ui/tabs.js +54 -0
  224. package/dist/components/ui/tabs.js.map +1 -0
  225. package/dist/components/ui/textarea.d.ts +6 -0
  226. package/dist/components/ui/textarea.js +19 -0
  227. package/dist/components/ui/textarea.js.map +1 -0
  228. package/dist/components/ui/toggle-group.d.ts +18 -0
  229. package/dist/components/ui/toggle-group.js +52 -0
  230. package/dist/components/ui/toggle-group.js.map +1 -0
  231. package/dist/components/ui/toggle.d.ts +15 -0
  232. package/dist/components/ui/toggle.js +33 -0
  233. package/dist/components/ui/toggle.js.map +1 -0
  234. package/dist/components/ui/tooltip.d.ts +21 -0
  235. package/dist/components/ui/tooltip.js +38 -0
  236. package/dist/components/ui/tooltip.js.map +1 -0
  237. package/dist/form.d.ts +5 -0
  238. package/dist/form.js +2 -0
  239. package/dist/form.js.map +1 -0
  240. package/dist/index.d.ts +111 -0
  241. package/dist/index.js +76 -0
  242. package/dist/index.js.map +1 -0
  243. package/dist/lib/cn.d.ts +16 -0
  244. package/dist/lib/cn.js +9 -0
  245. package/dist/lib/cn.js.map +1 -0
  246. package/dist/shell/app-header.d.ts +41 -0
  247. package/dist/shell/app-header.js +136 -0
  248. package/dist/shell/app-header.js.map +1 -0
  249. package/dist/shell/app-launcher.d.ts +33 -0
  250. package/dist/shell/app-launcher.js +189 -0
  251. package/dist/shell/app-launcher.js.map +1 -0
  252. package/dist/shell/app-shell.d.ts +44 -0
  253. package/dist/shell/app-shell.js +99 -0
  254. package/dist/shell/app-shell.js.map +1 -0
  255. package/dist/shell/comitor-lockup.d.ts +33 -0
  256. package/dist/shell/comitor-lockup.js +112 -0
  257. package/dist/shell/comitor-lockup.js.map +1 -0
  258. package/dist/shell/comitor-logo.d.ts +28 -0
  259. package/dist/shell/comitor-logo.js +92 -0
  260. package/dist/shell/comitor-logo.js.map +1 -0
  261. package/dist/shell/index.d.ts +24 -0
  262. package/dist/shell/index.js +23 -0
  263. package/dist/shell/index.js.map +1 -0
  264. package/dist/shell/mobile-menu.d.ts +26 -0
  265. package/dist/shell/mobile-menu.js +56 -0
  266. package/dist/shell/mobile-menu.js.map +1 -0
  267. package/dist/shell/notifications-button.d.ts +22 -0
  268. package/dist/shell/notifications-button.js +39 -0
  269. package/dist/shell/notifications-button.js.map +1 -0
  270. package/dist/shell/shell-context.d.ts +91 -0
  271. package/dist/shell/shell-context.js +233 -0
  272. package/dist/shell/shell-context.js.map +1 -0
  273. package/dist/shell/shell-link.d.ts +19 -0
  274. package/dist/shell/shell-link.js +18 -0
  275. package/dist/shell/shell-link.js.map +1 -0
  276. package/dist/shell/sidebar-group.d.ts +21 -0
  277. package/dist/shell/sidebar-group.js +43 -0
  278. package/dist/shell/sidebar-group.js.map +1 -0
  279. package/dist/shell/sidebar-menu-item.d.ts +27 -0
  280. package/dist/shell/sidebar-menu-item.js +152 -0
  281. package/dist/shell/sidebar-menu-item.js.map +1 -0
  282. package/dist/shell/sidebar.d.ts +26 -0
  283. package/dist/shell/sidebar.js +92 -0
  284. package/dist/shell/sidebar.js.map +1 -0
  285. package/dist/shell/theme-provider.d.ts +18 -0
  286. package/dist/shell/theme-provider.js +29 -0
  287. package/dist/shell/theme-provider.js.map +1 -0
  288. package/dist/shell/theme-toggle.d.ts +28 -0
  289. package/dist/shell/theme-toggle.js +67 -0
  290. package/dist/shell/theme-toggle.js.map +1 -0
  291. package/dist/shell/types.d.ts +168 -0
  292. package/dist/shell/types.js +38 -0
  293. package/dist/shell/types.js.map +1 -0
  294. package/dist/shell/user-menu.d.ts +42 -0
  295. package/dist/shell/user-menu.js +132 -0
  296. package/dist/shell/user-menu.js.map +1 -0
  297. package/dist/shell/utils.d.ts +62 -0
  298. package/dist/shell/utils.js +73 -0
  299. package/dist/shell/utils.js.map +1 -0
  300. package/dist/shell/workspace-switcher.d.ts +24 -0
  301. package/dist/shell/workspace-switcher.js +219 -0
  302. package/dist/shell/workspace-switcher.js.map +1 -0
  303. package/dist/tokens.d.ts +173 -0
  304. package/dist/tokens.js +161 -0
  305. package/dist/tokens.js.map +1 -0
  306. package/package.json +151 -0
  307. package/styles.css +357 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Comitor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,458 @@
1
+ # @comitor/ui
2
+
3
+ Hệ thống thiết kế dùng chung của hệ sinh thái **Comitor** (Tasks, Chat, CRM, HR, admin console,
4
+ marketing site). Đóng gói shadcn/ui style `new-york` đã tùy biến theo brand Comitor, cộng thêm các
5
+ component ghép sẵn và khung ứng dụng (shell) kiểu workspace × app.
6
+
7
+ - **React 19 · Tailwind v4 (CSS-first) · TypeScript strict · ESM-only**
8
+ - Token brand nằm ở **`styles.css`** — đổi token, bump version, mọi app nhận cùng một thay đổi.
9
+ - Server-component-friendly: `"use client"` chỉ nằm ở file thật sự cần.
10
+
11
+ > Trạng thái: `0.1.0` — **đã port xong** 51 primitive + 24 composite + 18 file shell + chart, và đã qua
12
+ > **một đợt sửa theo review** (tương phản màu theo WCAG, thang đổ bóng 4 bậc, a11y tầng shell, chuỗi
13
+ > hiển thị của Tier 2 override được). `typecheck`, `build`, `lint`, `verify:directives`, `check:exports`
14
+ > đều xanh. API còn có thể đổi cho tới `1.0.0` — thay đổi phá vỡ tương thích ghi ở
15
+ > [docs/quyet-dinh-tich-hop.md](docs/quyet-dinh-tich-hop.md).
16
+
17
+ ---
18
+
19
+ ## 1. Cài đặt
20
+
21
+ ```bash
22
+ pnpm add @comitor/ui
23
+ # peer bắt buộc
24
+ pnpm add react react-dom lucide-react
25
+ pnpm add -D tailwindcss @tailwindcss/postcss
26
+ # peer optional — cài khi dùng entry tương ứng
27
+ pnpm add next next-themes # cho @comitor/ui/shell
28
+ pnpm add recharts # cho @comitor/ui/chart
29
+ pnpm add react-hook-form # cho @comitor/ui/form
30
+ ```
31
+
32
+ | Peer | Range | Bắt buộc | Cần khi |
33
+ |---|---|---|---|
34
+ | `react`, `react-dom` | `^19.0.0` | ✅ | luôn |
35
+ | `tailwindcss` | `^4.1.0` | ✅ | luôn (`styles.css` phải được TW4 biên dịch) |
36
+ | `lucide-react` | `>=0.540.0` | ✅ | luôn (tránh 2 bản icon trong bundle) |
37
+ | `next` | `>=15` | — | `/shell` |
38
+ | `next-themes` | `>=0.4` | — | `/shell` (context singleton — app và package phải dùng **cùng một bản**) |
39
+ | `recharts` | `^2.15.0` | — | `/chart` |
40
+ | `react-hook-form` | `^7.54.0` | — | `/form` (context singleton) |
41
+
42
+ ## 2. Nạp `styles.css` — ĐỌC KỸ, đây là chỗ dễ sai nhất
43
+
44
+ Đặt import trong **`app/globals.css`**, ngay sau `@import "tailwindcss"`:
45
+
46
+ ```css
47
+ /* app/globals.css */
48
+ @import "tailwindcss";
49
+ @import "@comitor/ui/styles.css";
50
+ ```
51
+
52
+ ```ts
53
+ // app/layout.tsx — chỉ import globals.css của chính app
54
+ import "./globals.css";
55
+ ```
56
+
57
+ > **KHÔNG** viết `import "@comitor/ui/styles.css"` trong `layout.tsx` (thói quen Tailwind 3).
58
+ > File này chứa `@theme` / `@custom-variant` / `@source` — những at-rule **chỉ có nghĩa khi được
59
+ > Tailwind biên dịch**. Import ở `layout.tsx` thì biến CSS vẫn chạy nhưng **không utility nào**
60
+ > (`bg-gold-300`, `dark:*`…) được sinh ra → giao diện vỡ mà không có lỗi build.
61
+
62
+ `styles.css` đã tự khai `@source "./dist"` để Tailwind quét class trong package (Tailwind v4 mặc
63
+ định bỏ qua `node_modules`). Nếu dùng pnpm strict và thấy style bị mất, khai thêm ở app:
64
+
65
+ ```css
66
+ @source "../node_modules/@comitor/ui/dist";
67
+ ```
68
+
69
+ ### Font
70
+
71
+ App tự nạp Inter (subsets `latin` + `vietnamese`) và gán vào biến `--font-inter`; package chỉ tiêu thụ:
72
+
73
+ ```ts
74
+ // app/layout.tsx
75
+ import { Inter } from "next/font/google";
76
+
77
+ const inter = Inter({
78
+ subsets: ["latin", "vietnamese"],
79
+ weight: ["400", "500", "600", "700", "800"],
80
+ display: "swap",
81
+ variable: "--font-inter"
82
+ });
83
+ ```
84
+
85
+ Package đọc **hai** biến font, cả hai đều tuỳ chọn:
86
+
87
+ | Biến | Ai gán | Không gán thì sao |
88
+ |---|---|---|
89
+ | `--font-inter` | app (như trên) | `--font-sans` rơi về `"Inter", system-ui, sans-serif` — vẫn đúng nếu máy có Inter |
90
+ | `--font-inter-mono` | app, **nếu muốn** đổi font chữ đều (số trong tooltip biểu đồ, `Kbd`…) | `--font-mono` rơi về `"Geist Mono", ui-monospace, monospace` |
91
+
92
+ `--font-inter-mono` được tiêu thụ dưới dạng `var(--font-inter-mono, "Geist Mono")` — **fallback nằm ngay
93
+ trong `var()`** là bắt buộc: `var()` không có fallback mà biến lại chưa khai thì cả khai báo `--font-mono`
94
+ thành *invalid at computed-value time*, và `font-mono` rơi về font kế thừa chứ **không** rơi về phần còn
95
+ lại của danh sách. Muốn dùng font mono riêng thì gán y hệt cách gán `--font-inter`:
96
+
97
+ ```ts
98
+ const mono = Geist_Mono({ subsets: ["latin"], display: "swap", variable: "--font-inter-mono" });
99
+ ```
100
+
101
+ ### Dark mode
102
+
103
+ Theo **class** (`@custom-variant dark (&:is(.dark *))`), khớp `next-themes`:
104
+
105
+ ```tsx
106
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
107
+ ```
108
+
109
+ Lưu ý: biến thể `dark` chỉ khớp **con cháu** của `.dark`, không khớp chính phần tử mang class đó.
110
+ Muốn dựng preview 2 theme cạnh nhau phải bọc thêm một lớp: `<div class="dark"><div>…</div></div>`.
111
+
112
+ ## 3. Entry point
113
+
114
+ Package có **5 entry**. Lý do tách: `tsup` build ở chế độ `bundle: false` (ESM per-file), nên chỉ cần
115
+ barrel chính re-export **một** file có `import` tới peer optional là mọi app dùng `@comitor/ui`
116
+ đều buộc phải cài gói đó — không cài thì bundler gãy ngay ở bước resolve.
117
+
118
+ | Entry | Nội dung | Peer optional kéo theo |
119
+ |---|---|---|
120
+ | `@comitor/ui` | Tier 1 (primitive) + Tier 2 (composite) + `cn` + token TS | **không có** — chạy được ở Vite, Storybook, test runner |
121
+ | `@comitor/ui/shell` | Tier 3: `AppShell`, `Sidebar`, `AppHeader`, `WorkspaceSwitcher`, `AppLauncher`, `UserMenu`, `ThemeProvider`, `SonnerToaster` | `next`, `next-themes` |
122
+ | `@comitor/ui/chart` | `Chart*` (shadcn) + `ComitorAreaChart/BarChart/LineChart` + `CHART_COLORS` | `recharts` (~100KB) |
123
+ | `@comitor/ui/form` | Binding shadcn ⇄ react-hook-form: `Form`, `FormField`, `FormItem`, `FormControl`, `FormMessage`, `useFormField` | `react-hook-form` |
124
+ | `@comitor/ui/tokens` | Token dạng object TS | không — cho nơi không có Tailwind: canvas, email HTML, PDF |
125
+ | `@comitor/ui/styles.css` | Nguồn token Tailwind v4 | xem §2 |
126
+
127
+ ### Tên xuất hiện ở nhiều entry
128
+
129
+ Số tên mỗi entry xuất ra (đo bằng trình biên dịch TypeScript, không đếm tay): `.` **631** · `/shell` **70**
130
+ · `/chart` **27** · `/form` **15** · `/tokens` **11**. Giao nhau đúng **4 nhóm** dưới đây — `export *`
131
+ chỉ báo đỏ khi trùng tên trong **cùng một** entry, nên trùng xuyên entry phải tra bằng bảng này:
132
+
133
+ | Tên | Ở `@comitor/ui` | Ở entry khác | Cùng một cài đặt? |
134
+ |---|---|---|---|
135
+ | `FormField`, `FormFieldProps` | Tier 2 — bố cục 12 cột (label + control + mô tả + lỗi) | `/form` — wrapper của `Controller` (API shadcn) | **KHÔNG** — hai component khác hẳn nhau |
136
+ | `useIsMac` | `composite/keyboard-hint.tsx` — `useState` + `useEffect`, đọc `navigator.userAgent` | `/shell` (`shell-context.tsx`) — `useSyncExternalStore`, đọc `navigator.platform` rồi mới tới `userAgent` | **KHÔNG** — hai thân hàm. Hợp đồng thì giống nhau: **render đầu luôn trả `false`** ở cả hai bản, nên không bản nào gây hydration mismatch |
137
+ | `getVietnameseInitials` | `composite/text-utils.ts` (`toLocaleUpperCase("vi-VN")`) | `/shell` — **re-export đúng bản đó** | **CÓ** — một thân hàm duy nhất |
138
+ | 11 token: `gold`, `ash`, `accent`, `brandNeutral`, `semantic`, `status`, `chart`, `avatarPalette`, `layout`, `typography`, `ThemeMode` | `export * from "./tokens.js"` | `/tokens` — **cùng một module** | **CÓ** — `/tokens` là tập con của `.`, tách ra cho nơi chỉ cần hằng số (email HTML, PDF, canvas) |
139
+
140
+ Quy tắc dùng: hằng ngày lấy bản ở `@comitor/ui`; chỉ với tới `/form` khi app đã dùng react-hook-form.
141
+ Trong một app, hãy import `useIsMac` từ **một** entry duy nhất — trộn cả hai là hai đăng ký hook khác nhau
142
+ cho cùng một thông tin.
143
+
144
+ > **`Toaster` KHÔNG phải tên trùng.** Có hai toaster nhưng chúng mang hai tên: `Toaster` ở
145
+ > `@comitor/ui` (toast có icon theo trạng thái, nhận prop `theme`) và **`SonnerToaster`** ở
146
+ > `@comitor/ui/shell` (tự đọc `useTheme()` của next-themes). Dùng `SonnerToaster` khi app đã có
147
+ > `ThemeProvider` của shell, còn lại dùng `Toaster`. Đừng gộp hai cái làm một.
148
+
149
+ ## 4. Ví dụ
150
+
151
+ ### 4.1 Trang nội dung — primitive + composite
152
+
153
+ ```tsx
154
+ // Không có "use client": PageHeader/StatCard/StatusPill là component tĩnh → RSC render thẳng.
155
+ import { PageContainer, PageHeader, StatCard, STATUS_TONES, StatusPill } from "@comitor/ui";
156
+ import type { StatusConfig } from "@comitor/ui";
157
+
158
+ // Bảng trạng thái là của APP, không phải của package — truyền vào qua `configs`.
159
+ const TASK_STATUSES: StatusConfig<"todo" | "doing" | "done">[] = [
160
+ { value: "todo", label: "Cần làm", shortLabel: "CL", priority: 1, ...STATUS_TONES.neutral },
161
+ { value: "doing", label: "Đang làm", shortLabel: "ĐL", priority: 2, ...STATUS_TONES.info },
162
+ { value: "done", label: "Hoàn thành", shortLabel: "HT", priority: 3, ...STATUS_TONES.success }
163
+ ];
164
+
165
+ export default function TasksPage() {
166
+ return (
167
+ <PageContainer width="lg">
168
+ <PageHeader title="Công việc" description="Toàn bộ task của workspace" />
169
+
170
+ <div className="grid gap-4 sm:grid-cols-3">
171
+ <StatCard
172
+ label="Đang mở"
173
+ value={128}
174
+ delta={{ value: "+12,5%", direction: "up", label: "so với tuần trước" }}
175
+ />
176
+ {/* polarity: quá hạn TĂNG là xấu → mũi tên lên phải tô đỏ, không tô xanh */}
177
+ <StatCard
178
+ label="Quá hạn"
179
+ value={7}
180
+ tone="destructive"
181
+ delta={{ value: "3 việc", direction: "up", polarity: "positive-down" }}
182
+ />
183
+ <StatCard label="Hoàn thành tuần này" value={43} tone="success" />
184
+ </div>
185
+
186
+ <StatusPill configs={TASK_STATUSES} value="doing" />
187
+ </PageContainer>
188
+ );
189
+ }
190
+ ```
191
+
192
+ ### 4.2 Khung ứng dụng — `AppShell` + `WorkspaceSwitcher` + `AppLauncher`
193
+
194
+ Shell dựng trên **hai trục điều hướng** kiểu Linear/Lark:
195
+
196
+ - **Trục 1 — `WorkspaceSwitcher`**: đổi *ngữ cảnh dữ liệu* (Acme Corp ▸ Beta Ltd).
197
+ - **Trục 2 — `AppLauncher`**: đổi *sản phẩm* trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR).
198
+
199
+ Cả hai **không nhận dữ liệu qua prop** — chúng đọc từ context của `ShellProvider`, mà `AppShell`
200
+ đã bọc sẵn. Vì vậy chỉ cần truyền `workspaces` / `apps` / `nav` một lần cho `AppShell`.
201
+
202
+ ```tsx
203
+ // app/(app)/layout.tsx — root layout đặt ThemeProvider, layout con đặt AppShell.
204
+ "use client";
205
+
206
+ import { CheckSquare, LayoutDashboard, MessageSquare, Users } from "lucide-react";
207
+ import { useRouter } from "next/navigation";
208
+ import type { AppDescriptor, NavGroup, Workspace } from "@comitor/ui/shell";
209
+ import { AppShell } from "@comitor/ui/shell";
210
+
211
+ const workspaces: Workspace[] = [
212
+ { id: "acme", name: "Acme Corp", slug: "acme", plan: "Pro", role: "Quản trị viên", memberCount: 42 },
213
+ { id: "beta", name: "Beta Ltd", slug: "beta", plan: "Free", role: "Thành viên", memberCount: 6 }
214
+ ];
215
+
216
+ const apps: AppDescriptor[] = [
217
+ { id: "tasks", name: "Công việc", icon: CheckSquare, href: "/tasks", accent: "var(--color-teal)" },
218
+ { id: "chat", name: "Trò chuyện", icon: MessageSquare, href: "/chat", badge: 3 },
219
+ { id: "crm", name: "CRM", icon: Users, href: "/crm", entitled: false } // chưa mua → hiện khoá
220
+ ];
221
+
222
+ const nav: NavGroup[] = [
223
+ {
224
+ id: "main",
225
+ items: [
226
+ { id: "overview", label: "Tổng quan", href: "/tasks", icon: LayoutDashboard },
227
+ { id: "mine", label: "Của tôi", href: "/tasks/mine", icon: CheckSquare, badge: 12 }
228
+ ]
229
+ }
230
+ ];
231
+
232
+ export default function AppLayout({ children }: { children: React.ReactNode }) {
233
+ const router = useRouter();
234
+
235
+ return (
236
+ <AppShell
237
+ workspaces={workspaces}
238
+ currentWorkspaceId="acme"
239
+ apps={apps}
240
+ currentAppId="tasks"
241
+ nav={nav}
242
+ user={{ id: "u1", name: "Nguyễn Đức Thành", email: "thanh@acme.vn", role: "Quản trị viên" }}
243
+ onWorkspaceChange={(id) => router.push(`/w/${id}`)}
244
+ onAppSelect={(app) => router.push(app.href)}
245
+ onUpsell={(appId) => router.push(`/billing?app=${appId}`)}
246
+ onLogout={signOut}
247
+ >
248
+ {children}
249
+ </AppShell>
250
+ );
251
+ }
252
+ ```
253
+
254
+ Cần tự lắp thay vì dùng `AppShell` trọn gói (vd header riêng)? Bọc `ShellProvider` rồi đặt
255
+ `WorkspaceSwitcher` / `AppLauncher` ở đâu tuỳ ý — chúng vẫn lấy dữ liệu từ context:
256
+
257
+ ```tsx
258
+ import { AppLauncher, ShellProvider, WorkspaceSwitcher } from "@comitor/ui/shell";
259
+
260
+ <ShellProvider workspaces={workspaces} apps={apps} nav={nav} user={user}>
261
+ <header className="flex h-header items-center gap-2 px-3">
262
+ <WorkspaceSwitcher align="start" />
263
+ <AppLauncher /> {/* nút lưới 3×3, phím tắt ⌘/Ctrl + Shift + K */}
264
+ </header>
265
+ <main>{children}</main>
266
+ </ShellProvider>
267
+ ```
268
+
269
+ **Bọc `ThemeProvider` ở root layout, NGOÀI `AppShell`** — next-themes phải nằm trên cùng để không
270
+ nháy theme lúc hydrate:
271
+
272
+ ```tsx
273
+ // app/layout.tsx
274
+ import { ThemeProvider } from "@comitor/ui/shell";
275
+
276
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
277
+ {children}
278
+ </ThemeProvider>
279
+ ```
280
+
281
+ App **không chạy trên Next**: truyền `pathname` và `LinkComponent` cho `ShellProvider`/`AppShell`
282
+ để khỏi chạm `next/navigation`.
283
+
284
+ > ⚠ Entry `/shell` chỉ nạp được **qua bundler** (webpack/turbopack/vite). Nạp bằng Node ESM thuần sẽ
285
+ > lỗi `Cannot find module 'next/link'` — vì `next@16` không khai `exports` map nên Node không resolve
286
+ > được subpath không đuôi. Test runner chạy resolution kiểu Node thì alias `next/link` → `next/link.js`,
287
+ > hoặc dùng `ShellProvider` với `LinkComponent`/`pathname` tự cấp. Các entry `.`, `/chart`, `/form`,
288
+ > `/tokens` **không** dính vấn đề này.
289
+
290
+ ### 4.3 Token ngoài Tailwind
291
+
292
+ ```ts
293
+ import { chart, gold, layout } from "@comitor/ui/tokens";
294
+
295
+ const seriesColor = chart.light[0]; // #E8B824
296
+ const headerPx = layout.headerHeight; // 56
297
+ ```
298
+
299
+ ## 5. Bảng component
300
+
301
+ Số liệu thực tế của `0.1.0`: **51 file primitive · 24 file composite · 18 file shell · 2 file chart**
302
+ (chưa kể barrel) — số tên xuất ra theo entry: `.` **631** · `/shell` **70** · `/chart` **27** · `/form`
303
+ **15** · `/tokens` **11** (xem §3 cho các tên xuất hiện ở nhiều entry). Bảng dưới liệt kê component gốc;
304
+ các thành phần con (`CardHeader`, `DialogFooter`, `SelectItem`…) export kèm theo file tương ứng.
305
+
306
+ ### Tier 1 — primitive (`@comitor/ui`)
307
+
308
+ shadcn/ui style `new-york`, đã tùy biến token theo brand Comitor.
309
+
310
+ | Nhóm | Component |
311
+ |---|---|
312
+ | Hành động | `Button`, `ButtonGroup`, `Toggle`, `ToggleGroup`, `Kbd` |
313
+ | Nhập liệu | `Input`, `InputGroup`, `InputOTP`, `Textarea`, `Label`, `Field`, `Checkbox`, `RadioGroup`, `Switch`, `Slider`, `Select`, `Calendar` |
314
+ | Hiển thị | `Card`, `Badge`, `Avatar`, `Table`, `Separator`, `AspectRatio`, `Skeleton`, `Spinner`, `Progress`, `Empty`, `Item`, `Alert` |
315
+ | Lớp phủ | `Dialog`, `AlertDialog`, `Sheet`, `Drawer`, `Popover`, `HoverCard`, `Tooltip`, `DropdownMenu`, `ContextMenu`, `Menubar`, `Command` |
316
+ | Điều hướng | `Tabs`, `Breadcrumb`, `Pagination`, `NavigationMenu`, `Accordion`, `Collapsible` |
317
+ | Khác | `ScrollArea`, `Resizable`, `Carousel` |
318
+
319
+ > Hai file primitive **không** nằm ở entry chính: `form.tsx` → `@comitor/ui/form`;
320
+ > `sonner.tsx` → `@comitor/ui/shell` (tên `SonnerToaster`). Xem §3.
321
+ > **Không có `Sidebar` / `SidebarNav` ở Tier 1** — cố ý, không phải sót: shell Tier 3 đã có sidebar riêng
322
+ > (hai trục điều hướng, thu gọn, nhớ trạng thái theo workspace) và bản shadcn sẽ trùng đúng 3 tên
323
+ > `Sidebar` / `SidebarGroup` / `SidebarMenuItem` với Tier 3 nhưng API khác hẳn, lại giữ trạng thái bằng
324
+ > cookie riêng. App nào thật sự cần bản shadcn thì `npx shadcn@latest add sidebar` vào repo của mình.
325
+ > Lý do đầy đủ: [docs/quyet-dinh-tich-hop.md](docs/quyet-dinh-tich-hop.md) §17.
326
+
327
+ ### Tier 2 — composite (`@comitor/ui`)
328
+
329
+ Giá trị thật của package: pattern lặp đi lặp lại ở mọi app, ghép từ nhiều primitive.
330
+
331
+ | Component | Công dụng |
332
+ |---|---|
333
+ | `PageHeader`, `PageContainer` | Khung đầu trang (breadcrumb + hành động) và khung nội dung chuẩn |
334
+ | `DataTable`, `DataTableColumnToggle`, `TablePagination` | Bảng dữ liệu: sort, chọn dòng, ẩn/hiện cột, phân trang. Mọi chuỗi (kể cả `aria-label`) override được qua prop `labels` — xem `DataTableLabels` / `DEFAULT_DATA_TABLE_LABELS` |
335
+ | `SearchFilterBar`, `FilterChips` | Thanh tìm + lọc + đếm kết quả |
336
+ | `StatCard`, `StatusPill`, `EmptyState` | Số liệu có delta, nhãn trạng thái, trạng thái rỗng |
337
+ | `FormSection`, `FormField` | Bố cục form 12 cột; uỷ quyền label/mô tả/lỗi xuống `Field` |
338
+ | `Combobox`, `MultiCombobox` | Chọn có tìm kiếm, **khớp không dấu tiếng Việt** |
339
+ | `DatePicker`, `DateRangePicker` | Chọn ngày/khoảng ngày, locale `vi`, preset dựng sẵn |
340
+ | `LetterAvatar`, `AvatarGroup` | Avatar chữ cái theo tên tiếng Việt (tông màu deterministic), nhóm người tham gia |
341
+ | `ConfirmDialog`, `CommandPalette` | Hộp xác nhận, bảng lệnh `⌘K` |
342
+ | `Toaster`, `toast()`, `toastPromise()`, `dismissToast()` | Thông báo có icon theo trạng thái |
343
+ | `CopyButton`, `RelativeTime`, `InlineEdit`, `KeyboardHint` | Tiện ích nhỏ dùng khắp nơi |
344
+ | `useIsMac` | Hook đi kèm `KeyboardHint` — `true` khi chạy trên macOS (render đầu luôn `false`, xem §3) |
345
+
346
+ Kèm 2 module thuần (không phải component, dùng được cả hai phía):
347
+
348
+ | Module | Export |
349
+ |---|---|
350
+ | `status-config` | `STATUS_TONES`, `getStatusConfig`, `getStatusClasses`, `getStatusLabel`, `sortByStatusPriority` |
351
+ | `text-utils` | `removeVietnameseTones`, `matchesSearch`, `getVietnameseInitials`, `hashString`, `pickByString`, `truncate` |
352
+
353
+ ### Tier 3 — shell (`@comitor/ui/shell`)
354
+
355
+ | Component | Công dụng |
356
+ |---|---|
357
+ | `AppShell` | Lắp sẵn `ShellProvider` + `Sidebar` + `AppHeader` + `<main>` + `MobileMenu`; hai layout `sidebar-first` / `header-first` |
358
+ | `ShellProvider`, `useShell`, `useIsMac` | Context + phím tắt toàn cục; dùng khi tự lắp khung |
359
+ | `WorkspaceSwitcher` | **Trục 1** — đổi ngữ cảnh dữ liệu; dùng `Popover` (không `DropdownMenu`, vì typeahead của Radix cướp phím ô tìm kiếm) |
360
+ | `AppLauncher`, `AppLauncherPanel` | **Trục 2** — lưới 3×3 đổi sản phẩm; app chưa mua hiện khoá và gọi `onUpsell` |
361
+ | `AppHeader`, `Sidebar`, `SidebarGroup`, `SidebarMenuItem`, `MobileMenu` | Thành phần khung điều hướng |
362
+ | `UserMenu`, `UserAvatar`, `NotificationsButton` | Vùng phải của header |
363
+ | `ThemeProvider`, `ThemeToggle`, `SonnerToaster` | Theme (next-themes) + toast bám theme |
364
+ | `ShellNavLink` | Link nội bộ của shell; thay `next/link` bằng `LinkComponent` khi không chạy Next |
365
+ | `ComitorLogo`, `ComitorLockup` | Vector brand — **không** có `"use client"`, RSC render thẳng (hex trong SVG là brand book, không tokenize) |
366
+ | `DEFAULT_SHELL_LABELS` + `utils` | Chuỗi hiển thị mặc định; helper `flattenNavItems`, `isNavItemActive`, `resolveCurrentApp`, `getEntitledApps`, `normalizeVi`… |
367
+
368
+ Hợp đồng dữ liệu (type): `Workspace`, `AppDescriptor`, `NavItem`, `NavGroup`, `ShellNav`, `ShellUser`,
369
+ `UserMenuItem`, `ShellLabels`, `ShellIcon`, `ShellLinkComponent`.
370
+
371
+ ### Chart (`@comitor/ui/chart`)
372
+
373
+ | Component | Công dụng |
374
+ |---|---|
375
+ | `ChartContainer`, `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, `ChartStyle`, `useChart` | Lớp shadcn bọc recharts (giữ tên trần theo API gốc) |
376
+ | `ComitorAreaChart`, `ComitorBarChart`, `ComitorLineChart` | Bản dựng sẵn theo brand — truyền `data` + `series` là xong |
377
+ | `ComitorChartContainer`, `ComitorChartTooltip` | Bản tiện dụng; **có tiền tố `Comitor`** để không đụng tên trần ở trên |
378
+ | `CHART_COLORS`, `CHART_SERIES_COLORS` | Bảng màu series mặc định |
379
+
380
+ ### Form (`@comitor/ui/form`)
381
+
382
+ `Form`, `FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormDescription`, `FormMessage`, `useFormField`
383
+ — binding shadcn ⇄ react-hook-form. ⚠ `FormField` ở đây **khác** `FormField` của Tier 2 (§3).
384
+
385
+ ## 6. Token
386
+
387
+ Toàn bộ token nằm ở [`styles.css`](./styles.css); bản mirror TS ở [`src/tokens.ts`](./src/tokens.ts).
388
+
389
+ | Nhóm | Token | Ghi chú |
390
+ |---|---|---|
391
+ | Brand | `gold-50 … gold-600` | `gold-300 #E8B824` = `--primary` (light); dark nâng lên `gold-200 #F5D060` |
392
+ | Trung tính brand | `ink`, `ink-soft`, `paper`, `warm` | `warm #EDE8D8` = `--secondary` (light) |
393
+ | Thang xám | `ash-50/100/300/500/700/900` | **Không phải `gray-*`** — xem cạm bẫy bên dưới |
394
+ | Màu phụ | `navy`, `teal`, `red`, `green`, `yellow` | `red #D64545` = `--destructive` |
395
+ | Trạng thái | `success`, `warning`, `info` (+ `-foreground`) | Alias sang `green` / `yellow` / `teal`, tự sáng lên ở dark |
396
+ | Biểu đồ | `chart-1 … chart-5` | Đổi bảng màu biểu đồ độc lập với brand |
397
+ | Theo app | `app-accent`, `app-accent-foreground` | Mỗi app Comitor tô một accent riêng — set ở `<body>`, không cần build lại package |
398
+ | Khung | `--sidebar-width`, `--sidebar-width-icon`, `--header-height` | Có utility `w-sidebar`, `w-sidebar-icon`, `h-header` |
399
+ | Đổ bóng | `--elevation-surface`, `--elevation-control`, `--elevation-card`, `--elevation-overlay` | 4 bậc độ cao, sinh utility `shadow-surface` / `shadow-control` / `shadow-card` / `shadow-overlay`. Đổi theo theme (bản dark dùng đen thuần, alpha cao hơn) |
400
+ | Khác | `--radius 0.375rem` | Bo góc 6px là chữ ký thị giác |
401
+
402
+ Đổi accent theo app:
403
+
404
+ ```tsx
405
+ <body style={{ "--app-accent": "#1F8A8B", "--app-accent-foreground": "#FFFFFF" } as React.CSSProperties}>
406
+ ```
407
+
408
+ Đổi độ nổi (bóng) toàn hệ — ghi đè đúng 4 biến `--elevation-*`, **không** ghi đè `--shadow-*`:
409
+
410
+ ```css
411
+ /* app/globals.css — sau @import "@comitor/ui/styles.css" */
412
+ :root { --elevation-overlay: 0 12px 40px rgba(14, 17, 22, 0.2); }
413
+ ```
414
+
415
+ > Vì `--shadow-*` giữ giá trị dạng `var()`, Tailwind không tách được phần màu: `shadow-card/50` và
416
+ > `shadow-<màu>` **không** có tác dụng. Đổi tông/độ đậm thì ghi đè `--elevation-*` như trên.
417
+
418
+ ### Cạm bẫy phải nhớ
419
+
420
+ 1. **`ash-*` chứ không phải `gray-*`.** Thang xám Comitor chỉ có 6 bậc; nếu đặt tên `gray-*` thì
421
+ `gray-200/400/600/800` rơi về gray mặc định của Tailwind (ám xanh) và trộn với gray Comitor
422
+ (ám tím) → lệch tông. Đổi namespace để hai thang không đụng nhau.
423
+ 2. **Không sinh class động** (`` `bg-${color}-300` ``) ở bất kỳ đâu — Tailwind quét tĩnh, class động
424
+ bị loại khỏi bundle của app.
425
+ 3. **Không hardcode hex trong component** — trừ vector logo brand. Đọc hex từ `/tokens` rồi nhét vào
426
+ style inline là mất dark mode.
427
+ 4. `@theme **inline**` là chủ ý: `inline` nhúng thẳng `var(--x)` vào utility nên `.dark { --primary }`
428
+ đổi được màu tại runtime. Dùng `@theme` thường sẽ đóng băng giá trị lúc build → dark mode chết.
429
+
430
+ ## 7. Phát triển
431
+
432
+ ```bash
433
+ pnpm install
434
+ pnpm build # tsup: ESM, bundle:false (transpile từng file để giữ "use client"), dts
435
+ pnpm typecheck # tsc --noEmit
436
+ pnpm lint # biome check .
437
+ pnpm lint:fix
438
+ pnpm verify:directives # mọi file nguồn có "use client" ⇄ file dist tương ứng
439
+ pnpm check:exports # publint --strict + attw
440
+ ```
441
+
442
+ > Gặp `ERR_PNPM_IGNORED_BUILDS: esbuild` khi cài? Đã xử lý sẵn trong `pnpm-workspace.yaml`
443
+ > (`allowBuilds: { esbuild: true }`) — pnpm 11 không còn đọc setting từ trường `pnpm` của
444
+ > `package.json` nữa. esbuild cần chạy postinstall để nạp binary theo nền tảng, cấm chạy thì
445
+ > `pnpm build` fail.
446
+
447
+ Quy ước `"use client"`: đặt **per-file**, ở dòng đầu tiên. `tsup` chạy `bundle: false` nên mỗi file
448
+ `.tsx` ra một file `.js` và directive được giữ nguyên → Next.js đánh đúng ranh giới server/client.
449
+ File thuần dữ liệu (`lib/cn.ts`, `tokens.ts`, `shell/types.ts`) **không** có directive.
450
+
451
+ `scripts/verify-directives.mjs` đi bộ **toàn bộ `src/`** (không riêng `shell/`): file nguồn nào mở đầu
452
+ bằng `"use client"` thì file `dist/` tương ứng phải mở đầu y hệt — thiếu file build cũng tính là hỏng.
453
+ Hiện **70/101** file nguồn cần directive, trải khắp cả composite lẫn primitive. Đừng thay script bằng
454
+ `grep -L "use client" dist/shell/*.js`: lệnh đó soi chưa tới một phần ba số file phải soi.
455
+
456
+ ## 8. Giấy phép
457
+
458
+ [MIT](./LICENSE) © Comitor
@@ -0,0 +1,88 @@
1
+ import * as React from 'react';
2
+ import * as RechartsPrimitive from 'recharts';
3
+
4
+ /**
5
+ * Chart primitive của shadcn/ui (style new-york) — port nguyên trạng từ comitor-ds để
6
+ * `shadcn add` vẫn dùng lại được. Đây là tầng "thô": bạn tự lắp recharts bên trong.
7
+ * Muốn dựng nhanh 3 loại biểu đồ quen thuộc thì xem `comitor-chart.tsx`.
8
+ *
9
+ * Cơ chế màu (điểm hay nhất, và cũng dễ hiểu sai nhất):
10
+ * `<ChartContainer>` render kèm `<ChartStyle>` — thẻ này sinh ra biến CSS `--color-<key>`
11
+ * cho từng key trong `config`, giới hạn phạm vi bằng `[data-chart=<id>]`. Series recharts
12
+ * chỉ việc trỏ `fill="var(--color-desktop)"`. Nhờ vậy màu vẫn nằm trong TOKEN (không hex
13
+ * rải trong JSX) và đổi được theo light/dark khi config khai `theme: { light, dark }`.
14
+ */
15
+ declare const THEMES: {
16
+ readonly light: "";
17
+ readonly dark: ".dark";
18
+ };
19
+ /**
20
+ * Khai báo hiển thị cho từng series: nhãn, icon và màu.
21
+ * Dùng `color` cho màu dùng chung, hoặc `theme` khi cần màu khác nhau giữa light/dark.
22
+ * Giá trị màu nên là token: `"var(--chart-1)"`, `"var(--teal)"`… — đừng ghi hex.
23
+ */
24
+ type ChartConfig = {
25
+ [k in string]: {
26
+ label?: React.ReactNode;
27
+ icon?: React.ComponentType;
28
+ } & ({
29
+ color?: string;
30
+ theme?: never;
31
+ } | {
32
+ color?: never;
33
+ theme: Record<keyof typeof THEMES, string>;
34
+ });
35
+ };
36
+ type ChartContextProps = {
37
+ config: ChartConfig;
38
+ };
39
+ /** Đọc `config` của `<ChartContainer>` gần nhất. Ném lỗi nếu gọi ngoài container. */
40
+ declare function useChart(): ChartContextProps;
41
+ type ChartContainerProps = React.ComponentProps<"div"> & {
42
+ /** Khai báo nhãn/màu cho từng series — nguồn của biến `--color-<key>`. */
43
+ config: ChartConfig;
44
+ children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
45
+ };
46
+ /**
47
+ * Khung biểu đồ: cấp context `config`, bơm biến màu, và bọc sẵn `ResponsiveContainer`.
48
+ * Tỉ lệ mặc định `aspect-video`; muốn cao cố định thì truyền `className="h-64 aspect-auto"`.
49
+ */
50
+ declare function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps): React.JSX.Element;
51
+ type ChartStyleProps = {
52
+ /** Giá trị của thuộc tính `data-chart` trên container — dùng làm phạm vi cho biến màu. */
53
+ id: string;
54
+ config: ChartConfig;
55
+ };
56
+ /**
57
+ * Sinh thẻ `<style>` chứa `--color-<key>` cho từng series, phạm vi `[data-chart=<id>]`.
58
+ * `ChartContainer` đã render sẵn; chỉ export riêng cho ai tự dựng container.
59
+ */
60
+ declare const ChartStyle: ({ id, config }: ChartStyleProps) => React.JSX.Element | null;
61
+ /** `Tooltip` của recharts — đặt `content={<ChartTooltipContent />}` để có giao diện theo token. */
62
+ declare const ChartTooltip: typeof RechartsPrimitive.Tooltip;
63
+ type ChartTooltipContentProps = React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<"div"> & {
64
+ /** Ẩn dòng nhãn (trục X) ở đầu tooltip. */
65
+ hideLabel?: boolean;
66
+ /** Ẩn ô màu đứng trước tên series. */
67
+ hideIndicator?: boolean;
68
+ /** Kiểu ô màu: chấm tròn / vạch đứng / vạch đứt. */
69
+ indicator?: "line" | "dot" | "dashed";
70
+ /** Khoá dùng để tra `config` cho TÊN series (mặc định `name` rồi tới `dataKey`). */
71
+ nameKey?: string;
72
+ /** Khoá dùng để tra `config` cho NHÃN trục X. */
73
+ labelKey?: string;
74
+ };
75
+ /** Nội dung tooltip theo design token Comitor: nền `bg-background`, viền `border-border/50`. */
76
+ declare function ChartTooltipContent({ active, payload, className, indicator, hideLabel, hideIndicator, label, labelFormatter, labelClassName, formatter, color, nameKey, labelKey }: ChartTooltipContentProps): React.JSX.Element | null;
77
+ /** `Legend` của recharts — đặt `content={<ChartLegendContent />}` để lấy nhãn từ `config`. */
78
+ declare const ChartLegend: typeof RechartsPrimitive.Legend;
79
+ type ChartLegendContentProps = React.ComponentProps<"div"> & Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
80
+ /** Ẩn icon riêng của series (nếu `config` có khai `icon`). */
81
+ hideIcon?: boolean;
82
+ /** Khoá dùng để tra `config` (mặc định `dataKey`). */
83
+ nameKey?: string;
84
+ };
85
+ /** Chú giải nằm ngang, nhãn lấy từ `config` chứ không phải `dataKey` thô. */
86
+ declare function ChartLegendContent({ className, hideIcon, payload, verticalAlign, nameKey }: ChartLegendContentProps): React.JSX.Element | null;
87
+
88
+ export { type ChartConfig, ChartContainer, type ChartContainerProps, ChartLegend, ChartLegendContent, type ChartLegendContentProps, ChartStyle, type ChartStyleProps, ChartTooltip, ChartTooltipContent, type ChartTooltipContentProps, useChart };