@godxjp/ui 20.0.0 → 20.1.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 (250) hide show
  1. package/README.md +8 -2
  2. package/dist/app/theme-axes.d.ts +2 -0
  3. package/dist/app/theme-axes.js +45 -0
  4. package/dist/components/charts/chart-cartesian.d.ts +2 -1
  5. package/dist/components/charts/chart-cartesian.js +52 -3
  6. package/dist/components/charts/chart-category-axis.d.ts +55 -0
  7. package/dist/components/charts/chart-category-axis.js +93 -0
  8. package/dist/components/charts/chart-frame.d.ts +10 -1
  9. package/dist/components/charts/chart-frame.js +19 -3
  10. package/dist/components/charts/compact-bar-trend.d.ts +1 -1
  11. package/dist/components/charts/compact-bar-trend.js +2 -0
  12. package/dist/components/charts/pie-chart.d.ts +1 -1
  13. package/dist/components/charts/pie-chart.js +12 -1
  14. package/dist/components/charts/recharts-peer.d.ts +49 -0
  15. package/dist/components/charts/recharts-peer.js +45 -0
  16. package/dist/components/data-display/card.d.ts +12 -1
  17. package/dist/components/data-display/card.js +22 -4
  18. package/dist/components/data-display/code-block.js +6 -2
  19. package/dist/components/data-display/data-table.d.ts +39 -4
  20. package/dist/components/data-display/data-table.js +802 -270
  21. package/dist/components/data-display/descriptions.d.ts +27 -6
  22. package/dist/components/data-display/descriptions.js +58 -17
  23. package/dist/components/data-display/index.d.ts +2 -0
  24. package/dist/components/data-display/index.js +2 -0
  25. package/dist/components/data-display/list-row.d.ts +10 -1
  26. package/dist/components/data-display/list-row.js +7 -1
  27. package/dist/components/data-display/popover.d.ts +2 -2
  28. package/dist/components/data-display/popover.js +61 -11
  29. package/dist/components/data-display/range-timeline.d.ts +38 -0
  30. package/dist/components/data-display/range-timeline.js +161 -0
  31. package/dist/components/data-display/scroll-area.js +13 -2
  32. package/dist/components/data-display/service-launcher-card.js +10 -10
  33. package/dist/components/data-display/table.d.ts +11 -3
  34. package/dist/components/data-display/table.js +48 -18
  35. package/dist/components/data-display/tree-list.js +4 -9
  36. package/dist/components/data-entry/calendar.d.ts +2 -2
  37. package/dist/components/data-entry/calendar.js +12 -1
  38. package/dist/components/data-entry/cascader.d.ts +1 -1
  39. package/dist/components/data-entry/cascader.js +188 -24
  40. package/dist/components/data-entry/color-picker.d.ts +1 -1
  41. package/dist/components/data-entry/color-picker.js +17 -5
  42. package/dist/components/data-entry/control-appearance.d.ts +64 -0
  43. package/dist/components/data-entry/control-appearance.js +39 -0
  44. package/dist/components/data-entry/control-surface.d.ts +61 -0
  45. package/dist/components/data-entry/control-surface.js +39 -0
  46. package/dist/components/data-entry/date-picker.d.ts +1 -1
  47. package/dist/components/data-entry/date-picker.js +344 -113
  48. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  49. package/dist/components/data-entry/date-range-picker.js +278 -140
  50. package/dist/components/data-entry/form-field.d.ts +1 -1
  51. package/dist/components/data-entry/form-field.js +39 -4
  52. package/dist/components/data-entry/form.d.ts +4 -0
  53. package/dist/components/data-entry/form.js +4 -2
  54. package/dist/components/data-entry/index.d.ts +7 -3
  55. package/dist/components/data-entry/index.js +10 -1
  56. package/dist/components/data-entry/input-otp.d.ts +1 -1
  57. package/dist/components/data-entry/input.d.ts +12 -34
  58. package/dist/components/data-entry/input.js +92 -24
  59. package/dist/components/data-entry/month-picker.d.ts +1 -1
  60. package/dist/components/data-entry/month-picker.js +47 -10
  61. package/dist/components/data-entry/month-range-picker.d.ts +1 -1
  62. package/dist/components/data-entry/month-range-picker.js +51 -11
  63. package/dist/components/data-entry/number-input.d.ts +7 -0
  64. package/dist/components/data-entry/number-input.js +147 -98
  65. package/dist/components/data-entry/password-input.d.ts +1 -1
  66. package/dist/components/data-entry/radio.d.ts +1 -1
  67. package/dist/components/data-entry/radio.js +61 -13
  68. package/dist/components/data-entry/search-input.d.ts +1 -1
  69. package/dist/components/data-entry/search-input.js +16 -2
  70. package/dist/components/data-entry/search-select.d.ts +2 -2
  71. package/dist/components/data-entry/search-select.js +209 -78
  72. package/dist/components/data-entry/select.d.ts +22 -4
  73. package/dist/components/data-entry/select.js +215 -163
  74. package/dist/components/data-entry/slider.d.ts +9 -1
  75. package/dist/components/data-entry/slider.js +87 -9
  76. package/dist/components/data-entry/switch.d.ts +3 -0
  77. package/dist/components/data-entry/switch.js +29 -3
  78. package/dist/components/data-entry/textarea.d.ts +14 -56
  79. package/dist/components/data-entry/textarea.js +80 -33
  80. package/dist/components/data-entry/time-picker.d.ts +2 -2
  81. package/dist/components/data-entry/time-picker.js +333 -109
  82. package/dist/components/data-entry/time-range-picker.d.ts +5 -0
  83. package/dist/components/data-entry/time-range-picker.js +89 -0
  84. package/dist/components/data-entry/transfer.d.ts +1 -1
  85. package/dist/components/data-entry/transfer.js +86 -28
  86. package/dist/components/data-entry/tree-select.d.ts +1 -1
  87. package/dist/components/data-entry/tree-select.js +201 -113
  88. package/dist/components/data-entry/tree-utils.js +7 -14
  89. package/dist/components/data-entry/upload-files.d.ts +2 -0
  90. package/dist/components/data-entry/upload-files.js +31 -0
  91. package/dist/components/data-entry/upload-request.d.ts +4 -0
  92. package/dist/components/data-entry/upload-request.js +53 -0
  93. package/dist/components/data-entry/upload-types.d.ts +28 -0
  94. package/dist/components/data-entry/upload-types.js +2 -0
  95. package/dist/components/data-entry/upload.d.ts +2 -2
  96. package/dist/components/data-entry/upload.js +475 -115
  97. package/dist/components/feedback/dialog.js +16 -19
  98. package/dist/components/general/button.d.ts +1 -1
  99. package/dist/components/general/button.js +5 -1
  100. package/dist/components/general/index.d.ts +1 -0
  101. package/dist/components/general/index.js +2 -0
  102. package/dist/components/general/typography.d.ts +3 -0
  103. package/dist/components/general/typography.js +15 -1
  104. package/dist/components/general/visually-hidden.d.ts +3 -0
  105. package/dist/components/general/visually-hidden.js +9 -0
  106. package/dist/components/layout/app-launcher.d.ts +34 -0
  107. package/dist/components/layout/app-launcher.js +228 -0
  108. package/dist/components/layout/app-shell.d.ts +2 -0
  109. package/dist/components/layout/app-shell.js +20 -7
  110. package/dist/components/layout/breadcrumb.d.ts +14 -2
  111. package/dist/components/layout/breadcrumb.js +46 -4
  112. package/dist/components/layout/flex.d.ts +1 -1
  113. package/dist/components/layout/flex.js +23 -2
  114. package/dist/components/layout/index.d.ts +3 -0
  115. package/dist/components/layout/index.js +5 -1
  116. package/dist/components/layout/org-switcher.js +20 -1
  117. package/dist/components/layout/page-container.d.ts +1 -1
  118. package/dist/components/layout/page-container.js +5 -3
  119. package/dist/components/layout/responsive-grid.d.ts +16 -2
  120. package/dist/components/layout/responsive-grid.js +29 -2
  121. package/dist/components/layout/topbar-item.d.ts +3 -0
  122. package/dist/components/layout/topbar-item.js +18 -3
  123. package/dist/components/layout/topbar.d.ts +1 -1
  124. package/dist/components/layout/topbar.js +27 -6
  125. package/dist/components/navigation/dropdown-menu.d.ts +30 -2
  126. package/dist/components/navigation/dropdown-menu.js +68 -11
  127. package/dist/components/navigation/pagination.d.ts +2 -2
  128. package/dist/components/navigation/pagination.js +155 -83
  129. package/dist/components/navigation/steps.d.ts +2 -2
  130. package/dist/components/navigation/steps.js +29 -4
  131. package/dist/components/navigation/tabs.d.ts +5 -33
  132. package/dist/components/navigation/tabs.js +232 -64
  133. package/dist/components/ui/input-otp.d.ts +26 -28
  134. package/dist/components/ui/input-otp.js +50 -7
  135. package/dist/components/ui/password-input.d.ts +36 -2
  136. package/dist/components/ui/password-input.js +27 -7
  137. package/dist/components/ui/rating.d.ts +25 -0
  138. package/dist/components/ui/rating.js +67 -27
  139. package/dist/components/ui/segmented.d.ts +9 -0
  140. package/dist/components/ui/segmented.js +17 -2
  141. package/dist/components/ui/separator.d.ts +4 -0
  142. package/dist/components/ui/separator.js +13 -0
  143. package/dist/components/ui/tag-input.d.ts +45 -0
  144. package/dist/components/ui/tag-input.js +109 -27
  145. package/dist/form/form-context.d.ts +29 -0
  146. package/dist/form/form-context.js +44 -4
  147. package/dist/form/form-field-array.d.ts +22 -0
  148. package/dist/form/form-field-array.js +50 -0
  149. package/dist/form/form-field-control.d.ts +2 -1
  150. package/dist/form/form-field-control.js +116 -40
  151. package/dist/form/form-root.d.ts +2 -1
  152. package/dist/form/form-root.js +142 -23
  153. package/dist/form/index.d.ts +3 -1
  154. package/dist/form/index.js +12 -1
  155. package/dist/i18n/messages/en.json +56 -5
  156. package/dist/i18n/messages/ja.json +56 -5
  157. package/dist/i18n/messages/vi.json +56 -5
  158. package/dist/inertia/index.d.ts +20 -0
  159. package/dist/inertia/index.js +60 -1
  160. package/dist/lib/control-styles.d.ts +25 -3
  161. package/dist/lib/control-styles.js +9 -3
  162. package/dist/lib/datetime/picker-format.d.ts +8 -0
  163. package/dist/lib/datetime/picker-format.js +54 -0
  164. package/dist/props/components/charts.prop.d.ts +21 -0
  165. package/dist/props/components/data-display.prop.d.ts +55 -4
  166. package/dist/props/components/data-entry.prop.d.ts +687 -43
  167. package/dist/props/components/form.prop.d.ts +142 -4
  168. package/dist/props/components/general.prop.d.ts +17 -1
  169. package/dist/props/components/index.d.ts +2 -2
  170. package/dist/props/components/layout.prop.d.ts +163 -6
  171. package/dist/props/components/navigation.prop.d.ts +113 -3
  172. package/dist/props/registry.d.ts +376 -10
  173. package/dist/props/registry.js +493 -9
  174. package/dist/props/vocabulary/data.prop.d.ts +179 -1
  175. package/dist/props/vocabulary/index.d.ts +4 -4
  176. package/dist/props/vocabulary/interaction.prop.d.ts +51 -2
  177. package/dist/props/vocabulary/navigation.prop.d.ts +40 -0
  178. package/dist/props/vocabulary/shared.prop.d.ts +33 -0
  179. package/dist/styles/badge-layout.css +2 -1
  180. package/dist/styles/card-layout.css +26 -4
  181. package/dist/styles/chart-layout.css +15 -0
  182. package/dist/styles/control.css +515 -14
  183. package/dist/styles/data-display-layout.css +241 -6
  184. package/dist/styles/data-entry-layout.css +13 -0
  185. package/dist/styles/layout.css +221 -4
  186. package/dist/styles/navigation-layout.css +227 -0
  187. package/dist/styles/shell-layout.css +229 -5
  188. package/dist/styles/table-layout.css +95 -6
  189. package/dist/styles/text-layout.css +5 -0
  190. package/dist/tokens/base.css +1 -0
  191. package/dist/tokens/components/badge.css +1 -0
  192. package/dist/tokens/components/chart.css +6 -0
  193. package/dist/tokens/components/control.css +79 -2
  194. package/dist/tokens/components/data-display.css +9 -0
  195. package/dist/tokens/components/descriptions.css +11 -0
  196. package/dist/tokens/components/flex.css +8 -0
  197. package/dist/tokens/components/navigation.css +55 -0
  198. package/dist/tokens/components/shell.css +50 -2
  199. package/dist/tokens/components/table.css +14 -0
  200. package/dist/tokens/semantic/layout.css +9 -0
  201. package/docs/CONSUMER-RULES.md +13 -0
  202. package/docs/DESIGN-AUTHORITY.md +43 -6
  203. package/docs/DEVELOPMENT.md +4 -3
  204. package/docs/FORMS.md +151 -6
  205. package/docs/FRAME-COVERAGE-REPORT.md +29 -16
  206. package/docs/README.md +1 -1
  207. package/docs/STANDARDS-vocabulary-tokens.md +1 -1
  208. package/docs/TESTING.md +15 -6
  209. package/docs/charts/cjk-category-axis.tsx +81 -0
  210. package/docs/data-display/card/index.tsx +22 -0
  211. package/docs/data-display/data-table/examples/antd-parity.tsx +257 -0
  212. package/docs/data-display/data-table/index.tsx +23 -0
  213. package/docs/data-display/descriptions.tsx +41 -0
  214. package/docs/data-display/scroll-area.tsx +31 -25
  215. package/docs/data-display/table.tsx +104 -45
  216. package/docs/data-display/timeline.tsx +41 -0
  217. package/docs/data-entry/date-picker.tsx +85 -0
  218. package/docs/data-entry/date-range-picker.tsx +26 -0
  219. package/docs/data-entry/form-dynamic-fields.tsx +199 -0
  220. package/docs/data-entry/form.tsx +74 -4
  221. package/docs/data-entry/input-otp.tsx +24 -0
  222. package/docs/data-entry/input.tsx +46 -1
  223. package/docs/data-entry/month-range-picker.tsx +1 -1
  224. package/docs/data-entry/select.tsx +27 -0
  225. package/docs/data-entry/switch.tsx +41 -0
  226. package/docs/data-entry/textarea.tsx +38 -0
  227. package/docs/data-entry/time-picker.tsx +85 -1
  228. package/docs/data-entry/time-range-picker.tsx +55 -0
  229. package/docs/data-entry/transfer.tsx +17 -0
  230. package/docs/data-entry/upload.tsx +56 -12
  231. package/docs/feedback/tooltip.tsx +1 -1
  232. package/docs/general/activity.tsx +2 -2
  233. package/docs/general/button/index.tsx +14 -1
  234. package/docs/general/typography.tsx +14 -1
  235. package/docs/layout/app-launcher.tsx +151 -0
  236. package/docs/layout/app-shell.tsx +11 -0
  237. package/docs/layout/flex.tsx +50 -0
  238. package/docs/layout/responsive-grid.tsx +21 -1
  239. package/docs/layout/topbar.tsx +5 -9
  240. package/docs/navigation/app-setting-picker.tsx +11 -0
  241. package/docs/navigation/breadcrumb.tsx +48 -0
  242. package/docs/navigation/dropdown-menu.tsx +21 -0
  243. package/docs/navigation/pagination.tsx +52 -0
  244. package/docs/navigation/steps.tsx +37 -0
  245. package/docs/navigation/tabs.tsx +97 -1
  246. package/docs/query/button-refetch.tsx +1 -0
  247. package/package.json +19 -2
  248. package/scripts/_agent-setup.mjs +19 -2
  249. package/scripts/init-guinea-pig.mjs +26 -4
  250. package/scripts/ui-audit.mjs +243 -30
@@ -3,25 +3,56 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Clock, X } from "lucide-react";
5
5
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation.js";
6
- import { isValidHhmm, normalizeHhmm } from "../../lib/datetime/index.js";
6
+ import { normalizeHhmm } from "../../lib/datetime/index.js";
7
+ import { useControlledLatch } from "../../lib/hooks.js";
7
8
  import { pickFieldA11y } from "../../lib/field-a11y.js";
8
9
  import { cn } from "../../lib/utils.js";
10
+ import { resolveAllowClear } from "./control-surface.js";
9
11
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
12
+ import { Button } from "../general/button.js";
10
13
  import { Input } from "./input.js";
11
14
  function pad2(n) {
12
15
  return String(n).padStart(2, "0");
13
16
  }
14
- function buildMinutes(step) {
15
- const safe = Math.min(60, Math.max(1, step));
17
+ function buildMinutes(step, limit = 60) {
18
+ const safe = Number.isFinite(step) ? Math.min(limit, Math.max(1, Math.floor(step))) : 1;
16
19
  const items = [];
17
- for (let m = 0; m < 60; m += safe) items.push(m);
20
+ for (let m = 0; m < limit; m += safe) items.push(m);
18
21
  return items;
19
22
  }
23
+ function normalizeTime(raw, seconds = false) {
24
+ const match = /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(am|pm)?$/i.exec(raw.trim());
25
+ if (!match) return seconds ? null : normalizeHhmm(raw);
26
+ let hour = Number(match[1]);
27
+ const minute = Number(match[2]);
28
+ const second = Number(match[3] ?? 0);
29
+ if (match[4]) {
30
+ if (hour < 1 || hour > 12) return null;
31
+ hour = hour % 12 + (match[4].toLowerCase() === "pm" ? 12 : 0);
32
+ }
33
+ if (hour > 23 || minute > 59 || second > 59) return null;
34
+ return `${pad2(hour)}:${pad2(minute)}${seconds ? `:${pad2(second)}` : ""}`;
35
+ }
20
36
  function parseHhmm(value) {
21
- const normalized = value ? normalizeHhmm(value) : null;
22
- if (!normalized) return { hour: 9, minute: 0 };
23
- const [h, m] = normalized.split(":").map(Number);
24
- return { hour: h, minute: m };
37
+ const [hour, minute, second] = (normalizeTime(value ?? "", true) ?? "09:00:00").split(":").map(Number);
38
+ return { hour, minute, second };
39
+ }
40
+ function displayTime(value, pattern) {
41
+ if (!value || !pattern) return value;
42
+ const { hour, minute, second } = parseHhmm(value);
43
+ return pattern.replace(
44
+ /HH|hh|H|h|mm|ss|A|a/g,
45
+ (token) => ({
46
+ HH: pad2(hour),
47
+ H: String(hour),
48
+ hh: pad2(to12h(hour)),
49
+ h: String(to12h(hour)),
50
+ mm: pad2(minute),
51
+ ss: pad2(second),
52
+ A: hour >= 12 ? "PM" : "AM",
53
+ a: hour >= 12 ? "pm" : "am"
54
+ })[token] ?? token
55
+ );
25
56
  }
26
57
  function to12h(hour24) {
27
58
  const h = hour24 % 12;
@@ -31,43 +62,85 @@ function from12h(hour12, meridiem) {
31
62
  const base = hour12 % 12;
32
63
  return meridiem === "pm" ? base + 12 : base;
33
64
  }
65
+ function useTimeRefusals(disabledTime) {
66
+ const rules = disabledTime?.();
67
+ const hours = rules?.disabledHours?.();
68
+ return {
69
+ isHourRefused: (hour) => hours?.includes(hour) ?? false,
70
+ isMinuteRefused: (hour, minute) => rules?.disabledMinutes?.(hour)?.includes(minute) ?? false
71
+ };
72
+ }
73
+ function isTimeRefused(value, disabledTime) {
74
+ if (!disabledTime) return false;
75
+ const normalized = normalizeTime(value, true);
76
+ if (!normalized) return false;
77
+ const [hour, minute, second] = normalized.split(":").map(Number);
78
+ const rules = disabledTime();
79
+ return (rules.disabledHours?.().includes(hour) ?? false) || (rules.disabledMinutes?.(hour).includes(minute) ?? false) || (rules.disabledSeconds?.(hour, minute).includes(second) ?? false);
80
+ }
34
81
  function TimeColumn({
35
82
  label,
36
83
  items,
37
84
  selected,
38
85
  formatItem,
39
- onSelect
86
+ onSelect,
87
+ isDisabled,
88
+ hideDisabled = false,
89
+ onScrollSelect
40
90
  }) {
41
91
  const listRef = React.useRef(null);
42
92
  React.useEffect(() => {
43
93
  listRef.current?.querySelector('[data-selected="true"]')?.scrollIntoView({ block: "center" });
44
94
  }, [selected]);
45
- const moveFocus = (index) => {
95
+ const visible = hideDisabled && isDisabled ? items.filter((item) => !isDisabled(item)) : items;
96
+ React.useEffect(() => {
97
+ const list = listRef.current;
98
+ if (!list || !onScrollSelect) return;
99
+ const wheel = (event) => {
100
+ if (!event.deltaY) return;
101
+ event.preventDefault();
102
+ const step = Math.sign(event.deltaY);
103
+ for (let index = visible.indexOf(selected) + step; index >= 0 && index < visible.length; index += step) {
104
+ if (!isDisabled?.(visible[index])) {
105
+ onScrollSelect(visible[index]);
106
+ break;
107
+ }
108
+ }
109
+ };
110
+ list.addEventListener("wheel", wheel, { passive: false });
111
+ return () => list.removeEventListener("wheel", wheel);
112
+ }, [onScrollSelect, selected, visible, isDisabled]);
113
+ const moveFocus = (index, step) => {
46
114
  const options = listRef.current?.querySelectorAll('[role="option"]');
47
- options?.[index]?.focus();
115
+ if (!options) return;
116
+ for (let at = index; at >= 0 && at < visible.length; at += step) {
117
+ if (isDisabled?.(visible[at])) continue;
118
+ options[at]?.focus();
119
+ return;
120
+ }
48
121
  };
49
122
  const onKeyDown = (e, index) => {
50
123
  switch (e.key) {
51
124
  case "ArrowDown":
52
125
  e.preventDefault();
53
- moveFocus(Math.min(items.length - 1, index + 1));
126
+ moveFocus(Math.min(visible.length - 1, index + 1), 1);
54
127
  break;
55
128
  case "ArrowUp":
56
129
  e.preventDefault();
57
- moveFocus(Math.max(0, index - 1));
130
+ moveFocus(Math.max(0, index - 1), -1);
58
131
  break;
59
132
  case "Home":
60
133
  e.preventDefault();
61
- moveFocus(0);
134
+ moveFocus(0, 1);
62
135
  break;
63
136
  case "End":
64
137
  e.preventDefault();
65
- moveFocus(items.length - 1);
138
+ moveFocus(visible.length - 1, -1);
66
139
  break;
67
140
  case "Enter":
68
141
  case " ":
69
142
  e.preventDefault();
70
- onSelect(items[index]);
143
+ if (!isDisabled?.(visible[index])) onSelect(visible[index]);
71
144
  break;
72
145
  default:
73
146
  break;
@@ -75,8 +148,9 @@ function TimeColumn({
75
148
  };
76
149
  return /* @__PURE__ */ jsxs("div", { className: "ui-time-picker-column", children: [
77
150
  /* @__PURE__ */ jsx("div", { className: "ui-time-picker-column-heading", children: label }),
78
- /* @__PURE__ */ jsx("div", { ref: listRef, role: "listbox", "aria-label": label, className: "ui-time-picker-column-scroll", children: items.map((item, index) => {
151
+ /* @__PURE__ */ jsx("div", { ref: listRef, role: "listbox", "aria-label": label, className: "ui-time-picker-column-scroll", children: visible.map((item, index) => {
79
152
  const isSelected = item === selected;
153
+ const refused = isDisabled?.(item) ?? false;
80
154
  return /* @__PURE__ */ jsx(
81
155
  "button",
82
156
  {
@@ -84,10 +158,12 @@ function TimeColumn({
84
158
  role: "option",
85
159
  "aria-selected": isSelected,
86
160
  "data-selected": isSelected,
87
- tabIndex: isSelected ? 0 : -1,
161
+ "aria-disabled": refused || void 0,
162
+ "data-disabled": refused || void 0,
163
+ tabIndex: !refused && (isSelected || !visible.some((option) => option === selected && !isDisabled?.(option)) && index === visible.findIndex((option) => !isDisabled?.(option))) ? 0 : -1,
88
164
  className: "ui-time-picker-option",
89
165
  onClick: () => {
90
- onSelect(item);
166
+ if (!refused) onSelect(item);
91
167
  },
92
168
  onKeyDown: (e) => onKeyDown(e, index),
93
169
  children: formatItem(item)
@@ -97,56 +173,129 @@ function TimeColumn({
97
173
  }) })
98
174
  ] });
99
175
  }
100
- function TimePickerPanel({ value, minuteStep, use12h, onChange, onDone }) {
176
+ function TimePickerPanel({
177
+ value,
178
+ minuteStep,
179
+ hourStep,
180
+ secondStep,
181
+ showSeconds,
182
+ changeOnScroll,
183
+ use12h,
184
+ disabledTime,
185
+ hideDisabledOptions,
186
+ showNow,
187
+ needConfirm,
188
+ onChange,
189
+ onDone
190
+ }) {
101
191
  const { t } = useTranslation();
102
192
  const draftId = React.useId();
103
- const { hour, minute } = parseHhmm(value);
193
+ const { isHourRefused, isMinuteRefused } = useTimeRefusals(disabledTime);
104
194
  const minutes = buildMinutes(minuteStep);
105
- const snappedMinute = minutes.includes(minute) ? minute : minutes[0];
106
195
  const [draft, setDraft] = React.useState(value);
196
+ const [pending, setPending] = React.useState(null);
197
+ const working = needConfirm ? pending ?? value : value;
198
+ const { hour, minute, second } = parseHhmm(working);
199
+ const seconds = buildMinutes(secondStep);
200
+ const time = (h, m, s = second) => `${pad2(h)}:${pad2(m)}${showSeconds ? `:${pad2(s)}` : ""}`;
201
+ const snappedMinute = minutes.includes(minute) ? minute : minutes[0];
107
202
  const meridiem = hour >= 12 ? "pm" : "am";
108
203
  React.useEffect(() => {
109
204
  setDraft(value);
110
205
  }, [value]);
111
206
  const commitDraft = () => {
112
- const normalized = normalizeHhmm(draft);
113
- if (!normalized) return;
114
- onChange(normalized);
115
- onDone?.();
207
+ const normalized = normalizeTime(draft, showSeconds);
208
+ if (!normalized || isTimeRefused(normalized, disabledTime)) return;
209
+ choose(normalized, { done: true });
116
210
  };
117
- const commit = (next) => {
211
+ const choose = (next, { done }) => {
212
+ if (isTimeRefused(next, disabledTime)) return;
213
+ if (needConfirm) {
214
+ setPending(next);
215
+ setDraft(next);
216
+ return;
217
+ }
218
+ onChange(next);
219
+ if (done) onDone?.();
220
+ };
221
+ const chooseHour = (nextHour) => {
222
+ const candidates = [snappedMinute, ...minutes.filter((m) => m !== snappedMinute)];
223
+ for (const nextMinute of candidates) {
224
+ const candidateSeconds = showSeconds ? [second, ...seconds.filter((s) => s !== second)] : [0];
225
+ for (const nextSecond of candidateSeconds) {
226
+ const next = time(nextHour, nextMinute, nextSecond);
227
+ if (!isTimeRefused(next, disabledTime)) {
228
+ choose(next, { done: false });
229
+ return;
230
+ }
231
+ }
232
+ }
233
+ };
234
+ const confirm = () => {
235
+ const next = normalizeTime(draft, showSeconds);
236
+ if (!next || isTimeRefused(next, disabledTime)) return;
118
237
  onChange(next);
119
238
  onDone?.();
120
239
  };
121
- const hourItems = use12h ? Array.from({ length: 12 }, (_, i) => i + 1) : Array.from({ length: 24 }, (_, i) => i);
240
+ const now = () => {
241
+ const at = /* @__PURE__ */ new Date();
242
+ const snapped = minutes.filter((minute2) => minute2 <= at.getMinutes()).at(-1) ?? 0;
243
+ return time(
244
+ at.getHours(),
245
+ snapped,
246
+ seconds.filter((second2) => second2 <= at.getSeconds()).at(-1) ?? 0
247
+ );
248
+ };
249
+ const nowValue = now();
250
+ const nowRefused = isTimeRefused(nowValue, disabledTime);
251
+ const hourItems = use12h ? buildMinutes(hourStep, 12).map((hour2) => hour2 || 12) : buildMinutes(hourStep, 24);
122
252
  const selectedHourItem = use12h ? to12h(hour) : hour;
123
253
  return /* @__PURE__ */ jsxs("div", { className: "ui-time-picker-panel", "data-hour-cycle": use12h ? "h12" : "h23", children: [
124
254
  /* @__PURE__ */ jsxs("div", { className: "divide-border flex divide-x", children: [
125
255
  /* @__PURE__ */ jsx(
126
256
  TimeColumn,
127
257
  {
258
+ onScrollSelect: changeOnScroll ? (h) => choose(time(use12h ? from12h(h, meridiem) : h, minute), { done: false }) : void 0,
128
259
  label: t("dataEntry.timePicker.hour"),
129
260
  items: hourItems,
130
261
  selected: selectedHourItem,
131
262
  formatItem: (h) => use12h ? String(h) : pad2(h),
263
+ isDisabled: (h) => isHourRefused(use12h ? from12h(h, meridiem) : h),
264
+ hideDisabled: hideDisabledOptions,
132
265
  onSelect: (h) => {
133
266
  const hour24 = use12h ? from12h(h, meridiem) : h;
134
- onChange(`${pad2(hour24)}:${pad2(snappedMinute)}`);
267
+ chooseHour(hour24);
135
268
  }
136
269
  }
137
270
  ),
138
271
  /* @__PURE__ */ jsx(
139
272
  TimeColumn,
140
273
  {
274
+ onScrollSelect: changeOnScroll ? (m) => choose(time(hour, m), { done: false }) : void 0,
141
275
  label: t("dataEntry.timePicker.minute"),
142
276
  items: minutes,
143
277
  selected: snappedMinute,
144
278
  formatItem: (m) => pad2(m),
279
+ isDisabled: (m) => isMinuteRefused(hour, m),
280
+ hideDisabled: hideDisabledOptions,
145
281
  onSelect: (m) => {
146
- commit(`${pad2(hour)}:${pad2(m)}`);
282
+ choose(time(hour, m), { done: !showSeconds });
147
283
  }
148
284
  }
149
285
  ),
286
+ showSeconds && /* @__PURE__ */ jsx(
287
+ TimeColumn,
288
+ {
289
+ onScrollSelect: changeOnScroll ? (s) => choose(time(hour, minute, s), { done: false }) : void 0,
290
+ label: t("dataEntry.timePicker.second"),
291
+ items: seconds,
292
+ selected: second,
293
+ formatItem: pad2,
294
+ hideDisabled: hideDisabledOptions,
295
+ isDisabled: (s) => isTimeRefused(time(hour, minute, s), disabledTime),
296
+ onSelect: (s) => choose(time(hour, minute, s), { done: true })
297
+ }
298
+ ),
150
299
  use12h && /* @__PURE__ */ jsx(
151
300
  TimeColumn,
152
301
  {
@@ -157,36 +306,55 @@ function TimePickerPanel({ value, minuteStep, use12h, onChange, onDone }) {
157
306
  onSelect: (m) => {
158
307
  const nextMeridiem = m === 1 ? "pm" : "am";
159
308
  const hour24 = from12h(to12h(hour), nextMeridiem);
160
- onChange(`${pad2(hour24)}:${pad2(snappedMinute)}`);
309
+ chooseHour(hour24);
161
310
  }
162
311
  }
163
312
  )
164
313
  ] }),
165
- /* @__PURE__ */ jsx("div", { className: "ui-time-picker-footer", children: /* @__PURE__ */ jsx(
166
- Input,
167
- {
168
- id: draftId,
169
- value: draft,
170
- onChange: (e) => {
171
- setDraft(e.target.value);
172
- },
173
- inputMode: "numeric",
174
- autoComplete: "off",
175
- placeholder: t("dataEntry.timePicker.typeLabel"),
176
- "aria-label": t("dataEntry.timePicker.typeLabel"),
177
- className: "text-center tabular-nums",
178
- onKeyDown: (e) => {
179
- if (e.key === "Enter") {
180
- e.preventDefault();
181
- commitDraft();
314
+ /* @__PURE__ */ jsxs("div", { className: "ui-time-picker-footer", children: [
315
+ /* @__PURE__ */ jsx(
316
+ Input,
317
+ {
318
+ id: draftId,
319
+ value: draft,
320
+ onChange: (e) => {
321
+ setDraft(e.target.value);
322
+ },
323
+ inputMode: "numeric",
324
+ autoComplete: "off",
325
+ placeholder: t("dataEntry.timePicker.typeLabel"),
326
+ "aria-label": t("dataEntry.timePicker.typeLabel"),
327
+ className: "text-center tabular-nums",
328
+ onKeyDown: (e) => {
329
+ if (e.key === "Enter") {
330
+ e.preventDefault();
331
+ commitDraft();
332
+ }
333
+ },
334
+ onBlur: () => {
335
+ const normalized = normalizeTime(draft, showSeconds);
336
+ if (normalized) setDraft(normalized);
182
337
  }
183
- },
184
- onBlur: () => {
185
- const normalized = normalizeHhmm(draft);
186
- if (normalized) setDraft(normalized);
187
338
  }
188
- }
189
- ) })
339
+ ),
340
+ showNow || needConfirm ? /* @__PURE__ */ jsxs("div", { className: "ui-time-picker-footer-actions", children: [
341
+ showNow ? /* @__PURE__ */ jsx(
342
+ Button,
343
+ {
344
+ type: "button",
345
+ variant: "link",
346
+ size: "sm",
347
+ disabled: nowRefused,
348
+ onClick: () => {
349
+ setDraft(nowValue);
350
+ choose(nowValue, { done: true });
351
+ },
352
+ children: t("dataEntry.timePicker.now")
353
+ }
354
+ ) : null,
355
+ needConfirm ? /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: confirm, children: t("dataEntry.timePicker.confirm") }) : null
356
+ ] }) : null
357
+ ] })
190
358
  ] });
191
359
  }
192
360
  function TimePicker({
@@ -199,24 +367,52 @@ function TimePicker({
199
367
  id,
200
368
  name,
201
369
  minuteStep = 5,
202
- allowClear = true,
370
+ hourStep = 1,
371
+ changeOnScroll,
372
+ secondStep = 1,
373
+ showSeconds: showSecondsProp = false,
374
+ use12Hours,
375
+ format,
376
+ open: openProp,
377
+ defaultOpen = false,
378
+ onOpenChange,
379
+ inputReadOnly,
380
+ preserveInvalidOnBlur,
381
+ placement = "bottom-end",
382
+ renderExtraFooter,
383
+ size,
384
+ status,
385
+ variant,
386
+ ref,
387
+ disabledTime,
388
+ hideDisabledOptions,
389
+ showNow = true,
390
+ needConfirm = false,
391
+ allowClear,
203
392
  ...ariaProps
204
393
  }) {
205
394
  const { t } = useTranslation();
206
395
  const { timeFormat } = usePickerLocales();
207
- const use12h = timeFormat === "12h";
208
- const [open, setOpen] = React.useState(false);
396
+ const use12h = use12Hours ?? (format ? /h|a|A/.test(format) : timeFormat === "12h");
397
+ const showSeconds = showSecondsProp || Boolean(format?.includes("ss"));
398
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
399
+ const open = !disabled && (openProp ?? internalOpen);
400
+ const setOpen = (next) => {
401
+ if (disabled && next) return;
402
+ if (openProp === void 0) setInternalOpen(next);
403
+ onOpenChange?.(next);
404
+ };
209
405
  const fieldA11y = pickFieldA11y(ariaProps);
210
406
  const reactId = React.useId();
211
407
  const dialogId = `${id ?? reactId}-dialog`;
212
408
  const [internal, setInternal] = React.useState(defaultValue ?? "");
213
- const isControlled = controlledValue !== void 0;
214
- const value = isControlled ? controlledValue : internal;
409
+ const isControlled = useControlledLatch(controlledValue !== void 0);
410
+ const value = (isControlled ? controlledValue : internal) ?? "";
215
411
  const resolvedPlaceholder = placeholder ?? t("dataEntry.timePicker.placeholder") ?? "hh:mm";
216
- const [text, setText] = React.useState(value);
412
+ const [text, setText] = React.useState(displayTime(value, format));
217
413
  React.useEffect(() => {
218
- setText(value);
219
- }, [value]);
414
+ setText(displayTime(value, format));
415
+ }, [value, format]);
220
416
  const setValue = (next) => {
221
417
  if (!isControlled) setInternal(next);
222
418
  onValueChange?.(next);
@@ -225,13 +421,20 @@ function TimePicker({
225
421
  setValue("");
226
422
  setText("");
227
423
  };
228
- const showClear = allowClear && text !== "" && !disabled;
424
+ const clearControl = resolveAllowClear(allowClear, true, t("common.clear") ?? "Clear");
425
+ const showClear = clearControl.enabled && text !== "" && !disabled;
229
426
  return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
427
+ (format || needConfirm || preserveInvalidOnBlur) && name ? /* @__PURE__ */ jsx("input", { type: "hidden", disabled, name, value }) : null,
230
428
  /* @__PURE__ */ jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsx("div", { className: cn("relative", className), children: /* @__PURE__ */ jsx(
231
429
  Input,
232
430
  {
233
431
  id,
234
- name,
432
+ ref,
433
+ size,
434
+ status,
435
+ variant,
436
+ readOnly: inputReadOnly,
437
+ name: format || needConfirm || preserveInvalidOnBlur ? "" : name,
235
438
  value: text,
236
439
  disabled,
237
440
  placeholder: resolvedPlaceholder,
@@ -242,31 +445,32 @@ function TimePicker({
242
445
  "aria-haspopup": "dialog",
243
446
  "aria-controls": open ? dialogId : void 0,
244
447
  ...fieldA11y,
245
- className: cn("tabular-nums", showClear && "ui-control-inline-affix-pair-affixed"),
246
- trailingIcon: /* @__PURE__ */ jsxs("span", { className: "ui-time-picker-affix", children: [
247
- showClear ? /* @__PURE__ */ jsx(
248
- "button",
249
- {
250
- type: "button",
251
- tabIndex: -1,
252
- "aria-label": t("common.clear") ?? "Clear",
253
- onClick: clear,
254
- className: "ui-control-inline-affix-action",
255
- children: /* @__PURE__ */ jsx(X, { className: "ui-control-inline-affix-icon", "aria-hidden": "true" })
256
- }
257
- ) : null,
258
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
259
- "button",
260
- {
261
- type: "button",
262
- disabled,
263
- tabIndex: -1,
264
- "aria-label": t("dataEntry.timePicker.openPicker") ?? "Open time picker",
265
- className: "ui-control-inline-affix-action",
266
- children: /* @__PURE__ */ jsx(Clock, { className: "ui-control-inline-affix-icon", "aria-hidden": "true" })
267
- }
268
- ) })
269
- ] }),
448
+ className: "tabular-nums",
449
+ trailingIcon: /* @__PURE__ */ jsx("span", { className: "ui-time-picker-affix", children: showClear ? /* @__PURE__ */ jsx(
450
+ "button",
451
+ {
452
+ type: "button",
453
+ tabIndex: -1,
454
+ "aria-label": clearControl.label,
455
+ onClick: (event) => {
456
+ event.stopPropagation();
457
+ event.currentTarget.closest("div")?.querySelector("input:not([type=hidden])")?.focus();
458
+ clear();
459
+ },
460
+ className: "ui-control-inline-affix-action",
461
+ children: clearControl.clearIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-control-inline-affix-icon", "aria-hidden": "true" })
462
+ }
463
+ ) : /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
464
+ "button",
465
+ {
466
+ type: "button",
467
+ disabled,
468
+ tabIndex: -1,
469
+ "aria-label": t("dataEntry.timePicker.openPicker") ?? "Open time picker",
470
+ className: "ui-control-inline-affix-action",
471
+ children: /* @__PURE__ */ jsx(Clock, { className: "ui-control-inline-affix-icon", "aria-hidden": "true" })
472
+ }
473
+ ) }) }),
270
474
  onClick: () => {
271
475
  if (!disabled) setOpen(true);
272
476
  },
@@ -274,45 +478,65 @@ function TimePicker({
274
478
  if (event.key === "ArrowDown") {
275
479
  event.preventDefault();
276
480
  setOpen(true);
481
+ } else if (event.key === "Enter") {
482
+ const next = normalizeTime(text, showSeconds);
483
+ if (next && !isTimeRefused(next, disabledTime)) {
484
+ setValue(next);
485
+ setOpen(false);
486
+ }
277
487
  } else if (event.key === "Escape" && open) {
278
488
  setOpen(false);
279
489
  }
280
490
  },
281
491
  onChange: (event) => {
282
492
  setText(event.target.value);
283
- const normalized = normalizeHhmm(event.target.value);
284
- if (normalized) setValue(normalized);
493
+ const normalized = normalizeTime(event.target.value, showSeconds);
494
+ if (normalized && (!showSeconds || /^\d{1,2}:\d{2}:\d{2}/.test(event.target.value)) && (!format || !/[aA]/.test(format) || /[ap]m$/i.test(event.target.value.trim())) && !isTimeRefused(normalized, disabledTime) && !needConfirm)
495
+ setValue(normalized);
285
496
  },
286
497
  onBlur: (event) => {
287
- const normalized = normalizeHhmm(event.target.value);
288
- setText(normalized ?? (isValidHhmm(value) ? value : ""));
498
+ const normalized = normalizeTime(event.target.value, showSeconds);
499
+ const accepted = normalized && !isTimeRefused(normalized, disabledTime) ? normalized : void 0;
500
+ if (!preserveInvalidOnBlur) setText(displayTime(accepted ?? value, format));
289
501
  }
290
502
  }
291
503
  ) }) }),
292
- /* @__PURE__ */ jsx(
504
+ /* @__PURE__ */ jsxs(
293
505
  PopoverContent,
294
506
  {
295
507
  id: dialogId,
296
508
  role: "dialog",
297
509
  "aria-label": t("dataEntry.timePicker.openPicker") ?? "Time picker",
298
510
  className: "ui-time-picker-popover",
299
- align: "end",
511
+ side: placement.startsWith("top") ? "top" : "bottom",
512
+ align: placement.endsWith("start") ? "start" : "end",
300
513
  onOpenAutoFocus: (event) => event.preventDefault(),
301
- children: /* @__PURE__ */ jsx(
302
- TimePickerPanel,
303
- {
304
- value: value || "09:00",
305
- minuteStep,
306
- use12h,
307
- onChange: (next) => {
308
- setValue(next);
309
- setText(next);
310
- },
311
- onDone: () => {
312
- setOpen(false);
514
+ children: [
515
+ /* @__PURE__ */ jsx(
516
+ TimePickerPanel,
517
+ {
518
+ value: (needConfirm ? normalizeTime(text, showSeconds) : void 0) || value || "09:00",
519
+ minuteStep,
520
+ hourStep,
521
+ changeOnScroll,
522
+ secondStep,
523
+ showSeconds,
524
+ use12h,
525
+ disabledTime,
526
+ hideDisabledOptions,
527
+ showNow,
528
+ needConfirm,
529
+ onChange: (next) => {
530
+ setValue(next);
531
+ setText(displayTime(next, format));
532
+ },
533
+ onDone: () => {
534
+ setOpen(false);
535
+ }
313
536
  }
314
- }
315
- )
537
+ ),
538
+ renderExtraFooter?.()
539
+ ]
316
540
  }
317
541
  )
318
542
  ] });
@@ -0,0 +1,5 @@
1
+ import * as React from "react";
2
+ import type { TimeRangePickerProp } from "../../props/components/data-entry.prop.js";
3
+ export type { TimeRangePickerProp, TimeRangePickerProp as TimeRangePickerProps, } from "../../props/components/data-entry.prop.js";
4
+ /** A range owns ordering and partial endpoints; the time controls own editing and constraints. */
5
+ export declare function TimeRangePicker({ value: controlledValue, defaultValue, onValueChange, order, open: openProp, defaultOpen, onOpenChange, allowEmpty, name, id, placeholder, className, ref, ...props }: TimeRangePickerProp): React.JSX.Element;