@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,268 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ endOfMonth,
5
+ format as formatDate,
6
+ isAfter,
7
+ isBefore,
8
+ isValid,
9
+ parse,
10
+ startOfDay,
11
+ startOfMonth,
12
+ subDays,
13
+ subMonths
14
+ } from "date-fns";
15
+ import { vi } from "date-fns/locale";
16
+ import { CalendarDays, X } from "lucide-react";
17
+ import { useEffect, useState } from "react";
18
+ import { cn } from "../../lib/cn.js";
19
+ import { Button } from "../ui/button.js";
20
+ import { Calendar } from "../ui/calendar.js";
21
+ import { Input } from "../ui/input.js";
22
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
23
+ const VI_DATE_FORMAT = "dd/MM/yyyy";
24
+ const DEFAULT_DATE_PRESETS = [
25
+ { id: "today", label: "H\xF4m nay", getValue: () => startOfDay(/* @__PURE__ */ new Date()) },
26
+ { id: "yesterday", label: "H\xF4m qua", getValue: () => startOfDay(subDays(/* @__PURE__ */ new Date(), 1)) },
27
+ { id: "week-ago", label: "7 ng\xE0y tr\u01B0\u1EDBc", getValue: () => startOfDay(subDays(/* @__PURE__ */ new Date(), 7)) }
28
+ ];
29
+ const DEFAULT_DATE_RANGE_PRESETS = [
30
+ { id: "today", label: "H\xF4m nay", getValue: () => ({ from: startOfDay(/* @__PURE__ */ new Date()), to: startOfDay(/* @__PURE__ */ new Date()) }) },
31
+ {
32
+ id: "last-7",
33
+ label: "7 ng\xE0y qua",
34
+ getValue: () => ({ from: startOfDay(subDays(/* @__PURE__ */ new Date(), 6)), to: startOfDay(/* @__PURE__ */ new Date()) })
35
+ },
36
+ {
37
+ id: "last-30",
38
+ label: "30 ng\xE0y qua",
39
+ getValue: () => ({ from: startOfDay(subDays(/* @__PURE__ */ new Date(), 29)), to: startOfDay(/* @__PURE__ */ new Date()) })
40
+ },
41
+ {
42
+ id: "this-month",
43
+ label: "Th\xE1ng n\xE0y",
44
+ getValue: () => ({ from: startOfMonth(/* @__PURE__ */ new Date()), to: startOfDay(/* @__PURE__ */ new Date()) })
45
+ },
46
+ {
47
+ id: "last-month",
48
+ label: "Th\xE1ng tr\u01B0\u1EDBc",
49
+ getValue: () => {
50
+ const previous = subMonths(/* @__PURE__ */ new Date(), 1);
51
+ return { from: startOfMonth(previous), to: endOfMonth(previous) };
52
+ }
53
+ }
54
+ ];
55
+ function isOutOfRange(date, minDate, maxDate) {
56
+ if (minDate && isBefore(date, startOfDay(minDate))) return true;
57
+ if (maxDate && isAfter(date, startOfDay(maxDate))) return true;
58
+ return false;
59
+ }
60
+ function buildDisabledMatchers(minDate, maxDate) {
61
+ const matchers = [];
62
+ if (minDate) matchers.push({ before: startOfDay(minDate) });
63
+ if (maxDate) matchers.push({ after: startOfDay(maxDate) });
64
+ return matchers.length > 0 ? matchers : void 0;
65
+ }
66
+ function DatePicker({
67
+ value,
68
+ onValueChange,
69
+ placeholder = VI_DATE_FORMAT,
70
+ disabled = false,
71
+ minDate,
72
+ maxDate,
73
+ clearable = true,
74
+ presets = false,
75
+ className,
76
+ id,
77
+ name,
78
+ ...aria
79
+ }) {
80
+ const [open, setOpen] = useState(false);
81
+ const [text, setText] = useState(() => value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : "");
82
+ useEffect(() => {
83
+ setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : "");
84
+ }, [value]);
85
+ const commitText = (raw) => {
86
+ const trimmed = raw.trim();
87
+ if (trimmed === "") {
88
+ onValueChange(null);
89
+ return;
90
+ }
91
+ const parsed = parse(trimmed, VI_DATE_FORMAT, /* @__PURE__ */ new Date(), { locale: vi });
92
+ if (!isValid(parsed) || isOutOfRange(parsed, minDate, maxDate)) {
93
+ setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : "");
94
+ return;
95
+ }
96
+ onValueChange(startOfDay(parsed));
97
+ };
98
+ const presetList = presets === false ? [] : presets;
99
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
100
+ /* @__PURE__ */ jsx(
101
+ Input,
102
+ {
103
+ id,
104
+ name,
105
+ value: text,
106
+ disabled,
107
+ placeholder,
108
+ inputMode: "numeric",
109
+ autoComplete: "off",
110
+ onChange: (event) => setText(event.target.value),
111
+ onBlur: (event) => commitText(event.target.value),
112
+ onKeyDown: (event) => {
113
+ if (event.key === "Enter") {
114
+ event.preventDefault();
115
+ commitText(text);
116
+ }
117
+ },
118
+ className: "pr-16",
119
+ ...aria
120
+ }
121
+ ),
122
+ /* @__PURE__ */ jsxs("div", { className: "absolute right-1 top-1/2 flex -translate-y-1/2 items-center", children: [
123
+ clearable && value && !disabled && /* @__PURE__ */ jsxs(Button, { type: "button", variant: "ghost", size: "icon-sm", className: "size-7", onClick: () => onValueChange(null), children: [
124
+ /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" }),
125
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "X\xF3a ng\xE0y" })
126
+ ] }),
127
+ /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
128
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { type: "button", variant: "ghost", size: "icon-sm", className: "size-7", disabled, children: [
129
+ /* @__PURE__ */ jsx(CalendarDays, { "aria-hidden": "true", className: "size-4" }),
130
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "M\u1EDF l\u1ECBch" })
131
+ ] }) }),
132
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "end", className: "flex w-auto gap-2 p-2", children: [
133
+ presetList.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex w-32 flex-col gap-1 border-r border-border pr-2", children: presetList.map((preset) => /* @__PURE__ */ jsx(
134
+ Button,
135
+ {
136
+ type: "button",
137
+ variant: "ghost",
138
+ size: "sm",
139
+ className: "justify-start",
140
+ onClick: () => {
141
+ onValueChange(preset.getValue());
142
+ setOpen(false);
143
+ },
144
+ children: preset.label
145
+ },
146
+ preset.id
147
+ )) }),
148
+ /* @__PURE__ */ jsx(
149
+ Calendar,
150
+ {
151
+ mode: "single",
152
+ locale: vi,
153
+ autoFocus: true,
154
+ selected: value ?? void 0,
155
+ defaultMonth: value ?? void 0,
156
+ disabled: buildDisabledMatchers(minDate, maxDate),
157
+ onSelect: (date) => {
158
+ onValueChange(date ? startOfDay(date) : null);
159
+ setOpen(false);
160
+ }
161
+ }
162
+ )
163
+ ] })
164
+ ] })
165
+ ] })
166
+ ] });
167
+ }
168
+ function formatRange(range) {
169
+ if (!range?.from) return null;
170
+ const from = formatDate(range.from, VI_DATE_FORMAT, { locale: vi });
171
+ if (!range.to) return from;
172
+ return `${from} \u2013 ${formatDate(range.to, VI_DATE_FORMAT, { locale: vi })}`;
173
+ }
174
+ function DateRangePicker({
175
+ value,
176
+ onValueChange,
177
+ placeholder = "Ch\u1ECDn kho\u1EA3ng ng\xE0y",
178
+ disabled = false,
179
+ minDate,
180
+ maxDate,
181
+ clearable = true,
182
+ presets = DEFAULT_DATE_RANGE_PRESETS,
183
+ numberOfMonths = 2,
184
+ className,
185
+ id,
186
+ ...aria
187
+ }) {
188
+ const [open, setOpen] = useState(false);
189
+ const label = formatRange(value);
190
+ const presetList = presets === false ? [] : presets;
191
+ const showClear = clearable && Boolean(label) && !disabled;
192
+ return (
193
+ /* Nút xóa nằm NGOÀI trigger (anh em), không lồng trong <button> — vừa hợp lệ về HTML vừa
194
+ đến được bằng phím Tab. */
195
+ /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
196
+ /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
197
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
198
+ Button,
199
+ {
200
+ type: "button",
201
+ id,
202
+ variant: "outline",
203
+ disabled,
204
+ className: cn(
205
+ "w-full justify-start gap-2 font-normal",
206
+ !label && "text-muted-foreground",
207
+ showClear && "pr-9",
208
+ className
209
+ ),
210
+ ...aria,
211
+ children: [
212
+ /* @__PURE__ */ jsx(CalendarDays, { "aria-hidden": "true", className: "size-4 shrink-0" }),
213
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label ?? placeholder })
214
+ ]
215
+ }
216
+ ) }),
217
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "flex w-auto flex-col gap-2 p-2 sm:flex-row", children: [
218
+ presetList.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-row flex-wrap gap-1 sm:w-36 sm:flex-col sm:border-r sm:border-border sm:pr-2", children: presetList.map((preset) => /* @__PURE__ */ jsx(
219
+ Button,
220
+ {
221
+ type: "button",
222
+ variant: "ghost",
223
+ size: "sm",
224
+ className: "justify-start",
225
+ onClick: () => {
226
+ onValueChange(preset.getValue());
227
+ setOpen(false);
228
+ },
229
+ children: preset.label
230
+ },
231
+ preset.id
232
+ )) }),
233
+ /* @__PURE__ */ jsx(
234
+ Calendar,
235
+ {
236
+ mode: "range",
237
+ locale: vi,
238
+ autoFocus: true,
239
+ numberOfMonths,
240
+ selected: value ?? void 0,
241
+ defaultMonth: value?.from ?? void 0,
242
+ disabled: buildDisabledMatchers(minDate, maxDate),
243
+ onSelect: (range) => onValueChange(range ?? null)
244
+ }
245
+ )
246
+ ] })
247
+ ] }),
248
+ showClear && /* @__PURE__ */ jsx(
249
+ "button",
250
+ {
251
+ type: "button",
252
+ "aria-label": "X\xF3a kho\u1EA3ng ng\xE0y",
253
+ onClick: () => onValueChange(null),
254
+ className: "absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground",
255
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" })
256
+ }
257
+ )
258
+ ] })
259
+ );
260
+ }
261
+ export {
262
+ DEFAULT_DATE_PRESETS,
263
+ DEFAULT_DATE_RANGE_PRESETS,
264
+ DatePicker,
265
+ DateRangePicker,
266
+ VI_DATE_FORMAT
267
+ };
268
+ //# sourceMappingURL=date-picker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/date-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n endOfMonth,\n format as formatDate,\n isAfter,\n isBefore,\n isValid,\n parse,\n startOfDay,\n startOfMonth,\n subDays,\n subMonths\n} from \"date-fns\";\nimport { vi } from \"date-fns/locale\";\nimport { CalendarDays, X } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport type { DateRange, Matcher } from \"react-day-picker\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Calendar } from \"../ui/calendar.js\";\nimport { Input } from \"../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\n\nexport type { DateRange };\n\n/** Định dạng ngày của Việt Nam — dùng thống nhất ở mọi app Comitor. */\nexport const VI_DATE_FORMAT = \"dd/MM/yyyy\";\n\nexport interface DatePreset {\n id: string;\n label: string;\n /** Trả về ngày (DatePicker) hoặc khoảng ngày (DateRangePicker) tại thời điểm bấm */\n getValue: () => Date;\n}\n\nexport interface DateRangePreset {\n id: string;\n label: string;\n getValue: () => DateRange;\n}\n\n/** Preset mặc định cho ô chọn một ngày. */\nexport const DEFAULT_DATE_PRESETS: DatePreset[] = [\n { id: \"today\", label: \"Hôm nay\", getValue: () => startOfDay(new Date()) },\n { id: \"yesterday\", label: \"Hôm qua\", getValue: () => startOfDay(subDays(new Date(), 1)) },\n { id: \"week-ago\", label: \"7 ngày trước\", getValue: () => startOfDay(subDays(new Date(), 7)) }\n];\n\n/** Preset mặc định cho ô chọn khoảng ngày (báo cáo, bộ lọc danh sách). */\nexport const DEFAULT_DATE_RANGE_PRESETS: DateRangePreset[] = [\n { id: \"today\", label: \"Hôm nay\", getValue: () => ({ from: startOfDay(new Date()), to: startOfDay(new Date()) }) },\n {\n id: \"last-7\",\n label: \"7 ngày qua\",\n getValue: () => ({ from: startOfDay(subDays(new Date(), 6)), to: startOfDay(new Date()) })\n },\n {\n id: \"last-30\",\n label: \"30 ngày qua\",\n getValue: () => ({ from: startOfDay(subDays(new Date(), 29)), to: startOfDay(new Date()) })\n },\n {\n id: \"this-month\",\n label: \"Tháng này\",\n getValue: () => ({ from: startOfMonth(new Date()), to: startOfDay(new Date()) })\n },\n {\n id: \"last-month\",\n label: \"Tháng trước\",\n getValue: () => {\n const previous = subMonths(new Date(), 1);\n return { from: startOfMonth(previous), to: endOfMonth(previous) };\n }\n }\n];\n\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n if (minDate && isBefore(date, startOfDay(minDate))) return true;\n if (maxDate && isAfter(date, startOfDay(maxDate))) return true;\n return false;\n}\n\n/**\n * Chặn ngày ngoài khoảng cho phép — trả về MẢNG matcher, không phải một object.\n *\n * Cạm bẫy react-day-picker: `{ before, after }` trong CÙNG một object là `DateInterval`, nghĩa là\n * \"các ngày NẰM GIỮA\" — ngược hẳn ý định. Tách thành hai matcher riêng mới ra \"trước min HOẶC sau max\".\n */\nfunction buildDisabledMatchers(minDate?: Date, maxDate?: Date): Matcher[] | undefined {\n const matchers: Matcher[] = [];\n if (minDate) matchers.push({ before: startOfDay(minDate) });\n if (maxDate) matchers.push({ after: startOfDay(maxDate) });\n return matchers.length > 0 ? matchers : undefined;\n}\n\nexport interface DatePickerProps {\n value?: Date | null;\n onValueChange: (value: Date | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Ngày nhỏ nhất / lớn nhất được chọn */\n minDate?: Date;\n maxDate?: Date;\n /** Hiện nút xoá khi đã có giá trị (mặc định true) */\n clearable?: boolean;\n /** Cột preset bên trái lịch (mặc định ẩn) */\n presets?: DatePreset[] | false;\n className?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n}\n\n/**\n * Ô chọn ngày: gõ tay `dd/MM/yyyy` HOẶC bấm lịch.\n *\n * Vì sao vẫn giữ ô gõ tay thay vì chỉ có nút mở lịch: người nhập liệu chuyên nghiệp (kế toán,\n * nhân sự) gõ nhanh hơn bấm nhiều lần, và ngày sinh năm 1975 mà bắt lật lịch 50 năm thì không\n * ai chịu nổi. Chuỗi gõ dở KHÔNG bị ghi đè trong lúc gõ; sai định dạng thì trả về giá trị cũ\n * khi rời ô, không âm thầm nuốt dữ liệu.\n */\nexport function DatePicker({\n value,\n onValueChange,\n placeholder = VI_DATE_FORMAT,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = false,\n className,\n id,\n name,\n ...aria\n}: DatePickerProps) {\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(() => (value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\"));\n\n // Đồng bộ khi giá trị đổi từ bên ngoài (reset form, chọn preset, load dữ liệu về).\n useEffect(() => {\n setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\");\n }, [value]);\n\n const commitText = (raw: string) => {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n onValueChange(null);\n return;\n }\n const parsed = parse(trimmed, VI_DATE_FORMAT, new Date(), { locale: vi });\n if (!isValid(parsed) || isOutOfRange(parsed, minDate, maxDate)) {\n // Không hợp lệ → khôi phục hiển thị theo giá trị đang có (không đổi dữ liệu).\n setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\");\n return;\n }\n onValueChange(startOfDay(parsed));\n };\n\n const presetList = presets === false ? [] : presets;\n\n return (\n <div className={cn(\"relative\", className)}>\n <Input\n id={id}\n name={name}\n value={text}\n disabled={disabled}\n placeholder={placeholder}\n inputMode=\"numeric\"\n autoComplete=\"off\"\n onChange={(event) => setText(event.target.value)}\n onBlur={(event) => commitText(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commitText(text);\n }\n }}\n className=\"pr-16\"\n {...aria}\n />\n\n <div className=\"absolute right-1 top-1/2 flex -translate-y-1/2 items-center\">\n {clearable && value && !disabled && (\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" onClick={() => onValueChange(null)}>\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Xóa ngày</span>\n </Button>\n )}\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" disabled={disabled}>\n <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Mở lịch</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"flex w-auto gap-2 p-2\">\n {presetList.length > 0 && (\n <div className=\"flex w-32 flex-col gap-1 border-r border-border pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"single\"\n locale={vi}\n autoFocus\n selected={value ?? undefined}\n defaultMonth={value ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(date) => {\n onValueChange(date ? startOfDay(date) : null);\n setOpen(false);\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n );\n}\n\nexport interface DateRangePickerProps {\n value?: DateRange | null;\n onValueChange: (value: DateRange | null) => void;\n placeholder?: string;\n disabled?: boolean;\n minDate?: Date;\n maxDate?: Date;\n clearable?: boolean;\n /** Cột preset bên trái (mặc định DEFAULT_DATE_RANGE_PRESETS; `false` để tắt) */\n presets?: DateRangePreset[] | false;\n /** Số tháng hiện cùng lúc (mặc định 2 trên màn rộng) */\n numberOfMonths?: number;\n className?: string;\n id?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n}\n\nfunction formatRange(range: DateRange | null | undefined): string | null {\n if (!range?.from) return null;\n const from = formatDate(range.from, VI_DATE_FORMAT, { locale: vi });\n if (!range.to) return from;\n return `${from} – ${formatDate(range.to, VI_DATE_FORMAT, { locale: vi })}`;\n}\n\n/**\n * Ô chọn khoảng ngày cho báo cáo và bộ lọc danh sách: cột preset + lịch 2 tháng.\n *\n * Preset đứng trước lịch vì 9/10 lần người dùng muốn \"7 ngày qua\" chứ không phải một khoảng\n * tuỳ ý — bắt họ bấm hai lần trên lịch mỗi lần xem báo cáo là thuế lặp lại vô ích.\n */\nexport function DateRangePicker({\n value,\n onValueChange,\n placeholder = \"Chọn khoảng ngày\",\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = DEFAULT_DATE_RANGE_PRESETS,\n numberOfMonths = 2,\n className,\n id,\n ...aria\n}: DateRangePickerProps) {\n const [open, setOpen] = useState(false);\n const label = formatRange(value);\n const presetList = presets === false ? [] : presets;\n\n const showClear = clearable && Boolean(label) && !disabled;\n\n return (\n /* Nút xóa nằm NGOÀI trigger (anh em), không lồng trong <button> — vừa hợp lệ về HTML vừa\n đến được bằng phím Tab. */\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n id={id}\n variant=\"outline\"\n disabled={disabled}\n className={cn(\n \"w-full justify-start gap-2 font-normal\",\n !label && \"text-muted-foreground\",\n showClear && \"pr-9\",\n className\n )}\n {...aria}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{label ?? placeholder}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"flex w-auto flex-col gap-2 p-2 sm:flex-row\">\n {presetList.length > 0 && (\n <div className=\"flex flex-row flex-wrap gap-1 sm:w-36 sm:flex-col sm:border-r sm:border-border sm:pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"range\"\n locale={vi}\n autoFocus\n numberOfMonths={numberOfMonths}\n selected={value ?? undefined}\n defaultMonth={value?.from ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(range) => onValueChange(range ?? null)}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"Xóa khoảng ngày\"\n onClick={() => onValueChange(null)}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,38 @@
1
+ import * as React from 'react';
2
+ import { ComponentType, ComponentProps, ReactNode } from 'react';
3
+
4
+ /**
5
+ * Kiểu icon dùng chung ở tầng composite: nhận thẳng component icon (Lucide hoặc SVG tự vẽ),
6
+ * KHÔNG nhận element đã render.
7
+ *
8
+ * Vì sao: `LucideIcon` là `forwardRef` (object, không phải function) nên không thể phân biệt
9
+ * "component" với "element" bằng `typeof` lúc chạy. Cố đoán sẽ sai âm thầm — ép kiểu từ đầu
10
+ * cho trình biên dịch bắt lỗi thay ta. Cần node tuỳ ý thì dùng slot riêng (vd `media`).
11
+ */
12
+ type IconComponent = ComponentType<{
13
+ className?: string;
14
+ "aria-hidden"?: boolean | "true" | "false";
15
+ }>;
16
+ interface EmptyStateProps extends Omit<ComponentProps<"div">, "title"> {
17
+ /** Component icon (vd `Inbox` của lucide-react) — hiện trong ô bo góc nền muted */
18
+ icon?: IconComponent;
19
+ /** Node tuỳ ý thay cho icon (ảnh minh hoạ, illustration) — ưu tiên hơn `icon` */
20
+ media?: ReactNode;
21
+ title: ReactNode;
22
+ description?: ReactNode;
23
+ /** Nút hành động chính/phụ — render dưới mô tả */
24
+ action?: ReactNode;
25
+ /** Viền đứt quanh vùng rỗng (dùng khi nằm trong khung bảng/danh sách) */
26
+ bordered?: boolean;
27
+ /** "sm" cho vùng rỗng trong card nhỏ; "md" (mặc định) cho giữa trang */
28
+ size?: "sm" | "md";
29
+ }
30
+ /**
31
+ * Trạng thái rỗng chuẩn: icon + tiêu đề + mô tả + hành động.
32
+ *
33
+ * Bọc primitive `Empty` thay vì dựng lại markup, để "chưa có dữ liệu" ở Tasks, CRM, HR trông
34
+ * như nhau. Tĩnh hoàn toàn → dùng được trong React Server Component.
35
+ */
36
+ declare function EmptyState({ icon: Icon, media, title, description, action, bordered, size, className, ...props }: EmptyStateProps): React.JSX.Element;
37
+
38
+ export { EmptyState, type EmptyStateProps, type IconComponent };
@@ -0,0 +1,35 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../../lib/cn.js";
3
+ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty.js";
4
+ function EmptyState({
5
+ icon: Icon,
6
+ media,
7
+ title,
8
+ description,
9
+ action,
10
+ bordered = false,
11
+ size = "md",
12
+ className,
13
+ ...props
14
+ }) {
15
+ return /* @__PURE__ */ jsxs(
16
+ Empty,
17
+ {
18
+ "data-slot": "empty-state",
19
+ className: cn(bordered && "border", size === "sm" && "gap-4 p-4 md:p-6", className),
20
+ ...props,
21
+ children: [
22
+ /* @__PURE__ */ jsxs(EmptyHeader, { children: [
23
+ media ? /* @__PURE__ */ jsx(EmptyMedia, { children: media }) : Icon ? /* @__PURE__ */ jsx(EmptyMedia, { variant: "icon", className: cn(size === "sm" && "size-9"), children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" }) }) : null,
24
+ /* @__PURE__ */ jsx(EmptyTitle, { className: cn(size === "sm" && "text-base"), children: title }),
25
+ description && /* @__PURE__ */ jsx(EmptyDescription, { children: description })
26
+ ] }),
27
+ action && /* @__PURE__ */ jsx(EmptyContent, { className: "flex-row justify-center gap-2", children: action })
28
+ ]
29
+ }
30
+ );
31
+ }
32
+ export {
33
+ EmptyState
34
+ };
35
+ //# sourceMappingURL=empty-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/empty-state.tsx"],"sourcesContent":["import type { ComponentProps, ComponentType, ReactNode } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from \"../ui/empty.js\";\n\n/**\n * Kiểu icon dùng chung ở tầng composite: nhận thẳng component icon (Lucide hoặc SVG tự vẽ),\n * KHÔNG nhận element đã render.\n *\n * Vì sao: `LucideIcon` là `forwardRef` (object, không phải function) nên không thể phân biệt\n * \"component\" với \"element\" bằng `typeof` lúc chạy. Cố đoán sẽ sai âm thầm — ép kiểu từ đầu\n * cho trình biên dịch bắt lỗi thay ta. Cần node tuỳ ý thì dùng slot riêng (vd `media`).\n */\nexport type IconComponent = ComponentType<{ className?: string; \"aria-hidden\"?: boolean | \"true\" | \"false\" }>;\n\nexport interface EmptyStateProps extends Omit<ComponentProps<\"div\">, \"title\"> {\n /** Component icon (vd `Inbox` của lucide-react) — hiện trong ô bo góc nền muted */\n icon?: IconComponent;\n /** Node tuỳ ý thay cho icon (ảnh minh hoạ, illustration) — ưu tiên hơn `icon` */\n media?: ReactNode;\n title: ReactNode;\n description?: ReactNode;\n /** Nút hành động chính/phụ — render dưới mô tả */\n action?: ReactNode;\n /** Viền đứt quanh vùng rỗng (dùng khi nằm trong khung bảng/danh sách) */\n bordered?: boolean;\n /** \"sm\" cho vùng rỗng trong card nhỏ; \"md\" (mặc định) cho giữa trang */\n size?: \"sm\" | \"md\";\n}\n\n/**\n * Trạng thái rỗng chuẩn: icon + tiêu đề + mô tả + hành động.\n *\n * Bọc primitive `Empty` thay vì dựng lại markup, để \"chưa có dữ liệu\" ở Tasks, CRM, HR trông\n * như nhau. Tĩnh hoàn toàn → dùng được trong React Server Component.\n */\nexport function EmptyState({\n icon: Icon,\n media,\n title,\n description,\n action,\n bordered = false,\n size = \"md\",\n className,\n ...props\n}: EmptyStateProps) {\n return (\n <Empty\n data-slot=\"empty-state\"\n className={cn(bordered && \"border\", size === \"sm\" && \"gap-4 p-4 md:p-6\", className)}\n {...props}\n >\n <EmptyHeader>\n {media ? (\n <EmptyMedia>{media}</EmptyMedia>\n ) : Icon ? (\n <EmptyMedia variant=\"icon\" className={cn(size === \"sm\" && \"size-9\")}>\n <Icon aria-hidden=\"true\" />\n </EmptyMedia>\n ) : null}\n <EmptyTitle className={cn(size === \"sm\" && \"text-base\")}>{title}</EmptyTitle>\n {description && <EmptyDescription>{description}</EmptyDescription>}\n </EmptyHeader>\n {action && <EmptyContent className=\"flex-row justify-center gap-2\">{action}</EmptyContent>}\n </Empty>\n );\n}\n"],"mappings":"AAoDM,SAEI,KAFJ;AAnDN,SAAS,UAAU;AACnB,SAAS,OAAO,cAAc,kBAAkB,aAAa,YAAY,kBAAkB;AAiCpF,SAAS,WAAW;AAAA,EACzB,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,YAAY,UAAU,SAAS,QAAQ,oBAAoB,SAAS;AAAA,MACjF,GAAG;AAAA,MAEJ;AAAA,6BAAC,eACE;AAAA,kBACC,oBAAC,cAAY,iBAAM,IACjB,OACF,oBAAC,cAAW,SAAQ,QAAO,WAAW,GAAG,SAAS,QAAQ,QAAQ,GAChE,8BAAC,QAAK,eAAY,QAAO,GAC3B,IACE;AAAA,UACJ,oBAAC,cAAW,WAAW,GAAG,SAAS,QAAQ,WAAW,GAAI,iBAAM;AAAA,UAC/D,eAAe,oBAAC,oBAAkB,uBAAY;AAAA,WACjD;AAAA,QACC,UAAU,oBAAC,gBAAa,WAAU,iCAAiC,kBAAO;AAAA;AAAA;AAAA,EAC7E;AAEJ;","names":[]}
@@ -0,0 +1,39 @@
1
+ import * as React from 'react';
2
+ import { IconComponent } from './empty-state.js';
3
+ import { StatusTone } from './status-config.js';
4
+
5
+ interface FilterChipItem<T extends string = string> {
6
+ value: T;
7
+ label: string;
8
+ /** Số bản ghi thuộc nhóm này — hiện đậm trước nhãn */
9
+ count?: number;
10
+ icon?: IconComponent;
11
+ /** Tone khi chip đang bật (mặc định tone app-accent) */
12
+ tone?: StatusTone;
13
+ disabled?: boolean;
14
+ }
15
+ interface FilterChipsProps<T extends string = string> {
16
+ items: FilterChipItem<T>[];
17
+ /** Giá trị đang chọn — mảng (đa chọn) hoặc một giá trị/null (đơn chọn) */
18
+ value: T[] | T | null;
19
+ onValueChange: (value: T[] | T | null) => void;
20
+ /** Cho chọn nhiều chip cùng lúc (mặc định false) */
21
+ multiple?: boolean;
22
+ /** Thêm chip "Tất cả" ở đầu để bỏ mọi lựa chọn */
23
+ showAll?: boolean;
24
+ allLabel?: string;
25
+ allCount?: number;
26
+ /** Nhãn nhóm cho trình đọc màn hình */
27
+ label?: string;
28
+ className?: string;
29
+ }
30
+ /**
31
+ * Dãy chip lọc bật/tắt kèm số đếm — kiểu Linear/Lark, dùng cho trạng thái, nhãn, mức ưu tiên.
32
+ *
33
+ * Dùng `aria-pressed` trên `<button>` thay vì `role="tab"`: chip lọc không đổi vùng nội dung
34
+ * theo kiểu tab, nó bật/tắt điều kiện lọc. Đặt nhầm role sẽ khiến trình đọc màn hình hứa hẹn
35
+ * một panel không tồn tại.
36
+ */
37
+ declare function FilterChips<T extends string = string>({ items, value, onValueChange, multiple, showAll, allLabel, allCount, label, className }: FilterChipsProps<T>): React.JSX.Element;
38
+
39
+ export { type FilterChipItem, FilterChips, type FilterChipsProps };
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../../lib/cn.js";
4
+ function toArray(value) {
5
+ if (value == null) return [];
6
+ return Array.isArray(value) ? value : [value];
7
+ }
8
+ function FilterChips({
9
+ items,
10
+ value,
11
+ onValueChange,
12
+ multiple = false,
13
+ showAll = false,
14
+ allLabel = "T\u1EA5t c\u1EA3",
15
+ allCount,
16
+ label = "B\u1ED9 l\u1ECDc nhanh",
17
+ className
18
+ }) {
19
+ const selected = toArray(value);
20
+ const noneSelected = selected.length === 0;
21
+ const toggle = (item) => {
22
+ if (item.disabled) return;
23
+ if (!multiple) {
24
+ onValueChange(selected.includes(item.value) ? null : item.value);
25
+ return;
26
+ }
27
+ onValueChange(
28
+ selected.includes(item.value) ? selected.filter((current) => current !== item.value) : [...selected, item.value]
29
+ );
30
+ };
31
+ const chipClass = (active, tone) => cn(
32
+ "inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-sm font-medium transition-colors duration-150 ease-out",
33
+ "disabled:pointer-events-none disabled:opacity-50",
34
+ active ? tone ? cn("border-transparent", tone.bgColor, tone.textColor) : (
35
+ // Tô ĐẶC chứ không phải nền vàng 15%: chữ gold trên nền gold nhạt chỉ đạt ~1.9:1,
36
+ // dưới chuẩn AA. Nền đặc + `app-accent-foreground` (mực) mới đọc được ở cả 2 theme.
37
+ "border-transparent bg-app-accent text-app-accent-foreground"
38
+ ) : "border-border bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground"
39
+ );
40
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "filter-chips", role: "group", "aria-label": label, className: cn("flex flex-wrap gap-2", className), children: [
41
+ showAll && /* @__PURE__ */ jsxs(
42
+ "button",
43
+ {
44
+ type: "button",
45
+ "aria-pressed": noneSelected,
46
+ onClick: () => onValueChange(multiple ? [] : null),
47
+ className: chipClass(noneSelected),
48
+ children: [
49
+ allCount !== void 0 && /* @__PURE__ */ jsx("span", { className: "font-semibold tabular-nums", children: allCount }),
50
+ allLabel
51
+ ]
52
+ }
53
+ ),
54
+ items.map((item) => {
55
+ const active = selected.includes(item.value);
56
+ const Icon = item.icon;
57
+ return /* @__PURE__ */ jsxs(
58
+ "button",
59
+ {
60
+ type: "button",
61
+ "aria-pressed": active,
62
+ disabled: item.disabled,
63
+ onClick: () => toggle(item),
64
+ className: chipClass(active, item.tone),
65
+ children: [
66
+ Icon && /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true", className: "size-3.5" }),
67
+ item.count !== void 0 && /* @__PURE__ */ jsx("span", { className: "font-semibold tabular-nums", children: item.count }),
68
+ item.label
69
+ ]
70
+ },
71
+ item.value
72
+ );
73
+ })
74
+ ] });
75
+ }
76
+ export {
77
+ FilterChips
78
+ };
79
+ //# sourceMappingURL=filter-chips.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/filter-chips.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"../../lib/cn.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport type { StatusTone } from \"./status-config.js\";\n\nexport interface FilterChipItem<T extends string = string> {\n value: T;\n label: string;\n /** Số bản ghi thuộc nhóm này — hiện đậm trước nhãn */\n count?: number;\n icon?: IconComponent;\n /** Tone khi chip đang bật (mặc định tone app-accent) */\n tone?: StatusTone;\n disabled?: boolean;\n}\n\nexport interface FilterChipsProps<T extends string = string> {\n items: FilterChipItem<T>[];\n /** Giá trị đang chọn — mảng (đa chọn) hoặc một giá trị/null (đơn chọn) */\n value: T[] | T | null;\n onValueChange: (value: T[] | T | null) => void;\n /** Cho chọn nhiều chip cùng lúc (mặc định false) */\n multiple?: boolean;\n /** Thêm chip \"Tất cả\" ở đầu để bỏ mọi lựa chọn */\n showAll?: boolean;\n allLabel?: string;\n allCount?: number;\n /** Nhãn nhóm cho trình đọc màn hình */\n label?: string;\n className?: string;\n}\n\nfunction toArray<T extends string>(value: T[] | T | null): T[] {\n if (value == null) return [];\n return Array.isArray(value) ? value : [value];\n}\n\n/**\n * Dãy chip lọc bật/tắt kèm số đếm — kiểu Linear/Lark, dùng cho trạng thái, nhãn, mức ưu tiên.\n *\n * Dùng `aria-pressed` trên `<button>` thay vì `role=\"tab\"`: chip lọc không đổi vùng nội dung\n * theo kiểu tab, nó bật/tắt điều kiện lọc. Đặt nhầm role sẽ khiến trình đọc màn hình hứa hẹn\n * một panel không tồn tại.\n */\nexport function FilterChips<T extends string = string>({\n items,\n value,\n onValueChange,\n multiple = false,\n showAll = false,\n allLabel = \"Tất cả\",\n allCount,\n label = \"Bộ lọc nhanh\",\n className\n}: FilterChipsProps<T>) {\n const selected = toArray(value);\n const noneSelected = selected.length === 0;\n\n const toggle = (item: FilterChipItem<T>) => {\n if (item.disabled) return;\n if (!multiple) {\n onValueChange(selected.includes(item.value) ? null : item.value);\n return;\n }\n onValueChange(\n selected.includes(item.value) ? selected.filter((current) => current !== item.value) : [...selected, item.value]\n );\n };\n\n const chipClass = (active: boolean, tone?: StatusTone) =>\n cn(\n \"inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-sm font-medium transition-colors duration-150 ease-out\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n active\n ? tone\n ? cn(\"border-transparent\", tone.bgColor, tone.textColor)\n : // Tô ĐẶC chứ không phải nền vàng 15%: chữ gold trên nền gold nhạt chỉ đạt ~1.9:1,\n // dưới chuẩn AA. Nền đặc + `app-accent-foreground` (mực) mới đọc được ở cả 2 theme.\n \"border-transparent bg-app-accent text-app-accent-foreground\"\n : \"border-border bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n );\n\n return (\n <div data-slot=\"filter-chips\" role=\"group\" aria-label={label} className={cn(\"flex flex-wrap gap-2\", className)}>\n {showAll && (\n <button\n type=\"button\"\n aria-pressed={noneSelected}\n onClick={() => onValueChange(multiple ? [] : null)}\n className={chipClass(noneSelected)}\n >\n {allCount !== undefined && <span className=\"font-semibold tabular-nums\">{allCount}</span>}\n {allLabel}\n </button>\n )}\n\n {items.map((item) => {\n const active = selected.includes(item.value);\n const Icon = item.icon;\n return (\n <button\n key={item.value}\n type=\"button\"\n aria-pressed={active}\n disabled={item.disabled}\n onClick={() => toggle(item)}\n className={chipClass(active, item.tone)}\n >\n {Icon && <Icon aria-hidden=\"true\" className=\"size-3.5\" />}\n {item.count !== undefined && <span className=\"font-semibold tabular-nums\">{item.count}</span>}\n {item.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AAsFQ,SAM6B,KAN7B;AApFR,SAAS,UAAU;AA+BnB,SAAS,QAA0B,OAA4B;AAC7D,MAAI,SAAS,KAAM,QAAO,CAAC;AAC3B,SAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAC9C;AASO,SAAS,YAAuC;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAAwB;AACtB,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,eAAe,SAAS,WAAW;AAEzC,QAAM,SAAS,CAAC,SAA4B;AAC1C,QAAI,KAAK,SAAU;AACnB,QAAI,CAAC,UAAU;AACb,oBAAc,SAAS,SAAS,KAAK,KAAK,IAAI,OAAO,KAAK,KAAK;AAC/D;AAAA,IACF;AACA;AAAA,MACE,SAAS,SAAS,KAAK,KAAK,IAAI,SAAS,OAAO,CAAC,YAAY,YAAY,KAAK,KAAK,IAAI,CAAC,GAAG,UAAU,KAAK,KAAK;AAAA,IACjH;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,QAAiB,SAClC;AAAA,IACE;AAAA,IACA;AAAA,IACA,SACI,OACE,GAAG,sBAAsB,KAAK,SAAS,KAAK,SAAS;AAAA;AAAA;AAAA,MAGrD;AAAA,QACF;AAAA,EACN;AAEF,SACE,qBAAC,SAAI,aAAU,gBAAe,MAAK,SAAQ,cAAY,OAAO,WAAW,GAAG,wBAAwB,SAAS,GAC1G;AAAA,eACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,gBAAc;AAAA,QACd,SAAS,MAAM,cAAc,WAAW,CAAC,IAAI,IAAI;AAAA,QACjD,WAAW,UAAU,YAAY;AAAA,QAEhC;AAAA,uBAAa,UAAa,oBAAC,UAAK,WAAU,8BAA8B,oBAAS;AAAA,UACjF;AAAA;AAAA;AAAA,IACH;AAAA,IAGD,MAAM,IAAI,CAAC,SAAS;AACnB,YAAM,SAAS,SAAS,SAAS,KAAK,KAAK;AAC3C,YAAM,OAAO,KAAK;AAClB,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,gBAAc;AAAA,UACd,UAAU,KAAK;AAAA,UACf,SAAS,MAAM,OAAO,IAAI;AAAA,UAC1B,WAAW,UAAU,QAAQ,KAAK,IAAI;AAAA,UAErC;AAAA,oBAAQ,oBAAC,QAAK,eAAY,QAAO,WAAU,YAAW;AAAA,YACtD,KAAK,UAAU,UAAa,oBAAC,UAAK,WAAU,8BAA8B,eAAK,OAAM;AAAA,YACrF,KAAK;AAAA;AAAA;AAAA,QATD,KAAK;AAAA,MAUZ;AAAA,IAEJ,CAAC;AAAA,KACH;AAEJ;","names":[]}
@@ -0,0 +1,58 @@
1
+ import * as React from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ type FormFieldColSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
5
+ /**
6
+ * Thuộc tính a11y mà `FormField` tiêm xuống control con.
7
+ *
8
+ * Trải nguyên cụm này vào `<Input>`/`<Combobox>`/`<Select>` là control tự có id khớp nhãn,
9
+ * 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.
10
+ */
11
+ interface FormFieldControlProps {
12
+ id: string;
13
+ "aria-invalid"?: true;
14
+ "aria-required"?: true;
15
+ "aria-describedby"?: string;
16
+ }
17
+ interface FormFieldProps {
18
+ label?: ReactNode;
19
+ /** Đánh dấu bắt buộc: vẽ dấu * và phát `aria-required` xuống control */
20
+ required?: boolean;
21
+ /** Mô tả/gợi ý dưới trường — bị ẩn khi có lỗi để không đọc chồng lời */
22
+ description?: ReactNode;
23
+ /** Thông báo lỗi; có giá trị là trường vào trạng thái invalid */
24
+ error?: ReactNode;
25
+ /** id của control — bỏ trống thì tự sinh bằng useId */
26
+ htmlFor?: string;
27
+ /** Số cột chiếm trong lưới 12 cột của `FormSection` (mặc định 12 = cả hàng) */
28
+ colSpan?: FormFieldColSpan;
29
+ orientation?: "vertical" | "horizontal" | "responsive";
30
+ className?: string;
31
+ /**
32
+ * Control của trường. Dạng hàm sẽ nhận `FormFieldControlProps` để trải xuống —
33
+ * dạng node thì app tự lo id/aria (dùng khi control đã tự quản, vd Checkbox có nhãn riêng).
34
+ */
35
+ children: ReactNode | ((control: FormFieldControlProps) => ReactNode);
36
+ }
37
+ /**
38
+ * Một trường form: nhãn + control + mô tả/lỗi, đặt trong lưới 12 cột của `FormSection`.
39
+ *
40
+ * KHÔNG tự vẽ input. Nhãn và lỗi uỷ quyền xuống primitive `Field`, còn control do app chọn —
41
+ * nếu composite tự dựng input thì mỗi kiểu control mới (combobox, date range, editor) lại phải
42
+ * 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.
43
+ */
44
+ declare function FormField({ label, required, description, error, htmlFor, colSpan, orientation, className, children }: FormFieldProps): React.JSX.Element;
45
+ /**
46
+ * Sentinel cho `Select` của Radix.
47
+ *
48
+ * 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
49
+ * "— 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
50
+ * nơi tự đặt một sentinel rồi lọt xuống payload gửi server.
51
+ */
52
+ declare const SELECT_EMPTY_VALUE = "__empty__";
53
+ /** Giá trị model ("" = chưa chọn) → giá trị cho Radix Select. */
54
+ declare function toSelectValue(value: string | null | undefined): string;
55
+ /** Giá trị từ Radix Select → giá trị model ("" khi người dùng chọn mục rỗng). */
56
+ declare function fromSelectValue(value: string): string;
57
+
58
+ export { FormField, type FormFieldColSpan, type FormFieldControlProps, type FormFieldProps, SELECT_EMPTY_VALUE, fromSelectValue, toSelectValue };