@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,57 @@
1
+ import type { ComponentType, ReactNode, SVGProps } from "react";
2
+
3
+ import { Badge } from "../data-display/badge";
4
+
5
+ export type MobileFrameNavItem = {
6
+ label: string;
7
+ icon: ComponentType<SVGProps<SVGSVGElement>>;
8
+ active?: boolean;
9
+ };
10
+
11
+ export type MobileFrameProps = {
12
+ title: string;
13
+ subtitle?: string;
14
+ status?: string;
15
+ children: ReactNode;
16
+ navItems?: MobileFrameNavItem[];
17
+ };
18
+
19
+ export function MobileFrame({
20
+ title,
21
+ subtitle,
22
+ status,
23
+ children,
24
+ navItems = [],
25
+ }: MobileFrameProps) {
26
+ return (
27
+ <div className="ui-mobile-stage">
28
+ <div className="ui-mobile-frame">
29
+ <header className="ui-mobile-header">
30
+ <div>
31
+ <div className="ui-mobile-title">{title}</div>
32
+ {subtitle ? <div className="ui-mobile-subtitle">{subtitle}</div> : null}
33
+ </div>
34
+ {status ? <Badge variant="secondary">{status}</Badge> : null}
35
+ </header>
36
+ <main className="ui-mobile-main">{children}</main>
37
+ {navItems.length > 0 ? (
38
+ <footer className="ui-mobile-nav">
39
+ {navItems.map((item) => {
40
+ const Icon = item.icon;
41
+ return (
42
+ <div
43
+ className="ui-mobile-nav-item"
44
+ data-active={item.active ? "true" : undefined}
45
+ key={item.label}
46
+ >
47
+ <Icon aria-hidden="true" />
48
+ <span>{item.label}</span>
49
+ </div>
50
+ );
51
+ })}
52
+ </footer>
53
+ ) : null}
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,81 @@
1
+ /** PageContainer — mandatory shell for every admin page (Ant Design PageHeader equivalent). */
2
+ import { Link } from "react-router-dom";
3
+ import { ChevronRight } from "lucide-react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+ import { densityClass, pageContainerVariantClass } from "../../lib/variants";
7
+ import type { PageContainerProp } from "../../props/components/layout.prop";
8
+
9
+ export type {
10
+ PageContainerProp,
11
+ PageContainerProp as PageContainerProps,
12
+ } from "../../props/components/layout.prop";
13
+ export type {
14
+ BreadcrumbItemProp,
15
+ BreadcrumbItemProp as BreadcrumbItem,
16
+ } from "../../props/vocabulary/navigation.prop";
17
+
18
+ export function PageContainer({
19
+ title,
20
+ subtitle,
21
+ extra,
22
+ footer,
23
+ breadcrumb,
24
+ density = "default",
25
+ variant = "default",
26
+ stickyFooter = false,
27
+ children,
28
+ className,
29
+ }: PageContainerProp) {
30
+ return (
31
+ <div
32
+ className={cn(
33
+ "ui-page-container",
34
+ densityClass[density],
35
+ pageContainerVariantClass[variant],
36
+ stickyFooter && "ui-page-container--sticky-footer",
37
+ className,
38
+ )}
39
+ >
40
+ <header className="ui-page-header">
41
+ {breadcrumb && breadcrumb.length > 0 && (
42
+ <nav aria-label="Breadcrumb" className="ui-breadcrumb">
43
+ <ol className="ui-breadcrumb-list">
44
+ {breadcrumb.map((item, i) => {
45
+ const isLast = i === breadcrumb.length - 1;
46
+ return (
47
+ <li key={i} className="ui-inline-xs">
48
+ {item.to && !isLast ? (
49
+ <Link to={item.to} className="hover:text-foreground hover:underline">
50
+ {item.label}
51
+ </Link>
52
+ ) : (
53
+ <span
54
+ className={isLast ? "text-foreground" : ""}
55
+ aria-current={isLast ? "page" : undefined}
56
+ >
57
+ {item.label}
58
+ </span>
59
+ )}
60
+ {!isLast && <ChevronRight className="size-3" aria-hidden="true" />}
61
+ </li>
62
+ );
63
+ })}
64
+ </ol>
65
+ </nav>
66
+ )}
67
+ <div className="ui-page-header-row">
68
+ <div className="min-w-0">
69
+ <h1 className="ui-page-title">{title}</h1>
70
+ {subtitle && <p className="ui-page-subtitle">{subtitle}</p>}
71
+ </div>
72
+ {extra && <div className="ui-page-header-extra">{extra}</div>}
73
+ </div>
74
+ </header>
75
+
76
+ {children != null && <div className="ui-page-body">{children}</div>}
77
+
78
+ {footer && <footer className="ui-page-footer">{footer}</footer>}
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,16 @@
1
+ /** Horizontal padding aligned with PageContainer header — use inside `variant="flush"`. */
2
+ import { cn } from "../../lib/utils";
3
+ import type { PageInsetProp } from "../../props/components/layout.prop";
4
+
5
+ export type {
6
+ PageInsetProp,
7
+ PageInsetProp as PageInsetProps,
8
+ } from "../../props/components/layout.prop";
9
+
10
+ export function PageInset({ className, children, ...props }: PageInsetProp) {
11
+ return (
12
+ <div className={cn("ui-page-inset", className)} {...props}>
13
+ {children}
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type ResponsiveGridProps = {
4
+ columns?: 2 | 3 | 4;
5
+ children: ReactNode;
6
+ };
7
+
8
+ export function ResponsiveGrid({ columns = 3, children }: ResponsiveGridProps) {
9
+ return (
10
+ <div className="ui-responsive-grid" data-columns={columns}>
11
+ {children}
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,30 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { AppShell } from "./app-shell";
4
+ import { Topbar } from "./topbar";
5
+
6
+ export type ShellAppProps = {
7
+ menu: ReactNode;
8
+ breadcrumb?: ReactNode;
9
+ children: ReactNode;
10
+ };
11
+
12
+ export function ShellApp({ menu, breadcrumb, children }: ShellAppProps) {
13
+ return (
14
+ <AppShell
15
+ sidebar={menu}
16
+ breadcrumb={breadcrumb}
17
+ topbar={
18
+ <Topbar
19
+ product={{ name: "GodX", color: "hsl(var(--attention))" }}
20
+ project={{ name: "Japan to SEA lanes" }}
21
+ onSearchOpen={() => undefined}
22
+ onNotificationsOpen={() => undefined}
23
+ unread
24
+ />
25
+ }
26
+ >
27
+ {children}
28
+ </AppShell>
29
+ );
30
+ }
@@ -0,0 +1,98 @@
1
+ import { ChevronDown } from "lucide-react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import type { SidebarProp } from "../../props/components/layout.prop";
5
+
6
+ export type {
7
+ SidebarItemProp as SidebarItem,
8
+ SidebarProductProp as SidebarProduct,
9
+ SidebarProp,
10
+ SidebarProp as SidebarProps,
11
+ SidebarSectionProp as SidebarSection,
12
+ } from "../../props/components/layout.prop";
13
+
14
+ export function Sidebar({
15
+ activeId,
16
+ onSelect,
17
+ sections,
18
+ product,
19
+ onProductClick,
20
+ brand,
21
+ collapsed = false,
22
+ footer,
23
+ }: SidebarProp) {
24
+ return (
25
+ <div className="sb-root" data-collapsed={collapsed ? "true" : undefined}>
26
+ {brand !== undefined ? (
27
+ <div className="sb-brand">{brand}</div>
28
+ ) : product ? (
29
+ <button
30
+ type="button"
31
+ className="sb-product"
32
+ onClick={onProductClick}
33
+ aria-label={product.name}
34
+ >
35
+ <span
36
+ className="sb-logo-mark"
37
+ style={{ background: product.color ?? "hsl(var(--attention))" }}
38
+ >
39
+ {product.name[0]?.toUpperCase() ?? "?"}
40
+ </span>
41
+ {!collapsed && (
42
+ <span className="sb-product-meta">
43
+ <span className="sb-product-name">{product.name}</span>
44
+ {product.role ? <span className="sb-product-tenant">{product.role}</span> : null}
45
+ </span>
46
+ )}
47
+ {!collapsed && (
48
+ <span className="sb-product-caret">
49
+ <ChevronDown aria-hidden="true" />
50
+ </span>
51
+ )}
52
+ </button>
53
+ ) : null}
54
+
55
+ <div className="sb-nav-scroll">
56
+ {sections.map((section, sectionIndex) => (
57
+ <div className="sb-section" key={section.label ?? sectionIndex}>
58
+ {section.label && !collapsed ? (
59
+ <div className="sb-section-label">{section.label}</div>
60
+ ) : null}
61
+ <div className="sb-nav" role="navigation">
62
+ {section.items.map((item) => {
63
+ const Icon = item.icon;
64
+ const isActive = item.id === activeId;
65
+
66
+ return (
67
+ <button
68
+ type="button"
69
+ className={cn("sb-nav-item")}
70
+ data-active={isActive ? "true" : undefined}
71
+ aria-current={isActive ? "page" : undefined}
72
+ aria-disabled={item.disabled}
73
+ aria-label={collapsed ? item.label : undefined}
74
+ title={collapsed ? item.label : undefined}
75
+ key={item.id}
76
+ onClick={() => {
77
+ if (!item.disabled) onSelect?.(item.id);
78
+ }}
79
+ >
80
+ <span className="sb-icon">
81
+ <Icon aria-hidden="true" />
82
+ </span>
83
+ {!collapsed && <span className="sb-label">{item.label}</span>}
84
+ {!collapsed && item.badge !== undefined && item.badge !== "" ? (
85
+ <span className="sb-badge">{item.badge}</span>
86
+ ) : null}
87
+ </button>
88
+ );
89
+ })}
90
+ </div>
91
+ </div>
92
+ ))}
93
+ </div>
94
+
95
+ {footer ? <div className="sb-footer">{footer}</div> : null}
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type SplitPaneProps = {
4
+ children: ReactNode;
5
+ aside: ReactNode;
6
+ asideWidth?: "sm" | "md";
7
+ };
8
+
9
+ export function SplitPane({ children, aside, asideWidth = "md" }: SplitPaneProps) {
10
+ return (
11
+ <div className="ui-split-pane" data-aside-width={asideWidth}>
12
+ <div className="ui-split-pane-main">{children}</div>
13
+ <aside className="ui-split-pane-aside">{aside}</aside>
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,13 @@
1
+ import { cn } from "../../lib/utils";
2
+ import { stackGapClass } from "../../lib/variants";
3
+ import type { StackProp } from "../../props/components/layout.prop";
4
+
5
+ export type { StackProp, StackProp as StackProps } from "../../props/components/layout.prop";
6
+
7
+ export function Stack({ gap = "md", className, children, ...props }: StackProp) {
8
+ return (
9
+ <div className={cn(stackGapClass[gap], className)} {...props}>
10
+ {children}
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,108 @@
1
+ import {
2
+ Bell,
3
+ ChevronDown,
4
+ PanelLeftClose,
5
+ PanelLeftOpen,
6
+ Search,
7
+ SlidersHorizontal,
8
+ } from "lucide-react";
9
+
10
+ import type { TopbarProp } from "../../props/components/layout.prop";
11
+
12
+ export type {
13
+ TopbarProductProp as TopbarProduct,
14
+ TopbarProjectProp as TopbarProject,
15
+ TopbarProp,
16
+ TopbarProp as TopbarProps,
17
+ } from "../../props/components/layout.prop";
18
+
19
+ export function Topbar({
20
+ product,
21
+ project,
22
+ onProductOpen,
23
+ onProjectOpen,
24
+ onSearchOpen,
25
+ onTweaksOpen,
26
+ collapsed = false,
27
+ onToggleCollapsed,
28
+ rightSlot,
29
+ unread = false,
30
+ onNotificationsOpen,
31
+ user,
32
+ }: TopbarProp) {
33
+ return (
34
+ <>
35
+ {onToggleCollapsed ? (
36
+ <button
37
+ type="button"
38
+ className="tb-icon-btn"
39
+ aria-label="Toggle sidebar"
40
+ aria-pressed={collapsed}
41
+ onClick={onToggleCollapsed}
42
+ >
43
+ {collapsed ? <PanelLeftOpen aria-hidden="true" /> : <PanelLeftClose aria-hidden="true" />}
44
+ </button>
45
+ ) : null}
46
+
47
+ <div className="tb-switcher">
48
+ <button type="button" className="tb-chip" aria-label={product.name} onClick={onProductOpen}>
49
+ <span
50
+ className="tb-chip-icon"
51
+ style={{ background: product.color ?? "hsl(var(--attention))" }}
52
+ >
53
+ {product.name[0]?.toUpperCase() ?? "?"}
54
+ </span>
55
+ <span className="tb-chip-label">{product.name}</span>
56
+ <span className="tb-chip-caret">
57
+ <ChevronDown aria-hidden="true" />
58
+ </span>
59
+ </button>
60
+ <span className="tb-chip-sep">/</span>
61
+ <button
62
+ type="button"
63
+ className={`tb-chip ${project ? "" : "tb-chip-empty"}`}
64
+ aria-label={project ? project.name : "Pick project"}
65
+ onClick={onProjectOpen}
66
+ >
67
+ <span className="tb-chip-label">{project ? project.name : "Pick project"}</span>
68
+ <span className="tb-chip-caret">
69
+ <ChevronDown aria-hidden="true" />
70
+ </span>
71
+ </button>
72
+ </div>
73
+
74
+ <button type="button" className="tb-search" onClick={onSearchOpen}>
75
+ <Search aria-hidden="true" />
76
+ <span>Search...</span>
77
+ <kbd className="kbd">⌘K</kbd>
78
+ </button>
79
+
80
+ {rightSlot}
81
+
82
+ {onNotificationsOpen ? (
83
+ <button
84
+ type="button"
85
+ className="tb-icon-btn tb-bell"
86
+ aria-label="Notifications"
87
+ onClick={onNotificationsOpen}
88
+ >
89
+ <Bell aria-hidden="true" />
90
+ {unread ? <span className="tb-bell-dot" aria-hidden="true" /> : null}
91
+ </button>
92
+ ) : null}
93
+
94
+ {user}
95
+
96
+ {onTweaksOpen ? (
97
+ <button
98
+ type="button"
99
+ className="tb-icon-btn"
100
+ aria-label="Open tweaks"
101
+ onClick={onTweaksOpen}
102
+ >
103
+ <SlidersHorizontal aria-hidden="true" />
104
+ </button>
105
+ ) : null}
106
+ </>
107
+ );
108
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { AppProvider } from "../../../app/app-provider";
5
+ import { LocalePicker } from "../locale-picker";
6
+ import { DateFormatPicker } from "../date-format-picker";
7
+ import { TimeFormatPicker } from "../time-format-picker";
8
+ import { TimezonePicker } from "../timezone-picker";
9
+
10
+ describe("LocalePicker", () => {
11
+ it("renders current locale from AppProvider", () => {
12
+ render(
13
+ <AppProvider persist={false} defaultLocale="vi">
14
+ <LocalePicker />
15
+ </AppProvider>,
16
+ );
17
+
18
+ expect(screen.getByRole("combobox", { name: "Ngôn ngữ" })).toBeInTheDocument();
19
+ expect(screen.getByText("Tiếng Việt")).toBeInTheDocument();
20
+ });
21
+
22
+ it("works in controlled mode without AppProvider", async () => {
23
+ const user = userEvent.setup();
24
+ const onChange = vi.fn();
25
+
26
+ render(<LocalePicker value="en" onChange={onChange} />);
27
+ await user.click(screen.getByRole("combobox", { name: "Ngôn ngữ" }));
28
+ await user.click(screen.getByRole("option", { name: "日本語" }));
29
+
30
+ expect(onChange).toHaveBeenCalledWith("ja");
31
+ });
32
+ });
33
+
34
+ describe("TimezonePicker", () => {
35
+ it("lists configured timezones from AppProvider", async () => {
36
+ const user = userEvent.setup();
37
+
38
+ render(
39
+ <AppProvider
40
+ persist={false}
41
+ defaultLocale="en"
42
+ defaultTimezone="Asia/Tokyo"
43
+ timezoneOptions={["Asia/Tokyo", "Asia/Ho_Chi_Minh"]}
44
+ >
45
+ <TimezonePicker />
46
+ </AppProvider>,
47
+ );
48
+
49
+ await user.click(screen.getByRole("combobox", { name: "Timezone" }));
50
+ expect(screen.getByRole("option", { name: "Japan (Tokyo)" })).toBeInTheDocument();
51
+ expect(screen.getByRole("option", { name: "Vietnam (Ho Chi Minh)" })).toBeInTheDocument();
52
+ expect(screen.queryByRole("option", { name: /Paris/ })).not.toBeInTheDocument();
53
+ });
54
+ });
55
+
56
+ describe("DateFormatPicker", () => {
57
+ it("renders dmy for vi locale by default", () => {
58
+ render(
59
+ <AppProvider persist={false} defaultLocale="vi">
60
+ <DateFormatPicker />
61
+ </AppProvider>,
62
+ );
63
+
64
+ expect(screen.getByText("Ngày / Tháng / Năm")).toBeInTheDocument();
65
+ });
66
+
67
+ it("renders iso for ja locale by default", () => {
68
+ render(
69
+ <AppProvider persist={false} defaultLocale="ja">
70
+ <DateFormatPicker />
71
+ </AppProvider>,
72
+ );
73
+
74
+ expect(screen.getByText("YYYY-MM-DD(年-月-日)")).toBeInTheDocument();
75
+ });
76
+
77
+ it("works in controlled mode", async () => {
78
+ const user = userEvent.setup();
79
+ const onChange = vi.fn();
80
+
81
+ render(
82
+ <AppProvider persist={false} defaultLocale="en">
83
+ <DateFormatPicker value="mdy" onChange={onChange} />
84
+ </AppProvider>,
85
+ );
86
+ await user.click(screen.getByRole("combobox", { name: "Date format" }));
87
+ await user.click(screen.getByRole("option", { name: "ISO (yyyy-MM-dd)" }));
88
+
89
+ expect(onChange).toHaveBeenCalledWith("iso");
90
+ });
91
+ });
92
+
93
+ describe("TimeFormatPicker", () => {
94
+ it("renders 24h for vi locale by default", () => {
95
+ render(
96
+ <AppProvider persist={false} defaultLocale="vi">
97
+ <TimeFormatPicker />
98
+ </AppProvider>,
99
+ );
100
+
101
+ expect(screen.getByText("24 giờ")).toBeInTheDocument();
102
+ });
103
+
104
+ it("works in controlled mode", async () => {
105
+ const user = userEvent.setup();
106
+ const onChange = vi.fn();
107
+
108
+ render(
109
+ <AppProvider persist={false} defaultLocale="en">
110
+ <TimeFormatPicker value="24h" onChange={onChange} />
111
+ </AppProvider>,
112
+ );
113
+ await user.click(screen.getByRole("combobox", { name: "Time format" }));
114
+ await user.click(screen.getByRole("option", { name: "12-hour (AM/PM)" }));
115
+
116
+ expect(onChange).toHaveBeenCalledWith("12h");
117
+ });
118
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { renderWithUi, screen, userEvent } from "@/test/render";
3
+ import { Button } from "../../general/button";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuCheckboxItem,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ DropdownMenuRadioGroup,
11
+ DropdownMenuRadioItem,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuShortcut,
14
+ DropdownMenuTrigger,
15
+ } from "../dropdown-menu";
16
+
17
+ describe("DropdownMenu", () => {
18
+ it("invokes menu item onSelect", async () => {
19
+ const user = userEvent.setup();
20
+ const onSelect = vi.fn();
21
+ renderWithUi(
22
+ <DropdownMenu>
23
+ <DropdownMenuTrigger asChild>
24
+ <Button type="button">Actions</Button>
25
+ </DropdownMenuTrigger>
26
+ <DropdownMenuContent>
27
+ <DropdownMenuItem onSelect={onSelect}>Xuất CSV</DropdownMenuItem>
28
+ </DropdownMenuContent>
29
+ </DropdownMenu>,
30
+ );
31
+ await user.click(screen.getByRole("button", { name: "Actions" }));
32
+ const item = screen.getByRole("menuitem", { name: "Xuất CSV" });
33
+ expect(item).toHaveAttribute("data-slot", "dropdown-menu-item");
34
+ await user.click(item);
35
+ expect(onSelect).toHaveBeenCalled();
36
+ });
37
+
38
+ it("renders label and separator", async () => {
39
+ const user = userEvent.setup();
40
+ renderWithUi(
41
+ <DropdownMenu>
42
+ <DropdownMenuTrigger asChild>
43
+ <Button type="button">Menu</Button>
44
+ </DropdownMenuTrigger>
45
+ <DropdownMenuContent>
46
+ <DropdownMenuLabel>Hành động</DropdownMenuLabel>
47
+ <DropdownMenuSeparator />
48
+ <DropdownMenuItem>Sửa</DropdownMenuItem>
49
+ </DropdownMenuContent>
50
+ </DropdownMenu>,
51
+ );
52
+ await user.click(screen.getByRole("button", { name: "Menu" }));
53
+ expect(screen.getByText("Hành động")).toHaveAttribute("data-slot", "dropdown-menu-label");
54
+ expect(screen.getByRole("menuitem", { name: "Sửa" })).toBeInTheDocument();
55
+ });
56
+
57
+ it("closes after selecting an item", async () => {
58
+ const user = userEvent.setup();
59
+ renderWithUi(
60
+ <DropdownMenu>
61
+ <DropdownMenuTrigger asChild>
62
+ <Button type="button">Actions</Button>
63
+ </DropdownMenuTrigger>
64
+ <DropdownMenuContent>
65
+ <DropdownMenuItem>Xóa</DropdownMenuItem>
66
+ </DropdownMenuContent>
67
+ </DropdownMenu>,
68
+ );
69
+ await user.click(screen.getByRole("button", { name: "Actions" }));
70
+ await user.click(screen.getByRole("menuitem", { name: "Xóa" }));
71
+ expect(screen.queryByRole("menuitem", { name: "Xóa" })).not.toBeInTheDocument();
72
+ });
73
+
74
+ it("renders checkbox, radio, and shortcut slots", async () => {
75
+ const user = userEvent.setup();
76
+ renderWithUi(
77
+ <DropdownMenu>
78
+ <DropdownMenuTrigger asChild>
79
+ <Button type="button">Advanced</Button>
80
+ </DropdownMenuTrigger>
81
+ <DropdownMenuContent>
82
+ <DropdownMenuCheckboxItem checked>Notify</DropdownMenuCheckboxItem>
83
+ <DropdownMenuRadioGroup value="air">
84
+ <DropdownMenuRadioItem value="air">Air</DropdownMenuRadioItem>
85
+ </DropdownMenuRadioGroup>
86
+ <DropdownMenuItem>
87
+ Export
88
+ <DropdownMenuShortcut>Ctrl+E</DropdownMenuShortcut>
89
+ </DropdownMenuItem>
90
+ </DropdownMenuContent>
91
+ </DropdownMenu>,
92
+ );
93
+ await user.click(screen.getByRole("button", { name: "Advanced" }));
94
+ expect(screen.getByRole("menuitemcheckbox", { name: "Notify" })).toHaveAttribute(
95
+ "data-slot",
96
+ "dropdown-menu-checkbox-item",
97
+ );
98
+ expect(screen.getByRole("menuitemradio", { name: "Air" })).toHaveAttribute(
99
+ "data-slot",
100
+ "dropdown-menu-radio-item",
101
+ );
102
+ expect(screen.getByText("Ctrl+E")).toHaveAttribute("data-slot", "dropdown-menu-shortcut");
103
+ });
104
+ });