@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,76 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cva } from "class-variance-authority";
3
+ import { ArrowDownRight, ArrowUpRight, Minus } from "lucide-react";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { Skeleton } from "../ui/skeleton.js";
6
+ const statCardIconVariants = cva("flex size-9 shrink-0 items-center justify-center rounded-md", {
7
+ variants: {
8
+ tone: {
9
+ default: "bg-muted text-muted-foreground",
10
+ primary: "bg-gold-100 text-gold-600 dark:bg-gold-500/25 dark:text-gold-100",
11
+ success: "bg-success/15 text-success",
12
+ warning: "bg-warning/20 text-gold-500 dark:text-warning",
13
+ destructive: "bg-destructive/15 text-destructive",
14
+ info: "bg-info/15 text-info",
15
+ // text-foreground (không phải text-app-accent): accent mặc định là gold #E8B824, đặt làm
16
+ // màu icon trên nền sáng chỉ được ~1.9:1 — nhìn như bị mờ.
17
+ app: "bg-app-accent/10 text-foreground"
18
+ }
19
+ },
20
+ defaultVariants: {
21
+ tone: "default"
22
+ }
23
+ });
24
+ const deltaIcons = {
25
+ up: ArrowUpRight,
26
+ down: ArrowDownRight,
27
+ flat: Minus
28
+ };
29
+ function deltaToneClass({ direction, polarity = "positive-up" }) {
30
+ if (direction === "flat" || polarity === "neutral") return "text-muted-foreground";
31
+ const isGood = polarity === "positive-up" ? direction === "up" : direction === "down";
32
+ return isGood ? "text-success" : "text-destructive";
33
+ }
34
+ function StatCard({
35
+ label,
36
+ value,
37
+ icon: Icon,
38
+ tone = "default",
39
+ delta,
40
+ sparkline,
41
+ footer,
42
+ loading = false,
43
+ className,
44
+ ...props
45
+ }) {
46
+ const DeltaIcon = delta ? deltaIcons[delta.direction] : void 0;
47
+ return /* @__PURE__ */ jsxs(
48
+ "div",
49
+ {
50
+ "data-slot": "stat-card",
51
+ className: cn("rounded-xl border border-border bg-card p-4 shadow-card", className),
52
+ ...props,
53
+ children: [
54
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
55
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
56
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-muted-foreground", children: label }),
57
+ loading ? /* @__PURE__ */ jsx(Skeleton, { className: "mt-2 h-7 w-24" }) : /* @__PURE__ */ jsx("p", { className: "mt-1 text-2xl font-semibold tabular-nums leading-tight text-foreground", children: value }),
58
+ delta && !loading && DeltaIcon && /* @__PURE__ */ jsxs("p", { className: cn("mt-1 flex items-center gap-1 text-xs font-medium", deltaToneClass(delta)), children: [
59
+ /* @__PURE__ */ jsx(DeltaIcon, { "aria-hidden": "true", className: "size-3.5 shrink-0" }),
60
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: delta.value }),
61
+ delta.label && /* @__PURE__ */ jsx("span", { className: "font-normal text-muted-foreground", children: delta.label })
62
+ ] })
63
+ ] }),
64
+ Icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: statCardIconVariants({ tone }), children: /* @__PURE__ */ jsx(Icon, { className: "size-4.5" }) })
65
+ ] }),
66
+ sparkline && /* @__PURE__ */ jsx("div", { className: "mt-3 h-10", children: sparkline }),
67
+ footer && /* @__PURE__ */ jsx("div", { className: "mt-3 border-t border-border pt-2 text-xs text-muted-foreground", children: footer })
68
+ ]
69
+ }
70
+ );
71
+ }
72
+ export {
73
+ StatCard,
74
+ statCardIconVariants
75
+ };
76
+ //# sourceMappingURL=stat-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/stat-card.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport { ArrowDownRight, ArrowUpRight, Minus } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Skeleton } from \"../ui/skeleton.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\n/** Ô icon của StatCard, tô theo tone token (không hex). */\nexport const statCardIconVariants = cva(\"flex size-9 shrink-0 items-center justify-center rounded-md\", {\n variants: {\n tone: {\n default: \"bg-muted text-muted-foreground\",\n primary: \"bg-gold-100 text-gold-600 dark:bg-gold-500/25 dark:text-gold-100\",\n success: \"bg-success/15 text-success\",\n warning: \"bg-warning/20 text-gold-500 dark:text-warning\",\n destructive: \"bg-destructive/15 text-destructive\",\n info: \"bg-info/15 text-info\",\n // text-foreground (không phải text-app-accent): accent mặc định là gold #E8B824, đặt làm\n // màu icon trên nền sáng chỉ được ~1.9:1 — nhìn như bị mờ.\n app: \"bg-app-accent/10 text-foreground\"\n }\n },\n defaultVariants: {\n tone: \"default\"\n }\n});\n\nexport type StatCardTone = NonNullable<VariantProps<typeof statCardIconVariants>[\"tone\"]>;\n\nexport type StatCardDeltaDirection = \"up\" | \"down\" | \"flat\";\n\nexport interface StatCardDelta {\n /** Nội dung hiển thị, vd \"+12,5%\" hoặc \"3 việc\" */\n value: ReactNode;\n direction: StatCardDeltaDirection;\n /**\n * Tăng là TỐT hay XẤU — mặc định \"positive-up\" (tăng = tốt).\n *\n * Cần thiết vì có chỉ số ngược chiều: số ticket quá hạn tăng là xấu, tỷ lệ rời bỏ giảm là tốt.\n * Tô màu theo hướng mũi tên một cách máy móc sẽ nói dối người đọc.\n */\n polarity?: \"positive-up\" | \"positive-down\" | \"neutral\";\n /** Chú thích sau con số, vd \"so với tuần trước\" */\n label?: ReactNode;\n}\n\nexport interface StatCardProps extends Omit<ComponentProps<\"div\">, \"title\"> {\n /** Nhãn chỉ số, vd \"Việc đang mở\" */\n label: ReactNode;\n value: ReactNode;\n icon?: IconComponent;\n tone?: StatCardTone;\n delta?: StatCardDelta;\n /** Slot biểu đồ mini (sparkline) — package không ép thư viện biểu đồ nào */\n sparkline?: ReactNode;\n /** Dòng chú thích dưới cùng */\n footer?: ReactNode;\n /** Đang tải → khung xương thay cho số liệu */\n loading?: boolean;\n}\n\nconst deltaIcons: Record<StatCardDeltaDirection, IconComponent> = {\n up: ArrowUpRight,\n down: ArrowDownRight,\n flat: Minus\n};\n\n/** Màu của delta = hướng × cực tính, không phải chỉ hướng. */\nfunction deltaToneClass({ direction, polarity = \"positive-up\" }: StatCardDelta): string {\n if (direction === \"flat\" || polarity === \"neutral\") return \"text-muted-foreground\";\n const isGood = polarity === \"positive-up\" ? direction === \"up\" : direction === \"down\";\n return isGood ? \"text-success\" : \"text-destructive\";\n}\n\n/**\n * Thẻ KPI: nhãn + số liệu lớn + biến động + sparkline tuỳ chọn.\n *\n * Số liệu dùng `tabular-nums` để cột số không nhảy khi cập nhật realtime. Tĩnh → RSC-safe.\n */\nexport function StatCard({\n label,\n value,\n icon: Icon,\n tone = \"default\",\n delta,\n sparkline,\n footer,\n loading = false,\n className,\n ...props\n}: StatCardProps) {\n const DeltaIcon = delta ? deltaIcons[delta.direction] : undefined;\n\n return (\n <div\n data-slot=\"stat-card\"\n className={cn(\"rounded-xl border border-border bg-card p-4 shadow-card\", className)}\n {...props}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm text-muted-foreground\">{label}</p>\n {loading ? (\n <Skeleton className=\"mt-2 h-7 w-24\" />\n ) : (\n <p className=\"mt-1 text-2xl font-semibold tabular-nums leading-tight text-foreground\">{value}</p>\n )}\n {delta && !loading && DeltaIcon && (\n <p className={cn(\"mt-1 flex items-center gap-1 text-xs font-medium\", deltaToneClass(delta))}>\n <DeltaIcon aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n <span className=\"tabular-nums\">{delta.value}</span>\n {delta.label && <span className=\"font-normal text-muted-foreground\">{delta.label}</span>}\n </p>\n )}\n </div>\n {Icon && (\n <span aria-hidden=\"true\" className={statCardIconVariants({ tone })}>\n <Icon className=\"size-4.5\" />\n </span>\n )}\n </div>\n\n {sparkline && <div className=\"mt-3 h-10\">{sparkline}</div>}\n {footer && <div className=\"mt-3 border-t border-border pt-2 text-xs text-muted-foreground\">{footer}</div>}\n </div>\n );\n}\n"],"mappings":"AAqGU,cAOE,YAPF;AArGV,SAAS,WAA8B;AACvC,SAAS,gBAAgB,cAAc,aAAa;AAEpD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AAIlB,MAAM,uBAAuB,IAAI,+DAA+D;AAAA,EACrG,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM;AAAA;AAAA;AAAA,MAGN,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,MAAM;AAAA,EACR;AACF,CAAC;AAoCD,MAAM,aAA4D;AAAA,EAChE,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,MAAM;AACR;AAGA,SAAS,eAAe,EAAE,WAAW,WAAW,cAAc,GAA0B;AACtF,MAAI,cAAc,UAAU,aAAa,UAAW,QAAO;AAC3D,QAAM,SAAS,aAAa,gBAAgB,cAAc,OAAO,cAAc;AAC/E,SAAO,SAAS,iBAAiB;AACnC;AAOO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,YAAY,QAAQ,WAAW,MAAM,SAAS,IAAI;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,2DAA2D,SAAS;AAAA,MACjF,GAAG;AAAA,MAEJ;AAAA,6BAAC,SAAI,WAAU,0CACb;AAAA,+BAAC,SAAI,WAAU,kBACb;AAAA,gCAAC,OAAE,WAAU,0CAA0C,iBAAM;AAAA,YAC5D,UACC,oBAAC,YAAS,WAAU,iBAAgB,IAEpC,oBAAC,OAAE,WAAU,0EAA0E,iBAAM;AAAA,YAE9F,SAAS,CAAC,WAAW,aACpB,qBAAC,OAAE,WAAW,GAAG,oDAAoD,eAAe,KAAK,CAAC,GACxF;AAAA,kCAAC,aAAU,eAAY,QAAO,WAAU,qBAAoB;AAAA,cAC5D,oBAAC,UAAK,WAAU,gBAAgB,gBAAM,OAAM;AAAA,cAC3C,MAAM,SAAS,oBAAC,UAAK,WAAU,qCAAqC,gBAAM,OAAM;AAAA,eACnF;AAAA,aAEJ;AAAA,UACC,QACC,oBAAC,UAAK,eAAY,QAAO,WAAW,qBAAqB,EAAE,KAAK,CAAC,GAC/D,8BAAC,QAAK,WAAU,YAAW,GAC7B;AAAA,WAEJ;AAAA,QAEC,aAAa,oBAAC,SAAI,WAAU,aAAa,qBAAU;AAAA,QACnD,UAAU,oBAAC,SAAI,WAAU,kEAAkE,kBAAO;AAAA;AAAA;AAAA,EACrG;AAEJ;","names":[]}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Cơ chế StatusConfig generic — app khai map "trạng thái → lớp màu TOKEN".
3
+ *
4
+ * Vì sao generic thay vì liệt kê sẵn trạng thái: mỗi app Comitor có bộ trạng thái riêng
5
+ * (Tasks: todo/doing/done; CRM: lead/qualified/won/lost; HR: nộp/duyệt/từ chối). Package
6
+ * chỉ cấp KIỂU và bảng tone, còn nhãn tiếng Việt là chuyện của app.
7
+ *
8
+ * Bắt buộc dùng class token (`bg-success/15 text-success`), KHÔNG hex — hex sẽ chết ở dark mode.
9
+ *
10
+ * @example
11
+ * const TASK_STATUSES: StatusConfig<TaskStatus>[] = [
12
+ * { value: "todo", label: "Cần làm", shortLabel: "CL", ...STATUS_TONES.neutral },
13
+ * { value: "doing", label: "Đang làm", shortLabel: "ĐL", ...STATUS_TONES.info },
14
+ * { value: "done", label: "Hoàn thành", shortLabel: "HT", ...STATUS_TONES.success }
15
+ * ];
16
+ */
17
+ interface StatusTone {
18
+ /** Lớp nền tint — token, vd "bg-success/15" */
19
+ bgColor: string;
20
+ /** Lớp chữ — token, vd "text-success" */
21
+ textColor: string;
22
+ /** Lớp viền (tuỳ chọn) — dùng cho biến thể outline */
23
+ borderColor?: string;
24
+ }
25
+ interface StatusConfig<T extends string = string> extends StatusTone {
26
+ value: T;
27
+ label: string;
28
+ /** Nhãn rút gọn cho bảng/chip hẹp (vd "HT" thay "Hoàn thành") */
29
+ shortLabel?: string;
30
+ /** Ưu tiên sắp xếp (nhỏ = lên trước) */
31
+ priority?: number;
32
+ /** Tint nền dòng bảng khi trạng thái cần gây chú ý (vd quá hạn) */
33
+ rowClassName?: string;
34
+ }
35
+ /**
36
+ * Bảng tone dựng sẵn theo token semantic — trải (`...STATUS_TONES.success`) vào StatusConfig.
37
+ *
38
+ * Mọi chuỗi ở đây là class TĨNH để Tailwind quét được từ `dist`; tuyệt đối không sinh động
39
+ * kiểu `bg-${tone}/15` (class động bị loại khỏi bundle của app).
40
+ */
41
+ declare const STATUS_TONES: {
42
+ readonly neutral: {
43
+ readonly bgColor: "bg-muted";
44
+ readonly textColor: "text-muted-foreground";
45
+ readonly borderColor: "border-border";
46
+ };
47
+ readonly primary: {
48
+ readonly bgColor: "bg-gold-100 dark:bg-gold-500/25";
49
+ readonly textColor: "text-gold-600 dark:text-gold-100";
50
+ readonly borderColor: "border-gold-300";
51
+ };
52
+ readonly success: {
53
+ readonly bgColor: "bg-success/15";
54
+ readonly textColor: "text-success";
55
+ readonly borderColor: "border-success/40";
56
+ };
57
+ readonly warning: {
58
+ readonly bgColor: "bg-warning/20";
59
+ readonly textColor: "text-gold-500 dark:text-warning";
60
+ readonly borderColor: "border-warning/40";
61
+ };
62
+ readonly destructive: {
63
+ readonly bgColor: "bg-destructive/15";
64
+ readonly textColor: "text-destructive";
65
+ readonly borderColor: "border-destructive/40";
66
+ };
67
+ readonly info: {
68
+ readonly bgColor: "bg-info/15";
69
+ readonly textColor: "text-info";
70
+ readonly borderColor: "border-info/40";
71
+ };
72
+ };
73
+ type StatusToneName = keyof typeof STATUS_TONES;
74
+ /** Tìm config theo value trong danh sách app cung cấp. */
75
+ declare function getStatusConfig<T extends string>(configs: readonly StatusConfig<T>[], value: T | null | undefined): StatusConfig<T> | undefined;
76
+ /** Lớp màu (nền tint + chữ) của một trạng thái; không tìm thấy → chuỗi rỗng. */
77
+ declare function getStatusClasses<T extends string>(configs: readonly StatusConfig<T>[], value: T | null | undefined): string;
78
+ /** Nhãn hiển thị của trạng thái; không tìm thấy → trả về chính `value` để không mất dữ liệu. */
79
+ declare function getStatusLabel<T extends string>(configs: readonly StatusConfig<T>[], value: T | null | undefined, options?: {
80
+ short?: boolean;
81
+ }): string;
82
+ /** Sắp xếp theo `priority` (thiếu priority thì xuống cuối, giữ thứ tự khai báo). */
83
+ declare function sortByStatusPriority<T extends string>(configs: readonly StatusConfig<T>[]): StatusConfig<T>[];
84
+
85
+ export { STATUS_TONES, type StatusConfig, type StatusTone, type StatusToneName, getStatusClasses, getStatusConfig, getStatusLabel, sortByStatusPriority };
@@ -0,0 +1,41 @@
1
+ const STATUS_TONES = {
2
+ neutral: { bgColor: "bg-muted", textColor: "text-muted-foreground", borderColor: "border-border" },
3
+ primary: {
4
+ bgColor: "bg-gold-100 dark:bg-gold-500/25",
5
+ textColor: "text-gold-600 dark:text-gold-100",
6
+ borderColor: "border-gold-300"
7
+ },
8
+ success: { bgColor: "bg-success/15", textColor: "text-success", borderColor: "border-success/40" },
9
+ warning: { bgColor: "bg-warning/20", textColor: "text-gold-500 dark:text-warning", borderColor: "border-warning/40" },
10
+ destructive: {
11
+ bgColor: "bg-destructive/15",
12
+ textColor: "text-destructive",
13
+ borderColor: "border-destructive/40"
14
+ },
15
+ info: { bgColor: "bg-info/15", textColor: "text-info", borderColor: "border-info/40" }
16
+ };
17
+ function getStatusConfig(configs, value) {
18
+ if (value == null) return void 0;
19
+ return configs.find((config) => config.value === value);
20
+ }
21
+ function getStatusClasses(configs, value) {
22
+ const config = getStatusConfig(configs, value);
23
+ if (!config) return "";
24
+ return `${config.bgColor} ${config.textColor}`;
25
+ }
26
+ function getStatusLabel(configs, value, options = {}) {
27
+ const config = getStatusConfig(configs, value);
28
+ if (!config) return value ?? "";
29
+ return options.short ? config.shortLabel ?? config.label : config.label;
30
+ }
31
+ function sortByStatusPriority(configs) {
32
+ return [...configs].sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));
33
+ }
34
+ export {
35
+ STATUS_TONES,
36
+ getStatusClasses,
37
+ getStatusConfig,
38
+ getStatusLabel,
39
+ sortByStatusPriority
40
+ };
41
+ //# sourceMappingURL=status-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/status-config.ts"],"sourcesContent":["/**\n * Cơ chế StatusConfig generic — app khai map \"trạng thái → lớp màu TOKEN\".\n *\n * Vì sao generic thay vì liệt kê sẵn trạng thái: mỗi app Comitor có bộ trạng thái riêng\n * (Tasks: todo/doing/done; CRM: lead/qualified/won/lost; HR: nộp/duyệt/từ chối). Package\n * chỉ cấp KIỂU và bảng tone, còn nhãn tiếng Việt là chuyện của app.\n *\n * Bắt buộc dùng class token (`bg-success/15 text-success`), KHÔNG hex — hex sẽ chết ở dark mode.\n *\n * @example\n * const TASK_STATUSES: StatusConfig<TaskStatus>[] = [\n * { value: \"todo\", label: \"Cần làm\", shortLabel: \"CL\", ...STATUS_TONES.neutral },\n * { value: \"doing\", label: \"Đang làm\", shortLabel: \"ĐL\", ...STATUS_TONES.info },\n * { value: \"done\", label: \"Hoàn thành\", shortLabel: \"HT\", ...STATUS_TONES.success }\n * ];\n */\n\nexport interface StatusTone {\n /** Lớp nền tint — token, vd \"bg-success/15\" */\n bgColor: string;\n /** Lớp chữ — token, vd \"text-success\" */\n textColor: string;\n /** Lớp viền (tuỳ chọn) — dùng cho biến thể outline */\n borderColor?: string;\n}\n\nexport interface StatusConfig<T extends string = string> extends StatusTone {\n value: T;\n label: string;\n /** Nhãn rút gọn cho bảng/chip hẹp (vd \"HT\" thay \"Hoàn thành\") */\n shortLabel?: string;\n /** Ưu tiên sắp xếp (nhỏ = lên trước) */\n priority?: number;\n /** Tint nền dòng bảng khi trạng thái cần gây chú ý (vd quá hạn) */\n rowClassName?: string;\n}\n\n/**\n * Bảng tone dựng sẵn theo token semantic — trải (`...STATUS_TONES.success`) vào StatusConfig.\n *\n * Mọi chuỗi ở đây là class TĨNH để Tailwind quét được từ `dist`; tuyệt đối không sinh động\n * kiểu `bg-${tone}/15` (class động bị loại khỏi bundle của app).\n */\nexport const STATUS_TONES = {\n neutral: { bgColor: \"bg-muted\", textColor: \"text-muted-foreground\", borderColor: \"border-border\" },\n primary: {\n bgColor: \"bg-gold-100 dark:bg-gold-500/25\",\n textColor: \"text-gold-600 dark:text-gold-100\",\n borderColor: \"border-gold-300\"\n },\n success: { bgColor: \"bg-success/15\", textColor: \"text-success\", borderColor: \"border-success/40\" },\n warning: { bgColor: \"bg-warning/20\", textColor: \"text-gold-500 dark:text-warning\", borderColor: \"border-warning/40\" },\n destructive: {\n bgColor: \"bg-destructive/15\",\n textColor: \"text-destructive\",\n borderColor: \"border-destructive/40\"\n },\n info: { bgColor: \"bg-info/15\", textColor: \"text-info\", borderColor: \"border-info/40\" }\n} as const satisfies Record<string, StatusTone>;\n\nexport type StatusToneName = keyof typeof STATUS_TONES;\n\n/** Tìm config theo value trong danh sách app cung cấp. */\nexport function getStatusConfig<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): StatusConfig<T> | undefined {\n if (value == null) return undefined;\n return configs.find((config) => config.value === value);\n}\n\n/** Lớp màu (nền tint + chữ) của một trạng thái; không tìm thấy → chuỗi rỗng. */\nexport function getStatusClasses<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return \"\";\n return `${config.bgColor} ${config.textColor}`;\n}\n\n/** Nhãn hiển thị của trạng thái; không tìm thấy → trả về chính `value` để không mất dữ liệu. */\nexport function getStatusLabel<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined,\n options: { short?: boolean } = {}\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return value ?? \"\";\n return options.short ? (config.shortLabel ?? config.label) : config.label;\n}\n\n/** Sắp xếp theo `priority` (thiếu priority thì xuống cuối, giữ thứ tự khai báo). */\nexport function sortByStatusPriority<T extends string>(configs: readonly StatusConfig<T>[]): StatusConfig<T>[] {\n return [...configs].sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));\n}\n"],"mappings":"AA2CO,MAAM,eAAe;AAAA,EAC1B,SAAS,EAAE,SAAS,YAAY,WAAW,yBAAyB,aAAa,gBAAgB;AAAA,EACjG,SAAS;AAAA,IACP,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,SAAS,EAAE,SAAS,iBAAiB,WAAW,gBAAgB,aAAa,oBAAoB;AAAA,EACjG,SAAS,EAAE,SAAS,iBAAiB,WAAW,mCAAmC,aAAa,oBAAoB;AAAA,EACpH,aAAa;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,MAAM,EAAE,SAAS,cAAc,WAAW,aAAa,aAAa,iBAAiB;AACvF;AAKO,SAAS,gBACd,SACA,OAC6B;AAC7B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACxD;AAGO,SAAS,iBACd,SACA,OACQ;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,GAAG,OAAO,OAAO,IAAI,OAAO,SAAS;AAC9C;AAGO,SAAS,eACd,SACA,OACA,UAA+B,CAAC,GACxB;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO,SAAS;AAC7B,SAAO,QAAQ,QAAS,OAAO,cAAc,OAAO,QAAS,OAAO;AACtE;AAGO,SAAS,qBAAuC,SAAwD;AAC7G,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,qBAAqB,EAAE,YAAY,OAAO,iBAAiB;AACtH;","names":[]}
@@ -0,0 +1,26 @@
1
+ import * as React from 'react';
2
+ import { ComponentProps } from 'react';
3
+ import { StatusConfig } from './status-config.js';
4
+
5
+ interface StatusPillProps<T extends string = string> extends Omit<ComponentProps<"span">, "children"> {
6
+ /** Toàn bộ bảng trạng thái của app */
7
+ configs: readonly StatusConfig<T>[];
8
+ /** Trạng thái cần hiển thị */
9
+ value: T | null | undefined;
10
+ /** Dùng shortLabel nếu có (bảng hẹp) */
11
+ short?: boolean;
12
+ /** "soft" = nền tint (mặc định), "outline" = chỉ viền + chữ, "dot" = chấm màu + chữ thường */
13
+ variant?: StatusPillVariant;
14
+ /** Nhãn thay thế khi `value` không có trong `configs` */
15
+ fallbackLabel?: string;
16
+ }
17
+ type StatusPillVariant = "soft" | "outline" | "dot";
18
+ /**
19
+ * Chip trạng thái tô theo StatusConfig của app (màu lấy từ token, không hex).
20
+ *
21
+ * Tĩnh hoàn toàn → dùng được trong React Server Component (bảng danh sách render phía server).
22
+ * Trạng thái lạ (dữ liệu cũ, enum mới ở backend) KHÔNG bị nuốt: vẫn hiện nhãn ở tone neutral.
23
+ */
24
+ declare function StatusPill<T extends string = string>({ configs, value, short, variant, fallbackLabel, className, ...props }: StatusPillProps<T>): React.JSX.Element;
25
+
26
+ export { StatusPill, type StatusPillProps, type StatusPillVariant };
@@ -0,0 +1,59 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../../lib/cn.js";
3
+ import { getStatusConfig } from "./status-config.js";
4
+ function StatusPill({
5
+ configs,
6
+ value,
7
+ short = false,
8
+ variant = "soft",
9
+ fallbackLabel,
10
+ className,
11
+ ...props
12
+ }) {
13
+ const config = getStatusConfig(configs, value);
14
+ const label = config ? short ? config.shortLabel ?? config.label : config.label : fallbackLabel ?? value ?? "\u2014";
15
+ const base = "inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-2 py-0.5 text-xs font-medium";
16
+ if (variant === "dot") {
17
+ return /* @__PURE__ */ jsxs("span", { "data-slot": "status-pill", className: cn(base, "px-0 text-foreground", className), ...props, children: [
18
+ /* @__PURE__ */ jsx(
19
+ "span",
20
+ {
21
+ "aria-hidden": "true",
22
+ className: cn("size-1.5 shrink-0 rounded-full", config ? config.textColor : "text-muted-foreground"),
23
+ style: { backgroundColor: "currentColor" }
24
+ }
25
+ ),
26
+ label
27
+ ] });
28
+ }
29
+ if (variant === "outline") {
30
+ return /* @__PURE__ */ jsx(
31
+ "span",
32
+ {
33
+ "data-slot": "status-pill",
34
+ className: cn(
35
+ base,
36
+ "border bg-transparent",
37
+ config?.borderColor ?? "border-border",
38
+ config?.textColor ?? "text-muted-foreground",
39
+ className
40
+ ),
41
+ ...props,
42
+ children: label
43
+ }
44
+ );
45
+ }
46
+ return /* @__PURE__ */ jsx(
47
+ "span",
48
+ {
49
+ "data-slot": "status-pill",
50
+ className: cn(base, config?.bgColor ?? "bg-muted", config?.textColor ?? "text-muted-foreground", className),
51
+ ...props,
52
+ children: label
53
+ }
54
+ );
55
+ }
56
+ export {
57
+ StatusPill
58
+ };
59
+ //# sourceMappingURL=status-pill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/status-pill.tsx"],"sourcesContent":["import type { ComponentProps } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport type { StatusConfig } from \"./status-config.js\";\nimport { getStatusConfig } from \"./status-config.js\";\n\nexport interface StatusPillProps<T extends string = string> extends Omit<ComponentProps<\"span\">, \"children\"> {\n /** Toàn bộ bảng trạng thái của app */\n configs: readonly StatusConfig<T>[];\n /** Trạng thái cần hiển thị */\n value: T | null | undefined;\n /** Dùng shortLabel nếu có (bảng hẹp) */\n short?: boolean;\n /** \"soft\" = nền tint (mặc định), \"outline\" = chỉ viền + chữ, \"dot\" = chấm màu + chữ thường */\n variant?: StatusPillVariant;\n /** Nhãn thay thế khi `value` không có trong `configs` */\n fallbackLabel?: string;\n}\n\nexport type StatusPillVariant = \"soft\" | \"outline\" | \"dot\";\n\n/**\n * Chip trạng thái tô theo StatusConfig của app (màu lấy từ token, không hex).\n *\n * Tĩnh hoàn toàn → dùng được trong React Server Component (bảng danh sách render phía server).\n * Trạng thái lạ (dữ liệu cũ, enum mới ở backend) KHÔNG bị nuốt: vẫn hiện nhãn ở tone neutral.\n */\nexport function StatusPill<T extends string = string>({\n configs,\n value,\n short = false,\n variant = \"soft\",\n fallbackLabel,\n className,\n ...props\n}: StatusPillProps<T>) {\n const config = getStatusConfig(configs, value);\n const label = config ? (short ? (config.shortLabel ?? config.label) : config.label) : (fallbackLabel ?? value ?? \"—\");\n\n const base = \"inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-2 py-0.5 text-xs font-medium\";\n\n if (variant === \"dot\") {\n return (\n <span data-slot=\"status-pill\" className={cn(base, \"px-0 text-foreground\", className)} {...props}>\n <span\n aria-hidden=\"true\"\n className={cn(\"size-1.5 shrink-0 rounded-full\", config ? config.textColor : \"text-muted-foreground\")}\n style={{ backgroundColor: \"currentColor\" }}\n />\n {label}\n </span>\n );\n }\n\n if (variant === \"outline\") {\n return (\n <span\n data-slot=\"status-pill\"\n className={cn(\n base,\n \"border bg-transparent\",\n config?.borderColor ?? \"border-border\",\n config?.textColor ?? \"text-muted-foreground\",\n className\n )}\n {...props}\n >\n {label}\n </span>\n );\n }\n\n return (\n <span\n data-slot=\"status-pill\"\n className={cn(base, config?.bgColor ?? \"bg-muted\", config?.textColor ?? \"text-muted-foreground\", className)}\n {...props}\n >\n {label}\n </span>\n );\n}\n"],"mappings":"AA0CM,SACE,KADF;AAzCN,SAAS,UAAU;AAEnB,SAAS,uBAAuB;AAuBzB,SAAS,WAAsC;AAAA,EACpD;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuB;AACrB,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,QAAM,QAAQ,SAAU,QAAS,OAAO,cAAc,OAAO,QAAS,OAAO,QAAU,iBAAiB,SAAS;AAEjH,QAAM,OAAO;AAEb,MAAI,YAAY,OAAO;AACrB,WACE,qBAAC,UAAK,aAAU,eAAc,WAAW,GAAG,MAAM,wBAAwB,SAAS,GAAI,GAAG,OACxF;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,eAAY;AAAA,UACZ,WAAW,GAAG,kCAAkC,SAAS,OAAO,YAAY,uBAAuB;AAAA,UACnG,OAAO,EAAE,iBAAiB,eAAe;AAAA;AAAA,MAC3C;AAAA,MACC;AAAA,OACH;AAAA,EAEJ;AAEA,MAAI,YAAY,WAAW;AACzB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,QAAQ,eAAe;AAAA,UACvB,QAAQ,aAAa;AAAA,UACrB;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QAEH;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,MAAM,QAAQ,WAAW,YAAY,QAAQ,aAAa,yBAAyB,SAAS;AAAA,MACzG,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+
3
+ /** Cỡ trang mặc định — chốt ở 3 mức để mọi bảng trong hệ sinh thái giống nhau. */
4
+ declare const DEFAULT_PAGE_SIZE_OPTIONS: readonly [20, 50, 100];
5
+ interface TablePaginationProps {
6
+ /** Trang hiện tại, đếm từ 1 */
7
+ page: number;
8
+ pageSize: number;
9
+ /** Tổng số bản ghi (sau khi lọc) */
10
+ total: number;
11
+ pageSizeOptions?: readonly number[];
12
+ onPageChange: (page: number) => void;
13
+ onPageSizeChange: (pageSize: number) => void;
14
+ /** Đơn vị đếm, vd "việc", "liên hệ" — mặc định "mục" */
15
+ unitLabel?: string;
16
+ /** Ẩn ô chọn số dòng/trang (khi cỡ trang do hệ thống quyết định) */
17
+ hidePageSize?: boolean;
18
+ /** Nhãn vùng phân trang — cần khi một trang có nhiều bảng */
19
+ label?: string;
20
+ className?: string;
21
+ }
22
+ /**
23
+ * Thanh phân trang đi kèm `DataTable`.
24
+ *
25
+ * Tổng số trang tính từ `total`/`pageSize` ngay tại đây thay vì bắt app truyền vào — hai bên
26
+ * tự tính rất dễ lệch nhau một trang ở bản ghi cuối, và người dùng sẽ thấy nút "Trang sau"
27
+ * còn bấm được trong khi bảng đã hết dữ liệu.
28
+ */
29
+ declare function TablePagination({ page, pageSize, total, pageSizeOptions, onPageChange, onPageSizeChange, unitLabel, hidePageSize, label, className }: TablePaginationProps): React.JSX.Element;
30
+
31
+ export { DEFAULT_PAGE_SIZE_OPTIONS, TablePagination, type TablePaginationProps };
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { Button } from "../ui/button.js";
6
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select.js";
7
+ const DEFAULT_PAGE_SIZE_OPTIONS = [20, 50, 100];
8
+ function TablePagination({
9
+ page,
10
+ pageSize,
11
+ total,
12
+ pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
13
+ onPageChange,
14
+ onPageSizeChange,
15
+ unitLabel = "m\u1EE5c",
16
+ hidePageSize = false,
17
+ label = "Ph\xE2n trang",
18
+ className
19
+ }) {
20
+ const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
21
+ const current = Math.min(Math.max(1, page), pageCount);
22
+ const start = total === 0 ? 0 : (current - 1) * pageSize + 1;
23
+ const end = Math.min(current * pageSize, total);
24
+ const isFirst = current <= 1;
25
+ const isLast = current >= pageCount;
26
+ return /* @__PURE__ */ jsxs(
27
+ "nav",
28
+ {
29
+ "aria-label": label,
30
+ "data-slot": "table-pagination",
31
+ className: cn("flex flex-wrap items-center justify-between gap-3 py-2 text-sm", className),
32
+ children: [
33
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
34
+ !hidePageSize && /* @__PURE__ */ jsxs(Fragment, { children: [
35
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "Hi\u1EC3n th\u1ECB" }),
36
+ /* @__PURE__ */ jsxs(Select, { value: String(pageSize), onValueChange: (value) => onPageSizeChange(Number(value)), children: [
37
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-[4.5rem]", "aria-label": `S\u1ED1 ${unitLabel} m\u1ED7i trang`, children: /* @__PURE__ */ jsx(SelectValue, {}) }),
38
+ /* @__PURE__ */ jsx(SelectContent, { children: pageSizeOptions.map((size) => /* @__PURE__ */ jsx(SelectItem, { value: String(size), children: size }, size)) })
39
+ ] })
40
+ ] }),
41
+ /* @__PURE__ */ jsxs("span", { role: "status", className: "tabular-nums", children: [
42
+ start,
43
+ "\u2013",
44
+ end,
45
+ " tr\xEAn t\u1ED5ng ",
46
+ total,
47
+ " ",
48
+ unitLabel
49
+ ] })
50
+ ] }),
51
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
52
+ /* @__PURE__ */ jsxs(
53
+ Button,
54
+ {
55
+ variant: "outline",
56
+ size: "icon-sm",
57
+ disabled: isFirst,
58
+ onClick: () => onPageChange(1),
59
+ className: "hidden sm:inline-flex",
60
+ children: [
61
+ /* @__PURE__ */ jsx(ChevronsLeft, { "aria-hidden": "true", className: "size-4" }),
62
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Trang \u0111\u1EA7u" })
63
+ ]
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "icon-sm", disabled: isFirst, onClick: () => onPageChange(current - 1), children: [
67
+ /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true", className: "size-4" }),
68
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Trang tr\u01B0\u1EDBc" })
69
+ ] }),
70
+ /* @__PURE__ */ jsxs("span", { role: "status", className: "px-2 tabular-nums text-muted-foreground", children: [
71
+ "Trang ",
72
+ current,
73
+ " / ",
74
+ pageCount
75
+ ] }),
76
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "icon-sm", disabled: isLast, onClick: () => onPageChange(current + 1), children: [
77
+ /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true", className: "size-4" }),
78
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Trang sau" })
79
+ ] }),
80
+ /* @__PURE__ */ jsxs(
81
+ Button,
82
+ {
83
+ variant: "outline",
84
+ size: "icon-sm",
85
+ disabled: isLast,
86
+ onClick: () => onPageChange(pageCount),
87
+ className: "hidden sm:inline-flex",
88
+ children: [
89
+ /* @__PURE__ */ jsx(ChevronsRight, { "aria-hidden": "true", className: "size-4" }),
90
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Trang cu\u1ED1i" })
91
+ ]
92
+ }
93
+ )
94
+ ] })
95
+ ]
96
+ }
97
+ );
98
+ }
99
+ export {
100
+ DEFAULT_PAGE_SIZE_OPTIONS,
101
+ TablePagination
102
+ };
103
+ //# sourceMappingURL=table-pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/table-pagination.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from \"lucide-react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"../ui/select.js\";\n\n/** Cỡ trang mặc định — chốt ở 3 mức để mọi bảng trong hệ sinh thái giống nhau. */\nexport const DEFAULT_PAGE_SIZE_OPTIONS = [20, 50, 100] as const;\n\nexport interface TablePaginationProps {\n /** Trang hiện tại, đếm từ 1 */\n page: number;\n pageSize: number;\n /** Tổng số bản ghi (sau khi lọc) */\n total: number;\n pageSizeOptions?: readonly number[];\n onPageChange: (page: number) => void;\n onPageSizeChange: (pageSize: number) => void;\n /** Đơn vị đếm, vd \"việc\", \"liên hệ\" — mặc định \"mục\" */\n unitLabel?: string;\n /** Ẩn ô chọn số dòng/trang (khi cỡ trang do hệ thống quyết định) */\n hidePageSize?: boolean;\n /** Nhãn vùng phân trang — cần khi một trang có nhiều bảng */\n label?: string;\n className?: string;\n}\n\n/**\n * Thanh phân trang đi kèm `DataTable`.\n *\n * Tổng số trang tính từ `total`/`pageSize` ngay tại đây thay vì bắt app truyền vào — hai bên\n * tự tính rất dễ lệch nhau một trang ở bản ghi cuối, và người dùng sẽ thấy nút \"Trang sau\"\n * còn bấm được trong khi bảng đã hết dữ liệu.\n */\nexport function TablePagination({\n page,\n pageSize,\n total,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n onPageChange,\n onPageSizeChange,\n unitLabel = \"mục\",\n hidePageSize = false,\n label = \"Phân trang\",\n className\n}: TablePaginationProps) {\n const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));\n const current = Math.min(Math.max(1, page), pageCount);\n const start = total === 0 ? 0 : (current - 1) * pageSize + 1;\n const end = Math.min(current * pageSize, total);\n const isFirst = current <= 1;\n const isLast = current >= pageCount;\n\n return (\n <nav\n aria-label={label}\n data-slot=\"table-pagination\"\n className={cn(\"flex flex-wrap items-center justify-between gap-3 py-2 text-sm\", className)}\n >\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n {!hidePageSize && (\n <>\n <span className=\"hidden sm:inline\">Hiển thị</span>\n <Select value={String(pageSize)} onValueChange={(value) => onPageSizeChange(Number(value))}>\n <SelectTrigger className=\"h-8 w-[4.5rem]\" aria-label={`Số ${unitLabel} mỗi trang`}>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {pageSizeOptions.map((size) => (\n <SelectItem key={size} value={String(size)}>\n {size}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </>\n )}\n <span role=\"status\" className=\"tabular-nums\">\n {start}–{end} trên tổng {total} {unitLabel}\n </span>\n </div>\n\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={isFirst}\n onClick={() => onPageChange(1)}\n className=\"hidden sm:inline-flex\"\n >\n <ChevronsLeft aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Trang đầu</span>\n </Button>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isFirst} onClick={() => onPageChange(current - 1)}>\n <ChevronLeft aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Trang trước</span>\n </Button>\n\n <span role=\"status\" className=\"px-2 tabular-nums text-muted-foreground\">\n Trang {current} / {pageCount}\n </span>\n\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isLast} onClick={() => onPageChange(current + 1)}>\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Trang sau</span>\n </Button>\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={isLast}\n onClick={() => onPageChange(pageCount)}\n className=\"hidden sm:inline-flex\"\n >\n <ChevronsRight aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Trang cuối</span>\n </Button>\n </div>\n </nav>\n );\n}\n"],"mappings":";AA8DU,mBACE,KACA,YAFF;AA5DV,SAAS,aAAa,cAAc,cAAc,qBAAqB;AACvE,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,YAAY,eAAe,mBAAmB;AAGvE,MAAM,4BAA4B,CAAC,IAAI,IAAI,GAAG;AA2B9C,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,QAAQ;AAAA,EACR;AACF,GAAyB;AACvB,QAAM,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK,IAAI,GAAG,QAAQ,CAAC,CAAC;AACtE,QAAM,UAAU,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,SAAS;AACrD,QAAM,QAAQ,UAAU,IAAI,KAAK,UAAU,KAAK,WAAW;AAC3D,QAAM,MAAM,KAAK,IAAI,UAAU,UAAU,KAAK;AAC9C,QAAM,UAAU,WAAW;AAC3B,QAAM,SAAS,WAAW;AAE1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY;AAAA,MACZ,aAAU;AAAA,MACV,WAAW,GAAG,kEAAkE,SAAS;AAAA,MAEzF;AAAA,6BAAC,SAAI,WAAU,iDACZ;AAAA,WAAC,gBACA,iCACE;AAAA,gCAAC,UAAK,WAAU,oBAAmB,gCAAQ;AAAA,YAC3C,qBAAC,UAAO,OAAO,OAAO,QAAQ,GAAG,eAAe,CAAC,UAAU,iBAAiB,OAAO,KAAK,CAAC,GACvF;AAAA,kCAAC,iBAAc,WAAU,kBAAiB,cAAY,WAAM,SAAS,mBACnE,8BAAC,eAAY,GACf;AAAA,cACA,oBAAC,iBACE,0BAAgB,IAAI,CAAC,SACpB,oBAAC,cAAsB,OAAO,OAAO,IAAI,GACtC,kBADc,IAEjB,CACD,GACH;AAAA,eACF;AAAA,aACF;AAAA,UAEF,qBAAC,UAAK,MAAK,UAAS,WAAU,gBAC3B;AAAA;AAAA,YAAM;AAAA,YAAE;AAAA,YAAI;AAAA,YAAY;AAAA,YAAM;AAAA,YAAE;AAAA,aACnC;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,UAAU;AAAA,cACV,SAAS,MAAM,aAAa,CAAC;AAAA,cAC7B,WAAU;AAAA,cAEV;AAAA,oCAAC,gBAAa,eAAY,QAAO,WAAU,UAAS;AAAA,gBACpD,oBAAC,UAAK,WAAU,WAAU,iCAAS;AAAA;AAAA;AAAA,UACrC;AAAA,UACA,qBAAC,UAAO,SAAQ,WAAU,MAAK,WAAU,UAAU,SAAS,SAAS,MAAM,aAAa,UAAU,CAAC,GACjG;AAAA,gCAAC,eAAY,eAAY,QAAO,WAAU,UAAS;AAAA,YACnD,oBAAC,UAAK,WAAU,WAAU,mCAAW;AAAA,aACvC;AAAA,UAEA,qBAAC,UAAK,MAAK,UAAS,WAAU,2CAA0C;AAAA;AAAA,YAC/D;AAAA,YAAQ;AAAA,YAAI;AAAA,aACrB;AAAA,UAEA,qBAAC,UAAO,SAAQ,WAAU,MAAK,WAAU,UAAU,QAAQ,SAAS,MAAM,aAAa,UAAU,CAAC,GAChG;AAAA,gCAAC,gBAAa,eAAY,QAAO,WAAU,UAAS;AAAA,YACpD,oBAAC,UAAK,WAAU,WAAU,uBAAS;AAAA,aACrC;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,UAAU;AAAA,cACV,SAAS,MAAM,aAAa,SAAS;AAAA,cACrC,WAAU;AAAA,cAEV;AAAA,oCAAC,iBAAc,eAAY,QAAO,WAAU,UAAS;AAAA,gBACrD,oBAAC,UAAK,WAAU,WAAU,6BAAU;AAAA;AAAA;AAAA,UACtC;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Tiện ích chuỗi dùng chung cho tầng composite (không phụ thuộc React → dùng được cả 2 phía).
3
+ *
4
+ * Vì sao không nhét vào `lib/cn.ts`: những hàm này phục vụ riêng composite (tìm kiếm không dấu,
5
+ * initials avatar, tô màu deterministic). Tách file để `cn()` giữ đúng một việc.
6
+ */
7
+ /**
8
+ * Bỏ dấu tiếng Việt để so khớp tìm kiếm ("Nguyễn" ⇄ "nguyen").
9
+ *
10
+ * Dùng NFD tách dấu thành ký tự tổ hợp rồi loại dải U+0300–U+036F. `đ/Đ` KHÔNG phải chữ có dấu
11
+ * tổ hợp (là ký tự riêng trong Unicode) nên phải thay tay — thiếu bước này thì gõ "duc" sẽ không
12
+ * ra "Đức".
13
+ */
14
+ declare function removeVietnameseTones(input: string): string;
15
+ /**
16
+ * So khớp không dấu, không phân biệt hoa thường: `needle` có nằm trong `haystack` không.
17
+ */
18
+ declare function matchesSearch(haystack: string, needle: string): boolean;
19
+ /**
20
+ * Initials kiểu Việt Nam: chữ đầu của HỌ + chữ đầu của TÊN ("Nguyễn Đức Thành" → "NT").
21
+ *
22
+ * Người Việt gọi nhau bằng TÊN (từ cuối), nên lấy 2 từ đầu như phương Tây ("NĐ") là sai thói quen.
23
+ * Tên một từ → lấy 1 ký tự. Chuỗi rỗng → "?" (tránh ô avatar trống trơn).
24
+ */
25
+ declare function getVietnameseInitials(name: string): string;
26
+ /**
27
+ * Hash chuỗi → số nguyên không âm (biến thể FNV-1a 32 bit).
28
+ *
29
+ * Mục đích duy nhất: chọn màu/tone deterministic theo tên. KHÔNG dùng cho bảo mật.
30
+ * Chạy trên code point (không phải byte) để tên có dấu vẫn ổn định giữa server và client.
31
+ */
32
+ declare function hashString(input: string): number;
33
+ /** Chọn phần tử deterministic trong mảng theo chuỗi khoá (vd tên người → tone màu). */
34
+ declare function pickByString<T>(items: readonly T[], key: string, fallback: T): T;
35
+ /** Cắt chuỗi kèm dấu "…" — dùng cho tên workspace/task dài trong chip, tooltip. */
36
+ declare function truncate(input: string, maxLength: number): string;
37
+
38
+ export { getVietnameseInitials, hashString, matchesSearch, pickByString, removeVietnameseTones, truncate };
@@ -0,0 +1,40 @@
1
+ function removeVietnameseTones(input) {
2
+ return input.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d").replace(/Đ/g, "D");
3
+ }
4
+ function matchesSearch(haystack, needle) {
5
+ if (!needle) return true;
6
+ return removeVietnameseTones(haystack).toLowerCase().includes(removeVietnameseTones(needle).toLowerCase());
7
+ }
8
+ function getVietnameseInitials(name) {
9
+ const parts = name.trim().split(/\s+/).filter(Boolean);
10
+ if (parts.length === 0) return "?";
11
+ const first = parts[0] ?? "";
12
+ const last = parts[parts.length - 1] ?? "";
13
+ const initials = parts.length === 1 ? first.slice(0, 1) : `${first.slice(0, 1)}${last.slice(0, 1)}`;
14
+ return initials.toLocaleUpperCase("vi-VN");
15
+ }
16
+ function hashString(input) {
17
+ let hash = 2166136261;
18
+ for (let i = 0; i < input.length; i++) {
19
+ hash ^= input.charCodeAt(i);
20
+ hash = Math.imul(hash, 16777619);
21
+ }
22
+ return Math.abs(hash | 0);
23
+ }
24
+ function pickByString(items, key, fallback) {
25
+ if (items.length === 0) return fallback;
26
+ return items[hashString(key) % items.length] ?? fallback;
27
+ }
28
+ function truncate(input, maxLength) {
29
+ if (input.length <= maxLength) return input;
30
+ return `${input.slice(0, Math.max(0, maxLength - 1)).trimEnd()}\u2026`;
31
+ }
32
+ export {
33
+ getVietnameseInitials,
34
+ hashString,
35
+ matchesSearch,
36
+ pickByString,
37
+ removeVietnameseTones,
38
+ truncate
39
+ };
40
+ //# sourceMappingURL=text-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/text-utils.ts"],"sourcesContent":["/**\n * Tiện ích chuỗi dùng chung cho tầng composite (không phụ thuộc React → dùng được cả 2 phía).\n *\n * Vì sao không nhét vào `lib/cn.ts`: những hàm này phục vụ riêng composite (tìm kiếm không dấu,\n * initials avatar, tô màu deterministic). Tách file để `cn()` giữ đúng một việc.\n */\n\n/**\n * Bỏ dấu tiếng Việt để so khớp tìm kiếm (\"Nguyễn\" ⇄ \"nguyen\").\n *\n * Dùng NFD tách dấu thành ký tự tổ hợp rồi loại dải U+0300–U+036F. `đ/Đ` KHÔNG phải chữ có dấu\n * tổ hợp (là ký tự riêng trong Unicode) nên phải thay tay — thiếu bước này thì gõ \"duc\" sẽ không\n * ra \"Đức\".\n */\nexport function removeVietnameseTones(input: string): string {\n return input\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/đ/g, \"d\")\n .replace(/Đ/g, \"D\");\n}\n\n/**\n * So khớp không dấu, không phân biệt hoa thường: `needle` có nằm trong `haystack` không.\n */\nexport function matchesSearch(haystack: string, needle: string): boolean {\n if (!needle) return true;\n return removeVietnameseTones(haystack).toLowerCase().includes(removeVietnameseTones(needle).toLowerCase());\n}\n\n/**\n * Initials kiểu Việt Nam: chữ đầu của HỌ + chữ đầu của TÊN (\"Nguyễn Đức Thành\" → \"NT\").\n *\n * Người Việt gọi nhau bằng TÊN (từ cuối), nên lấy 2 từ đầu như phương Tây (\"NĐ\") là sai thói quen.\n * Tên một từ → lấy 1 ký tự. Chuỗi rỗng → \"?\" (tránh ô avatar trống trơn).\n */\nexport function getVietnameseInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n\n const first = parts[0] ?? \"\";\n const last = parts[parts.length - 1] ?? \"\";\n const initials = parts.length === 1 ? first.slice(0, 1) : `${first.slice(0, 1)}${last.slice(0, 1)}`;\n\n return initials.toLocaleUpperCase(\"vi-VN\");\n}\n\n/**\n * Hash chuỗi → số nguyên không âm (biến thể FNV-1a 32 bit).\n *\n * Mục đích duy nhất: chọn màu/tone deterministic theo tên. KHÔNG dùng cho bảo mật.\n * Chạy trên code point (không phải byte) để tên có dấu vẫn ổn định giữa server và client.\n */\nexport function hashString(input: string): number {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return Math.abs(hash | 0);\n}\n\n/** Chọn phần tử deterministic trong mảng theo chuỗi khoá (vd tên người → tone màu). */\nexport function pickByString<T>(items: readonly T[], key: string, fallback: T): T {\n if (items.length === 0) return fallback;\n return items[hashString(key) % items.length] ?? fallback;\n}\n\n/** Cắt chuỗi kèm dấu \"…\" — dùng cho tên workspace/task dài trong chip, tooltip. */\nexport function truncate(input: string, maxLength: number): string {\n if (input.length <= maxLength) return input;\n return `${input.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;\n}\n"],"mappings":"AAcO,SAAS,sBAAsB,OAAuB;AAC3D,SAAO,MACJ,UAAU,KAAK,EACf,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,MAAM,GAAG,EACjB,QAAQ,MAAM,GAAG;AACtB;AAKO,SAAS,cAAc,UAAkB,QAAyB;AACvE,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,sBAAsB,QAAQ,EAAE,YAAY,EAAE,SAAS,sBAAsB,MAAM,EAAE,YAAY,CAAC;AAC3G;AAQO,SAAS,sBAAsB,MAAsB;AAC1D,QAAM,QAAQ,KAAK,KAAK,EAAE,MAAM,KAAK,EAAE,OAAO,OAAO;AACrD,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,QAAQ,MAAM,CAAC,KAAK;AAC1B,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC,KAAK;AACxC,QAAM,WAAW,MAAM,WAAW,IAAI,MAAM,MAAM,GAAG,CAAC,IAAI,GAAG,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,KAAK,MAAM,GAAG,CAAC,CAAC;AAEjG,SAAO,SAAS,kBAAkB,OAAO;AAC3C;AAQO,SAAS,WAAW,OAAuB;AAChD,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAQ,MAAM,WAAW,CAAC;AAC1B,WAAO,KAAK,KAAK,MAAM,QAAU;AAAA,EACnC;AACA,SAAO,KAAK,IAAI,OAAO,CAAC;AAC1B;AAGO,SAAS,aAAgB,OAAqB,KAAa,UAAgB;AAChF,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO,MAAM,WAAW,GAAG,IAAI,MAAM,MAAM,KAAK;AAClD;AAGO,SAAS,SAAS,OAAe,WAA2B;AACjE,MAAI,MAAM,UAAU,UAAW,QAAO;AACtC,SAAO,GAAG,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,QAAQ,CAAC;AAChE;","names":[]}
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+ import { ReactNode, ComponentProps } from 'react';
3
+ import { Toaster as Toaster$1 } from 'sonner';
4
+
5
+ type ToasterProps = ComponentProps<typeof Toaster$1>;
6
+ type ToastVariant = "default" | "success" | "warning" | "destructive" | "info";
7
+ interface ToastOptions {
8
+ description?: ReactNode;
9
+ variant?: ToastVariant;
10
+ /** ms; mặc định theo sonner (4000). `Number.POSITIVE_INFINITY` = giữ đến khi người dùng đóng. */
11
+ duration?: number;
12
+ /** Nút hành động, vd { label: "Hoàn tác", onClick } */
13
+ action?: {
14
+ label: string;
15
+ onClick: () => void;
16
+ };
17
+ /** id để cập nhật/đóng đúng toast đó (dùng với luồng "đang lưu…" → "đã lưu") */
18
+ id?: string | number;
19
+ }
20
+ /**
21
+ * Vùng hiển thị toast — đặt MỘT LẦN ở gốc app.
22
+ *
23
+ * `theme` mặc định "system" và KHÔNG đọc `next-themes`: entry "." của package phải chạy được ở
24
+ * app không có next-themes. App dùng next-themes hãy truyền xuống, nếu không toast sẽ theo
25
+ * `prefers-color-scheme` của hệ điều hành và lệch với theme người dùng tự chọn:
26
+ *
27
+ * const { resolvedTheme } = useTheme();
28
+ * <Toaster theme={resolvedTheme as "light" | "dark"} />
29
+ */
30
+ declare function Toaster({ theme, position, ...props }: ToasterProps): React.JSX.Element;
31
+ /**
32
+ * Bắn toast theo biến thể của Comitor.
33
+ *
34
+ * Vì sao bọc `sonner` thay vì để app gọi thẳng: tên biến thể ở đây khớp với ngôn ngữ của design
35
+ * system (`destructive`, không phải `error`), và khi cần đổi thư viện toast thì chỉ sửa một file.
36
+ */
37
+ declare function toast(title: ReactNode, options?: ToastOptions): string | number;
38
+ /** Đóng một toast theo id, hoặc đóng tất cả khi không truyền id. */
39
+ declare function dismissToast(id?: string | number): void;
40
+ /**
41
+ * Toast bám theo một Promise: "Đang lưu…" → "Đã lưu" / "Lưu thất bại".
42
+ * Chuỗi mặc định là tiếng Việt, app ghi đè được từng chuỗi.
43
+ */
44
+ declare function toastPromise<T>(promise: Promise<T>, messages: {
45
+ loading?: ReactNode;
46
+ success: ReactNode | ((data: T) => ReactNode);
47
+ error: ReactNode | ((error: unknown) => ReactNode);
48
+ }): (string & {
49
+ unwrap: () => Promise<T>;
50
+ }) | (number & {
51
+ unwrap: () => Promise<T>;
52
+ }) | {
53
+ unwrap: () => Promise<T>;
54
+ };
55
+
56
+ export { type ToastOptions, type ToastVariant, Toaster, type ToasterProps, dismissToast, toast, toastPromise };