@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
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { AlertCircle } from "lucide-react";
4
+ import { useId } from "react";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from "../ui/field.js";
7
+ const colSpanClasses = {
8
+ 1: "col-span-12 md:col-span-1",
9
+ 2: "col-span-12 md:col-span-2",
10
+ 3: "col-span-12 sm:col-span-6 md:col-span-3",
11
+ 4: "col-span-12 sm:col-span-6 md:col-span-4",
12
+ 5: "col-span-12 sm:col-span-6 md:col-span-5",
13
+ 6: "col-span-12 sm:col-span-6",
14
+ 7: "col-span-12 md:col-span-7",
15
+ 8: "col-span-12 md:col-span-8",
16
+ 9: "col-span-12 md:col-span-9",
17
+ 10: "col-span-12 md:col-span-10",
18
+ 11: "col-span-12 md:col-span-11",
19
+ 12: "col-span-12"
20
+ };
21
+ function FormField({
22
+ label,
23
+ required = false,
24
+ description,
25
+ error,
26
+ htmlFor,
27
+ colSpan = 12,
28
+ orientation = "vertical",
29
+ className,
30
+ children
31
+ }) {
32
+ const generatedId = useId();
33
+ const id = htmlFor ?? generatedId;
34
+ const hasError = Boolean(error);
35
+ const errorId = `${id}-error`;
36
+ const descriptionId = `${id}-description`;
37
+ const control = {
38
+ id,
39
+ "aria-invalid": hasError ? true : void 0,
40
+ "aria-required": required ? true : void 0,
41
+ "aria-describedby": hasError ? errorId : description ? descriptionId : void 0
42
+ };
43
+ return /* @__PURE__ */ jsxs(
44
+ Field,
45
+ {
46
+ "data-slot": "form-field",
47
+ "data-invalid": hasError || void 0,
48
+ orientation,
49
+ className: cn(colSpanClasses[colSpan], className),
50
+ children: [
51
+ label && /* @__PURE__ */ jsxs(FieldLabel, { htmlFor: id, className: "text-sm", children: [
52
+ label,
53
+ required && // Dấu * chỉ là tín hiệu thị giác — trạng thái bắt buộc đã nằm ở aria-required trên
54
+ // control, để trình đọc màn hình khỏi đọc thừa "sao".
55
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-destructive", children: "*" })
56
+ ] }),
57
+ /* @__PURE__ */ jsxs(FieldContent, { children: [
58
+ typeof children === "function" ? children(control) : children,
59
+ hasError ? /* @__PURE__ */ jsxs(FieldError, { id: errorId, className: "flex items-center gap-1.5 text-xs animate-in fade-in slide-in-from-top-1", children: [
60
+ /* @__PURE__ */ jsx(AlertCircle, { "aria-hidden": "true", className: "size-3.5 shrink-0" }),
61
+ error
62
+ ] }) : description && /* @__PURE__ */ jsx(FieldDescription, { id: descriptionId, className: "text-xs", children: description })
63
+ ] })
64
+ ]
65
+ }
66
+ );
67
+ }
68
+ const SELECT_EMPTY_VALUE = "__empty__";
69
+ function toSelectValue(value) {
70
+ return value == null || value === "" ? SELECT_EMPTY_VALUE : value;
71
+ }
72
+ function fromSelectValue(value) {
73
+ return value === SELECT_EMPTY_VALUE ? "" : value;
74
+ }
75
+ export {
76
+ FormField,
77
+ SELECT_EMPTY_VALUE,
78
+ fromSelectValue,
79
+ toSelectValue
80
+ };
81
+ //# sourceMappingURL=form-field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/form-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertCircle } from \"lucide-react\";\nimport { type ReactNode, useId } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from \"../ui/field.js\";\n\nexport type FormFieldColSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;\n\n/**\n * Thuộc tính a11y mà `FormField` tiêm xuống control con.\n *\n * Trải nguyên cụm này vào `<Input>`/`<Combobox>`/`<Select>` là control tự có id khớp nhãn,\n * tự báo lỗi và trạng thái bắt buộc — không phải nhớ từng thuộc tính ở mỗi trang.\n */\nexport interface FormFieldControlProps {\n id: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n \"aria-describedby\"?: string;\n}\n\nexport interface FormFieldProps {\n label?: ReactNode;\n /** Đánh dấu bắt buộc: vẽ dấu * và phát `aria-required` xuống control */\n required?: boolean;\n /** Mô tả/gợi ý dưới trường — bị ẩn khi có lỗi để không đọc chồng lời */\n description?: ReactNode;\n /** Thông báo lỗi; có giá trị là trường vào trạng thái invalid */\n error?: ReactNode;\n /** id của control — bỏ trống thì tự sinh bằng useId */\n htmlFor?: string;\n /** Số cột chiếm trong lưới 12 cột của `FormSection` (mặc định 12 = cả hàng) */\n colSpan?: FormFieldColSpan;\n orientation?: \"vertical\" | \"horizontal\" | \"responsive\";\n className?: string;\n /**\n * Control của trường. Dạng hàm sẽ nhận `FormFieldControlProps` để trải xuống —\n * dạng node thì app tự lo id/aria (dùng khi control đã tự quản, vd Checkbox có nhãn riêng).\n */\n children: ReactNode | ((control: FormFieldControlProps) => ReactNode);\n}\n\nconst colSpanClasses: Record<FormFieldColSpan, string> = {\n 1: \"col-span-12 md:col-span-1\",\n 2: \"col-span-12 md:col-span-2\",\n 3: \"col-span-12 sm:col-span-6 md:col-span-3\",\n 4: \"col-span-12 sm:col-span-6 md:col-span-4\",\n 5: \"col-span-12 sm:col-span-6 md:col-span-5\",\n 6: \"col-span-12 sm:col-span-6\",\n 7: \"col-span-12 md:col-span-7\",\n 8: \"col-span-12 md:col-span-8\",\n 9: \"col-span-12 md:col-span-9\",\n 10: \"col-span-12 md:col-span-10\",\n 11: \"col-span-12 md:col-span-11\",\n 12: \"col-span-12\"\n};\n\n/**\n * Một trường form: nhãn + control + mô tả/lỗi, đặt trong lưới 12 cột của `FormSection`.\n *\n * KHÔNG tự vẽ input. Nhãn và lỗi uỷ quyền xuống primitive `Field`, còn control do app chọn —\n * nếu composite tự dựng input thì mỗi kiểu control mới (combobox, date range, editor) lại phải\n * mở lại file này — một FormField \"đa năng\" ôm hết mọi loại control sẽ phình ra không có điểm dừng.\n */\nexport function FormField({\n label,\n required = false,\n description,\n error,\n htmlFor,\n colSpan = 12,\n orientation = \"vertical\",\n className,\n children\n}: FormFieldProps) {\n const generatedId = useId();\n const id = htmlFor ?? generatedId;\n const hasError = Boolean(error);\n const errorId = `${id}-error`;\n const descriptionId = `${id}-description`;\n\n const control: FormFieldControlProps = {\n id,\n \"aria-invalid\": hasError ? true : undefined,\n \"aria-required\": required ? true : undefined,\n \"aria-describedby\": hasError ? errorId : description ? descriptionId : undefined\n };\n\n return (\n <Field\n data-slot=\"form-field\"\n data-invalid={hasError || undefined}\n orientation={orientation}\n className={cn(colSpanClasses[colSpan], className)}\n >\n {label && (\n <FieldLabel htmlFor={id} className=\"text-sm\">\n {label}\n {required && (\n // Dấu * chỉ là tín hiệu thị giác — trạng thái bắt buộc đã nằm ở aria-required trên\n // control, để trình đọc màn hình khỏi đọc thừa \"sao\".\n <span aria-hidden=\"true\" className=\"text-destructive\">\n *\n </span>\n )}\n </FieldLabel>\n )}\n\n <FieldContent>\n {typeof children === \"function\" ? children(control) : children}\n\n {hasError ? (\n <FieldError id={errorId} className=\"flex items-center gap-1.5 text-xs animate-in fade-in slide-in-from-top-1\">\n <AlertCircle aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n {error}\n </FieldError>\n ) : (\n description && (\n <FieldDescription id={descriptionId} className=\"text-xs\">\n {description}\n </FieldDescription>\n )\n )}\n </FieldContent>\n </Field>\n );\n}\n\n/**\n * Sentinel cho `Select` của Radix.\n *\n * Radix cấm `SelectItem value=\"\"` (chuỗi rỗng dành riêng cho việc xoá lựa chọn), nên mục\n * \"— Không chọn —\" phải mang một giá trị giả. Dùng chung hằng số này ở mọi app để tránh mỗi\n * nơi tự đặt một sentinel rồi lọt xuống payload gửi server.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\n\n/** Giá trị model (\"\" = chưa chọn) → giá trị cho Radix Select. */\nexport function toSelectValue(value: string | null | undefined): string {\n return value == null || value === \"\" ? SELECT_EMPTY_VALUE : value;\n}\n\n/** Giá trị từ Radix Select → giá trị model (\"\" khi người dùng chọn mục rỗng). */\nexport function fromSelectValue(value: string): string {\n return value === SELECT_EMPTY_VALUE ? \"\" : value;\n}\n"],"mappings":";AAiGQ,SAKI,KALJ;AA/FR,SAAS,mBAAmB;AAC5B,SAAyB,aAAa;AACtC,SAAS,UAAU;AACnB,SAAS,OAAO,cAAc,kBAAkB,YAAY,kBAAkB;AAsC9E,MAAM,iBAAmD;AAAA,EACvD,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AASO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAmB;AACjB,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,WAAW;AACtB,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,UAAU,GAAG,EAAE;AACrB,QAAM,gBAAgB,GAAG,EAAE;AAE3B,QAAM,UAAiC;AAAA,IACrC;AAAA,IACA,gBAAgB,WAAW,OAAO;AAAA,IAClC,iBAAiB,WAAW,OAAO;AAAA,IACnC,oBAAoB,WAAW,UAAU,cAAc,gBAAgB;AAAA,EACzE;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAc,YAAY;AAAA,MAC1B;AAAA,MACA,WAAW,GAAG,eAAe,OAAO,GAAG,SAAS;AAAA,MAE/C;AAAA,iBACC,qBAAC,cAAW,SAAS,IAAI,WAAU,WAChC;AAAA;AAAA,UACA;AAAA;AAAA,UAGC,oBAAC,UAAK,eAAY,QAAO,WAAU,oBAAmB,eAEtD;AAAA,WAEJ;AAAA,QAGF,qBAAC,gBACE;AAAA,iBAAO,aAAa,aAAa,SAAS,OAAO,IAAI;AAAA,UAErD,WACC,qBAAC,cAAW,IAAI,SAAS,WAAU,4EACjC;AAAA,gCAAC,eAAY,eAAY,QAAO,WAAU,qBAAoB;AAAA,YAC7D;AAAA,aACH,IAEA,eACE,oBAAC,oBAAiB,IAAI,eAAe,WAAU,WAC5C,uBACH;AAAA,WAGN;AAAA;AAAA;AAAA,EACF;AAEJ;AASO,MAAM,qBAAqB;AAG3B,SAAS,cAAc,OAA0C;AACtE,SAAO,SAAS,QAAQ,UAAU,KAAK,qBAAqB;AAC9D;AAGO,SAAS,gBAAgB,OAAuB;AACrD,SAAO,UAAU,qBAAqB,KAAK;AAC7C;","names":[]}
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { IconComponent } from './empty-state.js';
4
+
5
+ type FormSectionAccent = "none" | "primary" | "info" | "warning" | "success" | "destructive";
6
+ interface FormSectionProps {
7
+ title: ReactNode;
8
+ description?: ReactNode;
9
+ icon?: IconComponent;
10
+ /** Slot bên phải tiêu đề (nút phụ, công tắc…) — nằm NGOÀI nút gập để không lồng button */
11
+ actions?: ReactNode;
12
+ /** Cho phép gập/mở (mặc định false) */
13
+ collapsible?: boolean;
14
+ /** Trạng thái mở ban đầu khi không controlled */
15
+ defaultOpen?: boolean;
16
+ /** Controlled: app tự giữ trạng thái mở */
17
+ open?: boolean;
18
+ onOpenChange?: (open: boolean) => void;
19
+ /** Vạch màu bên trái, phân biệt nhóm trường (mặc định "none") */
20
+ accent?: FormSectionAccent;
21
+ children: ReactNode;
22
+ className?: string;
23
+ contentClassName?: string;
24
+ }
25
+ /**
26
+ * Nhóm trường trong form: tiêu đề + mô tả + lưới 12 cột cho các `FormField`.
27
+ *
28
+ * Nội dung là `grid-cols-12` để `FormField` chỉ cần khai `colSpan` — cách này giữ mọi form
29
+ * trong hệ sinh thái cùng một nhịp cột, thay vì mỗi trang tự chế `flex` với bề rộng riêng.
30
+ */
31
+ declare function FormSection({ title, description, icon, actions, collapsible, defaultOpen, open, onOpenChange, accent, children, className, contentClassName }: FormSectionProps): React.JSX.Element;
32
+
33
+ export { FormSection, type FormSectionAccent, type FormSectionProps };
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { ChevronDown } from "lucide-react";
4
+ import { useState } from "react";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
7
+ const accentClasses = {
8
+ none: "",
9
+ primary: "border-l-4 border-l-gold-300",
10
+ info: "border-l-4 border-l-info",
11
+ warning: "border-l-4 border-l-warning",
12
+ success: "border-l-4 border-l-success",
13
+ destructive: "border-l-4 border-l-destructive"
14
+ };
15
+ function SectionHeading({ title, description, icon: Icon }) {
16
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-start gap-3 text-left", children: [
17
+ Icon && /* @__PURE__ */ jsx(
18
+ "span",
19
+ {
20
+ "aria-hidden": "true",
21
+ className: "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground",
22
+ children: /* @__PURE__ */ jsx(Icon, { className: "size-4" })
23
+ }
24
+ ),
25
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
26
+ /* @__PURE__ */ jsx("h3", { className: "truncate text-sm font-semibold tracking-tight text-foreground", children: title }),
27
+ description && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: description })
28
+ ] })
29
+ ] });
30
+ }
31
+ function FormSection({
32
+ title,
33
+ description,
34
+ icon,
35
+ actions,
36
+ collapsible = false,
37
+ defaultOpen = true,
38
+ open,
39
+ onOpenChange,
40
+ accent = "none",
41
+ children,
42
+ className,
43
+ contentClassName
44
+ }) {
45
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
46
+ const isOpen = open ?? internalOpen;
47
+ const setOpen = (next) => {
48
+ if (open === void 0) setInternalOpen(next);
49
+ onOpenChange?.(next);
50
+ };
51
+ const shell = cn("rounded-xl border border-border bg-card", accentClasses[accent], className);
52
+ const grid = cn("grid grid-cols-12 gap-4 p-4 md:gap-5 md:p-5", contentClassName);
53
+ if (!collapsible) {
54
+ return /* @__PURE__ */ jsxs("section", { "data-slot": "form-section", className: shell, children: [
55
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 border-b border-border p-4 md:px-5", children: [
56
+ /* @__PURE__ */ jsx(SectionHeading, { title, description, icon }),
57
+ actions && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })
58
+ ] }),
59
+ /* @__PURE__ */ jsx("div", { className: grid, children })
60
+ ] });
61
+ }
62
+ return /* @__PURE__ */ jsx(Collapsible, { open: isOpen, onOpenChange: setOpen, asChild: true, children: /* @__PURE__ */ jsxs("section", { "data-slot": "form-section", className: shell, children: [
63
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 border-b border-border p-4 md:px-5", children: [
64
+ /* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "flex min-w-0 flex-1 items-start gap-3 rounded-md text-left", children: [
65
+ /* @__PURE__ */ jsx(SectionHeading, { title, description, icon }),
66
+ /* @__PURE__ */ jsx(
67
+ ChevronDown,
68
+ {
69
+ "aria-hidden": "true",
70
+ className: cn(
71
+ "ml-auto mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out",
72
+ isOpen && "rotate-180"
73
+ )
74
+ }
75
+ )
76
+ ] }),
77
+ actions && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })
78
+ ] }),
79
+ /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("div", { className: grid, children }) })
80
+ ] }) });
81
+ }
82
+ export {
83
+ FormSection
84
+ };
85
+ //# sourceMappingURL=form-section.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/form-section.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { type ReactNode, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../ui/collapsible.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport type FormSectionAccent = \"none\" | \"primary\" | \"info\" | \"warning\" | \"success\" | \"destructive\";\n\nexport interface FormSectionProps {\n title: ReactNode;\n description?: ReactNode;\n icon?: IconComponent;\n /** Slot bên phải tiêu đề (nút phụ, công tắc…) — nằm NGOÀI nút gập để không lồng button */\n actions?: ReactNode;\n /** Cho phép gập/mở (mặc định false) */\n collapsible?: boolean;\n /** Trạng thái mở ban đầu khi không controlled */\n defaultOpen?: boolean;\n /** Controlled: app tự giữ trạng thái mở */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Vạch màu bên trái, phân biệt nhóm trường (mặc định \"none\") */\n accent?: FormSectionAccent;\n children: ReactNode;\n className?: string;\n contentClassName?: string;\n}\n\nconst accentClasses: Record<FormSectionAccent, string> = {\n none: \"\",\n primary: \"border-l-4 border-l-gold-300\",\n info: \"border-l-4 border-l-info\",\n warning: \"border-l-4 border-l-warning\",\n success: \"border-l-4 border-l-success\",\n destructive: \"border-l-4 border-l-destructive\"\n};\n\nfunction SectionHeading({ title, description, icon: Icon }: Pick<FormSectionProps, \"title\" | \"description\" | \"icon\">) {\n return (\n <div className=\"flex min-w-0 items-start gap-3 text-left\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\"\n >\n <Icon className=\"size-4\" />\n </span>\n )}\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-sm font-semibold tracking-tight text-foreground\">{title}</h3>\n {description && <p className=\"mt-0.5 text-xs text-muted-foreground\">{description}</p>}\n </div>\n </div>\n );\n}\n\n/**\n * Nhóm trường trong form: tiêu đề + mô tả + lưới 12 cột cho các `FormField`.\n *\n * Nội dung là `grid-cols-12` để `FormField` chỉ cần khai `colSpan` — cách này giữ mọi form\n * trong hệ sinh thái cùng một nhịp cột, thay vì mỗi trang tự chế `flex` với bề rộng riêng.\n */\nexport function FormSection({\n title,\n description,\n icon,\n actions,\n collapsible = false,\n defaultOpen = true,\n open,\n onOpenChange,\n accent = \"none\",\n children,\n className,\n contentClassName\n}: FormSectionProps) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isOpen = open ?? internalOpen;\n\n const setOpen = (next: boolean) => {\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const shell = cn(\"rounded-xl border border-border bg-card\", accentClasses[accent], className);\n const grid = cn(\"grid grid-cols-12 gap-4 p-4 md:gap-5 md:p-5\", contentClassName);\n\n if (!collapsible) {\n return (\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <SectionHeading title={title} description={description} icon={icon} />\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <div className={grid}>{children}</div>\n </section>\n );\n }\n\n return (\n <Collapsible open={isOpen} onOpenChange={setOpen} asChild>\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <CollapsibleTrigger className=\"flex min-w-0 flex-1 items-start gap-3 rounded-md text-left\">\n <SectionHeading title={title} description={description} icon={icon} />\n <ChevronDown\n aria-hidden=\"true\"\n className={cn(\n \"ml-auto mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out\",\n isOpen && \"rotate-180\"\n )}\n />\n </CollapsibleTrigger>\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <CollapsibleContent>\n <div className={grid}>{children}</div>\n </CollapsibleContent>\n </section>\n </Collapsible>\n );\n}\n"],"mappings":";AA+CU,cAGJ,YAHI;AA7CV,SAAS,mBAAmB;AAC5B,SAAyB,gBAAgB;AACzC,SAAS,UAAU;AACnB,SAAS,aAAa,oBAAoB,0BAA0B;AAyBpE,MAAM,gBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAEA,SAAS,eAAe,EAAE,OAAO,aAAa,MAAM,KAAK,GAA6D;AACpH,SACE,qBAAC,SAAI,WAAU,4CACZ;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAU;AAAA,QAEV,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,IAC3B;AAAA,IAEF,qBAAC,SAAI,WAAU,WACb;AAAA,0BAAC,QAAG,WAAU,iEAAiE,iBAAM;AAAA,MACpF,eAAe,oBAAC,OAAE,WAAU,wCAAwC,uBAAY;AAAA,OACnF;AAAA,KACF;AAEJ;AAQO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,QAAM,SAAS,QAAQ;AAEvB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,SAAS,OAAW,iBAAgB,IAAI;AAC5C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,QAAQ,GAAG,2CAA2C,cAAc,MAAM,GAAG,SAAS;AAC5F,QAAM,OAAO,GAAG,+CAA+C,gBAAgB;AAE/E,MAAI,CAAC,aAAa;AAChB,WACE,qBAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,2BAAC,SAAI,WAAU,6EACb;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY;AAAA,QACnE,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,SACzE;AAAA,MACA,oBAAC,SAAI,WAAW,MAAO,UAAS;AAAA,OAClC;AAAA,EAEJ;AAEA,SACE,oBAAC,eAAY,MAAM,QAAQ,cAAc,SAAS,SAAO,MACvD,+BAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,yBAAC,SAAI,WAAU,6EACb;AAAA,2BAAC,sBAAmB,WAAU,8DAC5B;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY;AAAA,QACpE;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,UAAU;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACC,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,OACzE;AAAA,IACA,oBAAC,sBACC,8BAAC,SAAI,WAAW,MAAO,UAAS,GAClC;AAAA,KACF,GACF;AAEJ;","names":[]}
@@ -0,0 +1,38 @@
1
+ import * as React from 'react';
2
+
3
+ interface InlineEditProps {
4
+ /** Giá trị hiện tại (controlled từ phía dữ liệu) */
5
+ value: string;
6
+ /**
7
+ * Lưu giá trị mới. Trả Promise thì component hiện trạng thái đang lưu và chỉ thoát chế độ
8
+ * sửa khi lưu xong — thoát sớm sẽ nháy về giá trị cũ rồi mới nhảy sang giá trị mới.
9
+ */
10
+ onSave: (value: string) => void | Promise<void>;
11
+ placeholder?: string;
12
+ /** Ô nhập nhiều dòng (mô tả task) — Enter xuống dòng, Cmd/Ctrl+Enter mới lưu */
13
+ multiline?: boolean;
14
+ maxLength?: number;
15
+ disabled?: boolean;
16
+ /** Bắt buộc có nội dung: chuỗi rỗng bị coi là huỷ, không lưu (mặc định true) */
17
+ required?: boolean;
18
+ /** Rời ô là lưu (mặc định true, kiểu Linear) */
19
+ saveOnBlur?: boolean;
20
+ /** Hiện cặp nút ✓/✕ khi đang sửa (mặc định false — thao tác bằng phím) */
21
+ showActions?: boolean;
22
+ /** Nhãn cho trình đọc màn hình, vd "Sửa tên công việc" */
23
+ ariaLabel?: string;
24
+ onError?: (error: unknown) => void;
25
+ className?: string;
26
+ /** className riêng cho phần hiển thị (khi chưa sửa) — vd chỉnh cỡ chữ tiêu đề */
27
+ displayClassName?: string;
28
+ }
29
+ /**
30
+ * Sửa tại chỗ kiểu Linear: bấm vào chữ → thành ô nhập → Enter lưu, Esc huỷ.
31
+ *
32
+ * Vì sao không dùng `<input>` luôn cho gọn: ô nhập luôn hiện làm danh sách trông như một cái
33
+ * form khổng lồ và người dùng dễ sửa nhầm khi chỉ định bấm chọn. Hiển thị dạng chữ cho tới khi
34
+ * người dùng thật sự muốn sửa là ranh giới an toàn.
35
+ */
36
+ declare function InlineEdit({ value, onSave, placeholder, multiline, maxLength, disabled, required, saveOnBlur, showActions, ariaLabel, onError, className, displayClassName }: InlineEditProps): React.JSX.Element;
37
+
38
+ export { InlineEdit, type InlineEditProps };
@@ -0,0 +1,165 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Check, X } from "lucide-react";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { Button } from "../ui/button.js";
7
+ function InlineEdit({
8
+ value,
9
+ onSave,
10
+ placeholder = "Nh\u1EA5n \u0111\u1EC3 nh\u1EADp\u2026",
11
+ multiline = false,
12
+ maxLength,
13
+ disabled = false,
14
+ required = true,
15
+ saveOnBlur = true,
16
+ showActions = false,
17
+ ariaLabel = "S\u1EEDa t\u1EA1i ch\u1ED7",
18
+ onError,
19
+ className,
20
+ displayClassName
21
+ }) {
22
+ const [editing, setEditing] = useState(false);
23
+ const [draft, setDraft] = useState(value);
24
+ const [saving, setSaving] = useState(false);
25
+ const inputRef = useRef(null);
26
+ useEffect(() => {
27
+ if (!editing) setDraft(value);
28
+ }, [value, editing]);
29
+ useEffect(() => {
30
+ if (!editing) return;
31
+ const element = inputRef.current;
32
+ if (!element) return;
33
+ element.focus();
34
+ element.setSelectionRange(element.value.length, element.value.length);
35
+ }, [editing]);
36
+ const cancel = () => {
37
+ setDraft(value);
38
+ setEditing(false);
39
+ };
40
+ const commit = async () => {
41
+ const next = draft.trim();
42
+ if (next === value.trim() || required && next === "") {
43
+ cancel();
44
+ return;
45
+ }
46
+ try {
47
+ setSaving(true);
48
+ await onSave(next);
49
+ setSaving(false);
50
+ setEditing(false);
51
+ } catch (error) {
52
+ setSaving(false);
53
+ if (onError) onError(error);
54
+ else console.error("[InlineEdit] onSave th\u1EA5t b\u1EA1i:", error);
55
+ }
56
+ };
57
+ const handleKeyDown = (event) => {
58
+ if (event.key === "Escape") {
59
+ event.preventDefault();
60
+ cancel();
61
+ return;
62
+ }
63
+ if (event.key === "Enter" && (!multiline || event.metaKey || event.ctrlKey)) {
64
+ event.preventDefault();
65
+ void commit();
66
+ }
67
+ };
68
+ if (!editing) {
69
+ return /* @__PURE__ */ jsx(
70
+ "button",
71
+ {
72
+ type: "button",
73
+ disabled,
74
+ "aria-label": ariaLabel,
75
+ onClick: () => setEditing(true),
76
+ className: cn(
77
+ "-mx-1 w-full rounded-md px-1 py-0.5 text-left transition-colors duration-150 ease-out",
78
+ "hover:bg-muted disabled:pointer-events-none disabled:opacity-60",
79
+ !value && "text-muted-foreground",
80
+ className,
81
+ displayClassName
82
+ ),
83
+ children: /* @__PURE__ */ jsx("span", { className: cn(multiline ? "whitespace-pre-wrap" : "block truncate"), children: value || placeholder })
84
+ }
85
+ );
86
+ }
87
+ const sharedProps = {
88
+ value: draft,
89
+ maxLength,
90
+ placeholder,
91
+ disabled: saving,
92
+ "aria-label": ariaLabel,
93
+ onKeyDown: handleKeyDown,
94
+ onBlur: saveOnBlur ? () => void commit() : void 0,
95
+ className: cn(
96
+ "w-full rounded-md border border-input bg-background px-1.5 py-0.5 text-sm text-foreground",
97
+ "focus:border-gold-300 focus:outline-none focus:ring-2 focus:ring-ring/50",
98
+ "disabled:opacity-60",
99
+ className
100
+ )
101
+ };
102
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1", children: [
103
+ multiline ? /* @__PURE__ */ jsx(
104
+ "textarea",
105
+ {
106
+ ...sharedProps,
107
+ ref: (element) => {
108
+ inputRef.current = element;
109
+ },
110
+ rows: 3,
111
+ onChange: (event) => setDraft(event.target.value)
112
+ }
113
+ ) : /* @__PURE__ */ jsx(
114
+ "input",
115
+ {
116
+ ...sharedProps,
117
+ ref: (element) => {
118
+ inputRef.current = element;
119
+ },
120
+ type: "text",
121
+ onChange: (event) => setDraft(event.target.value)
122
+ }
123
+ ),
124
+ showActions && /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [
125
+ /* @__PURE__ */ jsxs(
126
+ Button,
127
+ {
128
+ type: "button",
129
+ variant: "ghost",
130
+ size: "icon-sm",
131
+ disabled: saving,
132
+ onMouseDown: (event) => {
133
+ event.preventDefault();
134
+ void commit();
135
+ },
136
+ children: [
137
+ /* @__PURE__ */ jsx(Check, { "aria-hidden": "true", className: "size-3.5" }),
138
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "L\u01B0u" })
139
+ ]
140
+ }
141
+ ),
142
+ /* @__PURE__ */ jsxs(
143
+ Button,
144
+ {
145
+ type: "button",
146
+ variant: "ghost",
147
+ size: "icon-sm",
148
+ disabled: saving,
149
+ onMouseDown: (event) => {
150
+ event.preventDefault();
151
+ cancel();
152
+ },
153
+ children: [
154
+ /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" }),
155
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "H\u1EE7y" })
156
+ ]
157
+ }
158
+ )
159
+ ] })
160
+ ] });
161
+ }
162
+ export {
163
+ InlineEdit
164
+ };
165
+ //# sourceMappingURL=inline-edit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/inline-edit.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, X } from \"lucide-react\";\nimport { type KeyboardEvent, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\n\nexport interface InlineEditProps {\n /** Giá trị hiện tại (controlled từ phía dữ liệu) */\n value: string;\n /**\n * Lưu giá trị mới. Trả Promise thì component hiện trạng thái đang lưu và chỉ thoát chế độ\n * sửa khi lưu xong — thoát sớm sẽ nháy về giá trị cũ rồi mới nhảy sang giá trị mới.\n */\n onSave: (value: string) => void | Promise<void>;\n placeholder?: string;\n /** Ô nhập nhiều dòng (mô tả task) — Enter xuống dòng, Cmd/Ctrl+Enter mới lưu */\n multiline?: boolean;\n maxLength?: number;\n disabled?: boolean;\n /** Bắt buộc có nội dung: chuỗi rỗng bị coi là huỷ, không lưu (mặc định true) */\n required?: boolean;\n /** Rời ô là lưu (mặc định true, kiểu Linear) */\n saveOnBlur?: boolean;\n /** Hiện cặp nút ✓/✕ khi đang sửa (mặc định false — thao tác bằng phím) */\n showActions?: boolean;\n /** Nhãn cho trình đọc màn hình, vd \"Sửa tên công việc\" */\n ariaLabel?: string;\n onError?: (error: unknown) => void;\n className?: string;\n /** className riêng cho phần hiển thị (khi chưa sửa) — vd chỉnh cỡ chữ tiêu đề */\n displayClassName?: string;\n}\n\n/**\n * Sửa tại chỗ kiểu Linear: bấm vào chữ → thành ô nhập → Enter lưu, Esc huỷ.\n *\n * Vì sao không dùng `<input>` luôn cho gọn: ô nhập luôn hiện làm danh sách trông như một cái\n * form khổng lồ và người dùng dễ sửa nhầm khi chỉ định bấm chọn. Hiển thị dạng chữ cho tới khi\n * người dùng thật sự muốn sửa là ranh giới an toàn.\n */\nexport function InlineEdit({\n value,\n onSave,\n placeholder = \"Nhấn để nhập…\",\n multiline = false,\n maxLength,\n disabled = false,\n required = true,\n saveOnBlur = true,\n showActions = false,\n ariaLabel = \"Sửa tại chỗ\",\n onError,\n className,\n displayClassName\n}: InlineEditProps) {\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(value);\n const [saving, setSaving] = useState(false);\n const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);\n\n // Giá trị đổi từ ngoài (người khác sửa qua realtime) — chỉ đồng bộ khi không sửa dở.\n useEffect(() => {\n if (!editing) setDraft(value);\n }, [value, editing]);\n\n useEffect(() => {\n if (!editing) return;\n const element = inputRef.current;\n if (!element) return;\n element.focus();\n element.setSelectionRange(element.value.length, element.value.length);\n }, [editing]);\n\n const cancel = () => {\n setDraft(value);\n setEditing(false);\n };\n\n const commit = async () => {\n const next = draft.trim();\n if (next === value.trim() || (required && next === \"\")) {\n cancel();\n return;\n }\n\n try {\n setSaving(true);\n await onSave(next);\n setSaving(false);\n setEditing(false);\n } catch (error) {\n setSaving(false);\n // Giữ nguyên chế độ sửa: người dùng không mất chữ vừa gõ khi mạng lỗi.\n if (onError) onError(error);\n else console.error(\"[InlineEdit] onSave thất bại:\", error);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n cancel();\n return;\n }\n if (event.key === \"Enter\" && (!multiline || event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n void commit();\n }\n };\n\n if (!editing) {\n return (\n <button\n type=\"button\"\n disabled={disabled}\n aria-label={ariaLabel}\n onClick={() => setEditing(true)}\n className={cn(\n \"-mx-1 w-full rounded-md px-1 py-0.5 text-left transition-colors duration-150 ease-out\",\n \"hover:bg-muted disabled:pointer-events-none disabled:opacity-60\",\n !value && \"text-muted-foreground\",\n className,\n displayClassName\n )}\n >\n <span className={cn(multiline ? \"whitespace-pre-wrap\" : \"block truncate\")}>{value || placeholder}</span>\n </button>\n );\n }\n\n const sharedProps = {\n value: draft,\n maxLength,\n placeholder,\n disabled: saving,\n \"aria-label\": ariaLabel,\n onKeyDown: handleKeyDown,\n onBlur: saveOnBlur ? () => void commit() : undefined,\n className: cn(\n \"w-full rounded-md border border-input bg-background px-1.5 py-0.5 text-sm text-foreground\",\n \"focus:border-gold-300 focus:outline-none focus:ring-2 focus:ring-ring/50\",\n \"disabled:opacity-60\",\n className\n )\n };\n\n return (\n <div className=\"flex items-start gap-1\">\n {multiline ? (\n <textarea\n {...sharedProps}\n ref={(element) => {\n inputRef.current = element;\n }}\n rows={3}\n onChange={(event) => setDraft(event.target.value)}\n />\n ) : (\n <input\n {...sharedProps}\n ref={(element) => {\n inputRef.current = element;\n }}\n type=\"text\"\n onChange={(event) => setDraft(event.target.value)}\n />\n )}\n\n {showActions && (\n <div className=\"flex shrink-0 items-center gap-0.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={saving}\n // onMouseDown: chạy TRƯỚC blur của ô nhập, nếu để onClick thì blur đã huỷ/lưu mất rồi.\n onMouseDown={(event) => {\n event.preventDefault();\n void commit();\n }}\n >\n <Check aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Lưu</span>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={saving}\n onMouseDown={(event) => {\n event.preventDefault();\n cancel();\n }}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Hủy</span>\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * Máy đang chạy có phải macOS không — chỉ biết được ở phía client.
5
+ *
6
+ * Trả về `false` ở lần render đầu (kể cả trên máy Mac) là CHỦ Ý: server không biết hệ điều hành
7
+ * của người dùng, nếu đoán bừa thì HTML server và client lệch nhau → React cảnh báo hydration
8
+ * và đôi khi vứt cả cây con. Hiện "Ctrl" trong một khung hình rồi đổi thành "⌘" là cái giá rẻ hơn.
9
+ */
10
+ declare function useIsMac(): boolean;
11
+ interface KeyboardHintProps {
12
+ /**
13
+ * Tổ hợp phím dạng chuỗi, vd `"mod+k"`, `"mod+shift+n"`, `"esc"`.
14
+ *
15
+ * Dùng `mod` cho phím lệnh chính: render thành `⌘` trên macOS và `Ctrl` ở nơi khác — viết
16
+ * cứng "Ctrl+K" trong giao diện là sai với một nửa người dùng.
17
+ */
18
+ keys: string;
19
+ /** Gộp các phím vào MỘT ô thay vì mỗi phím một ô (kiểu "⌘K") */
20
+ compact?: boolean;
21
+ className?: string;
22
+ }
23
+ /**
24
+ * Gợi ý phím tắt hiển thị đúng theo nền tảng.
25
+ *
26
+ * `aria-hidden` toàn bộ: ký hiệu ⌘⇧↵ đọc lên nghe vô nghĩa. Nút mang phím tắt nên tự mô tả
27
+ * trong `aria-label` (vd "Mở bảng lệnh, Ctrl K").
28
+ */
29
+ declare function KeyboardHint({ keys, compact, className }: KeyboardHintProps): React.JSX.Element | null;
30
+
31
+ export { KeyboardHint, type KeyboardHintProps, useIsMac };
@@ -0,0 +1,72 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { Kbd, KbdGroup } from "../ui/kbd.js";
6
+ function useIsMac() {
7
+ const [isMac, setIsMac] = useState(false);
8
+ useEffect(() => {
9
+ setIsMac(/mac|iphone|ipad|ipod/i.test(navigator.userAgent));
10
+ }, []);
11
+ return isMac;
12
+ }
13
+ const MAC_SYMBOLS = {
14
+ mod: "\u2318",
15
+ cmd: "\u2318",
16
+ meta: "\u2318",
17
+ ctrl: "\u2303",
18
+ control: "\u2303",
19
+ alt: "\u2325",
20
+ option: "\u2325",
21
+ shift: "\u21E7"
22
+ };
23
+ const OTHER_LABELS = {
24
+ mod: "Ctrl",
25
+ cmd: "Ctrl",
26
+ meta: "Ctrl",
27
+ ctrl: "Ctrl",
28
+ control: "Ctrl",
29
+ alt: "Alt",
30
+ option: "Alt",
31
+ shift: "Shift"
32
+ };
33
+ const COMMON_LABELS = {
34
+ enter: "\u21B5",
35
+ return: "\u21B5",
36
+ esc: "Esc",
37
+ escape: "Esc",
38
+ space: "Space",
39
+ tab: "Tab",
40
+ backspace: "\u232B",
41
+ delete: "Del",
42
+ up: "\u2191",
43
+ down: "\u2193",
44
+ left: "\u2190",
45
+ right: "\u2192"
46
+ };
47
+ function formatKey(token, isMac) {
48
+ const key = token.trim().toLowerCase();
49
+ if (!key) return "";
50
+ const modifier = isMac ? MAC_SYMBOLS[key] : OTHER_LABELS[key];
51
+ if (modifier) return modifier;
52
+ const common = COMMON_LABELS[key];
53
+ if (common) return common;
54
+ return key.length === 1 ? key.toUpperCase() : token.trim();
55
+ }
56
+ function KeyboardHint({ keys, compact = false, className }) {
57
+ const isMac = useIsMac();
58
+ const tokens = keys.split("+").map((token) => formatKey(token, isMac)).filter(Boolean);
59
+ if (tokens.length === 0) return null;
60
+ if (compact) {
61
+ return /* @__PURE__ */ jsx(Kbd, { "aria-hidden": "true", className, children: isMac ? tokens.join("") : tokens.join("+") });
62
+ }
63
+ return /* @__PURE__ */ jsx(KbdGroup, { "aria-hidden": "true", className: cn("shrink-0", className), children: tokens.map((token, index) => (
64
+ // biome-ignore lint/suspicious/noArrayIndexKey: token phím có thể trùng nhau và không có id riêng
65
+ /* @__PURE__ */ jsx(Kbd, { children: token }, `${index}-${token}`)
66
+ )) });
67
+ }
68
+ export {
69
+ KeyboardHint,
70
+ useIsMac
71
+ };
72
+ //# sourceMappingURL=keyboard-hint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/keyboard-hint.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Kbd, KbdGroup } from \"../ui/kbd.js\";\n\n/**\n * Máy đang chạy có phải macOS không — chỉ biết được ở phía client.\n *\n * Trả về `false` ở lần render đầu (kể cả trên máy Mac) là CHỦ Ý: server không biết hệ điều hành\n * của người dùng, nếu đoán bừa thì HTML server và client lệch nhau → React cảnh báo hydration\n * và đôi khi vứt cả cây con. Hiện \"Ctrl\" trong một khung hình rồi đổi thành \"⌘\" là cái giá rẻ hơn.\n */\nexport function useIsMac(): boolean {\n const [isMac, setIsMac] = useState(false);\n\n useEffect(() => {\n setIsMac(/mac|iphone|ipad|ipod/i.test(navigator.userAgent));\n }, []);\n\n return isMac;\n}\n\nconst MAC_SYMBOLS: Record<string, string> = {\n mod: \"⌘\",\n cmd: \"⌘\",\n meta: \"⌘\",\n ctrl: \"⌃\",\n control: \"⌃\",\n alt: \"⌥\",\n option: \"⌥\",\n shift: \"⇧\"\n};\n\nconst OTHER_LABELS: Record<string, string> = {\n mod: \"Ctrl\",\n cmd: \"Ctrl\",\n meta: \"Ctrl\",\n ctrl: \"Ctrl\",\n control: \"Ctrl\",\n alt: \"Alt\",\n option: \"Alt\",\n shift: \"Shift\"\n};\n\nconst COMMON_LABELS: Record<string, string> = {\n enter: \"↵\",\n return: \"↵\",\n esc: \"Esc\",\n escape: \"Esc\",\n space: \"Space\",\n tab: \"Tab\",\n backspace: \"⌫\",\n delete: \"Del\",\n up: \"↑\",\n down: \"↓\",\n left: \"←\",\n right: \"→\"\n};\n\n/** Chuyển một token phím thành nhãn hiển thị theo nền tảng. */\nfunction formatKey(token: string, isMac: boolean): string {\n const key = token.trim().toLowerCase();\n if (!key) return \"\";\n const modifier = isMac ? MAC_SYMBOLS[key] : OTHER_LABELS[key];\n if (modifier) return modifier;\n const common = COMMON_LABELS[key];\n if (common) return common;\n return key.length === 1 ? key.toUpperCase() : token.trim();\n}\n\nexport interface KeyboardHintProps {\n /**\n * Tổ hợp phím dạng chuỗi, vd `\"mod+k\"`, `\"mod+shift+n\"`, `\"esc\"`.\n *\n * Dùng `mod` cho phím lệnh chính: render thành `⌘` trên macOS và `Ctrl` ở nơi khác — viết\n * cứng \"Ctrl+K\" trong giao diện là sai với một nửa người dùng.\n */\n keys: string;\n /** Gộp các phím vào MỘT ô thay vì mỗi phím một ô (kiểu \"⌘K\") */\n compact?: boolean;\n className?: string;\n}\n\n/**\n * Gợi ý phím tắt hiển thị đúng theo nền tảng.\n *\n * `aria-hidden` toàn bộ: ký hiệu ⌘⇧↵ đọc lên nghe vô nghĩa. Nút mang phím tắt nên tự mô tả\n * trong `aria-label` (vd \"Mở bảng lệnh, Ctrl K\").\n */\nexport function KeyboardHint({ keys, compact = false, className }: KeyboardHintProps) {\n const isMac = useIsMac();\n const tokens = keys\n .split(\"+\")\n .map((token) => formatKey(token, isMac))\n .filter(Boolean);\n\n if (tokens.length === 0) return null;\n\n if (compact) {\n return (\n <Kbd aria-hidden=\"true\" className={className}>\n {isMac ? tokens.join(\"\") : tokens.join(\"+\")}\n </Kbd>\n );\n }\n\n return (\n <KbdGroup aria-hidden=\"true\" className={cn(\"shrink-0\", className)}>\n {tokens.map((token, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: token phím có thể trùng nhau và không có id riêng\n <Kbd key={`${index}-${token}`}>{token}</Kbd>\n ))}\n </KbdGroup>\n );\n}\n"],"mappings":";AAqGM;AAnGN,SAAS,WAAW,gBAAgB;AACpC,SAAS,UAAU;AACnB,SAAS,KAAK,gBAAgB;AASvB,SAAS,WAAoB;AAClC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AAExC,YAAU,MAAM;AACd,aAAS,wBAAwB,KAAK,UAAU,SAAS,CAAC;AAAA,EAC5D,GAAG,CAAC,CAAC;AAEL,SAAO;AACT;AAEA,MAAM,cAAsC;AAAA,EAC1C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,SAAS;AAAA,EACT,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,OAAO;AACT;AAEA,MAAM,eAAuC;AAAA,EAC3C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,SAAS;AAAA,EACT,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,OAAO;AACT;AAEA,MAAM,gBAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,KAAK;AAAA,EACL,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AACT;AAGA,SAAS,UAAU,OAAe,OAAwB;AACxD,QAAM,MAAM,MAAM,KAAK,EAAE,YAAY;AACrC,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,WAAW,QAAQ,YAAY,GAAG,IAAI,aAAa,GAAG;AAC5D,MAAI,SAAU,QAAO;AACrB,QAAM,SAAS,cAAc,GAAG;AAChC,MAAI,OAAQ,QAAO;AACnB,SAAO,IAAI,WAAW,IAAI,IAAI,YAAY,IAAI,MAAM,KAAK;AAC3D;AAqBO,SAAS,aAAa,EAAE,MAAM,UAAU,OAAO,UAAU,GAAsB;AACpF,QAAM,QAAQ,SAAS;AACvB,QAAM,SAAS,KACZ,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,UAAU,OAAO,KAAK,CAAC,EACtC,OAAO,OAAO;AAEjB,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,MAAI,SAAS;AACX,WACE,oBAAC,OAAI,eAAY,QAAO,WACrB,kBAAQ,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,GAAG,GAC5C;AAAA,EAEJ;AAEA,SACE,oBAAC,YAAS,eAAY,QAAO,WAAW,GAAG,YAAY,SAAS,GAC7D,iBAAO,IAAI,CAAC,OAAO;AAAA;AAAA,IAElB,oBAAC,OAA+B,mBAAtB,GAAG,KAAK,IAAI,KAAK,EAAW;AAAA,GACvC,GACH;AAEJ;","names":[]}
@@ -0,0 +1,47 @@
1
+ import * as React from 'react';
2
+ import { ComponentProps } from 'react';
3
+
4
+ type LetterAvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
5
+ /**
6
+ * 8 tone màu deterministic — TẤT CẢ là class token tĩnh.
7
+ *
8
+ * Vì sao không sinh `bg-${color}-100`: Tailwind quét tĩnh, class ghép chuỗi lúc chạy sẽ không
9
+ * có mặt trong CSS của app → avatar mất nền. Vì sao không dùng hex inline: mất dark mode.
10
+ * Mỗi tone đã chọn cặp nền/chữ đạt tương phản ở cả hai theme.
11
+ *
12
+ * ⚠ Đây là NGUỒN SỰ THẬT DUY NHẤT cho màu avatar chữ cái của cả package: tầng shell
13
+ * (`shell/utils.ts` → `getAvatarToneClass`) gọi lại `getLetterAvatarTone` chứ không giữ
14
+ * bảng tone riêng. Đừng dựng bảng thứ hai — cùng một người mà ra hai màu trên cùng một
15
+ * trang là lỗi đã từng xảy ra ở 0.1.0.
16
+ */
17
+ declare const LETTER_AVATAR_TONES: readonly ["bg-gold-100 text-gold-600 dark:bg-gold-500/30 dark:text-gold-100", "bg-teal/15 text-teal", "bg-green/15 text-green", "bg-red/15 text-red", "bg-yellow/25 text-gold-500 dark:bg-yellow/20 dark:text-yellow", "bg-chart-3/15 text-chart-3", "bg-ash-100 text-ash-700 dark:bg-ash-500/25 dark:text-ash-100", "bg-ink/10 text-ink-soft dark:bg-paper/15 dark:text-paper"];
18
+ /**
19
+ * Tone màu ổn định theo tên — cùng một người luôn ra cùng màu ở mọi app Comitor.
20
+ *
21
+ * SEED là TÊN (đã trim + hạ chữ thường theo locale vi-VN), không phải email/slug: mọi nơi
22
+ * hiển thị avatar đều chắc chắn có `name`, còn email/slug thì không — lấy khoá khác nhau ở
23
+ * từng chỗ là cách nhanh nhất để cùng một người ra hai màu.
24
+ */
25
+ declare function getLetterAvatarTone(name: string): string;
26
+ interface LetterAvatarProps extends Omit<ComponentProps<"span">, "children"> {
27
+ /** Họ tên đầy đủ — dùng cho cả initials lẫn màu */
28
+ name: string;
29
+ /** Có ảnh thì hiện ảnh, không thì rơi về initials */
30
+ src?: string | null;
31
+ size?: LetterAvatarSize;
32
+ /** Bo vuông (dùng cho workspace/app) thay vì tròn (dùng cho người) */
33
+ square?: boolean;
34
+ /** Ghi đè tone (chuỗi class token) — vd tô theo màu app thay vì theo tên */
35
+ toneClassName?: string;
36
+ }
37
+ /**
38
+ * Avatar chữ cái: ảnh nếu có, không thì initials kiểu Việt (chữ đầu HỌ + chữ đầu TÊN) trên nền
39
+ * màu suy ra từ tên.
40
+ *
41
+ * Không dùng primitive `Avatar` (Radix) một cách cố ý: Radix Avatar là client component vì phải
42
+ * theo dõi trạng thái tải ảnh; ở danh sách 200 dòng thì đó là 200 island vô ích. Bản này tĩnh
43
+ * hoàn toàn → render thẳng ở server.
44
+ */
45
+ declare function LetterAvatar({ name, src, size, square, toneClassName, className, ...props }: LetterAvatarProps): React.JSX.Element;
46
+
47
+ export { LETTER_AVATAR_TONES, LetterAvatar, type LetterAvatarProps, type LetterAvatarSize, getLetterAvatarTone };