@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,38 @@
1
+ import * as React from "react";
2
+ import * as SliderPrimitive from "@radix-ui/react-slider";
3
+ import { cn } from "../../lib/utils";
4
+ import type { SliderProp } from "../../props/components/data-entry.prop";
5
+
6
+ export type { SliderProp, SliderProp as SliderProps } from "../../props/components/data-entry.prop";
7
+
8
+ /** Numeric range slider (Radix Slider). */
9
+ export const Slider = React.forwardRef<React.ComponentRef<typeof SliderPrimitive.Root>, SliderProp>(
10
+ ({ className, defaultValue, value, min = 0, max = 100, ...props }, ref) => {
11
+ const values = React.useMemo(
12
+ () =>
13
+ Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max],
14
+ [defaultValue, max, min, value],
15
+ );
16
+
17
+ return (
18
+ <SliderPrimitive.Root
19
+ ref={ref}
20
+ data-slot="slider"
21
+ className={cn("ui-slider", className)}
22
+ defaultValue={defaultValue}
23
+ min={min}
24
+ max={max}
25
+ value={value}
26
+ {...props}
27
+ >
28
+ <SliderPrimitive.Track data-slot="slider-track" className="ui-slider-track">
29
+ <SliderPrimitive.Range data-slot="slider-range" className="ui-slider-range" />
30
+ </SliderPrimitive.Track>
31
+ {values.map((_, index) => (
32
+ <SliderPrimitive.Thumb key={index} data-slot="slider-thumb" className="ui-slider-thumb" />
33
+ ))}
34
+ </SliderPrimitive.Root>
35
+ );
36
+ },
37
+ );
38
+ Slider.displayName = SliderPrimitive.Root.displayName;
@@ -0,0 +1,91 @@
1
+ import { useId, useState } from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import type { SwitchFieldProp } from "../../props/components/data-entry.prop";
5
+ import { Label } from "./label";
6
+ import { Switch } from "./switch";
7
+
8
+ export type {
9
+ SwitchFieldProp,
10
+ SwitchFieldProp as SwitchFieldProps,
11
+ } from "../../props/components/data-entry.prop";
12
+
13
+ /**
14
+ * Labelled boolean switch for native HTML forms.
15
+ *
16
+ * Layout follows the shadcn `field-switch` standard: label (+ helper) on the
17
+ * left, the switch on the right (horizontal). Composes `Label` + `Switch` and
18
+ * mirrors a hidden `0`/`1` input so it submits inside an HTML `<form>`.
19
+ */
20
+ export function SwitchField({
21
+ id,
22
+ name,
23
+ label,
24
+ required,
25
+ helper,
26
+ error,
27
+ labelAddon,
28
+ className,
29
+ checked,
30
+ defaultChecked = false,
31
+ onCheckedChange,
32
+ disabled,
33
+ size,
34
+ }: SwitchFieldProp) {
35
+ const generatedId = useId();
36
+ const fieldId = id ?? generatedId;
37
+ const [internalChecked, setInternalChecked] = useState(defaultChecked);
38
+ const isControlled = checked !== undefined;
39
+ const isChecked = isControlled ? checked : internalChecked;
40
+
41
+ const helperId = helper && !error ? `${fieldId}-helper` : undefined;
42
+ const errorId = error ? `${fieldId}-error` : undefined;
43
+
44
+ const handleChange = (next: boolean) => {
45
+ if (!isControlled) {
46
+ setInternalChecked(next);
47
+ }
48
+ onCheckedChange?.(next);
49
+ };
50
+
51
+ return (
52
+ <div className={cn("ui-stack-sm", className)} data-invalid={error ? true : undefined}>
53
+ <input type="hidden" name={name} value={isChecked ? "1" : "0"} readOnly />
54
+ <div className="flex items-center gap-2">
55
+ <Switch
56
+ id={fieldId}
57
+ checked={isChecked}
58
+ onCheckedChange={handleChange}
59
+ disabled={disabled}
60
+ size={size}
61
+ aria-describedby={errorId ?? helperId}
62
+ aria-required={required ? true : undefined}
63
+ aria-invalid={!!error || undefined}
64
+ />
65
+ <div className="ui-stack-xs min-w-0">
66
+ <div className="flex items-center gap-1">
67
+ <Label htmlFor={fieldId} className="ui-inline-xs">
68
+ <span>{label}</span>
69
+ {required && (
70
+ <span aria-hidden="true" className="text-destructive">
71
+ *
72
+ </span>
73
+ )}
74
+ </Label>
75
+ {labelAddon}
76
+ </div>
77
+ {helper && !error ? (
78
+ <p id={helperId} className="text-muted-foreground text-xs">
79
+ {helper}
80
+ </p>
81
+ ) : null}
82
+ </div>
83
+ </div>
84
+ {error ? (
85
+ <p id={errorId} role="alert" className="text-destructive text-xs">
86
+ {error}
87
+ </p>
88
+ ) : null}
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
3
+ import { cn } from "../../lib/utils";
4
+ import type { SwitchProp } from "../../props/components/data-entry.prop";
5
+
6
+ export type { SwitchProp, SwitchProp as SwitchProps } from "../../props/components/data-entry.prop";
7
+
8
+ export const Switch = React.forwardRef<React.ComponentRef<typeof SwitchPrimitive.Root>, SwitchProp>(
9
+ ({ className, size = "default", ...props }, ref) => (
10
+ <SwitchPrimitive.Root
11
+ ref={ref}
12
+ data-slot="switch"
13
+ data-size={size}
14
+ className={cn(
15
+ "peer ui-switch focus-visible:border-ring focus-visible:ring-ring/50 shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16
+ className,
17
+ )}
18
+ {...props}
19
+ >
20
+ <SwitchPrimitive.Thumb data-slot="switch-thumb" className="ui-switch-thumb" />
21
+ </SwitchPrimitive.Root>
22
+ ),
23
+ );
24
+ Switch.displayName = SwitchPrimitive.Root.displayName;
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ import { cn } from "../../lib/utils";
3
+ import { controlMultilineClass } from "../../lib/control-styles";
4
+
5
+ export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
6
+
7
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
8
+ ({ className, ...props }, ref) => (
9
+ <textarea ref={ref} className={cn(controlMultilineClass, className)} {...props} />
10
+ ),
11
+ );
12
+ Textarea.displayName = "Textarea";
@@ -0,0 +1,214 @@
1
+ import * as React from "react";
2
+ import { Clock } from "lucide-react";
3
+
4
+ import { useTranslation } from "../../i18n/use-translation";
5
+ import { formatTimeOfDay, isValidHhmm, normalizeHhmm } from "../../lib/datetime";
6
+ import { cn } from "../../lib/utils";
7
+ import { Button } from "../general/button";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover";
9
+ import { Input } from "./input";
10
+ import type { TimePickerProp } from "../../props/components/data-entry.prop";
11
+
12
+ export type {
13
+ TimePickerProp,
14
+ TimePickerProp as TimePickerProps,
15
+ } from "../../props/components/data-entry.prop";
16
+
17
+ function pad2(n: number) {
18
+ return String(n).padStart(2, "0");
19
+ }
20
+
21
+ function buildMinutes(step: number) {
22
+ const safe = Math.min(60, Math.max(1, step));
23
+ const items: number[] = [];
24
+ for (let m = 0; m < 60; m += safe) items.push(m);
25
+ return items;
26
+ }
27
+
28
+ function parseHhmm(value: string | undefined): { hour: number; minute: number } {
29
+ const normalized = value ? normalizeHhmm(value) : null;
30
+ if (!normalized) return { hour: 9, minute: 0 };
31
+ const [h, m] = normalized.split(":").map(Number);
32
+ return { hour: h, minute: m };
33
+ }
34
+
35
+ interface TimePickerPanelProps {
36
+ value: string;
37
+ minuteStep: number;
38
+ onChange: (value: string) => void;
39
+ onDone?: () => void;
40
+ }
41
+
42
+ function TimeColumn({
43
+ label,
44
+ items,
45
+ selected,
46
+ onSelect,
47
+ }: {
48
+ label: string;
49
+ items: number[];
50
+ selected: number;
51
+ onSelect: (value: number) => void;
52
+ }) {
53
+ const listRef = React.useRef<HTMLDivElement>(null);
54
+
55
+ React.useEffect(() => {
56
+ listRef.current?.querySelector('[data-selected="true"]')?.scrollIntoView({ block: "center" });
57
+ }, [selected]);
58
+
59
+ return (
60
+ <div className="flex min-w-0 flex-1 flex-col">
61
+ <div className="text-muted-foreground border-b px-1 py-1.5 text-center text-xs font-medium">
62
+ {label}
63
+ </div>
64
+ <div
65
+ ref={listRef}
66
+ className="h-52 [scrollbar-width:thin] [scrollbar-gutter:stable] overflow-y-scroll overscroll-contain p-1"
67
+ >
68
+ {items.map((item) => (
69
+ <button
70
+ key={item}
71
+ type="button"
72
+ data-selected={item === selected}
73
+ className={cn(
74
+ "hover:bg-accent flex w-full items-center justify-center rounded-md py-1.5 text-sm tabular-nums transition-colors",
75
+ item === selected && "bg-primary text-primary-foreground hover:bg-primary/90",
76
+ )}
77
+ onClick={() => {
78
+ onSelect(item);
79
+ }}
80
+ >
81
+ {pad2(item)}
82
+ </button>
83
+ ))}
84
+ </div>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ function TimePickerPanel({ value, minuteStep, onChange, onDone }: TimePickerPanelProps) {
90
+ const { t } = useTranslation();
91
+ const { hour, minute } = parseHhmm(value);
92
+ const minutes = buildMinutes(minuteStep);
93
+ const snappedMinute = minutes.includes(minute) ? minute : minutes[0];
94
+ const [draft, setDraft] = React.useState(value);
95
+
96
+ React.useEffect(() => {
97
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- keep editable text in sync when controlled value changes.
98
+ setDraft(value);
99
+ }, [value]);
100
+
101
+ const commitDraft = () => {
102
+ const normalized = normalizeHhmm(draft);
103
+ if (!normalized) return;
104
+ onChange(normalized);
105
+ onDone?.();
106
+ };
107
+
108
+ const commit = (next: string) => {
109
+ onChange(next);
110
+ onDone?.();
111
+ };
112
+
113
+ return (
114
+ <div className="w-36">
115
+ <div className="divide-border flex divide-x">
116
+ <TimeColumn
117
+ label={t("dataEntry.timePicker.hour")}
118
+ items={Array.from({ length: 24 }, (_, i) => i)}
119
+ selected={hour}
120
+ onSelect={(h) => {
121
+ onChange(`${pad2(h)}:${pad2(snappedMinute)}`);
122
+ }}
123
+ />
124
+ <TimeColumn
125
+ label={t("dataEntry.timePicker.minute")}
126
+ items={minutes}
127
+ selected={snappedMinute}
128
+ onSelect={(m) => {
129
+ commit(`${pad2(hour)}:${pad2(m)}`);
130
+ }}
131
+ />
132
+ </div>
133
+ <div className="border-t p-2">
134
+ <Input
135
+ value={draft}
136
+ onChange={(e) => {
137
+ setDraft(e.target.value);
138
+ }}
139
+ inputMode="numeric"
140
+ autoComplete="off"
141
+ placeholder={t("dataEntry.timePicker.typeLabel")}
142
+ aria-label={t("dataEntry.timePicker.typeLabel")}
143
+ className="text-center tabular-nums"
144
+ onKeyDown={(e) => {
145
+ if (e.key === "Enter") {
146
+ e.preventDefault();
147
+ commitDraft();
148
+ }
149
+ }}
150
+ onBlur={() => {
151
+ const normalized = normalizeHhmm(draft);
152
+ if (normalized) setDraft(normalized);
153
+ }}
154
+ />
155
+ </div>
156
+ </div>
157
+ );
158
+ }
159
+
160
+ export function TimePicker({
161
+ value: controlledValue,
162
+ defaultValue,
163
+ onChange,
164
+ placeholder,
165
+ disabled,
166
+ className,
167
+ id,
168
+ minuteStep = 5,
169
+ }: TimePickerProp) {
170
+ const { t } = useTranslation();
171
+ const [open, setOpen] = React.useState(false);
172
+ const [internal, setInternal] = React.useState(defaultValue ?? "");
173
+ const isControlled = controlledValue !== undefined;
174
+ const value = isControlled ? controlledValue : internal;
175
+ const resolvedPlaceholder = placeholder ?? t("dataEntry.timePicker.placeholder");
176
+
177
+ const setValue = (next: string) => {
178
+ if (!isControlled) setInternal(next);
179
+ onChange?.(next);
180
+ };
181
+
182
+ const display = value && isValidHhmm(value) ? formatTimeOfDay(value) : value || null;
183
+
184
+ return (
185
+ <Popover open={open} onOpenChange={setOpen}>
186
+ <PopoverTrigger asChild>
187
+ <Button
188
+ id={id}
189
+ type="button"
190
+ variant="outline"
191
+ disabled={disabled}
192
+ className={cn(
193
+ "w-full justify-start text-left font-normal tabular-nums",
194
+ !display && "text-muted-foreground",
195
+ className,
196
+ )}
197
+ >
198
+ <Clock className="mr-2 size-4 shrink-0" aria-hidden="true" />
199
+ {display ?? resolvedPlaceholder}
200
+ </Button>
201
+ </PopoverTrigger>
202
+ <PopoverContent className="w-auto p-0" align="start">
203
+ <TimePickerPanel
204
+ value={value || "09:00"}
205
+ minuteStep={minuteStep}
206
+ onChange={setValue}
207
+ onDone={() => {
208
+ setOpen(false);
209
+ }}
210
+ />
211
+ </PopoverContent>
212
+ </Popover>
213
+ );
214
+ }
@@ -0,0 +1,231 @@
1
+ import * as React from "react";
2
+ import { ChevronLeft, ChevronRight } 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 { ScrollArea } from "../data-display/scroll-area";
8
+ import { Checkbox } from "./checkbox";
9
+ import { SearchInput } from "./search-input";
10
+ import { reactNodeText } from "./tree-utils";
11
+ import type { TransferItemProp, TransferProp } from "../../props/components/data-entry.prop";
12
+
13
+ export type {
14
+ TransferProp,
15
+ TransferProp as TransferProps,
16
+ TransferItemProp,
17
+ } from "../../props/components/data-entry.prop";
18
+
19
+ function TransferPanel({
20
+ title,
21
+ items,
22
+ selectedKeys,
23
+ onSelectChange,
24
+ onSelectAll,
25
+ showSearch,
26
+ disabled,
27
+ searchPlaceholder,
28
+ emptyText,
29
+ direction,
30
+ }: {
31
+ title?: React.ReactNode;
32
+ items: TransferItemProp[];
33
+ selectedKeys: string[];
34
+ onSelectChange: (keys: string[]) => void;
35
+ onSelectAll: (checked: boolean) => void;
36
+ showSearch?: boolean;
37
+ disabled?: boolean;
38
+ searchPlaceholder: string;
39
+ emptyText: string;
40
+ direction: "left" | "right";
41
+ }) {
42
+ const [query, setQuery] = React.useState("");
43
+ const filtered = React.useMemo(() => {
44
+ const q = query.trim().toLowerCase();
45
+ if (!q) return items;
46
+ return items.filter((item) => {
47
+ const titleMatch = reactNodeText(item.title).toLowerCase().includes(q);
48
+ const descMatch = item.description
49
+ ? reactNodeText(item.description).toLowerCase().includes(q)
50
+ : false;
51
+ return titleMatch || descMatch;
52
+ });
53
+ }, [items, query]);
54
+
55
+ const enabledItems = filtered.filter((i) => !i.disabled);
56
+ const allChecked =
57
+ enabledItems.length > 0 && enabledItems.every((i) => selectedKeys.includes(i.key));
58
+ const indeterminate = !allChecked && enabledItems.some((i) => selectedKeys.includes(i.key));
59
+
60
+ const toggleKey = (key: string, checked: boolean) => {
61
+ onSelectChange(checked ? [...selectedKeys, key] : selectedKeys.filter((k) => k !== key));
62
+ };
63
+
64
+ return (
65
+ <div className="bg-background flex min-h-[14rem] flex-1 flex-col rounded-md border">
66
+ <div className="flex items-center justify-between border-b px-3 py-2 text-sm">
67
+ <label className="flex cursor-pointer items-center gap-2 font-medium">
68
+ <Checkbox
69
+ checked={allChecked ? true : indeterminate ? "indeterminate" : false}
70
+ disabled={Boolean(disabled) || enabledItems.length === 0}
71
+ onCheckedChange={(v) => onSelectAll(v === true)}
72
+ aria-label={direction === "left" ? "Select all source" : "Select all target"}
73
+ />
74
+ <span>{title}</span>
75
+ </label>
76
+ <span className="text-muted-foreground text-xs">
77
+ {selectedKeys.length}/{filtered.length}
78
+ </span>
79
+ </div>
80
+ {showSearch && (
81
+ <div className="border-b p-2">
82
+ <SearchInput
83
+ onSearch={setQuery}
84
+ placeholder={searchPlaceholder}
85
+ ariaLabel={searchPlaceholder}
86
+ debounce={0}
87
+ className={disabled ? "pointer-events-none opacity-50" : undefined}
88
+ />
89
+ </div>
90
+ )}
91
+ <ScrollArea className="flex-1">
92
+ <ul className="p-1">
93
+ {filtered.length === 0 ? (
94
+ <li className="text-muted-foreground py-8 text-center text-sm">{emptyText}</li>
95
+ ) : (
96
+ filtered.map((item) => (
97
+ <li key={item.key}>
98
+ <label
99
+ className={cn(
100
+ "flex cursor-pointer items-start gap-2 rounded-sm px-2 py-2 text-sm",
101
+ "hover:bg-accent hover:text-accent-foreground",
102
+ item.disabled && "pointer-events-none opacity-50",
103
+ )}
104
+ >
105
+ <Checkbox
106
+ checked={selectedKeys.includes(item.key)}
107
+ disabled={Boolean(disabled) || Boolean(item.disabled)}
108
+ onCheckedChange={(v) => toggleKey(item.key, v === true)}
109
+ className="mt-0.5"
110
+ />
111
+ <span className="min-w-0 flex-1">
112
+ <span className="block truncate font-medium">{item.title}</span>
113
+ {item.description && (
114
+ <span className="text-muted-foreground block truncate text-xs">
115
+ {item.description}
116
+ </span>
117
+ )}
118
+ </span>
119
+ </label>
120
+ </li>
121
+ ))
122
+ )}
123
+ </ul>
124
+ </ScrollArea>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ export function Transfer({
130
+ dataSource,
131
+ targetKeys,
132
+ onChange,
133
+ titles,
134
+ showSearch,
135
+ oneWay,
136
+ disabled,
137
+ className,
138
+ selectedKeys: selectedKeysProp,
139
+ onSelectChange,
140
+ }: TransferProp) {
141
+ const { t } = useTranslation();
142
+ const [internalSelected, setInternalSelected] = React.useState<[string[], string[]]>([[], []]);
143
+ const selected: [string[], string[]] = selectedKeysProp ?? internalSelected;
144
+
145
+ const sourceItems = dataSource.filter((item) => !targetKeys.includes(item.key));
146
+ const targetItems = dataSource.filter((item) => targetKeys.includes(item.key));
147
+
148
+ const setSelected = (side: 0 | 1, keys: string[]) => {
149
+ const next: [string[], string[]] = side === 0 ? [keys, selected[1]] : [selected[0], keys];
150
+ if (selectedKeysProp) onSelectChange?.(next[0], next[1]);
151
+ else setInternalSelected(next);
152
+ };
153
+
154
+ const move = (direction: "right" | "left") => {
155
+ const fromSide = direction === "right" ? 0 : 1;
156
+ const keys = selected[fromSide];
157
+ if (!keys.length) return;
158
+
159
+ const nextTarget =
160
+ direction === "right"
161
+ ? [...targetKeys, ...keys.filter((k) => !targetKeys.includes(k))]
162
+ : targetKeys.filter((k) => !keys.includes(k));
163
+
164
+ onChange?.(nextTarget, direction, keys);
165
+ const cleared: [string[], string[]] = fromSide === 0 ? [[], selected[1]] : [selected[0], []];
166
+ if (selectedKeysProp) onSelectChange?.(cleared[0], cleared[1]);
167
+ else setInternalSelected(cleared);
168
+ };
169
+
170
+ const leftTitle = titles?.[0] ?? t("dataEntry.transfer.source");
171
+ const rightTitle = titles?.[1] ?? t("dataEntry.transfer.target");
172
+
173
+ return (
174
+ <div className={cn("flex flex-wrap items-stretch gap-3", className)}>
175
+ <TransferPanel
176
+ direction="left"
177
+ title={leftTitle}
178
+ items={sourceItems}
179
+ selectedKeys={selected[0]}
180
+ onSelectChange={(keys) => setSelected(0, keys)}
181
+ onSelectAll={(checked) =>
182
+ setSelected(0, checked ? sourceItems.filter((i) => !i.disabled).map((i) => i.key) : [])
183
+ }
184
+ showSearch={showSearch}
185
+ disabled={disabled}
186
+ searchPlaceholder={t("dataEntry.transfer.searchPlaceholder")}
187
+ emptyText={t("dataEntry.transfer.empty")}
188
+ />
189
+
190
+ <div className="flex flex-col justify-center gap-2">
191
+ <Button
192
+ type="button"
193
+ size="icon"
194
+ variant="outline"
195
+ disabled={Boolean(disabled) || selected[0].length === 0}
196
+ aria-label={t("dataEntry.transfer.moveRight")}
197
+ onClick={() => move("right")}
198
+ >
199
+ <ChevronRight className="size-4" aria-hidden="true" />
200
+ </Button>
201
+ {!oneWay && (
202
+ <Button
203
+ type="button"
204
+ size="icon"
205
+ variant="outline"
206
+ disabled={Boolean(disabled) || selected[1].length === 0}
207
+ aria-label={t("dataEntry.transfer.moveLeft")}
208
+ onClick={() => move("left")}
209
+ >
210
+ <ChevronLeft className="size-4" aria-hidden="true" />
211
+ </Button>
212
+ )}
213
+ </div>
214
+
215
+ <TransferPanel
216
+ direction="right"
217
+ title={rightTitle}
218
+ items={targetItems}
219
+ selectedKeys={selected[1]}
220
+ onSelectChange={(keys) => setSelected(1, keys)}
221
+ onSelectAll={(checked) =>
222
+ setSelected(1, checked ? targetItems.filter((i) => !i.disabled).map((i) => i.key) : [])
223
+ }
224
+ showSearch={showSearch}
225
+ disabled={disabled}
226
+ searchPlaceholder={t("dataEntry.transfer.searchPlaceholder")}
227
+ emptyText={t("dataEntry.transfer.empty")}
228
+ />
229
+ </div>
230
+ );
231
+ }
@@ -0,0 +1,6 @@
1
+ /** Ant Design `showCheckedStrategy` equivalents for TreeSelect. */
2
+ export const SHOW_CHILD = "SHOW_CHILD" as const;
3
+ export const SHOW_PARENT = "SHOW_PARENT" as const;
4
+ export const SHOW_ALL = "SHOW_ALL" as const;
5
+
6
+ export type ShowCheckedStrategy = typeof SHOW_CHILD | typeof SHOW_PARENT | typeof SHOW_ALL;