@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,167 @@
1
+ import * as React from "react";
2
+ import {
3
+ AlertCircle,
4
+ CheckCircle2,
5
+ Info,
6
+ RefreshCw,
7
+ TriangleAlert,
8
+ X,
9
+ type LucideIcon,
10
+ } from "lucide-react";
11
+
12
+ import { useTranslation } from "../../i18n/use-translation";
13
+ import { humanError } from "../../lib/format";
14
+ import { cn } from "../../lib/utils";
15
+ import { Inline } from "../layout/inline";
16
+ import { Button } from "../general/button";
17
+ import type { AlertVariantProp } from "../../props/vocabulary";
18
+ import type {
19
+ AlertActionsProp,
20
+ AlertContentProp,
21
+ AlertDescriptionProp,
22
+ AlertProp,
23
+ AlertQueryErrorProp,
24
+ AlertTitleProp,
25
+ } from "../../props/components/feedback.prop";
26
+
27
+ export type {
28
+ AlertProp,
29
+ AlertProp as AlertProps,
30
+ AlertTitleProp,
31
+ AlertTitleProp as AlertTitleProps,
32
+ AlertContentProp,
33
+ AlertContentProp as AlertContentProps,
34
+ AlertDescriptionProp,
35
+ AlertDescriptionProp as AlertDescriptionProps,
36
+ AlertActionsProp,
37
+ AlertActionsProp as AlertActionsProps,
38
+ AlertQueryErrorProp,
39
+ AlertQueryErrorProp as AlertQueryErrorProps,
40
+ } from "../../props/components/feedback.prop";
41
+
42
+ const AlertContext = React.createContext<AlertVariantProp>("default");
43
+
44
+ const DEFAULT_ICONS: Record<AlertVariantProp, LucideIcon> = {
45
+ default: Info,
46
+ destructive: AlertCircle,
47
+ warning: TriangleAlert,
48
+ success: CheckCircle2,
49
+ };
50
+
51
+ const AlertBase = React.forwardRef<HTMLDivElement, AlertProp>(
52
+ ({ variant = "default", icon, onDismiss, className, children, ...props }, ref) => {
53
+ const IconComponent = icon === false ? null : (icon ?? DEFAULT_ICONS[variant]);
54
+
55
+ return (
56
+ <AlertContext.Provider value={variant}>
57
+ <div
58
+ ref={ref}
59
+ role="alert"
60
+ data-slot="alert"
61
+ data-variant={variant}
62
+ data-dismissible={onDismiss ? "" : undefined}
63
+ className={className}
64
+ {...props}
65
+ >
66
+ {IconComponent && (
67
+ <IconComponent data-slot="alert-icon" data-variant={variant} aria-hidden="true" />
68
+ )}
69
+ <div data-slot="alert-body">{children}</div>
70
+ {onDismiss && (
71
+ <button
72
+ type="button"
73
+ onClick={() => {
74
+ void onDismiss();
75
+ }}
76
+ data-slot="alert-dismiss"
77
+ className="ring-offset-background focus-visible:ring-ring transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
78
+ aria-label="Dismiss"
79
+ >
80
+ <X className="size-4" aria-hidden="true" />
81
+ </button>
82
+ )}
83
+ </div>
84
+ </AlertContext.Provider>
85
+ );
86
+ },
87
+ );
88
+ AlertBase.displayName = "Alert";
89
+
90
+ export const AlertTitle = React.forwardRef<HTMLParagraphElement, AlertTitleProp>(
91
+ ({ className, ...props }, ref) => {
92
+ const variant = React.useContext(AlertContext);
93
+ return (
94
+ <p
95
+ ref={ref}
96
+ data-slot="alert-title"
97
+ data-variant={variant}
98
+ className={className}
99
+ {...props}
100
+ />
101
+ );
102
+ },
103
+ );
104
+ AlertTitle.displayName = "AlertTitle";
105
+
106
+ export const AlertContent = React.forwardRef<HTMLDivElement, AlertContentProp>(
107
+ ({ className, ...props }, ref) => (
108
+ <div
109
+ ref={ref}
110
+ data-slot="alert-content"
111
+ className={cn("min-w-0 flex-1", className)}
112
+ {...props}
113
+ />
114
+ ),
115
+ );
116
+ AlertContent.displayName = "AlertContent";
117
+
118
+ export const AlertDescription = React.forwardRef<HTMLParagraphElement, AlertDescriptionProp>(
119
+ ({ className, ...props }, ref) => (
120
+ <p ref={ref} data-slot="alert-description" className={className} {...props} />
121
+ ),
122
+ );
123
+ AlertDescription.displayName = "AlertDescription";
124
+
125
+ export const AlertActions = React.forwardRef<HTMLDivElement, AlertActionsProp>(
126
+ ({ className, ...props }, ref) => (
127
+ <div ref={ref} data-slot="alert-actions" className={className} {...props} />
128
+ ),
129
+ );
130
+ AlertActions.displayName = "AlertActions";
131
+
132
+ /** TanStack Query / API failure preset — same visual as `Alert variant="destructive"`. Used by `DataState` (@godxjp/ui/query). */
133
+ export function AlertQueryError({ error, onRetry, className }: AlertQueryErrorProp) {
134
+ const { t } = useTranslation();
135
+ return (
136
+ <Alert variant="destructive" className={className}>
137
+ <AlertTitle>{t("common.error")}</AlertTitle>
138
+ <AlertDescription>{humanError(error)}</AlertDescription>
139
+ {onRetry && (
140
+ <AlertActions>
141
+ <Button
142
+ variant="outline"
143
+ size="sm"
144
+ onClick={() => {
145
+ void onRetry();
146
+ }}
147
+ >
148
+ <Inline gap="xs">
149
+ <RefreshCw className="size-4" aria-hidden="true" />
150
+ {t("common.retry")}
151
+ </Inline>
152
+ </Button>
153
+ </AlertActions>
154
+ )}
155
+ </Alert>
156
+ );
157
+ }
158
+
159
+ export const Alert = Object.assign(AlertBase, {
160
+ Title: AlertTitle,
161
+ Content: AlertContent,
162
+ Description: AlertDescription,
163
+ Actions: AlertActions,
164
+ QueryError: AlertQueryError,
165
+ });
166
+
167
+ export { AlertBase };
@@ -0,0 +1,325 @@
1
+ import * as React from "react";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
4
+ import { X } from "lucide-react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+ import { buttonVariants } from "../general/button";
8
+ import { useTranslation } from "../../i18n/use-translation";
9
+ import { Button } from "../general/button";
10
+ import { Input } from "../data-entry/input";
11
+ import { Label } from "../data-entry/label";
12
+ import type { DialogConfirmProp } from "../../props/components/feedback.prop";
13
+
14
+ export type {
15
+ DialogConfirmProp,
16
+ DialogConfirmProp as DialogConfirmProps,
17
+ } from "../../props/components/feedback.prop";
18
+
19
+ /** form = nhập liệu / wizard (role=dialog, có nút X). confirm = quyết định (role=alertdialog, không X). */
20
+ export type DialogMode = "form" | "confirm";
21
+
22
+ const DialogModeContext = React.createContext<DialogMode>("form");
23
+
24
+ function useDialogMode(): DialogMode {
25
+ return React.useContext(DialogModeContext);
26
+ }
27
+
28
+ type DialogRootProps = React.ComponentProps<typeof DialogPrimitive.Root> & {
29
+ mode?: DialogMode;
30
+ };
31
+
32
+ function DialogRoot({ mode = "form", ...props }: DialogRootProps) {
33
+ if (mode === "confirm") {
34
+ return (
35
+ <DialogModeContext.Provider value="confirm">
36
+ <AlertDialogPrimitive.Root data-slot="dialog" {...props} />
37
+ </DialogModeContext.Provider>
38
+ );
39
+ }
40
+ return (
41
+ <DialogModeContext.Provider value="form">
42
+ <DialogPrimitive.Root data-slot="dialog" {...props} />
43
+ </DialogModeContext.Provider>
44
+ );
45
+ }
46
+
47
+ function DialogTrigger(props: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
48
+ const mode = useDialogMode();
49
+ if (mode === "confirm") {
50
+ return <AlertDialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
51
+ }
52
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
53
+ }
54
+
55
+ function DialogPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>) {
56
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
57
+ }
58
+
59
+ function DialogClose(props: React.ComponentProps<typeof DialogPrimitive.Close>) {
60
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
61
+ }
62
+
63
+ const DialogOverlay = React.forwardRef<
64
+ React.ComponentRef<typeof DialogPrimitive.Overlay>,
65
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
66
+ >(({ className, ...props }, ref) => (
67
+ <DialogPrimitive.Overlay
68
+ ref={ref}
69
+ data-slot="dialog-overlay"
70
+ className={cn(
71
+ "ui-dialog-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
72
+ className,
73
+ )}
74
+ {...props}
75
+ />
76
+ ));
77
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
78
+
79
+ const DialogContent = React.forwardRef<
80
+ HTMLDivElement,
81
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
82
+ showClose?: boolean;
83
+ showCloseButton?: boolean;
84
+ }
85
+ >(({ className, children, showClose, showCloseButton: showCloseButtonProp, ...props }, ref) => {
86
+ const mode = useDialogMode();
87
+
88
+ if (mode === "confirm") {
89
+ return (
90
+ <AlertDialogPrimitive.Portal>
91
+ <AlertDialogPrimitive.Overlay
92
+ data-slot="dialog-overlay"
93
+ className="ui-dialog-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0"
94
+ />
95
+ <AlertDialogPrimitive.Content
96
+ ref={ref}
97
+ data-slot="dialog-content"
98
+ className={cn(
99
+ "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 duration-200 outline-none",
100
+ className,
101
+ )}
102
+ {...props}
103
+ >
104
+ {children}
105
+ </AlertDialogPrimitive.Content>
106
+ </AlertDialogPrimitive.Portal>
107
+ );
108
+ }
109
+
110
+ const showCloseButton = showCloseButtonProp ?? showClose ?? true;
111
+ return (
112
+ <DialogPortal>
113
+ <DialogOverlay />
114
+ <DialogPrimitive.Content
115
+ ref={ref}
116
+ data-slot="dialog-content"
117
+ className={cn(
118
+ "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 duration-200 outline-none",
119
+ className,
120
+ )}
121
+ {...props}
122
+ >
123
+ {children}
124
+ {showCloseButton ? (
125
+ <DialogPrimitive.Close
126
+ data-slot="dialog-close"
127
+ className="ring-offset-background focus:ring-ring transition-opacity focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
128
+ >
129
+ <X className="size-4" aria-hidden="true" />
130
+ <span className="sr-only">Close</span>
131
+ </DialogPrimitive.Close>
132
+ ) : null}
133
+ </DialogPrimitive.Content>
134
+ </DialogPortal>
135
+ );
136
+ });
137
+ DialogContent.displayName = "DialogContent";
138
+
139
+ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
140
+ <div data-slot="dialog-header" className={className} {...props} />
141
+ );
142
+ DialogHeader.displayName = "DialogHeader";
143
+
144
+ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
145
+ <div data-slot="dialog-footer" className={className} {...props} />
146
+ );
147
+ DialogFooter.displayName = "DialogFooter";
148
+
149
+ const DialogTitle = React.forwardRef<
150
+ HTMLHeadingElement,
151
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
152
+ >(({ className, ...props }, ref) => {
153
+ const mode = useDialogMode();
154
+ const cls = cn(className);
155
+ if (mode === "confirm") {
156
+ return (
157
+ <AlertDialogPrimitive.Title ref={ref} data-slot="dialog-title" className={cls} {...props} />
158
+ );
159
+ }
160
+ return <DialogPrimitive.Title ref={ref} data-slot="dialog-title" className={cls} {...props} />;
161
+ });
162
+ DialogTitle.displayName = "DialogTitle";
163
+
164
+ const DialogDescription = React.forwardRef<
165
+ HTMLParagraphElement,
166
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
167
+ >(({ className, ...props }, ref) => {
168
+ const mode = useDialogMode();
169
+ const cls = cn(className);
170
+ if (mode === "confirm") {
171
+ return (
172
+ <AlertDialogPrimitive.Description
173
+ ref={ref}
174
+ data-slot="dialog-description"
175
+ className={cls}
176
+ {...props}
177
+ />
178
+ );
179
+ }
180
+ return (
181
+ <DialogPrimitive.Description
182
+ ref={ref}
183
+ data-slot="dialog-description"
184
+ className={cls}
185
+ {...props}
186
+ />
187
+ );
188
+ });
189
+ DialogDescription.displayName = "DialogDescription";
190
+
191
+ /** Confirm mode — primary action (maps to Radix AlertDialogAction). */
192
+ const DialogAction = React.forwardRef<
193
+ HTMLButtonElement,
194
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
195
+ >(({ className, ...props }, ref) => (
196
+ <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
197
+ ));
198
+ DialogAction.displayName = "DialogAction";
199
+
200
+ /** Confirm mode — dismiss without action (maps to Radix AlertDialogCancel). */
201
+ const DialogCancel = React.forwardRef<
202
+ HTMLButtonElement,
203
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
204
+ >(({ className, ...props }, ref) => (
205
+ <AlertDialogPrimitive.Cancel
206
+ ref={ref}
207
+ className={cn(buttonVariants({ variant: "outline" }), className)}
208
+ {...props}
209
+ />
210
+ ));
211
+ DialogCancel.displayName = "DialogCancel";
212
+
213
+ /** Preset: confirm / destructive / type-to-confirm — use case `mode="confirm"` without compound markup. */
214
+ function DialogConfirm({
215
+ open,
216
+ onOpenChange,
217
+ title,
218
+ description,
219
+ confirmLabel,
220
+ cancelLabel,
221
+ variant = "default",
222
+ confirmPhrase,
223
+ onConfirm,
224
+ keepOpenOnConfirm = false,
225
+ pending = false,
226
+ }: DialogConfirmProp) {
227
+ const { t } = useTranslation();
228
+ const [typed, setTyped] = React.useState("");
229
+ const inputId = React.useId();
230
+
231
+ const needsPhrase = confirmPhrase != null && confirmPhrase.length > 0;
232
+ const phraseMatches = !needsPhrase || typed === confirmPhrase;
233
+ const effectiveVariant = needsPhrase ? "destructive" : variant;
234
+ const resolvedConfirm = confirmLabel ?? (needsPhrase ? t("common.delete") : t("common.continue"));
235
+ const resolvedCancel = cancelLabel ?? t("common.cancel");
236
+
237
+ const handleOpenChange = (next: boolean) => {
238
+ setTyped("");
239
+ onOpenChange(next);
240
+ };
241
+
242
+ const handleConfirm = () => {
243
+ if (!phraseMatches) return;
244
+ void (async () => {
245
+ await onConfirm();
246
+ if (!keepOpenOnConfirm) onOpenChange(false);
247
+ })();
248
+ };
249
+
250
+ return (
251
+ <DialogRoot mode="confirm" open={open} onOpenChange={handleOpenChange}>
252
+ <DialogContent>
253
+ <DialogHeader>
254
+ <DialogTitle>{title}</DialogTitle>
255
+ {description ? <DialogDescription>{description}</DialogDescription> : null}
256
+ </DialogHeader>
257
+
258
+ {needsPhrase && (
259
+ <div className="ui-stack-xs">
260
+ <Label htmlFor={inputId} className="text-sm">
261
+ {t("common.typeToConfirm", { phrase: confirmPhrase })}
262
+ </Label>
263
+ <Input
264
+ id={inputId}
265
+ value={typed}
266
+ onChange={(e) => {
267
+ setTyped(e.target.value);
268
+ }}
269
+ autoComplete="off"
270
+ spellCheck={false}
271
+ placeholder={confirmPhrase}
272
+ aria-required="true"
273
+ />
274
+ </div>
275
+ )}
276
+
277
+ <DialogFooter>
278
+ <DialogCancel asChild>
279
+ <Button variant="ghost" disabled={pending}>
280
+ {resolvedCancel}
281
+ </Button>
282
+ </DialogCancel>
283
+ <Button
284
+ variant={effectiveVariant === "destructive" ? "destructive" : "default"}
285
+ onClick={handleConfirm}
286
+ disabled={pending || !phraseMatches}
287
+ >
288
+ {pending ? t("common.working") : resolvedConfirm}
289
+ </Button>
290
+ </DialogFooter>
291
+ </DialogContent>
292
+ </DialogRoot>
293
+ );
294
+ }
295
+
296
+ export const Dialog = Object.assign(DialogRoot, {
297
+ Trigger: DialogTrigger,
298
+ Portal: DialogPortal,
299
+ Overlay: DialogOverlay,
300
+ Content: DialogContent,
301
+ Header: DialogHeader,
302
+ Footer: DialogFooter,
303
+ Title: DialogTitle,
304
+ Description: DialogDescription,
305
+ Close: DialogClose,
306
+ Action: DialogAction,
307
+ Cancel: DialogCancel,
308
+ Confirm: DialogConfirm,
309
+ });
310
+
311
+ export {
312
+ DialogRoot,
313
+ DialogTrigger,
314
+ DialogPortal,
315
+ DialogOverlay,
316
+ DialogContent,
317
+ DialogHeader,
318
+ DialogFooter,
319
+ DialogTitle,
320
+ DialogDescription,
321
+ DialogClose,
322
+ DialogAction,
323
+ DialogCancel,
324
+ DialogConfirm,
325
+ };
@@ -0,0 +1,53 @@
1
+ export {
2
+ Dialog,
3
+ DialogClose,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogOverlay,
9
+ DialogPortal,
10
+ DialogTitle,
11
+ DialogTrigger,
12
+ DialogAction,
13
+ DialogCancel,
14
+ DialogConfirm,
15
+ } from "./dialog";
16
+ export type { DialogMode, DialogConfirmProp, DialogConfirmProps } from "./dialog";
17
+ export {
18
+ Sheet,
19
+ SheetClose,
20
+ SheetContent,
21
+ SheetDescription,
22
+ SheetFooter,
23
+ SheetHeader,
24
+ SheetOverlay,
25
+ SheetPortal,
26
+ SheetTitle,
27
+ SheetTrigger,
28
+ } from "./sheet";
29
+ export { Toaster } from "./sonner";
30
+ export { toast, useToast, type LegacyToastOptions } from "./use-toast";
31
+ export { SkeletonRows, SkeletonTable, SkeletonDetail, SkeletonCard } from "./skeleton";
32
+ export {
33
+ Alert,
34
+ AlertTitle,
35
+ AlertContent,
36
+ AlertDescription,
37
+ AlertActions,
38
+ AlertQueryError,
39
+ } from "./alert";
40
+ export type {
41
+ AlertProp,
42
+ AlertProps,
43
+ AlertTitleProp,
44
+ AlertTitleProps,
45
+ AlertContentProp,
46
+ AlertContentProps,
47
+ AlertDescriptionProp,
48
+ AlertDescriptionProps,
49
+ AlertActionsProp,
50
+ AlertActionsProps,
51
+ AlertQueryErrorProp,
52
+ AlertQueryErrorProps,
53
+ } from "./alert";
@@ -0,0 +1,130 @@
1
+ import * as React from "react";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { X } from "lucide-react";
5
+ import { cn } from "../../lib/utils";
6
+
7
+ export function Sheet(props: React.ComponentProps<typeof DialogPrimitive.Root>) {
8
+ return <DialogPrimitive.Root data-slot="sheet" {...props} />;
9
+ }
10
+
11
+ export function SheetTrigger(props: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
12
+ return <DialogPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
13
+ }
14
+
15
+ export function SheetClose(props: React.ComponentProps<typeof DialogPrimitive.Close>) {
16
+ return <DialogPrimitive.Close data-slot="sheet-close" {...props} />;
17
+ }
18
+
19
+ export function SheetPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>) {
20
+ return <DialogPrimitive.Portal data-slot="sheet-portal" {...props} />;
21
+ }
22
+
23
+ export const SheetOverlay = React.forwardRef<
24
+ React.ComponentRef<typeof DialogPrimitive.Overlay>,
25
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
26
+ >(({ className, ...props }, ref) => (
27
+ <DialogPrimitive.Overlay
28
+ ref={ref}
29
+ data-slot="sheet-overlay"
30
+ className={cn(
31
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ ));
37
+ SheetOverlay.displayName = DialogPrimitive.Overlay.displayName;
38
+
39
+ const sheetVariants = cva(
40
+ "fixed z-50 flex flex-col gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
41
+ {
42
+ variants: {
43
+ side: {
44
+ right:
45
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-md",
46
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md",
47
+ top: "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
48
+ bottom:
49
+ "inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
50
+ },
51
+ },
52
+ defaultVariants: { side: "right" },
53
+ },
54
+ );
55
+
56
+ interface SheetContentProps
57
+ extends
58
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
59
+ VariantProps<typeof sheetVariants> {
60
+ showCloseButton?: boolean;
61
+ }
62
+
63
+ export const SheetContent = React.forwardRef<
64
+ React.ComponentRef<typeof DialogPrimitive.Content>,
65
+ SheetContentProps
66
+ >(({ side = "right", className, children, showCloseButton = true, ...props }, ref) => (
67
+ <SheetPortal>
68
+ <SheetOverlay />
69
+ <DialogPrimitive.Content
70
+ ref={ref}
71
+ data-slot="sheet-content"
72
+ className={cn(sheetVariants({ side }), className)}
73
+ {...props}
74
+ >
75
+ {children}
76
+ {showCloseButton ? (
77
+ <DialogPrimitive.Close
78
+ data-slot="sheet-close"
79
+ className="ring-offset-background focus:ring-ring absolute end-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
80
+ >
81
+ <X className="size-4" aria-hidden="true" />
82
+ <span className="sr-only">Close</span>
83
+ </DialogPrimitive.Close>
84
+ ) : null}
85
+ </DialogPrimitive.Content>
86
+ </SheetPortal>
87
+ ));
88
+ SheetContent.displayName = DialogPrimitive.Content.displayName;
89
+
90
+ export const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
91
+ <div
92
+ data-slot="sheet-header"
93
+ className={cn("flex flex-col gap-1.5 text-center sm:text-left", className)}
94
+ {...props}
95
+ />
96
+ );
97
+
98
+ export const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
99
+ <div
100
+ data-slot="sheet-footer"
101
+ className={cn("mt-auto flex flex-col gap-2", className)}
102
+ {...props}
103
+ />
104
+ );
105
+
106
+ export const SheetTitle = React.forwardRef<
107
+ React.ComponentRef<typeof DialogPrimitive.Title>,
108
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
109
+ >(({ className, ...props }, ref) => (
110
+ <DialogPrimitive.Title
111
+ ref={ref}
112
+ data-slot="sheet-title"
113
+ className={cn("text-foreground text-lg font-semibold", className)}
114
+ {...props}
115
+ />
116
+ ));
117
+ SheetTitle.displayName = DialogPrimitive.Title.displayName;
118
+
119
+ export const SheetDescription = React.forwardRef<
120
+ React.ComponentRef<typeof DialogPrimitive.Description>,
121
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
122
+ >(({ className, ...props }, ref) => (
123
+ <DialogPrimitive.Description
124
+ ref={ref}
125
+ data-slot="sheet-description"
126
+ className={cn("text-muted-foreground text-sm", className)}
127
+ {...props}
128
+ />
129
+ ));
130
+ SheetDescription.displayName = DialogPrimitive.Description.displayName;