@godxjp/ui 5.0.2 → 6.0.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 (298) hide show
  1. package/README.md +101 -142
  2. package/package.json +124 -128
  3. package/scripts/ui-audit.mjs +179 -0
  4. package/src/app/__tests__/app-provider.test.tsx +232 -0
  5. package/src/app/__tests__/date-format-labels.test.ts +36 -0
  6. package/src/app/__tests__/date-formats.test.ts +44 -0
  7. package/src/app/__tests__/timezones.test.ts +65 -0
  8. package/src/app/app-provider.tsx +227 -0
  9. package/src/app/date-format-labels.ts +21 -0
  10. package/src/app/date-formats.ts +30 -0
  11. package/src/app/index.ts +40 -0
  12. package/src/app/locales.ts +32 -0
  13. package/src/app/request-headers.ts +31 -0
  14. package/src/app/storage.ts +44 -0
  15. package/src/app/time-format-labels.ts +19 -0
  16. package/src/app/time-formats.ts +15 -0
  17. package/src/app/timezones.ts +208 -0
  18. package/src/app/types.ts +39 -0
  19. package/src/app/use-formatting.ts +47 -0
  20. package/src/components/__tests__/accessibility-primitives.test.tsx +65 -0
  21. package/src/components/__tests__/docs-parity.test.ts +41 -0
  22. package/src/components/__tests__/shadcn-release-guardrails.test.ts +71 -0
  23. package/src/components/__tests__/theme-axes-integration.test.tsx +242 -0
  24. package/src/components/admin/index.ts +76 -0
  25. package/src/components/data-display/__tests__/card-table.test.tsx +328 -0
  26. package/src/components/data-display/__tests__/data-display.test.tsx +73 -0
  27. package/src/components/data-display/__tests__/data-table.test.tsx +84 -0
  28. package/src/components/data-display/__tests__/popover.test.tsx +92 -0
  29. package/src/components/data-display/__tests__/scroll-area-collapsible.test.tsx +66 -0
  30. package/src/components/data-display/badge.tsx +27 -0
  31. package/src/components/data-display/card.tsx +194 -0
  32. package/src/components/data-display/code-badge.tsx +28 -0
  33. package/src/components/data-display/collapsible.tsx +5 -0
  34. package/src/components/data-display/data-table.tsx +476 -0
  35. package/src/components/data-display/empty-state.tsx +22 -0
  36. package/src/components/data-display/index.ts +41 -0
  37. package/src/components/data-display/key-value-grid.tsx +46 -0
  38. package/src/components/data-display/popover.tsx +62 -0
  39. package/src/components/data-display/progress-meter.tsx +20 -0
  40. package/src/components/data-display/scan-panel.tsx +16 -0
  41. package/src/components/data-display/scroll-area.tsx +42 -0
  42. package/src/components/data-display/status-badge.tsx +83 -0
  43. package/src/components/data-display/table.tsx +59 -0
  44. package/src/components/data-display/timeline.tsx +42 -0
  45. package/src/components/data-display/tree-list.tsx +42 -0
  46. package/src/components/data-entry/__fixtures__/tree-options.ts +80 -0
  47. package/src/components/data-entry/__tests__/cascader-tree-transfer.test.tsx +417 -0
  48. package/src/components/data-entry/__tests__/checkbox-group.test.tsx +40 -0
  49. package/src/components/data-entry/__tests__/checkbox.test.tsx +20 -0
  50. package/src/components/data-entry/__tests__/date-autocomplete.test.tsx +94 -0
  51. package/src/components/data-entry/__tests__/form-field.test.tsx +49 -0
  52. package/src/components/data-entry/__tests__/input-textarea.test.tsx +38 -0
  53. package/src/components/data-entry/__tests__/label-select.test.tsx +62 -0
  54. package/src/components/data-entry/__tests__/pickers.test.tsx +74 -0
  55. package/src/components/data-entry/__tests__/radio.test.tsx +46 -0
  56. package/src/components/data-entry/__tests__/search-input.test.tsx +32 -0
  57. package/src/components/data-entry/__tests__/switch-field.test.tsx +52 -0
  58. package/src/components/data-entry/__tests__/upload.test.tsx +125 -0
  59. package/src/components/data-entry/autocomplete.tsx +91 -0
  60. package/src/components/data-entry/calendar.tsx +90 -0
  61. package/src/components/data-entry/cascader.tsx +305 -0
  62. package/src/components/data-entry/checkbox-group.tsx +90 -0
  63. package/src/components/data-entry/checkbox.tsx +30 -0
  64. package/src/components/data-entry/choice-field.tsx +27 -0
  65. package/src/components/data-entry/choice-option.ts +20 -0
  66. package/src/components/data-entry/color-picker.tsx +75 -0
  67. package/src/components/data-entry/command.tsx +56 -0
  68. package/src/components/data-entry/country-select.tsx +88 -0
  69. package/src/components/data-entry/date-picker.tsx +69 -0
  70. package/src/components/data-entry/date-range-picker.tsx +75 -0
  71. package/src/components/data-entry/form-field.tsx +59 -0
  72. package/src/components/data-entry/index.ts +62 -0
  73. package/src/components/data-entry/input.tsx +26 -0
  74. package/src/components/data-entry/label.tsx +25 -0
  75. package/src/components/data-entry/radio.tsx +109 -0
  76. package/src/components/data-entry/search-input.tsx +103 -0
  77. package/src/components/data-entry/select.tsx +149 -0
  78. package/src/components/data-entry/slider.tsx +38 -0
  79. package/src/components/data-entry/switch-field.tsx +91 -0
  80. package/src/components/data-entry/switch.tsx +24 -0
  81. package/src/components/data-entry/textarea.tsx +12 -0
  82. package/src/components/data-entry/time-picker.tsx +214 -0
  83. package/src/components/data-entry/transfer.tsx +231 -0
  84. package/src/components/data-entry/tree-select-strategy.ts +6 -0
  85. package/src/components/data-entry/tree-select.tsx +279 -0
  86. package/src/components/data-entry/tree-utils.ts +221 -0
  87. package/src/components/data-entry/upload-crop-dialog.tsx +109 -0
  88. package/src/components/data-entry/upload-types.ts +86 -0
  89. package/src/components/data-entry/upload.tsx +498 -0
  90. package/src/components/data-entry/use-upload-draft.ts +93 -0
  91. package/src/components/feedback/__tests__/alert.test.tsx +127 -0
  92. package/src/components/feedback/__tests__/dialog.test.tsx +290 -0
  93. package/src/components/feedback/__tests__/sheet.test.tsx +94 -0
  94. package/src/components/feedback/__tests__/skeleton.test.tsx +25 -0
  95. package/src/components/feedback/__tests__/toast.test.tsx +52 -0
  96. package/src/components/feedback/alert.tsx +167 -0
  97. package/src/components/feedback/dialog.tsx +325 -0
  98. package/src/components/feedback/index.ts +53 -0
  99. package/src/components/feedback/sheet.tsx +130 -0
  100. package/src/components/feedback/skeleton.tsx +95 -0
  101. package/src/components/feedback/sonner.tsx +54 -0
  102. package/src/components/feedback/toaster.tsx +1 -0
  103. package/src/components/feedback/use-toast.ts +62 -0
  104. package/src/components/general/__tests__/button.test.tsx +71 -0
  105. package/src/components/general/button.tsx +61 -0
  106. package/src/components/general/index.ts +2 -0
  107. package/src/components/layout/__tests__/page-container.test.tsx +69 -0
  108. package/src/components/layout/__tests__/page-inset.test.tsx +14 -0
  109. package/src/components/layout/__tests__/stack-inline.test.tsx +39 -0
  110. package/src/components/layout/app-shell.tsx +42 -0
  111. package/src/components/layout/breadcrumb.tsx +35 -0
  112. package/src/components/layout/index.ts +31 -0
  113. package/src/components/layout/inline.tsx +13 -0
  114. package/src/components/layout/menu.tsx +34 -0
  115. package/src/components/layout/mobile-frame.tsx +57 -0
  116. package/src/components/layout/page-container.tsx +81 -0
  117. package/src/components/layout/page-inset.tsx +16 -0
  118. package/src/components/layout/responsive-grid.tsx +14 -0
  119. package/src/components/layout/shell-app.tsx +30 -0
  120. package/src/components/layout/sidebar.tsx +98 -0
  121. package/src/components/layout/split-pane.tsx +16 -0
  122. package/src/components/layout/stack.tsx +13 -0
  123. package/src/components/layout/topbar.tsx +108 -0
  124. package/src/components/navigation/__tests__/app-pickers.test.tsx +118 -0
  125. package/src/components/navigation/__tests__/dropdown-menu.test.tsx +104 -0
  126. package/src/components/navigation/__tests__/navigation.test.tsx +61 -0
  127. package/src/components/navigation/__tests__/pagination-steps-tabs.test.tsx +76 -0
  128. package/src/components/navigation/date-format-picker.tsx +55 -0
  129. package/src/components/navigation/dropdown-menu.tsx +190 -0
  130. package/src/components/navigation/filter-bar.tsx +38 -0
  131. package/src/components/navigation/index.ts +28 -0
  132. package/src/components/navigation/locale-picker.tsx +49 -0
  133. package/src/components/navigation/page-header.tsx +50 -0
  134. package/src/components/navigation/pagination-utils.ts +35 -0
  135. package/src/components/navigation/pagination.tsx +168 -0
  136. package/src/components/navigation/steps.tsx +163 -0
  137. package/src/components/navigation/tabs-items.tsx +69 -0
  138. package/src/components/navigation/tabs.tsx +67 -0
  139. package/src/components/navigation/time-format-picker.tsx +55 -0
  140. package/src/components/navigation/timezone-picker.tsx +63 -0
  141. package/src/components/query/__tests__/data-state.test.tsx +214 -0
  142. package/src/components/query/__tests__/infinite-prefetch.test.tsx +105 -0
  143. package/src/components/query/__tests__/query-helpers.test.tsx +61 -0
  144. package/src/components/query/data-state.tsx +58 -0
  145. package/src/components/query/index.ts +10 -0
  146. package/src/components/query/infinite-query-state.tsx +99 -0
  147. package/src/components/query/mutation-feedback.tsx +31 -0
  148. package/src/components/query/prefetch-link.tsx +45 -0
  149. package/src/components/query/query-refetch-button.tsx +41 -0
  150. package/src/components/ui/alert-dialog.tsx +1 -0
  151. package/src/components/ui/alert.tsx +1 -0
  152. package/src/components/ui/autocomplete.tsx +1 -0
  153. package/src/components/ui/badge.tsx +1 -0
  154. package/src/components/ui/button.tsx +1 -0
  155. package/src/components/ui/calendar.tsx +1 -0
  156. package/src/components/ui/card.tsx +1 -0
  157. package/src/components/ui/checkbox.tsx +1 -0
  158. package/src/components/ui/color-picker.tsx +1 -0
  159. package/src/components/ui/command.tsx +1 -0
  160. package/src/components/ui/date-picker.tsx +1 -0
  161. package/src/components/ui/date-range-picker.tsx +1 -0
  162. package/src/components/ui/dialog.tsx +1 -0
  163. package/src/components/ui/dropdown-menu.tsx +1 -0
  164. package/src/components/ui/index.tsx +31 -0
  165. package/src/components/ui/input.tsx +1 -0
  166. package/src/components/ui/label.tsx +1 -0
  167. package/src/components/ui/pagination.tsx +1 -0
  168. package/src/components/ui/popover.tsx +1 -0
  169. package/src/components/ui/radio.tsx +1 -0
  170. package/src/components/ui/scroll-area.tsx +1 -0
  171. package/src/components/ui/select.tsx +1 -0
  172. package/src/components/ui/sheet.tsx +1 -0
  173. package/src/components/ui/slider.tsx +1 -0
  174. package/src/components/ui/sonner.tsx +1 -0
  175. package/src/components/ui/switch.tsx +1 -0
  176. package/src/components/ui/table.tsx +1 -0
  177. package/src/components/ui/tabs-items.tsx +1 -0
  178. package/src/components/ui/tabs.tsx +1 -0
  179. package/src/components/ui/textarea.tsx +1 -0
  180. package/src/components/ui/time-picker.tsx +1 -0
  181. package/src/components/ui/upload.tsx +1 -0
  182. package/src/form/__tests__/use-zod-form.test.tsx +97 -0
  183. package/src/form/form-field-control.tsx +44 -0
  184. package/src/form/form-root.tsx +29 -0
  185. package/src/form/index.ts +7 -0
  186. package/src/form/use-zod-form.ts +29 -0
  187. package/src/i18n/__tests__/translate.test.ts +23 -0
  188. package/src/i18n/index.ts +9 -0
  189. package/src/i18n/messages/en.json +171 -0
  190. package/src/i18n/messages/ja.json +171 -0
  191. package/src/i18n/messages/vi.json +171 -0
  192. package/src/i18n/translate.ts +74 -0
  193. package/src/i18n/use-translation.ts +53 -0
  194. package/src/index.ts +3 -0
  195. package/src/lib/__tests__/control-styles.test.ts +78 -0
  196. package/src/lib/__tests__/datetime.test.ts +77 -0
  197. package/src/lib/__tests__/format-date.test.ts +97 -0
  198. package/src/lib/__tests__/format.test.ts +62 -0
  199. package/src/lib/__tests__/theme-tokens-audit.test.ts +176 -0
  200. package/src/lib/__tests__/theme-tokens-css.test.ts +118 -0
  201. package/src/lib/__tests__/token-governance.test.ts +191 -0
  202. package/src/lib/__tests__/variants.test.ts +18 -0
  203. package/src/lib/control-styles.ts +33 -0
  204. package/src/lib/datetime/detect.ts +25 -0
  205. package/src/lib/datetime/format-date.ts +100 -0
  206. package/src/lib/datetime/format.ts +140 -0
  207. package/src/lib/datetime/index.ts +25 -0
  208. package/src/lib/datetime/parse.ts +51 -0
  209. package/src/lib/datetime/sync.ts +48 -0
  210. package/src/lib/format.ts +114 -0
  211. package/src/lib/hooks.ts +54 -0
  212. package/src/lib/utils.ts +6 -0
  213. package/src/lib/variants.ts +40 -0
  214. package/src/props/components/app.prop.ts +99 -0
  215. package/src/props/components/data-display.prop.ts +73 -0
  216. package/src/props/components/data-entry.prop.ts +334 -0
  217. package/src/props/components/feedback.prop.ts +80 -0
  218. package/src/props/components/form.prop.ts +46 -0
  219. package/src/props/components/general.prop.ts +18 -0
  220. package/src/props/components/index.ts +99 -0
  221. package/src/props/components/layout.prop.ts +130 -0
  222. package/src/props/components/navigation.prop.ts +88 -0
  223. package/src/props/components/query.prop.ts +94 -0
  224. package/src/props/index.ts +17 -0
  225. package/src/props/registry.ts +603 -0
  226. package/src/props/vocabulary/content.prop.ts +35 -0
  227. package/src/props/vocabulary/data.prop.ts +46 -0
  228. package/src/props/vocabulary/index.ts +73 -0
  229. package/src/props/vocabulary/interaction.prop.ts +42 -0
  230. package/src/props/vocabulary/layout.prop.ts +25 -0
  231. package/src/props/vocabulary/navigation.prop.ts +19 -0
  232. package/src/props/vocabulary/shared.prop.ts +59 -0
  233. package/src/styles/alert-layout.css +191 -0
  234. package/src/styles/badge-layout.css +22 -0
  235. package/src/styles/card-layout.css +373 -0
  236. package/src/styles/control.css +504 -0
  237. package/src/styles/data-display-layout.css +246 -0
  238. package/src/styles/density.css +43 -0
  239. package/src/styles/dialog-layout.css +84 -0
  240. package/src/styles/index.css +105 -0
  241. package/src/styles/layout.css +479 -0
  242. package/src/styles/shell-layout.css +604 -0
  243. package/src/styles/table-layout.css +109 -0
  244. package/src/test/__tests__/render-loop-guard.test.tsx +38 -0
  245. package/src/test/jest-dom.d.ts +4 -0
  246. package/src/test/render-loop-guard.tsx +50 -0
  247. package/src/test/render.tsx +29 -0
  248. package/src/test/theme-globals.test.ts +77 -0
  249. package/src/test/theme-globals.ts +134 -0
  250. package/src/test/theme-test-utils.tsx +67 -0
  251. package/src/theme/example.service.css +37 -0
  252. package/src/tokens/base.css +13 -0
  253. package/src/tokens/foundation.css +151 -0
  254. package/src/tokens/primitives/badge.css +13 -0
  255. package/src/tokens/primitives/card.css +29 -0
  256. package/src/tokens/primitives/control.css +55 -0
  257. package/src/tokens/primitives/feedback.css +17 -0
  258. package/src/tokens/primitives/layout.css +20 -0
  259. package/src/tokens/primitives/navigation.css +13 -0
  260. package/src/tokens/primitives/table.css +10 -0
  261. package/BRAND.md +0 -296
  262. package/CHANGELOG.md +0 -668
  263. package/config/eslint.js +0 -54
  264. package/config/prettier.cjs +0 -20
  265. package/config/tsconfig.base.json +0 -22
  266. package/config/vitest.base.ts +0 -26
  267. package/dist/MiniMonth-YAmPGEpC.d.ts +0 -143
  268. package/dist/Table.types-BbsxoIYE.d.ts +0 -352
  269. package/dist/color-DO0qqUAb.d.ts +0 -38
  270. package/dist/components/composites.d.ts +0 -963
  271. package/dist/components/composites.js +0 -7343
  272. package/dist/components/composites.js.map +0 -1
  273. package/dist/components/primitives.d.ts +0 -2744
  274. package/dist/components/primitives.js +0 -7356
  275. package/dist/components/primitives.js.map +0 -1
  276. package/dist/components/shell.d.ts +0 -182
  277. package/dist/components/shell.js +0 -774
  278. package/dist/components/shell.js.map +0 -1
  279. package/dist/hooks.d.ts +0 -100
  280. package/dist/hooks.js +0 -558
  281. package/dist/hooks.js.map +0 -1
  282. package/dist/i18n.d.ts +0 -61
  283. package/dist/i18n.js +0 -860
  284. package/dist/i18n.js.map +0 -1
  285. package/dist/index.d.ts +0 -33
  286. package/dist/index.js +0 -13062
  287. package/dist/index.js.map +0 -1
  288. package/dist/padding-DY0JV5Ja.d.ts +0 -16
  289. package/dist/preferences.d.ts +0 -132
  290. package/dist/preferences.js +0 -262
  291. package/dist/preferences.js.map +0 -1
  292. package/dist/props.d.ts +0 -86
  293. package/dist/props.js +0 -16
  294. package/dist/props.js.map +0 -1
  295. package/dist/size-CQwNvOWd.d.ts +0 -19
  296. package/dist/types-LTj-2bl-.d.ts +0 -30
  297. package/dist/useTableViews-D5NIAJ7h.d.ts +0 -154
  298. package/src/tokens/tailwind.css +0 -158
@@ -0,0 +1,328 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { renderWithUi, screen } from "@/test/render";
5
+ import {
6
+ Card,
7
+ CardAction,
8
+ CardContent,
9
+ CardCover,
10
+ CardDescription,
11
+ CardFooter,
12
+ CardHeader,
13
+ CardStat,
14
+ CardTitle,
15
+ } from "../card";
16
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../table";
17
+
18
+ describe("Card layout governance", () => {
19
+ it("keeps all slot padding in card-layout.css (not card.tsx)", () => {
20
+ const cardTs = readFileSync(join(__dirname, "../card.tsx"), "utf8");
21
+ expect(cardTs).not.toMatch(/ui-card-shell/);
22
+ expect(cardTs).not.toMatch(/ui-card-section-gap/);
23
+ expect(cardTs).not.toMatch(/padding-(top|bottom|inline)/);
24
+
25
+ const layoutCss = readFileSync(join(__dirname, "../../../styles/card-layout.css"), "utf8");
26
+ expect(layoutCss).toMatch(/\[data-slot="card-header"\]/);
27
+ expect(layoutCss).toMatch(/\[data-slot="card-content"\]/);
28
+ expect(layoutCss).toMatch(/\[data-slot="card-footer"\]/);
29
+ expect(layoutCss).toMatch(/--card-space-inset/);
30
+ expect(layoutCss).toMatch(/--card-space-header-y/);
31
+ expect(layoutCss).toMatch(/--card-space-body-y/);
32
+ });
33
+ });
34
+
35
+ describe("Card", () => {
36
+ it("renders header, content, and footer", () => {
37
+ renderWithUi(
38
+ <Card>
39
+ <CardHeader>
40
+ <CardTitle>HAWB detail</CardTitle>
41
+ <CardDescription>Osaka → HCM</CardDescription>
42
+ </CardHeader>
43
+ <CardContent>2.4 kg</CardContent>
44
+ <CardFooter>Footer</CardFooter>
45
+ </Card>,
46
+ );
47
+ expect(screen.getByRole("heading", { name: "HAWB detail" })).toBeInTheDocument();
48
+ expect(screen.getByText("Osaka → HCM")).toBeInTheDocument();
49
+ expect(screen.getByText("2.4 kg")).toBeInTheDocument();
50
+ expect(screen.getByText("Footer")).toBeInTheDocument();
51
+ });
52
+
53
+ it("renders CardAction in header row", () => {
54
+ renderWithUi(
55
+ <Card>
56
+ <CardHeader className="flex flex-row justify-between">
57
+ <CardTitle>Section</CardTitle>
58
+ <CardAction>
59
+ <button type="button">Edit</button>
60
+ </CardAction>
61
+ </CardHeader>
62
+ </Card>,
63
+ );
64
+ expect(screen.getByRole("button", { name: "Edit" })).toBeInTheDocument();
65
+ });
66
+
67
+ it("renders separated footer with data-separated flag", () => {
68
+ renderWithUi(
69
+ <Card>
70
+ <CardHeader>
71
+ <CardTitle>Form</CardTitle>
72
+ </CardHeader>
73
+ <CardContent data-testid="content">Body</CardContent>
74
+ <CardFooter separated data-testid="footer">
75
+ <button type="button">Save</button>
76
+ </CardFooter>
77
+ </Card>,
78
+ );
79
+ const footer = screen.getByTestId("footer");
80
+ expect(footer).toHaveAttribute("data-separated", "");
81
+ expect(footer).toHaveAttribute("data-slot", "card-footer");
82
+ expect(screen.getByTestId("content")).toHaveAttribute("data-slot", "card-content");
83
+ });
84
+
85
+ it("renders banded header with data attribute", () => {
86
+ renderWithUi(
87
+ <Card>
88
+ <CardHeader banded data-testid="header">
89
+ <CardTitle>Section</CardTitle>
90
+ </CardHeader>
91
+ <CardContent>Body</CardContent>
92
+ </Card>,
93
+ );
94
+ const header = screen.getByTestId("header");
95
+ expect(header).toHaveAttribute("data-banded", "");
96
+ expect(header.className).toMatch(/ui-card-header--banded/);
97
+ });
98
+
99
+ it("banded header exposes chrome band flags only (padding in CSS)", () => {
100
+ renderWithUi(
101
+ <Card data-testid="card">
102
+ <CardHeader banded data-testid="header">
103
+ <CardTitle>Audit events</CardTitle>
104
+ <CardDescription>Realm internal · last 24h</CardDescription>
105
+ </CardHeader>
106
+ <CardContent data-testid="content">Body copy</CardContent>
107
+ </Card>,
108
+ );
109
+ const header = screen.getByTestId("header");
110
+ expect(header).toHaveAttribute("data-banded", "");
111
+ expect(header.className).toMatch(/ui-card-header--banded/);
112
+ expect(header.className).not.toMatch(/ui-card-shell/);
113
+ expect(screen.getByRole("heading", { name: "Audit events" })).toHaveAttribute(
114
+ "data-slot",
115
+ "card-title",
116
+ );
117
+ expect(screen.getByTestId("content")).not.toHaveAttribute("data-tight");
118
+ });
119
+
120
+ it("renders banded header with title and description slots", () => {
121
+ renderWithUi(
122
+ <Card>
123
+ <CardHeader banded>
124
+ <CardTitle>Audit events</CardTitle>
125
+ <CardDescription>Realm internal · last 24h</CardDescription>
126
+ </CardHeader>
127
+ <CardContent>Prop banded trên CardHeader.</CardContent>
128
+ </Card>,
129
+ );
130
+ expect(screen.getByRole("heading", { name: "Audit events" })).toBeInTheDocument();
131
+ expect(screen.getByText("Realm internal · last 24h")).toHaveAttribute(
132
+ "data-slot",
133
+ "card-description",
134
+ );
135
+ expect(screen.getByText("Prop banded trên CardHeader.")).toBeInTheDocument();
136
+ });
137
+
138
+ it("renders banded header with CardAction for toolbar layout", () => {
139
+ renderWithUi(
140
+ <Card>
141
+ <CardHeader banded data-testid="header">
142
+ <div>
143
+ <CardTitle>Lô gom hôm nay</CardTitle>
144
+ <CardDescription>Osaka Hub · cut-off 17:00 JST</CardDescription>
145
+ </div>
146
+ <CardAction>
147
+ <button type="button">Export CSV</button>
148
+ </CardAction>
149
+ </CardHeader>
150
+ <CardContent flush tight>
151
+ <Table>
152
+ <TableBody>
153
+ <TableRow>
154
+ <TableCell>GX-001</TableCell>
155
+ </TableRow>
156
+ </TableBody>
157
+ </Table>
158
+ </CardContent>
159
+ </Card>,
160
+ );
161
+ const header = screen.getByTestId("header");
162
+ expect(header.querySelector('[data-slot="card-action"]')).toBeTruthy();
163
+ expect(screen.getByRole("button", { name: "Export CSV" })).toBeInTheDocument();
164
+ expect(screen.getByRole("cell", { name: "GX-001" })).toBeInTheDocument();
165
+ expect(screen.getByRole("cell", { name: "GX-001" }).closest("[data-flush]")).toBeTruthy();
166
+ });
167
+
168
+ it("plain header has no banded flag; banded header does", () => {
169
+ renderWithUi(
170
+ <div>
171
+ <Card>
172
+ <CardHeader data-testid="plain">
173
+ <CardTitle>Plain header</CardTitle>
174
+ </CardHeader>
175
+ </Card>
176
+ <Card>
177
+ <CardHeader banded data-testid="banded">
178
+ <CardTitle>Banded header</CardTitle>
179
+ </CardHeader>
180
+ </Card>
181
+ </div>,
182
+ );
183
+ expect(screen.getByTestId("plain")).not.toHaveAttribute("data-banded");
184
+ expect(screen.getByTestId("banded")).toHaveAttribute("data-banded", "");
185
+ });
186
+
187
+ it("flush tight content sets layout modifier flags", () => {
188
+ renderWithUi(
189
+ <Card>
190
+ <CardHeader banded>
191
+ <CardTitle>Table section</CardTitle>
192
+ </CardHeader>
193
+ <CardContent flush tight data-testid="content">
194
+ <Table>
195
+ <TableBody>
196
+ <TableRow>
197
+ <TableCell>HAWB</TableCell>
198
+ </TableRow>
199
+ </TableBody>
200
+ </Table>
201
+ </CardContent>
202
+ </Card>,
203
+ );
204
+ const content = screen.getByTestId("content");
205
+ expect(content).toHaveAttribute("data-tight", "");
206
+ expect(content).toHaveAttribute("data-flush", "");
207
+ expect(content.className).not.toMatch(/ui-card-/);
208
+ });
209
+
210
+ it("banded card pairs with separated footer band", () => {
211
+ renderWithUi(
212
+ <Card>
213
+ <CardHeader banded>
214
+ <CardTitle>Lô gom hôm nay</CardTitle>
215
+ </CardHeader>
216
+ <CardContent flush tight>
217
+ Table body
218
+ </CardContent>
219
+ <CardFooter separated data-testid="footer">
220
+ 3 / 12 kiện
221
+ </CardFooter>
222
+ </Card>,
223
+ );
224
+ expect(screen.getByTestId("footer")).toHaveAttribute("data-separated", "");
225
+ expect(screen.getByText("3 / 12 kiện")).toBeInTheDocument();
226
+ });
227
+
228
+ it("uses section top for toolbar header with CardAction", () => {
229
+ renderWithUi(
230
+ <Card>
231
+ <CardHeader data-testid="header" className="flex flex-row justify-between">
232
+ <CardTitle>Providers</CardTitle>
233
+ <CardAction>
234
+ <button type="button">Cancel</button>
235
+ </CardAction>
236
+ </CardHeader>
237
+ <CardContent>Body</CardContent>
238
+ </Card>,
239
+ );
240
+ expect(screen.getByTestId("header")).toHaveAttribute("data-slot", "card-header");
241
+ expect(
242
+ screen.getByRole("button", { name: "Cancel" }).closest("[data-slot=card-action]"),
243
+ ).toBeTruthy();
244
+ });
245
+
246
+ it("renders cover + meta with card-cover slot", () => {
247
+ renderWithUi(
248
+ <Card>
249
+ <CardCover data-testid="cover">Cover</CardCover>
250
+ <CardHeader data-testid="header">
251
+ <CardTitle>Meta</CardTitle>
252
+ </CardHeader>
253
+ </Card>,
254
+ );
255
+ expect(screen.getByTestId("cover")).toHaveAttribute("data-slot", "card-cover");
256
+ expect(screen.getByTestId("header")).toHaveAttribute("data-slot", "card-header");
257
+ });
258
+
259
+ it("marks tight content for symmetric header band spacing", () => {
260
+ renderWithUi(
261
+ <Card>
262
+ <CardHeader data-testid="header">
263
+ <CardTitle>Table</CardTitle>
264
+ </CardHeader>
265
+ <CardContent tight flush data-testid="content">
266
+ Body
267
+ </CardContent>
268
+ </Card>,
269
+ );
270
+ expect(screen.getByTestId("content")).toHaveAttribute("data-tight", "");
271
+ expect(screen.getByTestId("header")).toHaveAttribute("data-slot", "card-header");
272
+ });
273
+
274
+ it("renders solo content with data-solo flag", () => {
275
+ renderWithUi(
276
+ <Card>
277
+ <CardContent solo data-testid="content">
278
+ Only body
279
+ </CardContent>
280
+ </Card>,
281
+ );
282
+ expect(screen.getByTestId("content")).toHaveAttribute("data-solo", "");
283
+ });
284
+
285
+ it("CardStat uses compact stat slot token path", () => {
286
+ renderWithUi(
287
+ <CardStat
288
+ label="Đơn hôm nay"
289
+ value={128}
290
+ delta={<span data-testid="delta">+12%</span>}
291
+ data-testid="stat"
292
+ />,
293
+ );
294
+ const card = screen.getByTestId("stat");
295
+ expect(card).toHaveAttribute("data-size", "compact");
296
+ expect(card).toHaveAttribute("data-stat-card", "");
297
+ expect(card).toHaveAttribute("data-stat-layout", "stacked");
298
+ expect(screen.getByText("Đơn hôm nay")).toHaveAttribute("data-slot", "card-stat-label");
299
+ expect(screen.getByText("128")).toHaveAttribute("data-slot", "card-stat-value");
300
+ expect(screen.getByText("128")).toBeInTheDocument();
301
+ expect(screen.getByTestId("delta")).toBeInTheDocument();
302
+ expect(card.querySelector("[data-slot='card-stat-value-row']")).toBeTruthy();
303
+ });
304
+ });
305
+
306
+ describe("Table", () => {
307
+ it("renders semantic table structure", () => {
308
+ renderWithUi(
309
+ <Table>
310
+ <TableHeader>
311
+ <TableRow>
312
+ <TableHead>HAWB</TableHead>
313
+ <TableHead>Status</TableHead>
314
+ </TableRow>
315
+ </TableHeader>
316
+ <TableBody>
317
+ <TableRow>
318
+ <TableCell>GX-001</TableCell>
319
+ <TableCell>In transit</TableCell>
320
+ </TableRow>
321
+ </TableBody>
322
+ </Table>,
323
+ );
324
+ expect(screen.getByRole("table")).toBeInTheDocument();
325
+ expect(screen.getByRole("columnheader", { name: "HAWB" })).toBeInTheDocument();
326
+ expect(screen.getByRole("cell", { name: "GX-001" })).toBeInTheDocument();
327
+ });
328
+ });
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Inbox } from "lucide-react";
3
+ import { renderWithUi, screen } from "@/test/render";
4
+ import { EmptyState } from "../empty-state";
5
+ import { Button } from "../../general/button";
6
+ import { Badge } from "../badge";
7
+ import { KeyValueGrid } from "../key-value-grid";
8
+ import { StatusBadge } from "../status-badge";
9
+
10
+ describe("EmptyState", () => {
11
+ it("renders title and description", () => {
12
+ renderWithUi(<EmptyState title="No data" description="Try changing filters" />);
13
+ expect(screen.getByRole("heading", { level: 3, name: "No data" })).toBeInTheDocument();
14
+ expect(screen.getByText("Try changing filters")).toBeInTheDocument();
15
+ });
16
+
17
+ it("renders icon and action", () => {
18
+ renderWithUi(<EmptyState icon={Inbox} title="Empty" action={<Button>Add</Button>} />);
19
+ expect(screen.getByRole("button", { name: "Add" })).toBeInTheDocument();
20
+ });
21
+ });
22
+
23
+ describe("Badge", () => {
24
+ it("renders children", () => {
25
+ renderWithUi(<Badge>New</Badge>);
26
+ expect(screen.getByText("New")).toBeInTheDocument();
27
+ });
28
+
29
+ it("success variant uses semantic success token (not green-*)", () => {
30
+ renderWithUi(<Badge variant="success">OK</Badge>);
31
+ const el = screen.getByText("OK");
32
+ expect(el.className).toContain("success");
33
+ expect(el.className).not.toMatch(/green-/);
34
+ });
35
+ });
36
+
37
+ describe("KeyValueGrid", () => {
38
+ it("renders label/value pairs via Item children", () => {
39
+ renderWithUi(
40
+ <KeyValueGrid>
41
+ <KeyValueGrid.Item label="ID">01HF</KeyValueGrid.Item>
42
+ <KeyValueGrid.Item label="Status">active</KeyValueGrid.Item>
43
+ </KeyValueGrid>,
44
+ );
45
+ expect(screen.getByText("ID")).toBeInTheDocument();
46
+ expect(screen.getByText("01HF")).toBeInTheDocument();
47
+ expect(screen.getByText("Status")).toBeInTheDocument();
48
+ });
49
+ });
50
+
51
+ describe("StatusBadge", () => {
52
+ it("maps known status to label", () => {
53
+ renderWithUi(<StatusBadge status="pending" />);
54
+ expect(screen.getByText("Chờ xử lý")).toBeInTheDocument();
55
+ });
56
+
57
+ it("pending tone uses warning token on root badge element", () => {
58
+ renderWithUi(<StatusBadge status="pending" />);
59
+ const text = screen.getByText("Chờ xử lý");
60
+ expect(text.parentElement?.className).toContain("warning");
61
+ });
62
+
63
+ it("scheduled tone uses info token on root badge element", () => {
64
+ renderWithUi(<StatusBadge status="scheduled" />);
65
+ const text = screen.getByText("Đã lên lịch");
66
+ expect(text.parentElement?.className).toContain("info");
67
+ });
68
+
69
+ it("falls back for unknown status", () => {
70
+ renderWithUi(<StatusBadge status="custom_unknown_xyz" />);
71
+ expect(screen.getByText("custom_unknown_xyz")).toBeInTheDocument();
72
+ });
73
+ });
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderWithUi, screen, userEvent } from "@/test/render";
3
+ import { DataTable } from "../data-table";
4
+
5
+ type Row = { id: string; name: string; status: string };
6
+
7
+ const rows: Row[] = [
8
+ { id: "1", name: "Mai Nguyen", status: "active" },
9
+ { id: "2", name: "Ken Tanaka", status: "pending" },
10
+ ];
11
+
12
+ const columns = [
13
+ { key: "name", header: "Khách hàng" },
14
+ { key: "status", header: "Trạng thái" },
15
+ ] as const;
16
+
17
+ describe("DataTable", () => {
18
+ it("renders column headers and row data", () => {
19
+ renderWithUi(<DataTable data={rows} columns={[...columns]} getRowId={(row) => row.id} />);
20
+ expect(screen.getByRole("columnheader", { name: "Khách hàng" })).toBeInTheDocument();
21
+ expect(screen.getByText("Mai Nguyen")).toBeInTheDocument();
22
+ expect(screen.getByText("Ken Tanaka")).toBeInTheDocument();
23
+ });
24
+
25
+ it("applies ui-density class wrapper for token-based row height", () => {
26
+ const { container } = renderWithUi(
27
+ <DataTable data={rows} columns={[...columns]} getRowId={(row) => row.id} density="compact" />,
28
+ );
29
+ expect(container.querySelector(".ui-density-compact")).toBeInTheDocument();
30
+ });
31
+
32
+ it("supports row selection when selectable", async () => {
33
+ const user = userEvent.setup();
34
+ const onSelectChange = vi.fn();
35
+ renderWithUi(
36
+ <DataTable
37
+ data={rows}
38
+ columns={[...columns]}
39
+ getRowId={(row) => row.id}
40
+ selectable
41
+ selected={new Set()}
42
+ onSelectChange={onSelectChange}
43
+ />,
44
+ );
45
+ const rowCheckbox = screen.getAllByRole("checkbox", { name: /chọn dòng/i })[0];
46
+ await user.click(rowCheckbox);
47
+ expect(onSelectChange).toHaveBeenCalledWith(new Set(["1"]));
48
+ });
49
+
50
+ it("calls onRowClick when row is clicked", async () => {
51
+ const user = userEvent.setup();
52
+ const onRowClick = vi.fn();
53
+ renderWithUi(
54
+ <DataTable
55
+ data={rows}
56
+ columns={[...columns]}
57
+ getRowId={(row) => row.id}
58
+ onRowClick={onRowClick}
59
+ />,
60
+ );
61
+ await user.click(screen.getByText("Mai Nguyen"));
62
+ expect(onRowClick).toHaveBeenCalledWith(rows[0]);
63
+ });
64
+
65
+ it("shows bulk actions when rows selected", () => {
66
+ renderWithUi(
67
+ <DataTable
68
+ data={rows}
69
+ columns={[...columns]}
70
+ getRowId={(row) => row.id}
71
+ selectable
72
+ selected={new Set(["1"])}
73
+ >
74
+ <DataTable.Toolbar>
75
+ <DataTable.BulkActions>
76
+ <button type="button">Xóa</button>
77
+ </DataTable.BulkActions>
78
+ </DataTable.Toolbar>
79
+ </DataTable>,
80
+ );
81
+ expect(screen.getByRole("region", { name: /thao tác hàng loạt/i })).toBeInTheDocument();
82
+ expect(screen.getByRole("button", { name: "Xóa" })).toBeInTheDocument();
83
+ });
84
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import * as React from "react";
3
+ import { renderWithUi, screen, userEvent } from "@/test/render";
4
+ import { Button } from "../../general/button";
5
+ import {
6
+ Popover,
7
+ PopoverContent,
8
+ PopoverDescription,
9
+ PopoverHeader,
10
+ PopoverTitle,
11
+ PopoverTrigger,
12
+ } from "../popover";
13
+
14
+ describe("Popover", () => {
15
+ it("shows content when trigger is clicked", async () => {
16
+ const user = userEvent.setup();
17
+ renderWithUi(
18
+ <Popover>
19
+ <PopoverTrigger asChild>
20
+ <Button type="button">Mở popover</Button>
21
+ </PopoverTrigger>
22
+ <PopoverContent>Lọc HAWB nâng cao</PopoverContent>
23
+ </Popover>,
24
+ );
25
+ await user.click(screen.getByRole("button", { name: "Mở popover" }));
26
+ expect(screen.getByText("Lọc HAWB nâng cao")).toHaveAttribute("data-slot", "popover-content");
27
+ });
28
+
29
+ it("closes when pressing Escape", async () => {
30
+ const user = userEvent.setup();
31
+ renderWithUi(
32
+ <Popover>
33
+ <PopoverTrigger asChild>
34
+ <Button type="button">Mở popover</Button>
35
+ </PopoverTrigger>
36
+ <PopoverContent>Lọc HAWB nâng cao</PopoverContent>
37
+ </Popover>,
38
+ );
39
+ await user.click(screen.getByRole("button", { name: "Mở popover" }));
40
+ expect(screen.getByText("Lọc HAWB nâng cao")).toBeInTheDocument();
41
+ await user.keyboard("{Escape}");
42
+ expect(screen.queryByText("Lọc HAWB nâng cao")).not.toBeInTheDocument();
43
+ });
44
+
45
+ it("calls onOpenChange in controlled mode", async () => {
46
+ const user = userEvent.setup();
47
+ const onOpenChange = vi.fn();
48
+
49
+ function Demo() {
50
+ const [open, setOpen] = React.useState(false);
51
+ return (
52
+ <Popover
53
+ open={open}
54
+ onOpenChange={(next) => {
55
+ setOpen(next);
56
+ onOpenChange(next);
57
+ }}
58
+ >
59
+ <PopoverTrigger asChild>
60
+ <Button type="button">Controlled</Button>
61
+ </PopoverTrigger>
62
+ <PopoverContent>Panel</PopoverContent>
63
+ </Popover>
64
+ );
65
+ }
66
+
67
+ renderWithUi(<Demo />);
68
+ await user.click(screen.getByRole("button", { name: "Controlled" }));
69
+ expect(onOpenChange).toHaveBeenCalledWith(true);
70
+ expect(screen.getByText("Panel")).toBeInTheDocument();
71
+ });
72
+
73
+ it("renders shadcn header slots", async () => {
74
+ const user = userEvent.setup();
75
+ renderWithUi(
76
+ <Popover>
77
+ <PopoverTrigger asChild>
78
+ <Button type="button">Details</Button>
79
+ </PopoverTrigger>
80
+ <PopoverContent>
81
+ <PopoverHeader>
82
+ <PopoverTitle>Title</PopoverTitle>
83
+ <PopoverDescription>Description</PopoverDescription>
84
+ </PopoverHeader>
85
+ </PopoverContent>
86
+ </Popover>,
87
+ );
88
+ await user.click(screen.getByRole("button", { name: "Details" }));
89
+ expect(screen.getByText("Title")).toHaveAttribute("data-slot", "popover-title");
90
+ expect(screen.getByText("Description")).toHaveAttribute("data-slot", "popover-description");
91
+ });
92
+ });
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as React from "react";
3
+ import { renderWithUi, screen, userEvent } from "@/test/render";
4
+ import { Button } from "../../general/button";
5
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../collapsible";
6
+ import { ScrollArea } from "../scroll-area";
7
+
8
+ describe("ScrollArea", () => {
9
+ it("renders scrollable content", () => {
10
+ renderWithUi(
11
+ <ScrollArea className="h-24 w-48">
12
+ <div>
13
+ {Array.from({ length: 20 }, (_, i) => (
14
+ <p key={i}>Dòng {i + 1}</p>
15
+ ))}
16
+ </div>
17
+ </ScrollArea>,
18
+ );
19
+ expect(screen.getByText("Dòng 1")).toBeInTheDocument();
20
+ expect(screen.getByText("Dòng 20")).toBeInTheDocument();
21
+ });
22
+ });
23
+
24
+ describe("Collapsible", () => {
25
+ it("toggles content visibility", async () => {
26
+ const user = userEvent.setup();
27
+ renderWithUi(
28
+ <Collapsible>
29
+ <CollapsibleTrigger asChild>
30
+ <Button type="button">Mở rộng</Button>
31
+ </CollapsibleTrigger>
32
+ <CollapsibleContent>Chi tiết lô hàng GX-001</CollapsibleContent>
33
+ </Collapsible>,
34
+ );
35
+
36
+ expect(screen.queryByText("Chi tiết lô hàng GX-001")).not.toBeInTheDocument();
37
+ await user.click(screen.getByRole("button", { name: "Mở rộng" }));
38
+ expect(screen.getByText("Chi tiết lô hàng GX-001")).toBeInTheDocument();
39
+ await user.click(screen.getByRole("button", { name: "Mở rộng" }));
40
+ expect(screen.queryByText("Chi tiết lô hàng GX-001")).not.toBeInTheDocument();
41
+ });
42
+
43
+ it("supports controlled open state", async () => {
44
+ const user = userEvent.setup();
45
+ function Demo() {
46
+ const [open, setOpen] = React.useState(false);
47
+ return (
48
+ <>
49
+ <Button type="button" onClick={() => setOpen(true)}>
50
+ Mở ngoài
51
+ </Button>
52
+ <Collapsible open={open} onOpenChange={setOpen}>
53
+ <CollapsibleTrigger asChild>
54
+ <Button type="button">Trigger</Button>
55
+ </CollapsibleTrigger>
56
+ <CollapsibleContent>Nội dung controlled</CollapsibleContent>
57
+ </Collapsible>
58
+ </>
59
+ );
60
+ }
61
+
62
+ renderWithUi(<Demo />);
63
+ await user.click(screen.getByRole("button", { name: "Mở ngoài" }));
64
+ expect(screen.getByText("Nội dung controlled")).toBeInTheDocument();
65
+ });
66
+ });
@@ -0,0 +1,27 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import { cn } from "../../lib/utils";
3
+ import { toneSuccessClass, toneWarningClass } from "../../lib/control-styles";
4
+
5
+ const badgeVariants = cva(
6
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "border-transparent bg-primary text-primary-foreground",
11
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
12
+ destructive: "border-transparent bg-destructive text-destructive-foreground",
13
+ outline: "text-foreground",
14
+ success: cn("border-transparent", toneSuccessClass),
15
+ warning: cn("border-transparent", toneWarningClass),
16
+ },
17
+ },
18
+ defaultVariants: { variant: "default" },
19
+ },
20
+ );
21
+
22
+ export interface BadgeProps
23
+ extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
24
+
25
+ export function Badge({ className, variant, ...props }: BadgeProps) {
26
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
27
+ }