@godxjp/ui 5.0.1 → 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 -650
  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 -7340
  272. package/dist/components/composites.js.map +0 -1
  273. package/dist/components/primitives.d.ts +0 -2736
  274. package/dist/components/primitives.js +0 -7353
  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 -13059
  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,305 @@
1
+ import * as React from "react";
2
+ import { Check, ChevronRight, ChevronsUpDown, X } from "lucide-react";
3
+
4
+ import { useTranslation } from "../../i18n/use-translation";
5
+ import { cn } from "../../lib/utils";
6
+ import { Button } from "../general/button";
7
+ import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover";
8
+ import { ScrollArea, ScrollBar } from "../data-display/scroll-area";
9
+ import { Checkbox } from "./checkbox";
10
+ import { Command, CommandInput } from "./command";
11
+ import {
12
+ filterTreeOptions,
13
+ formatPathLabels,
14
+ getNodeByPath,
15
+ normalizeTreeOptions,
16
+ pathKey,
17
+ pathsEqual,
18
+ type NormalizedTreeOption,
19
+ } from "./tree-utils";
20
+ import type { CascaderProp } from "../../props/components/data-entry.prop";
21
+
22
+ export type {
23
+ CascaderProp,
24
+ CascaderProp as CascaderProps,
25
+ } from "../../props/components/data-entry.prop";
26
+ export type { TreeOption, TreeFieldNames } from "./tree-utils";
27
+
28
+ function pathInValues(path: string[], values: string[][]): boolean {
29
+ return values.some((v) => pathsEqual(v, path));
30
+ }
31
+
32
+ function togglePath(values: string[][], path: string[]): string[][] {
33
+ if (pathInValues(path, values)) return values.filter((v) => !pathsEqual(v, path));
34
+ return [...values, path];
35
+ }
36
+
37
+ export function Cascader({
38
+ options: optionsProp,
39
+ value,
40
+ defaultValue,
41
+ onChange,
42
+ multiple,
43
+ changeOnSelect,
44
+ showSearch,
45
+ placeholder,
46
+ disabled,
47
+ className,
48
+ id,
49
+ expandTrigger = "click",
50
+ fieldNames,
51
+ allowClear = true,
52
+ }: CascaderProp) {
53
+ const { t } = useTranslation();
54
+ const options = React.useMemo(
55
+ () => normalizeTreeOptions(optionsProp as Record<string, unknown>[], fieldNames),
56
+ [optionsProp, fieldNames],
57
+ );
58
+
59
+ const [open, setOpen] = React.useState(false);
60
+ const [activePath, setActivePath] = React.useState<string[]>([]);
61
+ const [search, setSearch] = React.useState("");
62
+
63
+ const isControlledSingle = !multiple && value !== undefined;
64
+ const isControlledMulti = multiple && value !== undefined;
65
+ const [internalSingle, setInternalSingle] = React.useState<string[]>(
66
+ multiple ? [] : ((defaultValue as string[] | undefined) ?? []),
67
+ );
68
+ const [internalMulti, setInternalMulti] = React.useState<string[][]>(
69
+ (defaultValue as string[][] | undefined) ?? [],
70
+ );
71
+
72
+ const singleValue = isControlledSingle ? (value as string[]) : internalSingle;
73
+ const multiValue = isControlledMulti ? (value as string[][]) : internalMulti;
74
+
75
+ const resolvedPlaceholder = placeholder ?? t("dataEntry.cascader.placeholder");
76
+
77
+ const displayLabel = React.useMemo(() => {
78
+ if (multiple) {
79
+ if (!multiValue.length) return null;
80
+ return multiValue.map((path) => formatPathLabels(getNodeByPath(options, path))).join(", ");
81
+ }
82
+ if (!singleValue.length) return null;
83
+ return formatPathLabels(getNodeByPath(options, singleValue));
84
+ }, [multiple, multiValue, singleValue, options]);
85
+
86
+ const commitSingle = (path: string[]) => {
87
+ if (!isControlledSingle) setInternalSingle(path);
88
+ onChange?.(path, getNodeByPath(options, path));
89
+ setOpen(false);
90
+ setActivePath([]);
91
+ setSearch("");
92
+ };
93
+
94
+ const commitMulti = (paths: string[][]) => {
95
+ if (!isControlledMulti) setInternalMulti(paths);
96
+ onChange?.(
97
+ paths,
98
+ paths.map((p) => getNodeByPath(options, p)),
99
+ );
100
+ };
101
+
102
+ const columns: NormalizedTreeOption[][] = React.useMemo(() => {
103
+ const cols: NormalizedTreeOption[][] = [options];
104
+ for (const segment of activePath) {
105
+ const col = cols.at(-1);
106
+ const node = col?.find((n) => n.value === segment);
107
+ if (node?.children?.length) cols.push(node.children);
108
+ else break;
109
+ }
110
+ return cols;
111
+ }, [options, activePath]);
112
+
113
+ const handleSelectNode = (node: NormalizedTreeOption, path: string[]) => {
114
+ const hasChildren = (node.children?.length ?? 0) > 0 && node.isLeaf !== true;
115
+ if (hasChildren && !changeOnSelect) {
116
+ setActivePath(path);
117
+ return;
118
+ }
119
+ if (multiple) {
120
+ commitMulti(togglePath(multiValue, path));
121
+ return;
122
+ }
123
+ commitSingle(path);
124
+ };
125
+
126
+ const isSearching = showSearch && search.trim().length > 0;
127
+
128
+ const searchResults = React.useMemo(
129
+ () => (isSearching ? filterTreeOptions(options, search) : []),
130
+ [options, search, isSearching],
131
+ );
132
+
133
+ const handleOpenChange = (next: boolean) => {
134
+ setOpen(next);
135
+ if (!next) {
136
+ setSearch("");
137
+ setActivePath([]);
138
+ }
139
+ };
140
+
141
+ const clearValue = (e: React.MouseEvent) => {
142
+ e.stopPropagation();
143
+ if (multiple) commitMulti([]);
144
+ else commitSingle([]);
145
+ };
146
+
147
+ const renderCascadeColumns = () => (
148
+ <ScrollArea className="w-full">
149
+ <div className="flex max-h-[min(280px,50vh)]">
150
+ {columns.map((col, colIndex) => (
151
+ <ul
152
+ key={colIndex}
153
+ className="min-w-[9rem] border-r last:border-r-0"
154
+ onMouseLeave={
155
+ expandTrigger === "hover"
156
+ ? () => setActivePath(activePath.slice(0, colIndex))
157
+ : undefined
158
+ }
159
+ >
160
+ {col.map((node) => {
161
+ const path = [...activePath.slice(0, colIndex), node.value];
162
+ const hasChildren = (node.children?.length ?? 0) > 0 && node.isLeaf !== true;
163
+ const active = activePath[colIndex] === node.value;
164
+ const selected = multiple
165
+ ? pathInValues(path, multiValue)
166
+ : pathsEqual(path, singleValue);
167
+
168
+ return (
169
+ <li key={node.value}>
170
+ <button
171
+ type="button"
172
+ disabled={node.disabled}
173
+ className={cn(
174
+ "flex w-full items-center gap-1 px-3 py-2 text-sm outline-none",
175
+ "hover:bg-accent hover:text-accent-foreground",
176
+ active && "bg-accent/70 font-medium",
177
+ node.disabled && "pointer-events-none opacity-50",
178
+ )}
179
+ onMouseEnter={
180
+ expandTrigger === "hover" && hasChildren
181
+ ? () => setActivePath(path)
182
+ : undefined
183
+ }
184
+ onClick={() => !node.disabled && handleSelectNode(node, path)}
185
+ >
186
+ {multiple && (
187
+ <Checkbox
188
+ checked={selected}
189
+ disabled={node.disabled}
190
+ className="mr-1"
191
+ aria-hidden
192
+ tabIndex={-1}
193
+ />
194
+ )}
195
+ {!multiple && selected && (
196
+ <Check className="mr-1 size-4 shrink-0" aria-hidden="true" />
197
+ )}
198
+ <span className="flex-1 truncate text-left">{node.label}</span>
199
+ {hasChildren && (
200
+ <ChevronRight className="size-4 shrink-0 opacity-50" aria-hidden="true" />
201
+ )}
202
+ </button>
203
+ </li>
204
+ );
205
+ })}
206
+ </ul>
207
+ ))}
208
+ </div>
209
+ <ScrollBar orientation="horizontal" />
210
+ </ScrollArea>
211
+ );
212
+
213
+ return (
214
+ <Popover open={open} onOpenChange={handleOpenChange}>
215
+ <PopoverTrigger asChild>
216
+ <Button
217
+ id={id}
218
+ type="button"
219
+ variant="outline"
220
+ role="combobox"
221
+ aria-expanded={open}
222
+ disabled={disabled}
223
+ className={cn(
224
+ "w-full justify-between font-normal",
225
+ !displayLabel && "text-muted-foreground",
226
+ className,
227
+ )}
228
+ >
229
+ <span className="truncate">{displayLabel ?? resolvedPlaceholder}</span>
230
+ <span className="ml-2 flex shrink-0 items-center gap-1">
231
+ {allowClear && displayLabel && !disabled && (
232
+ <X
233
+ className="size-4 opacity-50 hover:opacity-100"
234
+ aria-hidden="true"
235
+ onClick={clearValue}
236
+ />
237
+ )}
238
+ <ChevronsUpDown className="size-4 opacity-50" aria-hidden="true" />
239
+ </span>
240
+ </Button>
241
+ </PopoverTrigger>
242
+ <PopoverContent
243
+ className="w-auto min-w-[var(--radix-popover-trigger-width)] p-0"
244
+ align="start"
245
+ >
246
+ {showSearch && (
247
+ <div className="border-b p-2">
248
+ <Command shouldFilter={false}>
249
+ <CommandInput
250
+ placeholder={t("dataEntry.cascader.searchPlaceholder")}
251
+ value={search}
252
+ onValueChange={setSearch}
253
+ />
254
+ </Command>
255
+ </div>
256
+ )}
257
+ {isSearching ? (
258
+ <ScrollArea className="max-h-[min(300px,50vh)]">
259
+ <div className="p-1">
260
+ {searchResults.length === 0 ? (
261
+ <p className="text-muted-foreground py-6 text-center text-sm">
262
+ {t("dataEntry.cascader.empty")}
263
+ </p>
264
+ ) : (
265
+ searchResults.map(({ path, labels }) => {
266
+ const label = labels.join(" / ");
267
+ const selected = multiple
268
+ ? pathInValues(path, multiValue)
269
+ : pathsEqual(path, singleValue);
270
+ return (
271
+ <button
272
+ key={pathKey(path)}
273
+ type="button"
274
+ className={cn(
275
+ "flex w-full items-center rounded-sm px-2 py-1.5 text-sm outline-none",
276
+ "hover:bg-accent hover:text-accent-foreground",
277
+ selected && "bg-accent/60",
278
+ )}
279
+ onClick={() => handleSelectNode({ value: path.at(-1)!, label }, path)}
280
+ >
281
+ {multiple ? (
282
+ <Checkbox checked={selected} className="mr-2" aria-hidden tabIndex={-1} />
283
+ ) : (
284
+ <Check
285
+ className={cn(
286
+ "mr-2 size-4 shrink-0",
287
+ selected ? "opacity-100" : "opacity-0",
288
+ )}
289
+ aria-hidden="true"
290
+ />
291
+ )}
292
+ <span className="truncate text-left">{label}</span>
293
+ </button>
294
+ );
295
+ })
296
+ )}
297
+ </div>
298
+ </ScrollArea>
299
+ ) : (
300
+ renderCascadeColumns()
301
+ )}
302
+ </PopoverContent>
303
+ </Popover>
304
+ );
305
+ }
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import { Checkbox } from "./checkbox";
5
+ import { ChoiceField } from "./choice-field";
6
+ import { choiceGroupClassName, type ChoiceOption } from "./choice-option";
7
+ import type { CheckboxGroupProp } from "../../props/components/data-entry.prop";
8
+
9
+ export type {
10
+ CheckboxGroupProp,
11
+ CheckboxGroupProp as CheckboxGroupProps,
12
+ } from "../../props/components/data-entry.prop";
13
+
14
+ function useControllableArray(
15
+ controlled: string[] | undefined,
16
+ defaultValue: string[],
17
+ ): [string[], (next: string[]) => void] {
18
+ const [internal, setInternal] = React.useState(defaultValue);
19
+ const value = controlled ?? internal;
20
+ const setValue = React.useCallback(
21
+ (next: string[]) => {
22
+ if (controlled === undefined) setInternal(next);
23
+ },
24
+ [controlled],
25
+ );
26
+ return [value, setValue];
27
+ }
28
+
29
+ export function CheckboxGroup({
30
+ value: controlledValue,
31
+ defaultValue = [],
32
+ onChange,
33
+ options,
34
+ orientation = "vertical",
35
+ disabled,
36
+ name,
37
+ className,
38
+ children,
39
+ }: CheckboxGroupProp) {
40
+ const reactId = React.useId();
41
+ const [value, setValue] = useControllableArray(controlledValue, defaultValue);
42
+
43
+ const toggle = (optionValue: string) => {
44
+ const next = value.includes(optionValue)
45
+ ? value.filter((v) => v !== optionValue)
46
+ : [...value, optionValue];
47
+ setValue(next);
48
+ onChange?.(next);
49
+ };
50
+
51
+ if (options && options.length > 0) {
52
+ return (
53
+ <div
54
+ role="group"
55
+ aria-disabled={disabled ? true : undefined}
56
+ data-orientation={orientation}
57
+ className={choiceGroupClassName(orientation, className)}
58
+ >
59
+ {options.map((opt: ChoiceOption, index) => {
60
+ const id = `${reactId}-${opt.value}-${index}`;
61
+ const checked = value.includes(opt.value);
62
+ return (
63
+ <ChoiceField key={opt.value} id={id} label={opt.label} description={opt.description}>
64
+ <Checkbox
65
+ id={id}
66
+ name={name}
67
+ value={opt.value}
68
+ checked={checked}
69
+ disabled={Boolean(disabled) || Boolean(opt.disabled)}
70
+ onCheckedChange={() => {
71
+ toggle(opt.value);
72
+ }}
73
+ />
74
+ </ChoiceField>
75
+ );
76
+ })}
77
+ </div>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <div
83
+ role="group"
84
+ data-orientation={orientation}
85
+ className={cn(choiceGroupClassName(orientation), className)}
86
+ >
87
+ {children}
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ import { Check } from "lucide-react";
4
+ import { cn } from "../../lib/utils";
5
+ import { CheckboxGroup } from "./checkbox-group";
6
+
7
+ const CheckboxRoot = React.forwardRef<
8
+ React.ComponentRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ data-slot="checkbox"
14
+ className={cn(
15
+ "peer ui-checkbox focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground shrink-0 shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16
+ className,
17
+ )}
18
+ {...props}
19
+ >
20
+ <CheckboxPrimitive.Indicator data-slot="checkbox-indicator" className="ui-choice-indicator">
21
+ <Check className="ui-checkbox-icon" aria-hidden="true" />
22
+ </CheckboxPrimitive.Indicator>
23
+ </CheckboxPrimitive.Root>
24
+ ));
25
+ CheckboxRoot.displayName = CheckboxPrimitive.Root.displayName;
26
+
27
+ /** Checkbox — dùng standalone hoặc `Checkbox.Group` với `options` (Ant Design style). */
28
+ export const Checkbox = Object.assign(CheckboxRoot, {
29
+ Group: CheckboxGroup,
30
+ });
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import { Label } from "./label";
5
+
6
+ interface ChoiceFieldProps {
7
+ id: string;
8
+ label: React.ReactNode;
9
+ description?: React.ReactNode;
10
+ className?: string;
11
+ children: React.ReactNode;
12
+ }
13
+
14
+ /** Label + optional description beside a checkbox/radio control. */
15
+ export function ChoiceField({ id, label, description, className, children }: ChoiceFieldProps) {
16
+ return (
17
+ <div className={cn("ui-choice-field", className)}>
18
+ <div className="ui-choice-control">{children}</div>
19
+ <div className="ui-choice-content">
20
+ <Label htmlFor={id} className="ui-choice-label">
21
+ {label}
22
+ </Label>
23
+ {description ? <p className="ui-choice-description">{description}</p> : null}
24
+ </div>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,20 @@
1
+ import type * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ /** Shared option shape — Ant Design `CheckboxOptionType` / `Radio` options. */
6
+ export type ChoiceOption = {
7
+ label: React.ReactNode;
8
+ value: string;
9
+ disabled?: boolean;
10
+ description?: React.ReactNode;
11
+ };
12
+
13
+ export type ChoiceOrientation = "horizontal" | "vertical";
14
+
15
+ export function choiceGroupClassName(
16
+ _orientation: ChoiceOrientation = "vertical",
17
+ className?: string,
18
+ ) {
19
+ return cn("ui-choice-group", className);
20
+ }
@@ -0,0 +1,75 @@
1
+ import * as React from "react";
2
+ import { useTranslation } from "../../i18n/use-translation";
3
+ import { cn } from "../../lib/utils";
4
+ import { controlIconClass } from "../../lib/control-styles";
5
+ import type { ColorPickerProp } from "../../props/components/data-entry.prop";
6
+ import { Input } from "./input";
7
+
8
+ export type {
9
+ ColorPickerProp,
10
+ ColorPickerProp as ColorPickerProps,
11
+ } from "../../props/components/data-entry.prop";
12
+
13
+ const HEX_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
14
+
15
+ function normalizeHex(value: string): string {
16
+ if (!value.startsWith("#")) return `#${value}`;
17
+ return value;
18
+ }
19
+
20
+ export function ColorPicker({
21
+ value = "#2563eb",
22
+ onChange,
23
+ disabled,
24
+ className,
25
+ id,
26
+ showHexInput = true,
27
+ }: ColorPickerProp) {
28
+ const { t } = useTranslation();
29
+ const [draft, setDraft] = React.useState<string | null>(null);
30
+ const display = draft ?? value;
31
+
32
+ const commit = (next: string) => {
33
+ const normalized = normalizeHex(next);
34
+ if (!HEX_PATTERN.test(normalized)) {
35
+ setDraft(null);
36
+ return;
37
+ }
38
+ setDraft(null);
39
+ onChange?.(normalized);
40
+ };
41
+
42
+ return (
43
+ <div className={cn("ui-color-picker", className)}>
44
+ <div className={cn("ui-color-picker-swatch", controlIconClass)}>
45
+ <div
46
+ className="ui-color-picker-preview"
47
+ style={{ backgroundColor: HEX_PATTERN.test(display) ? display : value }}
48
+ aria-hidden="true"
49
+ />
50
+ <input
51
+ id={id}
52
+ type="color"
53
+ value={HEX_PATTERN.test(display) ? display : value}
54
+ disabled={disabled}
55
+ onChange={(event) => commit(event.target.value)}
56
+ className="ui-color-picker-input"
57
+ aria-label={t("dataEntry.colorPicker.ariaLabel")}
58
+ />
59
+ </div>
60
+ {showHexInput && (
61
+ <Input
62
+ value={display}
63
+ disabled={disabled}
64
+ onChange={(event) => setDraft(event.target.value)}
65
+ onBlur={() => commit(display)}
66
+ onKeyDown={(event) => {
67
+ if (event.key === "Enter") commit(display);
68
+ }}
69
+ className="ui-color-picker-hex"
70
+ spellCheck={false}
71
+ />
72
+ )}
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { Command as CommandPrimitive } from "cmdk";
3
+ import { Search } from "lucide-react";
4
+ import { cn } from "../../lib/utils";
5
+ import { controlIconLeadingClass } from "../../lib/control-styles";
6
+
7
+ export const Command = React.forwardRef<
8
+ React.ComponentRef<typeof CommandPrimitive>,
9
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
10
+ >(({ className, ...props }, ref) => (
11
+ <CommandPrimitive ref={ref} className={cn("ui-command", className)} {...props} />
12
+ ));
13
+ Command.displayName = CommandPrimitive.displayName;
14
+
15
+ export const CommandInput = React.forwardRef<
16
+ React.ComponentRef<typeof CommandPrimitive.Input>,
17
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
18
+ >(({ className, ...props }, ref) => (
19
+ // cmdk uses this non-standard attribute for input wrapper styling
20
+ // eslint-disable-next-line react/no-unknown-property -- cmdk convention
21
+ <div className="ui-command-input-wrapper" cmdk-input-wrapper="">
22
+ <Search className={cn(controlIconLeadingClass, "ui-command-search-icon")} aria-hidden="true" />
23
+ <CommandPrimitive.Input ref={ref} className={cn("ui-command-input", className)} {...props} />
24
+ </div>
25
+ ));
26
+ CommandInput.displayName = CommandPrimitive.Input.displayName;
27
+
28
+ export const CommandList = React.forwardRef<
29
+ React.ComponentRef<typeof CommandPrimitive.List>,
30
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
31
+ >(({ className, ...props }, ref) => (
32
+ <CommandPrimitive.List ref={ref} className={cn("ui-command-list", className)} {...props} />
33
+ ));
34
+ CommandList.displayName = CommandPrimitive.List.displayName;
35
+
36
+ export const CommandEmpty = React.forwardRef<
37
+ React.ComponentRef<typeof CommandPrimitive.Empty>,
38
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
39
+ >((props, ref) => <CommandPrimitive.Empty ref={ref} className="ui-command-empty" {...props} />);
40
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
41
+
42
+ export const CommandGroup = React.forwardRef<
43
+ React.ComponentRef<typeof CommandPrimitive.Group>,
44
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
45
+ >(({ className, ...props }, ref) => (
46
+ <CommandPrimitive.Group ref={ref} className={cn("ui-command-group", className)} {...props} />
47
+ ));
48
+ CommandGroup.displayName = CommandPrimitive.Group.displayName;
49
+
50
+ export const CommandItem = React.forwardRef<
51
+ React.ComponentRef<typeof CommandPrimitive.Item>,
52
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
53
+ >(({ className, ...props }, ref) => (
54
+ <CommandPrimitive.Item ref={ref} className={cn("ui-command-item", className)} {...props} />
55
+ ));
56
+ CommandItem.displayName = CommandPrimitive.Item.displayName;
@@ -0,0 +1,88 @@
1
+ import { cn } from "../../lib/utils";
2
+ import type {
3
+ CountryOptionLabelProp,
4
+ CountrySelectProp,
5
+ } from "../../props/components/data-entry.prop";
6
+ import { Inline } from "../layout/inline";
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectGroup,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "./select";
15
+
16
+ export type {
17
+ CountryOptionProp,
18
+ CountryOptionLabelProp,
19
+ CountrySelectProp,
20
+ CountrySelectProp as CountrySelectProps,
21
+ } from "../../props/components/data-entry.prop";
22
+
23
+ /** Flag + name (+ optional code) — accepts a select-option or a country summary. */
24
+ export function CountryOptionLabel({
25
+ country,
26
+ showCode = false,
27
+ className,
28
+ }: CountryOptionLabelProp) {
29
+ const code = country.value ?? country.code ?? "";
30
+ const text =
31
+ country.nativeName != null && country.nativeName !== ""
32
+ ? `${country.name} (${country.nativeName})`
33
+ : country.name;
34
+
35
+ return (
36
+ <Inline gap="xs" className={cn("items-center", className)}>
37
+ {country.flagSvgPath != null && country.flagSvgPath !== "" && (
38
+ <img
39
+ src={country.flagSvgPath}
40
+ alt=""
41
+ className="h-3 w-5 shrink-0 rounded-sm object-cover"
42
+ aria-hidden="true"
43
+ />
44
+ )}
45
+ <span className="truncate">
46
+ {text}
47
+ {showCode && code !== "" && <span className="text-muted-foreground"> · {code}</span>}
48
+ </span>
49
+ </Inline>
50
+ );
51
+ }
52
+
53
+ /** Country picker on top of Select; submits the country code via a hidden form value. */
54
+ export function CountrySelect({
55
+ id,
56
+ name,
57
+ options,
58
+ defaultValue,
59
+ required = false,
60
+ allowEmpty = false,
61
+ emptyLabel = "—",
62
+ placeholder,
63
+ invalid = false,
64
+ }: CountrySelectProp) {
65
+ const emptyValue = "0";
66
+ const resolvedDefault = defaultValue && defaultValue !== "" ? defaultValue : emptyValue;
67
+
68
+ return (
69
+ <Select
70
+ name={name}
71
+ defaultValue={allowEmpty ? resolvedDefault : (defaultValue ?? options[0]?.value)}
72
+ >
73
+ <SelectTrigger id={id} className="w-full" aria-invalid={invalid} aria-required={required}>
74
+ <SelectValue placeholder={placeholder} />
75
+ </SelectTrigger>
76
+ <SelectContent>
77
+ <SelectGroup>
78
+ {allowEmpty && <SelectItem value={emptyValue}>{emptyLabel}</SelectItem>}
79
+ {options.map((option) => (
80
+ <SelectItem key={option.value} value={option.value ?? ""}>
81
+ <CountryOptionLabel country={option} />
82
+ </SelectItem>
83
+ ))}
84
+ </SelectGroup>
85
+ </SelectContent>
86
+ </Select>
87
+ );
88
+ }