@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,39 @@
1
+ import type { AppTimeFormat } from "./time-formats";
2
+ import type { AppDateFormat } from "./date-formats";
3
+
4
+ /** Supported UI locales — sent as `x-locale` to backend. */
5
+ export type AppLocale = "vi" | "en" | "ja";
6
+
7
+ /** IANA timezone identifier — sent as `x-timezone` to backend. */
8
+ export type AppTimezone = string;
9
+
10
+ /** How to resolve the initial timezone when nothing is stored. */
11
+ export type AppTimezoneDefault = "browser" | "system" | (AppTimezone & {});
12
+
13
+ export type { AppTimeFormat } from "./time-formats";
14
+ export type { AppDateFormat } from "./date-formats";
15
+ export {
16
+ APP_TIME_FORMATS,
17
+ APP_REQUEST_HEADER_TIME_FORMAT,
18
+ getTimePattern,
19
+ isAppTimeFormat,
20
+ } from "./time-formats";
21
+ export {
22
+ APP_DATE_FORMATS,
23
+ APP_REQUEST_HEADER_DATE_FORMAT,
24
+ getDatePattern,
25
+ getDateTimePattern,
26
+ isAppDateFormat,
27
+ } from "./date-formats";
28
+
29
+ export const APP_LOCALES = ["vi", "en", "ja"] as const satisfies readonly AppLocale[];
30
+
31
+ export const APP_REQUEST_HEADER_LOCALE = "x-locale" as const;
32
+ export const APP_REQUEST_HEADER_TIMEZONE = "x-timezone" as const;
33
+
34
+ export type AppRequestHeaders = {
35
+ "x-locale": AppLocale;
36
+ "x-timezone": AppTimezone;
37
+ "x-time-format": AppTimeFormat;
38
+ "x-date-format": AppDateFormat;
39
+ };
@@ -0,0 +1,47 @@
1
+ import { useMemo } from "react";
2
+ import { formatDate, type FormatDateOptions } from "../lib/datetime";
3
+ import { useAppContext } from "./app-provider";
4
+
5
+ /** Date/time formatters bound to the current AppProvider preferences. */
6
+ export function useFormatting() {
7
+ const { locale, timezone, timeFormat, dateFormat, dateFnsLocale } = useAppContext();
8
+
9
+ const defaults = useMemo(
10
+ () => ({ locale: dateFnsLocale, timezone, timeFormat, dateFormat }),
11
+ [dateFnsLocale, timezone, timeFormat, dateFormat],
12
+ );
13
+
14
+ const bind = useMemo(
15
+ () => (value: Parameters<typeof formatDate>[0], options?: FormatDateOptions) =>
16
+ formatDate(value, { ...defaults, ...options }),
17
+ [defaults],
18
+ );
19
+
20
+ return useMemo(
21
+ () => ({
22
+ locale,
23
+ timezone,
24
+ timeFormat,
25
+ dateFormat,
26
+ /** Primary formatter — auto-detect + AppProvider defaults. */
27
+ format: bind,
28
+ formatDate: bind,
29
+ formatCalendarDate: (value: Date | null | undefined, options?: FormatDateOptions) =>
30
+ formatDate(value, { ...defaults, ...options, kind: "calendar" }),
31
+ formatTime: (value: Parameters<typeof formatDate>[0], options?: FormatDateOptions) =>
32
+ formatDate(value, { ...defaults, ...options, kind: "time" }),
33
+ formatDateTime: (value: Parameters<typeof formatDate>[0], options?: FormatDateOptions) =>
34
+ formatDate(value, { ...defaults, ...options, kind: "datetime" }),
35
+ formatDateLong: (value: Parameters<typeof formatDate>[0], options?: FormatDateOptions) =>
36
+ formatDate(value, { ...defaults, ...options, kind: "long" }),
37
+ formatRelative: (value: Parameters<typeof formatDate>[0], options?: FormatDateOptions) =>
38
+ formatDate(value, { ...defaults, ...options, kind: "relative" }),
39
+ formatTimeOfDay: (value: Parameters<typeof formatDate>[0], options?: FormatDateOptions) =>
40
+ formatDate(value, { ...defaults, ...options, kind: "time" }),
41
+ }),
42
+ [locale, timezone, timeFormat, dateFormat, defaults, bind],
43
+ );
44
+ }
45
+
46
+ /** Alias for useFormatting — emphasises timezone-aware datetime helpers. */
47
+ export const useDateTime = useFormatting;
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderWithUi, screen } from "@/test/render";
3
+ import { Checkbox } from "../data-entry/checkbox";
4
+ import { Command, CommandInput } from "../data-entry/command";
5
+ import { Radio } from "../data-entry/radio";
6
+ import { Select, SelectTrigger, SelectValue } from "../data-entry/select";
7
+ import { Dialog, DialogContent, DialogDescription, DialogTitle } from "../feedback/dialog";
8
+ import { Sheet, SheetContent, SheetDescription, SheetTitle } from "../feedback/sheet";
9
+
10
+ function expectDecorativeIconsHidden(container: HTMLElement) {
11
+ container.querySelectorAll("svg").forEach((icon) => {
12
+ expect(icon).toHaveAttribute("aria-hidden", "true");
13
+ });
14
+ }
15
+
16
+ describe("primitive accessibility", () => {
17
+ it("hides decorative icons from accessibility tree", () => {
18
+ const { baseElement } = renderWithUi(
19
+ <>
20
+ <Select>
21
+ <SelectTrigger aria-label="Hub">
22
+ <SelectValue placeholder="Hub" />
23
+ </SelectTrigger>
24
+ </Select>
25
+ <Checkbox defaultChecked aria-label="Accept" />
26
+ <Radio.Root defaultValue="air">
27
+ <Radio.Item value="air" aria-label="Air" />
28
+ </Radio.Root>
29
+ <Command>
30
+ <CommandInput aria-label="Search" />
31
+ </Command>
32
+ </>,
33
+ );
34
+
35
+ expectDecorativeIconsHidden(baseElement);
36
+ });
37
+
38
+ it("keeps dialog close button named while hiding close icon", () => {
39
+ const { baseElement } = renderWithUi(
40
+ <Dialog open onOpenChange={() => undefined}>
41
+ <DialogContent>
42
+ <DialogTitle>Dialog title</DialogTitle>
43
+ <DialogDescription>Dialog description</DialogDescription>
44
+ </DialogContent>
45
+ </Dialog>,
46
+ );
47
+
48
+ expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
49
+ expectDecorativeIconsHidden(baseElement);
50
+ });
51
+
52
+ it("keeps sheet close button named while hiding close icon", () => {
53
+ const { baseElement } = renderWithUi(
54
+ <Sheet open onOpenChange={() => undefined}>
55
+ <SheetContent>
56
+ <SheetTitle>Sheet title</SheetTitle>
57
+ <SheetDescription>Sheet description</SheetDescription>
58
+ </SheetContent>
59
+ </Sheet>,
60
+ );
61
+
62
+ expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
63
+ expectDecorativeIconsHidden(baseElement);
64
+ });
65
+ });
@@ -0,0 +1,41 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const root = process.cwd();
6
+
7
+ function read(path: string) {
8
+ return readFileSync(join(root, path), "utf8");
9
+ }
10
+
11
+ describe("docs and story parity", () => {
12
+ it("documents shadcn-compatible compound exports", () => {
13
+ expect(read("docs/primitives/data-entry/select/index.md")).toContain("SelectLabel");
14
+ expect(read("docs/primitives/data-entry/select/index.md")).toContain("SelectSeparator");
15
+
16
+ const dropdownDocs = read("docs/primitives/navigation/dropdown-menu/index.md");
17
+ expect(dropdownDocs).toContain("DropdownMenuCheckboxItem");
18
+ expect(dropdownDocs).toContain("DropdownMenuRadioItem");
19
+ expect(dropdownDocs).toContain("DropdownMenuShortcut");
20
+
21
+ const popoverDocs = read("docs/primitives/data-display/popover/index.md");
22
+ expect(popoverDocs).toContain("PopoverHeader");
23
+ expect(popoverDocs).toContain("PopoverTitle");
24
+ expect(popoverDocs).toContain("PopoverDescription");
25
+
26
+ expect(read("docs/primitives/feedback/sheet/index.md")).toContain("SheetFooter");
27
+ expect(read("docs/primitives/navigation/tabs/index.md")).toContain("variant");
28
+ });
29
+
30
+ it("has preview examples for new shadcn parity surfaces", () => {
31
+ expect(
32
+ read("docs/primitives/navigation/dropdown-menu/examples/checked-radio-items.tsx"),
33
+ ).toContain("DropdownMenuCheckboxItem");
34
+ expect(read("docs/primitives/navigation/tabs/examples/line-variant.tsx")).toContain(
35
+ 'variant="line"',
36
+ );
37
+ expect(read("docs/primitives/data-display/popover/examples/shipment-summary.tsx")).toContain(
38
+ "PopoverHeader",
39
+ );
40
+ });
41
+ });
@@ -0,0 +1,71 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const root = process.cwd();
6
+
7
+ function readJson<T>(path: string): T {
8
+ return JSON.parse(readFileSync(join(root, path), "utf8")) as T;
9
+ }
10
+
11
+ describe("shadcn release guardrails", () => {
12
+ it("keeps components.json aligned to the package UI bridge", () => {
13
+ const config = readJson<{
14
+ aliases?: Record<string, string>;
15
+ tailwind?: { css?: string; cssVariables?: boolean };
16
+ iconLibrary?: string;
17
+ }>("components.json");
18
+
19
+ expect(config.aliases?.ui).toBe("@/components/ui");
20
+ expect(config.aliases?.utils).toBe("@/lib/utils");
21
+ expect(config.tailwind?.css).toBe("src/styles/index.css");
22
+ expect(config.tailwind?.cssVariables).toBe(true);
23
+ expect(config.iconLibrary).toBe("lucide");
24
+ });
25
+
26
+ it("exports shadcn-compatible entrypoints from package.json", () => {
27
+ const pkg = readJson<{ exports?: Record<string, string>; scripts?: Record<string, string> }>(
28
+ "package.json",
29
+ );
30
+ const exports = pkg.exports ?? {};
31
+
32
+ expect(exports["./ui"]).toBe("./src/components/ui/index.tsx");
33
+ for (const key of [
34
+ "button",
35
+ "select",
36
+ "checkbox",
37
+ "radio",
38
+ "switch",
39
+ "slider",
40
+ "dialog",
41
+ "sheet",
42
+ "popover",
43
+ "dropdown-menu",
44
+ "tabs",
45
+ "command",
46
+ ]) {
47
+ expect(exports[`./ui/${key}`], `missing ./ui/${key}`).toBeDefined();
48
+ }
49
+ expect(pkg.scripts?.["verify:release"]).toContain("preview:build");
50
+ });
51
+
52
+ it("keeps bridge files present for installed shadcn components", () => {
53
+ for (const file of [
54
+ "button",
55
+ "select",
56
+ "checkbox",
57
+ "radio",
58
+ "switch",
59
+ "slider",
60
+ "dialog",
61
+ "sheet",
62
+ "popover",
63
+ "dropdown-menu",
64
+ "tabs",
65
+ "command",
66
+ "index",
67
+ ]) {
68
+ expect(existsSync(join(root, `src/components/ui/${file}.tsx`)), `${file}.tsx`).toBe(true);
69
+ }
70
+ });
71
+ });
@@ -0,0 +1,242 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+ import { screen } from "@testing-library/react";
6
+ import { Button } from "../general/button";
7
+ import { Input } from "../data-entry/input";
8
+ import { Textarea } from "../data-entry/textarea";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "../data-entry/select";
16
+ import { Checkbox } from "../data-entry/checkbox";
17
+ import { Switch } from "../data-entry/switch";
18
+ import { Badge } from "../data-display/badge";
19
+ import { StatusBadge } from "../data-display/status-badge";
20
+ import {
21
+ Table,
22
+ TableBody,
23
+ TableCell,
24
+ TableHead,
25
+ TableHeader,
26
+ TableRow,
27
+ } from "../data-display/table";
28
+ import { Pagination } from "../navigation/pagination";
29
+ import { Calendar } from "../data-entry/calendar";
30
+ import { renderWithTheme } from "@/test/theme-test-utils";
31
+ import {
32
+ controlMultilineClass,
33
+ tableRowHeightClass,
34
+ toneInfoClass,
35
+ toneSuccessClass,
36
+ toneWarningClass,
37
+ } from "@/lib/control-styles";
38
+
39
+ const componentsDir = join(dirname(fileURLToPath(import.meta.url)), "..");
40
+
41
+ function readComponent(relative: string): string {
42
+ return readFileSync(join(componentsDir, relative), "utf8");
43
+ }
44
+
45
+ function statusBadgeRoot(label: string): HTMLElement {
46
+ const text = screen.getByText(label);
47
+ const root = text.parentElement;
48
+ expect(root).toBeTruthy();
49
+ return root!;
50
+ }
51
+
52
+ describe("theme axes integration (render + class contracts)", () => {
53
+ describe("density — ui-control on interactive primitives", () => {
54
+ it("Button default size uses semantic button size class", () => {
55
+ renderWithTheme(<Button>Save</Button>);
56
+ expect(screen.getByRole("button", { name: "Save" })).toHaveClass("ui-button--default-size");
57
+ });
58
+
59
+ it("Input uses ui-control", () => {
60
+ renderWithTheme(<Input aria-label="HAWB" />);
61
+ expect(screen.getByRole("textbox", { name: "HAWB" })).toHaveClass("ui-control");
62
+ });
63
+
64
+ it("Textarea uses ui-control-multiline", () => {
65
+ renderWithTheme(<Textarea aria-label="Notes" />);
66
+ expect(screen.getByRole("textbox", { name: "Notes" })).toHaveClass("ui-control-multiline");
67
+ expect(controlMultilineClass.split(" ")[0]).toBe("ui-control-multiline");
68
+ });
69
+
70
+ it("SelectTrigger uses ui-control flex layout", () => {
71
+ renderWithTheme(
72
+ <Select defaultValue="a">
73
+ <SelectTrigger aria-label="Hub">
74
+ <SelectValue />
75
+ </SelectTrigger>
76
+ <SelectContent>
77
+ <SelectItem value="a">A</SelectItem>
78
+ </SelectContent>
79
+ </Select>,
80
+ );
81
+ const trigger = screen.getByRole("combobox", { name: "Hub" });
82
+ expect(trigger).toHaveClass("ui-control");
83
+ expect(trigger.className).toContain("flex");
84
+ });
85
+
86
+ it.each(["compact", "default", "comfortable"] as const)(
87
+ "renders Input under ui-density-%s",
88
+ (density) => {
89
+ renderWithTheme(<Input aria-label={`field-${density}`} />, { density });
90
+ expect(screen.getByRole("textbox", { name: `field-${density}` })).toHaveClass("ui-control");
91
+ },
92
+ );
93
+ });
94
+
95
+ describe("semantic badges — token tone classes (no raw palette)", () => {
96
+ it("Badge success variant uses success token", () => {
97
+ renderWithTheme(<Badge variant="success">Cleared</Badge>);
98
+ const el = screen.getByText("Cleared");
99
+ expect(el.className).toContain("success");
100
+ expect(el.className).not.toMatch(/green-/);
101
+ });
102
+
103
+ it("StatusBadge pending uses warning token", () => {
104
+ renderWithTheme(<StatusBadge status="pending" />);
105
+ const el = statusBadgeRoot("Chờ xử lý");
106
+ expect(el.className).toContain("warning");
107
+ expect(el.className).not.toMatch(/amber-/);
108
+ });
109
+
110
+ it("StatusBadge scheduled uses info token", () => {
111
+ renderWithTheme(<StatusBadge status="scheduled" />);
112
+ const el = statusBadgeRoot("Đã lên lịch");
113
+ expect(el.className).toContain("info");
114
+ expect(el.className).not.toMatch(/blue-/);
115
+ });
116
+
117
+ it("StatusBadge active uses success token", () => {
118
+ renderWithTheme(<StatusBadge status="active" />);
119
+ const el = statusBadgeRoot("Đang hoạt động");
120
+ expect(el.className).toContain("success");
121
+ });
122
+
123
+ it("tone class constants match expected token names", () => {
124
+ expect(toneSuccessClass).toContain("success");
125
+ expect(toneWarningClass).toContain("warning");
126
+ expect(toneInfoClass).toContain("info");
127
+ });
128
+ });
129
+
130
+ describe("table density tokens", () => {
131
+ it("TableHead references --table-row-height", () => {
132
+ renderWithTheme(
133
+ <Table>
134
+ <TableHeader>
135
+ <TableRow>
136
+ <TableHead>HAWB</TableHead>
137
+ </TableRow>
138
+ </TableHeader>
139
+ <TableBody>
140
+ <TableRow className={tableRowHeightClass}>
141
+ <TableCell>GX-1</TableCell>
142
+ </TableRow>
143
+ </TableBody>
144
+ </Table>,
145
+ );
146
+ expect(screen.getByRole("columnheader", { name: "HAWB" }).className).toContain(
147
+ "var(--table-row-height)",
148
+ );
149
+ });
150
+ });
151
+
152
+ describe("primary brand — checkbox/switch", () => {
153
+ it("Checkbox uses centralized semantic class", () => {
154
+ renderWithTheme(<Checkbox defaultChecked aria-label="ok" />);
155
+ expect(screen.getByRole("checkbox", { name: "ok" })).toHaveClass("ui-checkbox");
156
+ });
157
+
158
+ it("Switch uses centralized semantic class", () => {
159
+ renderWithTheme(<Switch defaultChecked aria-label="notify" />);
160
+ expect(screen.getByRole("switch", { name: "notify" })).toHaveClass("ui-switch");
161
+ });
162
+ });
163
+
164
+ describe("Preview-equivalent theme globals on wrapper", () => {
165
+ it("fontSize sm sets --font-size-sm on root", () => {
166
+ const { container } = renderWithTheme(<span>x</span>, { fontSize: "sm" });
167
+ const root = container.firstElementChild as HTMLElement;
168
+ expect(root.style.getPropertyValue("--font-size-sm")).toBe("0.8125rem");
169
+ });
170
+
171
+ it("fontSize lg sets --font-size-sm on root", () => {
172
+ const { container } = renderWithTheme(<span>x</span>, { fontSize: "lg" });
173
+ const root = container.firstElementChild as HTMLElement;
174
+ expect(root.style.getPropertyValue("--font-size-sm")).toBe("0.9375rem");
175
+ });
176
+
177
+ it("primaryColor logistics sets primary + accent + ring", () => {
178
+ const { container } = renderWithTheme(<span>x</span>, { primaryColor: "logistics" });
179
+ const root = container.firstElementChild as HTMLElement;
180
+ expect(root.style.getPropertyValue("--primary")).toBe("173 80% 36%");
181
+ expect(root.style.getPropertyValue("--accent")).toBe("173 80% 94%");
182
+ expect(root.style.getPropertyValue("--ring")).toBe("173 80% 36%");
183
+ });
184
+
185
+ it.each(["brand", "crm", "partner", "slate"] as const)(
186
+ "primaryColor %s sets matching accent foreground pair",
187
+ (primaryColor) => {
188
+ const { container } = renderWithTheme(<span>x</span>, { primaryColor });
189
+ const root = container.firstElementChild as HTMLElement;
190
+ const primary = root.style.getPropertyValue("--primary");
191
+ const accent = root.style.getPropertyValue("--accent");
192
+ expect(primary.length).toBeGreaterThan(0);
193
+ expect(accent.length).toBeGreaterThan(0);
194
+ expect(primary).not.toBe(accent);
195
+ },
196
+ );
197
+ });
198
+
199
+ describe("pagination + calendar avoid legacy hardcoded sizes", () => {
200
+ it("pagination prev button uses semantic compact icon class", () => {
201
+ renderWithTheme(
202
+ <Pagination current={2} total={100} pageSize={20} onChange={() => undefined} />,
203
+ );
204
+ const prev = screen.getByRole("button", { name: /trang trước/i });
205
+ expect(prev.className).not.toMatch(/\bsize-8\b/);
206
+ expect(prev).toHaveClass("ui-button--compact-icon");
207
+ });
208
+
209
+ it("calendar day button uses var(--control-height)", () => {
210
+ renderWithTheme(<Calendar mode="single" />);
211
+ const dayButton = screen.getByRole("grid").querySelector("button");
212
+ expect(dayButton?.className ?? "").toContain("var(--control-height)");
213
+ expect(dayButton?.className ?? "").not.toMatch(/\bsize-9\b/);
214
+ });
215
+ });
216
+ });
217
+
218
+ describe("theme axes — control-styles import contracts", () => {
219
+ const requiredImports: { file: string; exportName: string }[] = [
220
+ { file: "data-entry/select.tsx", exportName: "controlTriggerClass" },
221
+ { file: "data-entry/textarea.tsx", exportName: "controlMultilineClass" },
222
+ { file: "data-entry/command.tsx", exportName: "controlIconLeadingClass" },
223
+ { file: "data-entry/calendar.tsx", exportName: "controlIconSmClass" },
224
+ { file: "data-entry/color-picker.tsx", exportName: "controlIconClass" },
225
+ { file: "data-entry/upload.tsx", exportName: "controlIconClass" },
226
+ { file: "data-display/badge.tsx", exportName: "toneSuccessClass" },
227
+ { file: "data-display/table.tsx", exportName: "tableHeadHeightClass" },
228
+ { file: "data-display/data-table.tsx", exportName: "tableRowHeightClass" },
229
+ { file: "data-display/status-badge.tsx", exportName: "toneSuccessClass" },
230
+ { file: "navigation/steps.tsx", exportName: "controlIconClass" },
231
+ { file: "feedback/skeleton.tsx", exportName: "tableRowHeightClass" },
232
+ ];
233
+
234
+ it.each(requiredImports)(
235
+ "$file imports $exportName from control-styles",
236
+ ({ file, exportName }) => {
237
+ const content = readComponent(file);
238
+ expect(content, `${file} must import from control-styles`).toContain("control-styles");
239
+ expect(content, `${file} must use ${exportName}`).toContain(exportName);
240
+ },
241
+ );
242
+ });
@@ -0,0 +1,76 @@
1
+ // Backward-compatible admin barrel.
2
+ export type { BreadcrumbItemProp as BreadcrumbItem } from "../../props/vocabulary/navigation.prop";
3
+ export type {
4
+ PageContainerProp,
5
+ PageContainerProp as PageContainerProps,
6
+ } from "../../props/components/layout.prop";
7
+
8
+ export { PageHeader } from "../navigation/page-header";
9
+ /* eslint-disable-next-line @typescript-eslint/no-deprecated -- backward-compat export */
10
+ export type { PageHeaderProp } from "../../props/components/layout.prop";
11
+
12
+ export { PageContainer } from "../layout/page-container";
13
+ export { Stack, Inline } from "../layout";
14
+ export type { StackProp, InlineProp } from "../../props/components/layout.prop";
15
+
16
+ export { EmptyState } from "../data-display/empty-state";
17
+ export { StatusBadge } from "../data-display/status-badge";
18
+ export { FormField } from "../data-entry/form-field";
19
+ export { KeyValueGrid } from "../data-display/key-value-grid";
20
+ export { SkeletonRows, SkeletonTable, SkeletonDetail, SkeletonCard } from "../feedback/skeleton";
21
+ export {
22
+ DataState,
23
+ MutationFeedback,
24
+ QueryRefetchButton,
25
+ InfiniteQueryState,
26
+ flattenItemPages,
27
+ PrefetchLink,
28
+ } from "../query";
29
+ export {
30
+ Alert,
31
+ AlertTitle,
32
+ AlertContent,
33
+ AlertDescription,
34
+ AlertActions,
35
+ AlertQueryError,
36
+ } from "../feedback/alert";
37
+ export { SearchInput } from "../data-entry/search-input";
38
+ export {
39
+ Upload,
40
+ collectUploadCommitActions,
41
+ createUploadItem,
42
+ useUploadDraft,
43
+ } from "../data-entry/upload";
44
+ export { Cascader } from "../data-entry/cascader";
45
+ export { TreeSelect } from "../data-entry/tree-select";
46
+ export { Transfer } from "../data-entry/transfer";
47
+ export {
48
+ Pagination,
49
+ Steps,
50
+ TabsItems,
51
+ Tabs,
52
+ TabsContent,
53
+ TabsList,
54
+ TabsTrigger,
55
+ } from "../navigation";
56
+ export { FilterBar, FilterGroup } from "../navigation/filter-bar";
57
+ export {
58
+ Dialog,
59
+ DialogTrigger,
60
+ DialogContent,
61
+ DialogHeader,
62
+ DialogFooter,
63
+ DialogTitle,
64
+ DialogDescription,
65
+ DialogConfirm,
66
+ } from "../feedback/dialog";
67
+ export { DataTable } from "../data-display/data-table";
68
+ export type { ColumnDef, Density } from "../data-display/data-table";
69
+ export { Toaster } from "../feedback/sonner";
70
+ export { toast, useToast } from "../feedback/use-toast";
71
+ export type { LegacyToastOptions } from "../feedback/use-toast";
72
+ export { useDebouncedValue, useTimeoutFlag } from "../../lib/hooks";
73
+ export { formatDate, formatBytes, formatCurrency, shortId, humanError } from "../../lib/format";
74
+ /* eslint-disable @typescript-eslint/no-deprecated -- backward-compatible admin barrel re-exports legacy helpers. */
75
+ export { formatDateTime, formatDateLong, formatRelative } from "../../lib/format";
76
+ /* eslint-enable @typescript-eslint/no-deprecated */