@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,61 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderWithUi, screen, userEvent } from "@/test/render";
3
+ import { FilterBar, FilterGroup } from "../filter-bar";
4
+ import { PageHeader } from "../page-header";
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../tabs";
6
+
7
+ describe("FilterBar", () => {
8
+ it("shows clear button when filters active", async () => {
9
+ const user = userEvent.setup();
10
+ const onClear = vi.fn();
11
+ renderWithUi(
12
+ <FilterBar onClear={onClear} hasActiveFilters>
13
+ <FilterGroup label="Status">
14
+ <span>Active</span>
15
+ </FilterGroup>
16
+ </FilterBar>,
17
+ );
18
+ await user.click(screen.getByRole("button", { name: /xóa bộ lọc/i }));
19
+ expect(onClear).toHaveBeenCalledOnce();
20
+ });
21
+
22
+ it("hides clear when hasActiveFilters=false", () => {
23
+ renderWithUi(
24
+ <FilterBar onClear={() => undefined} hasActiveFilters={false}>
25
+ <span>f</span>
26
+ </FilterBar>,
27
+ );
28
+ expect(screen.queryByRole("button", { name: /xóa bộ lọc/i })).not.toBeInTheDocument();
29
+ });
30
+ });
31
+
32
+ describe("PageHeader", () => {
33
+ it("renders title and description (legacy)", () => {
34
+ renderWithUi(
35
+ <PageHeader title="Legacy" description="Sub" actions={<button type="button">Go</button>} />,
36
+ );
37
+ expect(screen.getByRole("heading", { name: "Legacy" })).toBeInTheDocument();
38
+ expect(screen.getByText("Sub")).toBeInTheDocument();
39
+ });
40
+ });
41
+
42
+ describe("Tabs", () => {
43
+ it("switches tab panels", async () => {
44
+ const user = userEvent.setup();
45
+ renderWithUi(
46
+ <Tabs defaultValue="a">
47
+ <TabsList>
48
+ <TabsTrigger value="a">Tab A</TabsTrigger>
49
+ <TabsTrigger value="b">Tab B</TabsTrigger>
50
+ </TabsList>
51
+ <TabsContent value="a">Content A</TabsContent>
52
+ <TabsContent value="b">Content B</TabsContent>
53
+ </Tabs>,
54
+ );
55
+ expect(screen.getByText("Content A")).toBeInTheDocument();
56
+ expect(screen.getByRole("tablist")).toHaveAttribute("data-slot", "tabs-list");
57
+ await user.click(screen.getByRole("tab", { name: "Tab B" }));
58
+ expect(screen.getByRole("tab", { name: "Tab B" })).toHaveAttribute("data-slot", "tabs-trigger");
59
+ expect(screen.getByText("Content B")).toBeInTheDocument();
60
+ });
61
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderWithUi, screen, userEvent } from "@/test/render";
3
+ import { Pagination } from "../pagination";
4
+ import { buildPageRange } from "../pagination-utils";
5
+ import { Steps } from "../steps";
6
+ import { TabsItems } from "../tabs-items";
7
+
8
+ describe("buildPageRange", () => {
9
+ it("returns all pages when total is small", () => {
10
+ expect(buildPageRange(2, 5)).toEqual([1, 2, 3, 4, 5]);
11
+ });
12
+
13
+ it("inserts ellipsis for large totals", () => {
14
+ expect(buildPageRange(5, 20)).toContain("ellipsis");
15
+ expect(buildPageRange(5, 20)[0]).toBe(1);
16
+ expect(buildPageRange(5, 20).at(-1)).toBe(20);
17
+ });
18
+ });
19
+
20
+ describe("Pagination", () => {
21
+ it("changes page on click", async () => {
22
+ const user = userEvent.setup();
23
+ const onChange = vi.fn();
24
+
25
+ renderWithUi(<Pagination current={1} total={100} pageSize={10} onChange={onChange} />);
26
+
27
+ await user.click(screen.getByRole("button", { name: /trang 2/i }));
28
+ expect(onChange).toHaveBeenCalledWith(2, 10);
29
+ });
30
+
31
+ it("renders simple mode", () => {
32
+ renderWithUi(<Pagination simple current={2} total={50} pageSize={10} />);
33
+ expect(screen.getByText("2 / 5")).toBeInTheDocument();
34
+ });
35
+ });
36
+
37
+ describe("Steps", () => {
38
+ it("renders step titles", () => {
39
+ renderWithUi(
40
+ <Steps
41
+ current={1}
42
+ items={[{ title: "Tạo đơn" }, { title: "Thanh toán" }, { title: "Giao hàng" }]}
43
+ />,
44
+ );
45
+ expect(screen.getByText("Tạo đơn")).toBeInTheDocument();
46
+ expect(screen.getByText("Thanh toán")).toBeInTheDocument();
47
+ });
48
+
49
+ it("calls onChange when clickable", async () => {
50
+ const user = userEvent.setup();
51
+ const onChange = vi.fn();
52
+ renderWithUi(
53
+ <Steps current={0} onChange={onChange} items={[{ title: "A" }, { title: "B" }]} />,
54
+ );
55
+ await user.click(screen.getByText("B"));
56
+ expect(onChange).toHaveBeenCalledWith(1);
57
+ });
58
+ });
59
+
60
+ describe("TabsItems", () => {
61
+ it("switches panels via items API", async () => {
62
+ const user = userEvent.setup();
63
+ renderWithUi(
64
+ <TabsItems
65
+ defaultValue="orders"
66
+ items={[
67
+ { key: "orders", label: "Đơn hàng", children: "Panel đơn" },
68
+ { key: "shipments", label: "Kiện hàng", children: "Panel kiện" },
69
+ ]}
70
+ />,
71
+ );
72
+ expect(screen.getByText("Panel đơn")).toBeInTheDocument();
73
+ await user.click(screen.getByRole("tab", { name: "Kiện hàng" }));
74
+ expect(screen.getByText("Panel kiện")).toBeInTheDocument();
75
+ });
76
+ });
@@ -0,0 +1,55 @@
1
+ import { CalendarDays } from "lucide-react";
2
+ import { APP_DATE_FORMAT_OPTIONS, getDateFormatLabel } from "../../app/date-format-labels";
3
+ import { useOptionalAppContext } from "../../app/app-provider";
4
+ import { useTranslation } from "../../i18n/use-translation";
5
+ import { cn } from "../../lib/utils";
6
+ import type { DateFormatPickerProp } from "../../props/components/app.prop";
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from "../data-entry/select";
14
+
15
+ export type {
16
+ DateFormatPickerProp,
17
+ DateFormatPickerProp as DateFormatPickerProps,
18
+ } from "../../props/components/app.prop";
19
+
20
+ export function DateFormatPicker({
21
+ className,
22
+ disabled,
23
+ id,
24
+ value,
25
+ onChange,
26
+ }: DateFormatPickerProp) {
27
+ const ctx = useOptionalAppContext();
28
+ const { t, locale, fallbackLocale } = useTranslation();
29
+ const current = value ?? ctx?.dateFormat;
30
+ const handleChange = onChange ?? ctx?.setDateFormat;
31
+
32
+ if (current === undefined || !handleChange) {
33
+ throw new Error("DateFormatPicker requires <AppProvider> or controlled value + onChange");
34
+ }
35
+
36
+ return (
37
+ <Select value={current} onValueChange={handleChange} disabled={disabled}>
38
+ <SelectTrigger
39
+ id={id}
40
+ className={cn("w-full sm:w-44", className)}
41
+ aria-label={t("navigation.dateFormatPicker.ariaLabel")}
42
+ >
43
+ <CalendarDays className="mr-2 size-4 shrink-0 opacity-70" aria-hidden="true" />
44
+ <SelectValue />
45
+ </SelectTrigger>
46
+ <SelectContent>
47
+ {APP_DATE_FORMAT_OPTIONS.map((option) => (
48
+ <SelectItem key={option.value} value={option.value}>
49
+ {getDateFormatLabel(option.value, locale, fallbackLocale)}
50
+ </SelectItem>
51
+ ))}
52
+ </SelectContent>
53
+ </Select>
54
+ );
55
+ }
@@ -0,0 +1,190 @@
1
+ import * as React from "react";
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
3
+ import { Check, ChevronRight } from "lucide-react";
4
+ import { cn } from "../../lib/utils";
5
+
6
+ export function DropdownMenu(props: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
7
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
8
+ }
9
+
10
+ export function DropdownMenuTrigger(
11
+ props: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>,
12
+ ) {
13
+ return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
14
+ }
15
+
16
+ export function DropdownMenuPortal(
17
+ props: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>,
18
+ ) {
19
+ return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
20
+ }
21
+
22
+ export function DropdownMenuGroup(props: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
23
+ return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
24
+ }
25
+
26
+ export function DropdownMenuRadioGroup(
27
+ props: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>,
28
+ ) {
29
+ return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
30
+ }
31
+
32
+ export function DropdownMenuSub(props: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
33
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
34
+ }
35
+
36
+ export const DropdownMenuContent = React.forwardRef<
37
+ React.ComponentRef<typeof DropdownMenuPrimitive.Content>,
38
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
39
+ >(({ className, sideOffset = 4, ...props }, ref) => (
40
+ <DropdownMenuPortal>
41
+ <DropdownMenuPrimitive.Content
42
+ ref={ref}
43
+ data-slot="dropdown-menu-content"
44
+ sideOffset={sideOffset}
45
+ className={cn(
46
+ "bg-popover text-popover-foreground data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-32 origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
47
+ className,
48
+ )}
49
+ {...props}
50
+ />
51
+ </DropdownMenuPortal>
52
+ ));
53
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
54
+
55
+ export const DropdownMenuItem = React.forwardRef<
56
+ React.ComponentRef<typeof DropdownMenuPrimitive.Item>,
57
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
58
+ inset?: boolean;
59
+ variant?: "default" | "destructive";
60
+ }
61
+ >(({ className, inset, variant = "default", ...props }, ref) => (
62
+ <DropdownMenuPrimitive.Item
63
+ ref={ref}
64
+ data-slot="dropdown-menu-item"
65
+ data-inset={inset}
66
+ data-variant={variant}
67
+ className={cn(
68
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset=true]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
69
+ className,
70
+ )}
71
+ {...props}
72
+ />
73
+ ));
74
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
75
+
76
+ export const DropdownMenuLabel = React.forwardRef<
77
+ React.ComponentRef<typeof DropdownMenuPrimitive.Label>,
78
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }
79
+ >(({ className, inset, ...props }, ref) => (
80
+ <DropdownMenuPrimitive.Label
81
+ ref={ref}
82
+ data-slot="dropdown-menu-label"
83
+ data-inset={inset}
84
+ className={cn("px-2 py-1.5 text-sm font-semibold data-[inset=true]:ps-8", className)}
85
+ {...props}
86
+ />
87
+ ));
88
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
89
+
90
+ export const DropdownMenuSeparator = React.forwardRef<
91
+ React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,
92
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
93
+ >(({ className, ...props }, ref) => (
94
+ <DropdownMenuPrimitive.Separator
95
+ ref={ref}
96
+ data-slot="dropdown-menu-separator"
97
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
98
+ {...props}
99
+ />
100
+ ));
101
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
102
+
103
+ export const DropdownMenuCheckboxItem = React.forwardRef<
104
+ React.ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
105
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
106
+ >(({ className, children, checked, ...props }, ref) => (
107
+ <DropdownMenuPrimitive.CheckboxItem
108
+ ref={ref}
109
+ data-slot="dropdown-menu-checkbox-item"
110
+ className={cn(
111
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 ps-8 pe-2 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
112
+ className,
113
+ )}
114
+ checked={checked}
115
+ {...props}
116
+ >
117
+ <span className="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center">
118
+ <DropdownMenuPrimitive.ItemIndicator>
119
+ <Check className="size-4" aria-hidden="true" />
120
+ </DropdownMenuPrimitive.ItemIndicator>
121
+ </span>
122
+ {children}
123
+ </DropdownMenuPrimitive.CheckboxItem>
124
+ ));
125
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
126
+
127
+ export const DropdownMenuRadioItem = React.forwardRef<
128
+ React.ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
129
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
130
+ >(({ className, children, ...props }, ref) => (
131
+ <DropdownMenuPrimitive.RadioItem
132
+ ref={ref}
133
+ data-slot="dropdown-menu-radio-item"
134
+ className={cn(
135
+ "focus:bg-accent focus:text-accent-foreground data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary relative flex cursor-default items-center gap-2 rounded-sm px-2.5 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[state=checked]:font-medium [&_svg]:pointer-events-none [&_svg]:shrink-0",
136
+ className,
137
+ )}
138
+ {...props}
139
+ >
140
+ {children}
141
+ </DropdownMenuPrimitive.RadioItem>
142
+ ));
143
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
144
+
145
+ export const DropdownMenuSubTrigger = React.forwardRef<
146
+ React.ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
147
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }
148
+ >(({ className, inset, children, ...props }, ref) => (
149
+ <DropdownMenuPrimitive.SubTrigger
150
+ ref={ref}
151
+ data-slot="dropdown-menu-sub-trigger"
152
+ data-inset={inset}
153
+ className={cn(
154
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[inset=true]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
155
+ className,
156
+ )}
157
+ {...props}
158
+ >
159
+ {children}
160
+ <ChevronRight className="ms-auto size-4" aria-hidden="true" />
161
+ </DropdownMenuPrimitive.SubTrigger>
162
+ ));
163
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
164
+
165
+ export const DropdownMenuSubContent = React.forwardRef<
166
+ React.ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
167
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
168
+ >(({ className, ...props }, ref) => (
169
+ <DropdownMenuPrimitive.SubContent
170
+ ref={ref}
171
+ data-slot="dropdown-menu-sub-content"
172
+ className={cn(
173
+ "bg-popover text-popover-foreground data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-32 origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-lg",
174
+ className,
175
+ )}
176
+ {...props}
177
+ />
178
+ ));
179
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
180
+
181
+ export const DropdownMenuShortcut = ({
182
+ className,
183
+ ...props
184
+ }: React.HTMLAttributes<HTMLSpanElement>) => (
185
+ <span
186
+ data-slot="dropdown-menu-shortcut"
187
+ className={cn("text-muted-foreground ms-auto text-xs tracking-widest", className)}
188
+ {...props}
189
+ />
190
+ );
@@ -0,0 +1,38 @@
1
+ import { X } from "lucide-react";
2
+
3
+ import { useTranslation } from "../../i18n/use-translation";
4
+ import { Button } from "../general/button";
5
+ import { cn } from "../../lib/utils";
6
+ import type { FilterBarProp, FilterGroupProp } from "../../props/components/navigation.prop";
7
+
8
+ export type { FilterBarProp, FilterGroupProp } from "../../props/components/navigation.prop";
9
+
10
+ export function FilterBar({
11
+ onClear,
12
+ hasActiveFilters = true,
13
+ className,
14
+ children,
15
+ }: FilterBarProp) {
16
+ const { t } = useTranslation();
17
+
18
+ return (
19
+ <div className={cn("ui-filter-bar", className)}>
20
+ {children}
21
+ {onClear && hasActiveFilters && (
22
+ <Button variant="ghost" size="sm" onClick={onClear} className="ui-filter-clear">
23
+ <X aria-hidden="true" />
24
+ {t("common.clearFilters")}
25
+ </Button>
26
+ )}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ export function FilterGroup({ label, className, children }: FilterGroupProp) {
32
+ return (
33
+ <div className={cn("ui-stack-xs ui-filter-group", className)}>
34
+ <div className="ui-filter-label">{label}</div>
35
+ {children}
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,28 @@
1
+ export { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
2
+ export { TabsItems } from "./tabs-items";
3
+ export { Pagination } from "./pagination";
4
+ export { Steps } from "./steps";
5
+ export { FilterBar, FilterGroup } from "./filter-bar";
6
+ export { PageHeader } from "./page-header";
7
+ export { LocalePicker } from "./locale-picker";
8
+ export { TimezonePicker } from "./timezone-picker";
9
+ export { TimeFormatPicker } from "./time-format-picker";
10
+ export { DateFormatPicker } from "./date-format-picker";
11
+ export type { BreadcrumbItem } from "./page-header";
12
+ export {
13
+ DropdownMenu,
14
+ DropdownMenuCheckboxItem,
15
+ DropdownMenuContent,
16
+ DropdownMenuGroup,
17
+ DropdownMenuItem,
18
+ DropdownMenuLabel,
19
+ DropdownMenuPortal,
20
+ DropdownMenuRadioGroup,
21
+ DropdownMenuRadioItem,
22
+ DropdownMenuSeparator,
23
+ DropdownMenuShortcut,
24
+ DropdownMenuSub,
25
+ DropdownMenuSubContent,
26
+ DropdownMenuSubTrigger,
27
+ DropdownMenuTrigger,
28
+ } from "./dropdown-menu";
@@ -0,0 +1,49 @@
1
+ import { Languages } from "lucide-react";
2
+ import { APP_LOCALES } from "../../app/types";
3
+ import { useOptionalAppContext } from "../../app/app-provider";
4
+ import { useTranslation } from "../../i18n/use-translation";
5
+ import { cn } from "../../lib/utils";
6
+ import type { LocalePickerProp } from "../../props/components/app.prop";
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from "../data-entry/select";
14
+
15
+ export type {
16
+ LocalePickerProp,
17
+ LocalePickerProp as LocalePickerProps,
18
+ } from "../../props/components/app.prop";
19
+
20
+ export function LocalePicker({ className, disabled, id, value, onChange }: LocalePickerProp) {
21
+ const ctx = useOptionalAppContext();
22
+ const { t } = useTranslation();
23
+ const current = value ?? ctx?.locale;
24
+ const handleChange = onChange ?? ctx?.setLocale;
25
+
26
+ if (current === undefined || !handleChange) {
27
+ throw new Error("LocalePicker requires <AppProvider> or controlled value + onChange");
28
+ }
29
+
30
+ return (
31
+ <Select value={current} onValueChange={handleChange} disabled={disabled}>
32
+ <SelectTrigger
33
+ id={id}
34
+ className={cn("w-full sm:w-40", className)}
35
+ aria-label={t("navigation.localePicker.ariaLabel")}
36
+ >
37
+ <Languages className="mr-2 size-4 shrink-0 opacity-70" aria-hidden="true" />
38
+ <SelectValue />
39
+ </SelectTrigger>
40
+ <SelectContent>
41
+ {APP_LOCALES.map((code) => (
42
+ <SelectItem key={code} value={code}>
43
+ {t(`locale.${code}`)}
44
+ </SelectItem>
45
+ ))}
46
+ </SelectContent>
47
+ </Select>
48
+ );
49
+ }
@@ -0,0 +1,50 @@
1
+ /** @deprecated Prefer PageContainer. Header-only legacy shell. */
2
+ /* eslint-disable @typescript-eslint/no-deprecated -- legacy component intentionally uses PageHeaderProp */
3
+ import { Link } from "react-router-dom";
4
+ import { ChevronRight } from "lucide-react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+ import type { PageHeaderProp } from "../../props/components/layout.prop";
8
+
9
+ export type { PageHeaderProp } from "../../props/components/layout.prop";
10
+ export type { BreadcrumbItemProp as BreadcrumbItem } from "../../props/vocabulary/navigation.prop";
11
+
12
+ export function PageHeader({ title, description, breadcrumb, actions, className }: PageHeaderProp) {
13
+ return (
14
+ <header className={cn("ui-page-header", className)}>
15
+ {breadcrumb && breadcrumb.length > 0 && (
16
+ <nav aria-label="Breadcrumb" className="ui-breadcrumb">
17
+ <ol className="ui-breadcrumb-list">
18
+ {breadcrumb.map((item, i) => {
19
+ const isLast = i === breadcrumb.length - 1;
20
+ return (
21
+ <li key={i} className="ui-inline-xs">
22
+ {item.to && !isLast ? (
23
+ <Link to={item.to} className="hover:text-foreground hover:underline">
24
+ {item.label}
25
+ </Link>
26
+ ) : (
27
+ <span
28
+ className={isLast ? "text-foreground" : ""}
29
+ aria-current={isLast ? "page" : undefined}
30
+ >
31
+ {item.label}
32
+ </span>
33
+ )}
34
+ {!isLast && <ChevronRight className="size-3" aria-hidden="true" />}
35
+ </li>
36
+ );
37
+ })}
38
+ </ol>
39
+ </nav>
40
+ )}
41
+ <div className="ui-page-header-row">
42
+ <div className="min-w-0">
43
+ <h1 className="ui-page-title">{title}</h1>
44
+ {description && <p className="ui-page-subtitle">{description}</p>}
45
+ </div>
46
+ {actions && <div className="ui-inline-sm shrink-0">{actions}</div>}
47
+ </div>
48
+ </header>
49
+ );
50
+ }
@@ -0,0 +1,35 @@
1
+ /** Build visible page numbers with ellipsis — Ant Design pagination style. */
2
+ export function buildPageRange(
3
+ current: number,
4
+ totalPages: number,
5
+ siblingCount = 1,
6
+ ): (number | "ellipsis")[] {
7
+ if (totalPages <= 1) return totalPages === 1 ? [1] : [];
8
+
9
+ const totalNumbers = siblingCount * 2 + 5;
10
+ if (totalPages <= totalNumbers) {
11
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
12
+ }
13
+
14
+ const left = Math.max(current - siblingCount, 1);
15
+ const right = Math.min(current + siblingCount, totalPages);
16
+ const showLeftEllipsis = left > 2;
17
+ const showRightEllipsis = right < totalPages - 1;
18
+
19
+ const range: (number | "ellipsis")[] = [];
20
+ range.push(1);
21
+
22
+ if (showLeftEllipsis) range.push("ellipsis");
23
+ else for (let i = 2; i < left; i++) range.push(i);
24
+
25
+ for (let i = left; i <= right; i++) {
26
+ if (i !== 1 && i !== totalPages) range.push(i);
27
+ }
28
+
29
+ if (showRightEllipsis) range.push("ellipsis");
30
+ else for (let i = right + 1; i < totalPages; i++) range.push(i);
31
+
32
+ if (totalPages > 1) range.push(totalPages);
33
+
34
+ return range;
35
+ }