@godxjp/ui 21.0.0 → 22.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 (103) hide show
  1. package/dist/app/app-provider.js +2 -1
  2. package/dist/components/data-display/badge.d.ts +28 -1
  3. package/dist/components/data-display/badge.js +2 -0
  4. package/dist/components/data-display/data-table.d.ts +18 -2
  5. package/dist/components/data-display/data-table.js +4 -0
  6. package/dist/components/data-display/index.d.ts +1 -1
  7. package/dist/components/data-display/popover.d.ts +19 -1
  8. package/dist/components/data-display/popover.js +9 -1
  9. package/dist/components/data-display/progress.d.ts +37 -0
  10. package/dist/components/data-display/progress.js +57 -5
  11. package/dist/components/data-entry/date-picker.d.ts +10 -3
  12. package/dist/components/data-entry/date-picker.js +503 -238
  13. package/dist/components/data-entry/form-field.d.ts +1 -1
  14. package/dist/components/data-entry/form-field.js +4 -1
  15. package/dist/components/data-entry/form.js +10 -1
  16. package/dist/components/data-entry/index.d.ts +0 -6
  17. package/dist/components/data-entry/index.js +0 -6
  18. package/dist/components/data-entry/label.d.ts +14 -1
  19. package/dist/components/data-entry/label.js +6 -1
  20. package/dist/components/data-entry/radio.d.ts +27 -6
  21. package/dist/components/data-entry/radio.js +88 -43
  22. package/dist/components/data-entry/search-select.js +8 -1
  23. package/dist/components/data-entry/select.js +2 -0
  24. package/dist/components/data-entry/switch.d.ts +6 -3
  25. package/dist/components/data-entry/switch.js +34 -11
  26. package/dist/components/layout/mobile-shell.d.ts +1 -1
  27. package/dist/components/layout/mobile-shell.js +2 -0
  28. package/dist/components/navigation/menubar.d.ts +10 -3
  29. package/dist/components/navigation/steps.d.ts +1 -1
  30. package/dist/components/navigation/steps.js +3 -1
  31. package/dist/components/navigation/tabs-scroll.d.ts +27 -0
  32. package/dist/components/navigation/tabs-scroll.js +52 -1
  33. package/dist/components/navigation/tabs.d.ts +2 -2
  34. package/dist/components/navigation/tabs.js +88 -23
  35. package/dist/components/ui/index.d.ts +0 -1
  36. package/dist/components/ui/index.js +0 -1
  37. package/dist/components/ui/segmented.d.ts +15 -1
  38. package/dist/components/ui/segmented.js +13 -6
  39. package/dist/i18n/messages/en.json +3 -2
  40. package/dist/i18n/messages/ja.json +3 -2
  41. package/dist/i18n/messages/vi.json +3 -2
  42. package/dist/lib/datetime/picker-format.d.ts +18 -0
  43. package/dist/lib/datetime/picker-format.js +35 -1
  44. package/dist/lib/field-a11y.d.ts +12 -0
  45. package/dist/lib/field-a11y.js +14 -1
  46. package/dist/props/components/data-display.prop.d.ts +7 -0
  47. package/dist/props/components/data-entry.prop.d.ts +180 -99
  48. package/dist/props/components/index.d.ts +1 -1
  49. package/dist/props/components/layout.prop.d.ts +12 -1
  50. package/dist/props/components/navigation.prop.d.ts +76 -1
  51. package/dist/props/registry.d.ts +16 -16
  52. package/dist/props/registry.js +16 -15
  53. package/dist/props/vocabulary/data.prop.d.ts +12 -0
  54. package/dist/props/vocabulary/index.d.ts +2 -2
  55. package/dist/props/vocabulary/layout.prop.d.ts +11 -0
  56. package/dist/styles/badge-layout.css +4 -0
  57. package/dist/styles/card-layout.css +6 -6
  58. package/dist/styles/control.css +15 -6
  59. package/dist/styles/data-display-layout.css +76 -13
  60. package/dist/styles/focus-ring.css +4 -2
  61. package/dist/styles/navigation-layout.css +25 -0
  62. package/dist/styles/shell-layout.css +5 -0
  63. package/dist/styles/table-layout.css +34 -0
  64. package/dist/tokens/components/data-display.css +9 -0
  65. package/dist/tokens/components/navigation.css +6 -0
  66. package/dist/tokens/components/segmented.css +0 -2
  67. package/dist/tokens/components/shell.css +2 -0
  68. package/dist/tokens/components/table.css +3 -0
  69. package/dist/tokens/foundation.css +7 -0
  70. package/docs/COMPONENTS.md +14 -14
  71. package/docs/CONSUMER-RULES.md +24 -1
  72. package/docs/FRAME-COVERAGE-REPORT.md +3 -6
  73. package/docs/TOKENS.md +65 -0
  74. package/docs/data-display/badge.tsx +37 -0
  75. package/docs/data-display/data-table/index.tsx +18 -0
  76. package/docs/data-display/popover.tsx +41 -1
  77. package/docs/data-display/progress.tsx +28 -0
  78. package/docs/data-entry/calendar.tsx +2 -2
  79. package/docs/data-entry/date-picker.tsx +143 -3
  80. package/docs/data-entry/form/examples/invoice-form.tsx +2 -2
  81. package/docs/data-entry/form-field/examples/a11y-contract.tsx +3 -5
  82. package/docs/data-entry/form.tsx +4 -4
  83. package/docs/data-entry/number-input.tsx +31 -0
  84. package/docs/data-entry/segmented.tsx +103 -1
  85. package/docs/layout/mobile-shell.tsx +5 -0
  86. package/docs/navigation/tabs.tsx +149 -0
  87. package/docs/roadmap/antd-parity.md +35 -34
  88. package/docs/roadmap/parity-audit-data-entry.md +178 -165
  89. package/docs/roadmap/parity-backlog.md +6 -7
  90. package/docs/showcase/acme-portal.tsx +7 -0
  91. package/package.json +11 -23
  92. package/scripts/ui-audit.mjs +17 -4
  93. package/dist/components/data-entry/date-range-picker.d.ts +0 -11
  94. package/dist/components/data-entry/date-range-picker.js +0 -349
  95. package/dist/components/data-entry/month-picker.d.ts +0 -10
  96. package/dist/components/data-entry/month-picker.js +0 -241
  97. package/dist/components/data-entry/month-range-picker.d.ts +0 -10
  98. package/dist/components/data-entry/month-range-picker.js +0 -301
  99. package/dist/components/ui/date-range-picker.d.ts +0 -1
  100. package/dist/components/ui/date-range-picker.js +0 -2
  101. package/docs/data-entry/date-range-picker.tsx +0 -175
  102. package/docs/data-entry/month-picker.tsx +0 -101
  103. package/docs/data-entry/month-range-picker.tsx +0 -115
@@ -1,26 +1,37 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { CalendarIcon, ChevronLeft, ChevronRight, X } from "lucide-react";
4
+ import { ArrowRight, CalendarIcon, ChevronLeft, ChevronRight, X } from "lucide-react";
5
5
  import { usePickerLocales, useTranslation } from "../../i18n/use-translation.js";
6
- import { toIsoDate } from "../../lib/datetime/parse.js";
7
6
  import {
8
7
  formatPickerDate,
8
+ parseIsoPeriod,
9
9
  parsePickerDate,
10
10
  pickerDateAllowed,
11
- pickerPeriodStart
11
+ pickerPeriodStart,
12
+ toIsoPeriod
12
13
  } from "../../lib/datetime/picker-format.js";
13
14
  import { Button } from "../general/button.js";
14
15
  import { Flex } from "../layout/flex.js";
15
16
  import { TimePicker } from "./time-picker.js";
16
17
  import { useControlledLatch } from "../../lib/hooks.js";
17
- import { pickFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
18
+ import { pickFieldA11y, pickGroupFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
18
19
  import { cn } from "../../lib/utils.js";
19
20
  import { resolveAllowClear } from "./control-surface.js";
21
+ import { CONTROL_STATUS_CHROME_CLASS, CONTROL_VARIANT_CHROME_CLASS } from "./control-appearance.js";
20
22
  import { Input } from "./input.js";
21
23
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
22
24
  import { Calendar } from "./calendar.js";
23
25
  const ISO_HINT = "yyyy-mm-dd";
26
+ const PERIOD_PICKERS = /* @__PURE__ */ new Set(["month", "quarter", "year"]);
27
+ const PERIOD_COLUMNS = 3;
28
+ const PERIOD_HINT = {
29
+ date: ISO_HINT,
30
+ week: "yyyy-Www",
31
+ month: "yyyy-mm",
32
+ quarter: "yyyy-mm",
33
+ year: "yyyy"
34
+ };
24
35
  function DatePicker(props) {
25
36
  const {
26
37
  value: valueProp,
@@ -44,8 +55,11 @@ function DatePicker(props) {
44
55
  maxDate,
45
56
  showWeek,
46
57
  showTime,
58
+ defaultPickerValue,
59
+ pickerValue,
47
60
  presets,
48
61
  multiple = false,
62
+ range = false,
49
63
  picker = "date",
50
64
  order = true,
51
65
  needConfirm = Boolean(showTime),
@@ -60,17 +74,23 @@ function DatePicker(props) {
60
74
  status,
61
75
  variant,
62
76
  ref,
63
- ...ariaProps
77
+ ...rest
64
78
  } = props;
79
+ const { allowEmpty = [true, true], ...ariaProps } = rest;
65
80
  const format = formatProp ?? (typeof showTime === "object" && showTime.showSeconds ? "yyyy-MM-dd HH:mm:ss" : void 0);
66
81
  const { t } = useTranslation();
67
82
  const { dayPickerLocale, locale } = usePickerLocales(localeProp);
83
+ const isPeriod = PERIOD_PICKERS.has(picker);
84
+ const disabledEdges = Array.isArray(disabled) ? disabled : [Boolean(disabled), Boolean(disabled)];
85
+ const allDisabled = disabledEdges[0] && disabledEdges[1];
86
+ const anyDisabled = disabledEdges[0] || disabledEdges[1];
87
+ const edgeDisabled = (edge) => disabledEdges[edge === "from" ? 0 : 1];
68
88
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
69
- const open = !disabled && (openProp ?? internalOpen);
89
+ const open = !allDisabled && (openProp ?? internalOpen);
70
90
  const [pending, setPending] = React.useState();
71
91
  const [hasPending, setHasPending] = React.useState(false);
72
92
  const setOpen = (next) => {
73
- if (disabled && next) return;
93
+ if (allDisabled && next) return;
74
94
  if (openProp === void 0) setInternalOpen(next);
75
95
  onOpenChange?.(next);
76
96
  if (!next) {
@@ -85,41 +105,91 @@ function DatePicker(props) {
85
105
  const minimum = minDate ?? fromDate;
86
106
  const maximum = maxDate ?? toDate;
87
107
  const allowed = (date) => pickerDateAllowed(date, minimum, maximum, disabledDate);
88
- const display = (date) => Array.isArray(date) ? date.map((day) => formatPickerDate(day, format, locale)).join(", ") : formatPickerDate(date, format, locale, Boolean(showTime));
89
- const parse = (text2) => parsePickerDate(text2, format, parseFormat, Boolean(showTime));
90
- const fieldA11y = pickFieldA11y(ariaProps);
108
+ const display = (date) => format || showTime ? formatPickerDate(date, format, locale, Boolean(showTime)) : toIsoPeriod(date, picker);
109
+ const displayAll = (date) => Array.isArray(date) ? date.map((day) => display(day)).join(", ") : display(date);
110
+ const parse = (text2) => parsePickerDate(text2, format, parseFormat, Boolean(showTime)) ?? parseIsoPeriod(text2, picker);
111
+ const periodDate = (date) => pickerPeriodStart(date, picker, dayPickerLocale);
112
+ const fieldA11y = range ? pickGroupFieldA11y(ariaProps) : pickFieldA11y(ariaProps);
91
113
  const identity = useFieldIdentity({ id, name, "data-field": fieldA11y["data-field"] });
92
114
  const resolvedName = name ?? identity.name;
93
115
  const resolvedField = fieldA11y["data-field"] ?? identity["data-field"];
94
116
  const reactId = React.useId();
95
- const dialogId = `${id ?? reactId}-dialog`;
117
+ const rootId = id ?? reactId;
118
+ const dialogId = `${rootId}-dialog`;
119
+ const fromId = `${rootId}-from`;
120
+ const toId = `${rootId}-to`;
96
121
  const isControlled = useControlledLatch(valueProp !== void 0);
97
- const [internalValue, setInternalValue] = React.useState(defaultValue);
122
+ const [internalValue, setInternalValue] = React.useState(
123
+ defaultValue
124
+ );
98
125
  const value = isControlled ? valueProp : internalValue;
99
126
  const working = hasPending ? pending : value;
100
- const selectedDate = Array.isArray(working) ? working[0] : working;
101
- const [viewYear, setViewYear] = React.useState((selectedDate ?? /* @__PURE__ */ new Date()).getFullYear());
102
- const periodDate = (date) => pickerPeriodStart(date, picker, dayPickerLocale);
127
+ const rangeValue = range ? working : void 0;
128
+ const selectedDate = range ? rangeValue?.from : Array.isArray(working) ? working[0] : working;
103
129
  const emit = (next) => {
104
130
  if (!isControlled) setInternalValue(next);
105
- if (props.multiple)
106
- props.onValueChange?.(Array.isArray(next) ? next : next ? [next] : void 0);
107
- else props.onValueChange?.(Array.isArray(next) ? next[0] : next);
131
+ const onValueChange = props.onValueChange;
132
+ if (range) onValueChange?.(next);
133
+ else if (multiple) onValueChange?.(Array.isArray(next) ? next : next ? [next] : void 0);
134
+ else onValueChange?.(Array.isArray(next) ? next[0] : next);
108
135
  };
109
- const [text, setText] = React.useState(() => display(value));
136
+ const committedSingle = range ? void 0 : value;
137
+ const committedRange = range ? value : void 0;
138
+ const [text, setText] = React.useState(() => range ? "" : displayAll(committedSingle));
139
+ const [fromText, setFromText] = React.useState(() => display(committedRange?.from));
140
+ const [toText, setToText] = React.useState(() => display(committedRange?.to));
141
+ const committedFrom = committedRange?.from;
142
+ const committedTo = committedRange?.to;
110
143
  React.useEffect(() => {
111
- setText(
112
- Array.isArray(value) ? value.map((date) => formatPickerDate(date, format, locale)).join(", ") : formatPickerDate(value, format, locale, Boolean(showTime))
113
- );
114
- }, [value, format, locale, showTime]);
115
- const resolvedPlaceholder = placeholder ?? t("dataEntry.datePicker.placeholder") ?? ISO_HINT;
144
+ if (range) {
145
+ setFromText(display(committedFrom));
146
+ setToText(display(committedTo));
147
+ } else {
148
+ setText(displayAll(committedSingle));
149
+ }
150
+ }, [range, value, committedFrom, committedTo, format, locale, showTime]);
151
+ const valueAnchor = range ? committedRange?.from : Array.isArray(committedSingle) ? committedSingle[0] : committedSingle;
152
+ const [internalView, setInternalView] = React.useState(
153
+ () => defaultPickerValue ?? valueAnchor
154
+ );
155
+ const viewAnchor = pickerValue ?? internalView ?? /* @__PURE__ */ new Date();
156
+ const viewYear = viewAnchor.getFullYear();
157
+ const setViewAnchor = (next) => {
158
+ if (pickerValue === void 0) setInternalView(next);
159
+ };
160
+ const setViewYear = (year) => setViewAnchor(new Date(year, viewAnchor.getMonth(), 1));
161
+ React.useEffect(() => {
162
+ if (open) setInternalView(defaultPickerValue ?? valueAnchor);
163
+ }, [open]);
164
+ const resolvedPlaceholder = placeholder ?? (isPeriod ? t("dataEntry.monthPicker.placeholder") : range ? t("dataEntry.dateRangePicker.placeholder") : t("dataEntry.datePicker.placeholder")) ?? PERIOD_HINT[picker];
165
+ const clearControl = resolveAllowClear(allowClear, true, t("common.clear") ?? "Clear");
166
+ const showClear = clearControl.enabled && !anyDisabled && (range ? allowEmpty.every(Boolean) && Boolean(committedRange?.from || committedRange?.to) : text !== "");
116
167
  const clear = () => {
117
168
  emit(void 0);
118
169
  setText("");
170
+ setFromText("");
171
+ setToText("");
119
172
  };
120
- const clearControl = resolveAllowClear(allowClear, true, t("common.clear") ?? "Clear");
121
- const showClear = clearControl.enabled && text !== "" && !disabled;
173
+ const validRange = (next) => Boolean(
174
+ next && (next.from ? allowed(next.from) : allowEmpty[0]) && (next.to ? allowed(next.to) : allowEmpty[1])
175
+ );
122
176
  const choose = (input) => {
177
+ if (range) {
178
+ let next = input;
179
+ if (anyDisabled && next)
180
+ next = {
181
+ from: disabledEdges[0] ? committedRange?.from : next.from,
182
+ to: disabledEdges[1] ? committedRange?.to : next.to
183
+ };
184
+ if (next?.from && !allowed(next.from) || next?.to && !allowed(next.to)) return;
185
+ if (order && next?.from && next.to && next.from > next.to)
186
+ next = { from: next.to, to: next.from };
187
+ if (needConfirm) {
188
+ setPending(next);
189
+ setHasPending(true);
190
+ } else emit(next);
191
+ return;
192
+ }
123
193
  let date = input;
124
194
  if (Array.isArray(date)) {
125
195
  if (date.some((day) => !allowed(day))) return;
@@ -129,7 +199,8 @@ function DatePicker(props) {
129
199
  if (!allowed(date) || !needConfirm && !timeAllowed(date)) return;
130
200
  if (multiple) {
131
201
  const current = Array.isArray(working) ? working : [];
132
- date = current.some((day) => +day === +date) ? current.filter((day) => +day !== +date) : [...current, date];
202
+ const picked = date;
203
+ date = current.some((day) => +day === +picked) ? current.filter((day) => +day !== +picked) : [...current, picked];
133
204
  if (order) date.sort((a, b) => +a - +b);
134
205
  }
135
206
  }
@@ -138,7 +209,7 @@ function DatePicker(props) {
138
209
  setHasPending(true);
139
210
  } else {
140
211
  emit(date);
141
- setText(display(date));
212
+ setText(displayAll(date));
142
213
  }
143
214
  };
144
215
  const commit = (raw) => {
@@ -149,22 +220,410 @@ function DatePicker(props) {
149
220
  const parsed = parse(raw);
150
221
  if (parsed && allowed(parsed)) choose(parsed);
151
222
  };
152
- return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
153
- (format || showTime || multiple || needConfirm || preserveInvalidOnBlur) && resolvedName ? /* @__PURE__ */ jsx(
223
+ const commitEdge = (edge, raw) => {
224
+ if (edgeDisabled(edge)) return;
225
+ const trimmed = raw.trim();
226
+ const parsed = parse(trimmed);
227
+ if (trimmed && (!parsed || !allowed(parsed))) return;
228
+ if (!trimmed && !allowEmpty[edge === "from" ? 0 : 1]) return;
229
+ const base = working;
230
+ const next = { from: base?.from, to: base?.to, [edge]: parsed && periodDate(parsed) };
231
+ choose(next.from || next.to ? next : void 0);
232
+ };
233
+ const keyHandlers = (onEnter) => ({
234
+ onKeyDown: (event) => {
235
+ if (event.key === "ArrowDown") {
236
+ event.preventDefault();
237
+ setOpen(true);
238
+ } else if (event.key === "Enter") {
239
+ event.preventDefault();
240
+ onEnter();
241
+ } else if (event.key === "Escape" && open) {
242
+ setOpen(false);
243
+ }
244
+ }
245
+ });
246
+ const enterSingle = () => {
247
+ const parsed = parse(text);
248
+ if (parsed && allowed(parsed) && timeAllowed(parsed)) {
249
+ const next = periodDate(parsed);
250
+ emit(next);
251
+ setText(display(parsed));
252
+ }
253
+ setOpen(false);
254
+ };
255
+ const enterEdge = (edge) => () => {
256
+ commitEdge(edge, edge === "from" ? fromText : toText);
257
+ setOpen(false);
258
+ };
259
+ const displayIsCanonical = !format && !showTime && !multiple && !needConfirm && !preserveInvalidOnBlur && picker === "date";
260
+ const isoValue = (date) => showTime ? date?.toISOString() ?? "" : toIsoPeriod(date, picker);
261
+ const hiddenFields = displayIsCanonical ? null : range ? /* @__PURE__ */ jsxs(Fragment, { children: [
262
+ /* @__PURE__ */ jsx(
263
+ "input",
264
+ {
265
+ type: "hidden",
266
+ disabled: allDisabled,
267
+ name: `${resolvedName}_from`,
268
+ value: isoValue(committedRange?.from)
269
+ }
270
+ ),
271
+ /* @__PURE__ */ jsx(
154
272
  "input",
155
273
  {
156
274
  type: "hidden",
157
- disabled,
158
- name: resolvedName,
159
- value: showTime ? (Array.isArray(value) ? value[0] : value)?.toISOString() ?? "" : Array.isArray(value) ? value.map(toIsoDate).join(",") : toIsoDate(value)
275
+ disabled: allDisabled,
276
+ name: `${resolvedName}_to`,
277
+ value: isoValue(committedRange?.to)
278
+ }
279
+ )
280
+ ] }) : /* @__PURE__ */ jsx(
281
+ "input",
282
+ {
283
+ type: "hidden",
284
+ disabled: allDisabled,
285
+ name: resolvedName,
286
+ value: Array.isArray(committedSingle) ? committedSingle.map((day) => isoValue(day)).join(",") : isoValue(committedSingle)
287
+ }
288
+ );
289
+ const periodCells = picker === "quarter" ? 4 : 12;
290
+ const periodPage = (year) => Array.from(
291
+ { length: periodCells },
292
+ (_, index) => picker === "year" ? new Date(year + index, 0, 1) : new Date(year, index * (picker === "quarter" ? 3 : 1), 1)
293
+ );
294
+ const periodDateAt = (index) => periodPage(viewYear)[index];
295
+ const periodRow = (row) => Array.from({ length: PERIOD_COLUMNS }, (_, column) => row * PERIOD_COLUMNS + column).filter(
296
+ (index) => index < periodCells
297
+ );
298
+ const pageStep = picker === "year" ? periodCells : 1;
299
+ const prevDisabled = !pickerDateAllowed(
300
+ periodPage(viewYear - pageStep)[periodCells - 1],
301
+ minimum,
302
+ void 0
303
+ );
304
+ const nextDisabled = !pickerDateAllowed(periodPage(viewYear + pageStep)[0], void 0, maximum);
305
+ const periodLabel = (date, index) => picker === "year" ? new Intl.DateTimeFormat(locale, { year: "numeric" }).format(date) : picker === "quarter" ? t("dataEntry.datePicker.quarter", { quarter: index + 1 }) ?? `Q${index + 1}` : new Intl.DateTimeFormat(locale, { month: "short" }).format(date);
306
+ const periodPick = (date) => {
307
+ if (!range) {
308
+ choose(date);
309
+ if (!needConfirm && !multiple) setOpen(false);
310
+ return;
311
+ }
312
+ const base = working;
313
+ const pendingFrom = base?.from && !base.to ? base.from : void 0;
314
+ if (!pendingFrom) {
315
+ choose({ from: date, to: void 0 });
316
+ return;
317
+ }
318
+ choose({ from: pendingFrom, to: date });
319
+ if (!needConfirm) setOpen(false);
320
+ };
321
+ const periodPanel = /* @__PURE__ */ jsxs("div", { className: "ui-month-picker-panel", children: [
322
+ /* @__PURE__ */ jsxs("div", { className: "ui-month-picker-nav", children: [
323
+ /* @__PURE__ */ jsx(
324
+ Button,
325
+ {
326
+ type: "button",
327
+ variant: "outline",
328
+ size: "icon-sm",
329
+ disabled: prevDisabled,
330
+ "aria-label": t("dataEntry.monthPicker.previousYear") ?? "Previous year",
331
+ className: "ui-month-picker-nav-button",
332
+ onClick: () => setViewYear(viewYear - pageStep),
333
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "ui-month-picker-icon", "aria-hidden": "true" })
334
+ }
335
+ ),
336
+ /* @__PURE__ */ jsx("span", { className: "ui-month-picker-nav-label", "aria-live": "polite", children: new Intl.NumberFormat(locale, { useGrouping: false }).format(viewYear) }),
337
+ /* @__PURE__ */ jsx(
338
+ Button,
339
+ {
340
+ type: "button",
341
+ variant: "outline",
342
+ size: "icon-sm",
343
+ disabled: nextDisabled,
344
+ "aria-label": t("dataEntry.monthPicker.nextYear") ?? "Next year",
345
+ className: "ui-month-picker-nav-button",
346
+ onClick: () => setViewYear(viewYear + pageStep),
347
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "ui-month-picker-icon", "aria-hidden": "true" })
348
+ }
349
+ )
350
+ ] }),
351
+ /* @__PURE__ */ jsx("div", { role: "grid", "aria-label": String(viewYear), className: "ui-month-picker-grid", children: Array.from({ length: Math.ceil(periodCells / PERIOD_COLUMNS) }, (_, row) => /* @__PURE__ */ jsx("div", { role: "row", className: "ui-month-picker-row", children: periodRow(row).map((index) => {
352
+ const date = periodDateAt(index);
353
+ const selected = range ? [rangeValue?.from, rangeValue?.to].some(
354
+ (edge) => edge && +periodDate(edge) === +date
355
+ ) : (Array.isArray(working) ? working : working ? [working] : []).some(
356
+ (day) => +periodDate(day) === +date
357
+ );
358
+ const inRange = range && rangeValue?.from && rangeValue.to ? +date > +periodDate(rangeValue.from) && +date < +periodDate(rangeValue.to) : false;
359
+ return /* @__PURE__ */ jsx("div", { role: "gridcell", className: "ui-month-picker-gridcell", children: /* @__PURE__ */ jsx(
360
+ Button,
361
+ {
362
+ type: "button",
363
+ variant: selected ? "default" : inRange ? "secondary" : "ghost",
364
+ size: "sm",
365
+ "aria-pressed": selected,
366
+ disabled: !allowed(date),
367
+ className: "ui-month-picker-cell",
368
+ onClick: () => periodPick(date),
369
+ children: periodLabel(date, index)
370
+ }
371
+ ) }, index);
372
+ }) }, row)) })
373
+ ] });
374
+ const calendarBounds = [
375
+ ...minimum ? [{ before: minimum }] : [],
376
+ ...maximum ? [{ after: maximum }] : [],
377
+ ...disabledDate ? [disabledDate] : []
378
+ ];
379
+ const calendarShared = {
380
+ // The day grid takes its month from the SAME anchor the period grid uses, so `pickerValue` and
381
+ // `defaultPickerValue` mean one thing at every granularity.
382
+ month: viewAnchor,
383
+ onMonthChange: setViewAnchor,
384
+ locale: dayPickerLocale,
385
+ cellRender,
386
+ showWeekNumber: showWeek,
387
+ startMonth: minimum,
388
+ endMonth: maximum,
389
+ showToday,
390
+ showClose,
391
+ onClose: () => setOpen(false)
392
+ };
393
+ const dayPanel = range ? /* @__PURE__ */ jsx(
394
+ Calendar,
395
+ {
396
+ mode: "range",
397
+ selected: rangeValue,
398
+ numberOfMonths: 2,
399
+ onSelect: (next) => {
400
+ choose(next);
401
+ setFromText(display(next?.from));
402
+ setToText(display(next?.to));
403
+ },
404
+ disabled: calendarBounds,
405
+ ...calendarShared
406
+ }
407
+ ) : multiple ? /* @__PURE__ */ jsx(
408
+ Calendar,
409
+ {
410
+ mode: "multiple",
411
+ selected: Array.isArray(working) ? working : [],
412
+ onSelect: choose,
413
+ disabled: (day) => !allowed(day),
414
+ ...calendarShared
415
+ }
416
+ ) : /* @__PURE__ */ jsx(
417
+ Calendar,
418
+ {
419
+ mode: "single",
420
+ selected: selectedDate,
421
+ onSelect: (date) => {
422
+ if (date && showTime && selectedDate)
423
+ date.setHours(
424
+ selectedDate.getHours(),
425
+ selectedDate.getMinutes(),
426
+ selectedDate.getSeconds()
427
+ );
428
+ choose(date);
429
+ if (!needConfirm && !multiple) setOpen(false);
430
+ },
431
+ disabled: calendarBounds,
432
+ ...calendarShared
433
+ }
434
+ );
435
+ const trailing = showClear ? /* @__PURE__ */ jsx(
436
+ "button",
437
+ {
438
+ type: "button",
439
+ tabIndex: -1,
440
+ "aria-label": clearControl.label,
441
+ onClick: (event) => {
442
+ event.stopPropagation();
443
+ event.currentTarget.closest("div")?.querySelector("input:not([type=hidden])")?.focus();
444
+ clear();
445
+ },
446
+ className: range ? "text-muted-foreground hover:text-foreground shrink-0" : "ui-control-inline-affix-action",
447
+ children: clearControl.clearIcon ?? /* @__PURE__ */ jsx(
448
+ X,
449
+ {
450
+ className: range ? "ui-month-picker-icon" : "ui-control-inline-affix-icon",
451
+ "aria-hidden": "true"
452
+ }
453
+ )
454
+ }
455
+ ) : /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
456
+ "button",
457
+ {
458
+ type: "button",
459
+ disabled: allDisabled,
460
+ tabIndex: -1,
461
+ "aria-label": (isPeriod ? t("dataEntry.monthPicker.openGrid") : range ? t("dataEntry.dateRangePicker.openCalendar") : t("dataEntry.datePicker.openCalendar")) ?? "Open calendar",
462
+ className: range ? "text-muted-foreground hover:text-foreground shrink-0" : "ui-control-inline-affix-action",
463
+ children: /* @__PURE__ */ jsx(
464
+ CalendarIcon,
465
+ {
466
+ className: range ? "ui-month-picker-icon" : "ui-control-inline-affix-icon",
467
+ "aria-hidden": "true"
468
+ }
469
+ )
470
+ }
471
+ ) });
472
+ const panel = /* @__PURE__ */ jsxs(
473
+ PopoverContent,
474
+ {
475
+ id: dialogId,
476
+ role: "dialog",
477
+ "aria-label": (isPeriod ? t("dataEntry.monthPicker.openGrid") : t("dataEntry.datePicker.openCalendar")) ?? "Calendar",
478
+ className: isPeriod ? "ui-month-picker-panel" : "ui-control-panel-flush",
479
+ side: placement.startsWith("top") ? "top" : "bottom",
480
+ align: placement.endsWith("end") ? "end" : "start",
481
+ onOpenAutoFocus: (event) => event.preventDefault(),
482
+ onClick: (event) => event.stopPropagation(),
483
+ children: [
484
+ presets?.length ? /* @__PURE__ */ jsx(Flex, { wrap: true, gap: "xs", pad: "sm", children: presets.map((preset, index) => /* @__PURE__ */ jsx(
485
+ Button,
486
+ {
487
+ variant: "ghost",
488
+ size: "sm",
489
+ type: "button",
490
+ onClick: () => {
491
+ const picked = typeof preset.value === "function" ? preset.value() : preset.value;
492
+ if (range) {
493
+ if (!validRange(picked)) return;
494
+ choose(picked);
495
+ return;
496
+ }
497
+ if (!allowed(picked)) return;
498
+ choose(picked);
499
+ if (!needConfirm && !multiple) setOpen(false);
500
+ },
501
+ children: preset.label
502
+ },
503
+ index
504
+ )) }) : null,
505
+ isPeriod ? periodPanel : dayPanel,
506
+ showTime ? /* @__PURE__ */ jsx(Flex, { pad: "sm", children: /* @__PURE__ */ jsx(
507
+ TimePicker,
508
+ {
509
+ ...typeof showTime === "object" ? showTime : {},
510
+ "aria-label": t("dataEntry.timePicker.openPicker"),
511
+ value: selectedDate ? [
512
+ selectedDate.getHours(),
513
+ selectedDate.getMinutes(),
514
+ ...typeof showTime === "object" && showTime.showSeconds ? [selectedDate.getSeconds()] : []
515
+ ].map((n) => String(n).padStart(2, "0")).join(":") : "",
516
+ onValueChange: (time) => {
517
+ if (!time) return;
518
+ const date = new Date(selectedDate ?? /* @__PURE__ */ new Date());
519
+ const [h, m, sec = 0] = time.split(":").map(Number);
520
+ date.setHours(h, m, sec, 0);
521
+ choose(date);
522
+ }
523
+ }
524
+ ) }) : null,
525
+ needConfirm ? /* @__PURE__ */ jsx(Flex, { pad: "sm", justify: "end", children: /* @__PURE__ */ jsx(
526
+ Button,
527
+ {
528
+ type: "button",
529
+ disabled: range ? !validRange(rangeValue) : !selectedDate || !allowed(selectedDate) || !timeAllowed(selectedDate),
530
+ onClick: () => {
531
+ if (range ? validRange(rangeValue) : selectedDate && allowed(selectedDate)) {
532
+ emit(working);
533
+ if (range) {
534
+ setFromText(display(rangeValue?.from));
535
+ setToText(display(rangeValue?.to));
536
+ } else {
537
+ setText(displayAll(working));
538
+ }
539
+ setOpen(false);
540
+ }
541
+ },
542
+ children: t("dataEntry.timePicker.confirm")
543
+ }
544
+ ) }) : null,
545
+ renderExtraFooter?.()
546
+ ]
547
+ }
548
+ );
549
+ if (range) {
550
+ const edgeInput = (edge) => /* @__PURE__ */ jsx(
551
+ "input",
552
+ {
553
+ id: edge === "from" ? fromId : toId,
554
+ "data-field": resolvedField ? `${resolvedField}_${edge}` : void 0,
555
+ name: displayIsCanonical && resolvedName ? `${resolvedName}_${edge}` : void 0,
556
+ ref: edge === "from" ? ref : void 0,
557
+ readOnly: inputReadOnly,
558
+ "aria-readonly": inputReadOnly || void 0,
559
+ value: edge === "from" ? fromText : toText,
560
+ disabled: edgeDisabled(edge),
561
+ placeholder: resolvedPlaceholder,
562
+ inputMode: "numeric",
563
+ autoComplete: "off",
564
+ "aria-label": (edge === "from" ? t("dataEntry.dateRangePicker.from") : t("dataEntry.dateRangePicker.to")) ?? edge,
565
+ className: "ui-month-picker-input",
566
+ ...keyHandlers(enterEdge(edge)),
567
+ onChange: (event) => {
568
+ if (edge === "from") setFromText(event.target.value);
569
+ else setToText(event.target.value);
570
+ commitEdge(edge, event.target.value);
571
+ },
572
+ onBlur: (event) => {
573
+ if (preserveInvalidOnBlur) return;
574
+ const parsed = parse(event.target.value);
575
+ const accepted = parsed && allowed(parsed) ? parsed : void 0;
576
+ const fallback = edge === "from" ? committedRange?.from : committedRange?.to;
577
+ const next = accepted ? display(accepted) : display(fallback);
578
+ if (edge === "from") setFromText(next);
579
+ else setToText(next);
580
+ }
160
581
  }
161
- ) : null,
582
+ );
583
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
584
+ hiddenFields,
585
+ /* @__PURE__ */ jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsxs(
586
+ "div",
587
+ {
588
+ role: "group",
589
+ id: rootId,
590
+ ...fieldA11y,
591
+ "data-field": resolvedField,
592
+ "data-size": size,
593
+ "data-status": status,
594
+ "data-variant": variant,
595
+ "aria-disabled": allDisabled ? true : void 0,
596
+ "data-disabled": allDisabled ? "" : void 0,
597
+ "data-state": open ? "open" : "closed",
598
+ className: cn(
599
+ "ui-control ui-control-composite-field",
600
+ "aria-invalid:border-destructive",
601
+ CONTROL_VARIANT_CHROME_CLASS[variant ?? "outlined"],
602
+ CONTROL_STATUS_CHROME_CLASS,
603
+ className
604
+ ),
605
+ onClick: () => {
606
+ if (!allDisabled) setOpen(true);
607
+ },
608
+ children: [
609
+ edgeInput("from"),
610
+ /* @__PURE__ */ jsx(ArrowRight, { className: "ui-month-picker-separator-icon", "aria-hidden": "true" }),
611
+ edgeInput("to"),
612
+ trailing,
613
+ panel
614
+ ]
615
+ }
616
+ ) })
617
+ ] });
618
+ }
619
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
620
+ hiddenFields,
162
621
  /* @__PURE__ */ jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
163
622
  /* @__PURE__ */ jsx(
164
623
  Input,
165
624
  {
166
- id,
167
- name: format || showTime || multiple || needConfirm || preserveInvalidOnBlur ? "" : resolvedName,
625
+ id: rootId,
626
+ name: displayIsCanonical ? resolvedName : "",
168
627
  ref,
169
628
  size,
170
629
  status,
@@ -172,7 +631,7 @@ function DatePicker(props) {
172
631
  readOnly: inputReadOnly || multiple,
173
632
  "data-field": resolvedField,
174
633
  value: text,
175
- disabled,
634
+ disabled: allDisabled,
176
635
  placeholder: resolvedPlaceholder,
177
636
  inputMode: "numeric",
178
637
  autoComplete: "off",
@@ -181,50 +640,11 @@ function DatePicker(props) {
181
640
  "aria-haspopup": "dialog",
182
641
  "aria-controls": open ? dialogId : void 0,
183
642
  ...fieldA11y,
184
- trailingIcon: /* @__PURE__ */ jsx("span", { className: "ui-time-picker-affix", children: showClear ? /* @__PURE__ */ jsx(
185
- "button",
186
- {
187
- type: "button",
188
- tabIndex: -1,
189
- "aria-label": clearControl.label,
190
- onClick: (event) => {
191
- event.stopPropagation();
192
- event.currentTarget.closest("div")?.querySelector("input:not([type=hidden])")?.focus();
193
- clear();
194
- },
195
- className: "ui-control-inline-affix-action",
196
- children: clearControl.clearIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-control-inline-affix-icon", "aria-hidden": "true" })
197
- }
198
- ) : /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
199
- "button",
200
- {
201
- type: "button",
202
- disabled,
203
- tabIndex: -1,
204
- "aria-label": t("dataEntry.datePicker.openCalendar") ?? "Open calendar",
205
- className: "ui-control-inline-affix-action",
206
- children: /* @__PURE__ */ jsx(CalendarIcon, { className: "ui-control-inline-affix-icon", "aria-hidden": "true" })
207
- }
208
- ) }) }),
643
+ trailingIcon: /* @__PURE__ */ jsx("span", { className: "ui-time-picker-affix", children: trailing }),
209
644
  onClick: () => {
210
- if (!disabled) setOpen(true);
211
- },
212
- onKeyDown: (event) => {
213
- if (event.key === "ArrowDown") {
214
- event.preventDefault();
215
- setOpen(true);
216
- } else if (event.key === "Enter") {
217
- const parsed = parse(text);
218
- if (parsed && allowed(parsed) && timeAllowed(parsed)) {
219
- const next = periodDate(parsed);
220
- emit(next);
221
- setText(display(parsed));
222
- setOpen(false);
223
- }
224
- } else if (event.key === "Escape" && open) {
225
- setOpen(false);
226
- }
645
+ if (!allDisabled) setOpen(true);
227
646
  },
647
+ ...keyHandlers(enterSingle),
228
648
  onChange: (event) => {
229
649
  setText(event.target.value);
230
650
  commit(event.target.value);
@@ -232,167 +652,12 @@ function DatePicker(props) {
232
652
  onBlur: (event) => {
233
653
  const parsed = parse(event.target.value);
234
654
  const accepted = parsed && allowed(parsed) && timeAllowed(parsed) ? parsed : void 0;
235
- if (!preserveInvalidOnBlur) setText(accepted ? display(accepted) : display(value));
655
+ if (!preserveInvalidOnBlur)
656
+ setText(accepted ? display(accepted) : displayAll(committedSingle));
236
657
  }
237
658
  }
238
659
  ),
239
- /* @__PURE__ */ jsxs(
240
- PopoverContent,
241
- {
242
- id: dialogId,
243
- role: "dialog",
244
- "aria-label": t("dataEntry.datePicker.openCalendar") ?? "Calendar",
245
- className: "ui-control-panel-flush",
246
- side: placement.startsWith("top") ? "top" : "bottom",
247
- align: placement.endsWith("end") ? "end" : "start",
248
- onOpenAutoFocus: (event) => event.preventDefault(),
249
- children: [
250
- presets?.length ? /* @__PURE__ */ jsx(Flex, { wrap: true, gap: "xs", pad: "sm", children: presets.map((preset, index) => /* @__PURE__ */ jsx(
251
- Button,
252
- {
253
- variant: "ghost",
254
- size: "sm",
255
- type: "button",
256
- onClick: () => {
257
- const date = typeof preset.value === "function" ? preset.value() : preset.value;
258
- if (!allowed(date)) return;
259
- choose(date);
260
- if (!needConfirm && !multiple) setOpen(false);
261
- },
262
- children: preset.label
263
- },
264
- index
265
- )) }) : null,
266
- picker === "date" || picker === "week" ? multiple ? /* @__PURE__ */ jsx(
267
- Calendar,
268
- {
269
- mode: "multiple",
270
- selected: Array.isArray(working) ? working : [],
271
- defaultMonth: selectedDate,
272
- locale: dayPickerLocale,
273
- onSelect: choose,
274
- disabled: (day) => !allowed(day),
275
- cellRender,
276
- showWeekNumber: showWeek,
277
- startMonth: minimum,
278
- endMonth: maximum,
279
- showToday,
280
- showClose,
281
- onClose: () => setOpen(false)
282
- }
283
- ) : /* @__PURE__ */ jsx(
284
- Calendar,
285
- {
286
- mode: "single",
287
- selected: selectedDate,
288
- defaultMonth: selectedDate,
289
- showWeekNumber: showWeek,
290
- onSelect: (date) => {
291
- if (date && showTime && selectedDate)
292
- date.setHours(
293
- selectedDate.getHours(),
294
- selectedDate.getMinutes(),
295
- selectedDate.getSeconds()
296
- );
297
- choose(date);
298
- if (!needConfirm && !multiple) setOpen(false);
299
- },
300
- locale: dayPickerLocale,
301
- disabled: [
302
- ...minimum ? [{ before: minimum }] : [],
303
- ...maximum ? [{ after: maximum }] : [],
304
- ...disabledDate ? [disabledDate] : []
305
- ],
306
- cellRender,
307
- startMonth: minimum,
308
- endMonth: maximum,
309
- showToday,
310
- showClose,
311
- onClose: () => setOpen(false)
312
- }
313
- ) : /* @__PURE__ */ jsxs("div", { className: "ui-month-picker-panel", children: [
314
- /* @__PURE__ */ jsxs(Flex, { justify: "between", align: "center", children: [
315
- /* @__PURE__ */ jsx(
316
- Button,
317
- {
318
- type: "button",
319
- variant: "ghost",
320
- "aria-label": t("dataEntry.monthPicker.previousYear"),
321
- onClick: () => setViewYear(viewYear - (picker === "year" ? 12 : 1)),
322
- children: /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" })
323
- }
324
- ),
325
- /* @__PURE__ */ jsx("span", { "aria-live": "polite", children: new Intl.NumberFormat(locale, { useGrouping: false }).format(viewYear) }),
326
- /* @__PURE__ */ jsx(
327
- Button,
328
- {
329
- type: "button",
330
- variant: "ghost",
331
- "aria-label": t("dataEntry.monthPicker.nextYear"),
332
- onClick: () => setViewYear(viewYear + (picker === "year" ? 12 : 1)),
333
- children: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })
334
- }
335
- )
336
- ] }),
337
- /* @__PURE__ */ jsx("div", { className: "ui-month-picker-grid", children: Array.from({ length: picker === "quarter" ? 4 : 12 }, (_, index) => {
338
- const date = picker === "year" ? new Date(viewYear + index, 0, 1) : new Date(viewYear, index * (picker === "quarter" ? 3 : 1), 1);
339
- const label = picker === "year" ? new Intl.DateTimeFormat(locale, { year: "numeric" }).format(date) : picker === "quarter" ? t("dataEntry.datePicker.quarter", { quarter: index + 1 }) : new Intl.DateTimeFormat(locale, { month: "short" }).format(date);
340
- const selected = (Array.isArray(working) ? working : working ? [working] : []).some((day) => +periodDate(day) === +date);
341
- return /* @__PURE__ */ jsx(
342
- Button,
343
- {
344
- type: "button",
345
- variant: selected ? "default" : "ghost",
346
- "aria-pressed": selected,
347
- disabled: !allowed(date),
348
- onClick: () => {
349
- choose(date);
350
- if (!needConfirm && !multiple) setOpen(false);
351
- },
352
- children: label
353
- },
354
- index
355
- );
356
- }) })
357
- ] }),
358
- showTime ? /* @__PURE__ */ jsx(Flex, { pad: "sm", children: /* @__PURE__ */ jsx(
359
- TimePicker,
360
- {
361
- ...typeof showTime === "object" ? showTime : {},
362
- "aria-label": t("dataEntry.timePicker.openPicker"),
363
- value: selectedDate ? [
364
- selectedDate.getHours(),
365
- selectedDate.getMinutes(),
366
- ...typeof showTime === "object" && showTime.showSeconds ? [selectedDate.getSeconds()] : []
367
- ].map((n) => String(n).padStart(2, "0")).join(":") : "",
368
- onValueChange: (time) => {
369
- if (!time) return;
370
- const date = new Date(selectedDate ?? /* @__PURE__ */ new Date());
371
- const [h, m, sec = 0] = time.split(":").map(Number);
372
- date.setHours(h, m, sec, 0);
373
- choose(date);
374
- }
375
- }
376
- ) }) : null,
377
- needConfirm ? /* @__PURE__ */ jsx(Flex, { pad: "sm", justify: "end", children: /* @__PURE__ */ jsx(
378
- Button,
379
- {
380
- type: "button",
381
- disabled: !selectedDate || !allowed(selectedDate) || !timeAllowed(selectedDate),
382
- onClick: () => {
383
- if (selectedDate && allowed(selectedDate) && timeAllowed(selectedDate)) {
384
- emit(working);
385
- setText(display(working));
386
- setOpen(false);
387
- }
388
- },
389
- children: t("dataEntry.timePicker.confirm")
390
- }
391
- ) }) : null,
392
- renderExtraFooter?.()
393
- ]
394
- }
395
- )
660
+ panel
396
661
  ] }) })
397
662
  ] });
398
663
  }