@sincpro/web-ui 0.1.0 → 0.2.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 (119) hide show
  1. package/README.md +37 -10
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  5. package/dist/components/{chatter → activity}/Messages.js +9 -6
  6. package/dist/components/{chatter → activity}/index.d.ts +1 -1
  7. package/dist/components/activity/index.js +4 -0
  8. package/dist/components/assist/AssistMark.d.ts +3 -3
  9. package/dist/components/assist/AssistMark.js +1 -1
  10. package/dist/components/assist/assist.d.ts +1 -1
  11. package/dist/components/assist/assist.js +1 -1
  12. package/dist/components/form/Sheet.d.ts +4 -0
  13. package/dist/components/form/Sheet.js +11 -4
  14. package/dist/components/index.d.ts +1 -1
  15. package/dist/components/index.js +1 -1
  16. package/dist/components/lines/LineItems.d.ts +42 -5
  17. package/dist/components/lines/LineItems.js +106 -18
  18. package/dist/components/listing/Listing.js +1 -1
  19. package/dist/components/listing/ListingBar.d.ts +41 -2
  20. package/dist/components/listing/ListingBar.js +43 -4
  21. package/dist/components/listing/Row.d.ts +5 -1
  22. package/dist/components/listing/Row.js +3 -2
  23. package/dist/components/record/RecordHeader.d.ts +21 -17
  24. package/dist/components/record/RecordHeader.js +46 -14
  25. package/dist/components/record/RecordNotices.d.ts +37 -0
  26. package/dist/components/record/RecordNotices.js +75 -0
  27. package/dist/components/record/ThumbBar.js +1 -1
  28. package/dist/foundations/copy.d.ts +96 -12
  29. package/dist/foundations/copy.es.js +63 -10
  30. package/dist/foundations/copy.js +55 -7
  31. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  32. package/dist/primitives/choice/ComboBox.js +2 -2
  33. package/dist/primitives/choice/SearchField.d.ts +10 -1
  34. package/dist/primitives/choice/SearchField.js +21 -2
  35. package/dist/primitives/date/Calendar.js +30 -6
  36. package/dist/primitives/date/DatePicker.d.ts +11 -3
  37. package/dist/primitives/date/DatePicker.js +12 -8
  38. package/dist/primitives/date/DateTime.d.ts +35 -0
  39. package/dist/primitives/date/DateTime.js +100 -0
  40. package/dist/primitives/date/index.d.ts +1 -0
  41. package/dist/primitives/date/index.js +4 -2
  42. package/dist/primitives/dialog/Drill.d.ts +90 -0
  43. package/dist/primitives/dialog/Drill.js +172 -0
  44. package/dist/primitives/dialog/index.d.ts +1 -0
  45. package/dist/primitives/dialog/index.js +2 -0
  46. package/dist/primitives/field/DropZone.js +1 -1
  47. package/dist/primitives/field/Field.d.ts +14 -2
  48. package/dist/primitives/field/Field.js +25 -14
  49. package/dist/primitives/field/copy.d.ts +16 -0
  50. package/dist/primitives/field/copy.js +69 -0
  51. package/dist/primitives/field/index.d.ts +2 -0
  52. package/dist/primitives/field/index.js +4 -0
  53. package/dist/primitives/field/note.d.ts +55 -0
  54. package/dist/primitives/field/note.js +117 -0
  55. package/dist/primitives/index.d.ts +1 -0
  56. package/dist/primitives/index.js +1 -0
  57. package/dist/primitives/menu/Menu.d.ts +28 -0
  58. package/dist/primitives/menu/Menu.js +47 -17
  59. package/dist/primitives/notice/Callout.js +1 -1
  60. package/dist/primitives/notice/Flag.d.ts +13 -0
  61. package/dist/primitives/notice/Flag.js +31 -0
  62. package/dist/primitives/notice/index.d.ts +1 -0
  63. package/dist/primitives/notice/index.js +1 -0
  64. package/dist/primitives/overlay/Preview.d.ts +19 -0
  65. package/dist/primitives/overlay/Preview.js +60 -0
  66. package/dist/primitives/overlay/index.d.ts +1 -0
  67. package/dist/primitives/overlay/index.js +3 -1
  68. package/dist/primitives/picker/Picker.d.ts +41 -0
  69. package/dist/primitives/picker/Picker.js +62 -8
  70. package/dist/primitives/table/CellCopy.d.ts +20 -0
  71. package/dist/primitives/table/CellCopy.js +23 -0
  72. package/dist/primitives/table/DataTable.d.ts +55 -1
  73. package/dist/primitives/table/DataTable.js +126 -40
  74. package/dist/primitives/table/EditableTable.d.ts +97 -2
  75. package/dist/primitives/table/EditableTable.js +223 -29
  76. package/dist/primitives/table/SheetTable.d.ts +4 -1
  77. package/dist/primitives/table/SheetTable.js +38 -5
  78. package/dist/primitives/table/index.d.ts +2 -2
  79. package/dist/primitives/table/index.js +1 -1
  80. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  81. package/dist/primitives/tabs/Tabs.js +1 -1
  82. package/dist/primitives/tag/TagsField.d.ts +3 -0
  83. package/dist/primitives/tag/TagsField.js +6 -2
  84. package/dist/primitives/tree/Tree.d.ts +56 -0
  85. package/dist/primitives/tree/Tree.js +74 -0
  86. package/dist/primitives/tree/index.d.ts +1 -0
  87. package/dist/primitives/tree/index.js +3 -0
  88. package/dist/primitives/value/Copyable.d.ts +11 -0
  89. package/dist/primitives/value/Copyable.js +27 -6
  90. package/dist/primitives/value/Fecha.js +10 -1
  91. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  92. package/dist/primitives/value/ProgressBar.js +53 -0
  93. package/dist/primitives/value/Relation.d.ts +6 -1
  94. package/dist/primitives/value/Relation.js +27 -10
  95. package/dist/primitives/value/Totals.d.ts +102 -7
  96. package/dist/primitives/value/Totals.js +52 -2
  97. package/dist/primitives/value/Value.d.ts +0 -37
  98. package/dist/primitives/value/Value.js +0 -39
  99. package/dist/primitives/value/index.d.ts +4 -3
  100. package/dist/primitives/value/index.js +4 -3
  101. package/dist/tokens.css +44 -7
  102. package/dist/views/list/List.d.ts +8 -2
  103. package/dist/views/list/List.js +6 -2
  104. package/dist/views/list/ListView.d.ts +1 -1
  105. package/dist/views/list/ListView.js +34 -8
  106. package/dist/views/list/derive.d.ts +12 -1
  107. package/dist/views/list/derive.js +29 -3
  108. package/dist/views/list/presentations.d.ts +2 -2
  109. package/dist/views/list/presentations.js +5 -3
  110. package/dist/views/list/types.d.ts +13 -1
  111. package/dist/views/record/Record.d.ts +27 -8
  112. package/dist/views/record/Record.js +97 -27
  113. package/dist/views/record/index.d.ts +1 -1
  114. package/package.json +5 -1
  115. package/dist/components/chatter/Chatter.d.ts +0 -161
  116. package/dist/components/chatter/Chatter.js +0 -232
  117. package/dist/components/chatter/index.js +0 -4
  118. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  119. /package/dist/components/{chatter → activity}/Timeline.js +0 -0
@@ -49,6 +49,10 @@ export const ENGLISH_COPY = {
49
49
  saving: "Saving…",
50
50
  discard: "Discard",
51
51
  failed: "failed",
52
+ notices: "Notices about this record",
53
+ noticeOne: "{count} notice",
54
+ noticeMany: "{count} notices",
55
+ blocking: "What is wrong with this record",
52
56
  },
53
57
  shell: {
54
58
  modules: "All modules",
@@ -77,6 +81,7 @@ export const ENGLISH_COPY = {
77
81
  },
78
82
  choice: {
79
83
  noResults: "No results",
84
+ search: "Search",
80
85
  },
81
86
  search: {
82
87
  tools: "Filter, group and favorites",
@@ -119,6 +124,7 @@ export const ENGLISH_COPY = {
119
124
  retry: "Try again",
120
125
  },
121
126
  table: {
127
+ add: "Add",
122
128
  row: "row",
123
129
  rows: "rows",
124
130
  yes: "yes",
@@ -134,8 +140,10 @@ export const ENGLISH_COPY = {
134
140
  list: {
135
141
  empty: "There is nothing here yet.",
136
142
  emptyFiltered: "Nothing matches what is on.",
143
+ countOne: "1 record",
144
+ count: "{count} records",
137
145
  },
138
- chatter: {
146
+ activity: {
139
147
  bySystem: "the system",
140
148
  more: "show the whole error",
141
149
  less: "show less",
@@ -146,13 +154,26 @@ export const ENGLISH_COPY = {
146
154
  recipients: "Who receives it",
147
155
  noRecipients: "nobody to send it to yet",
148
156
  messagePlaceholder: "Write a message…",
149
- empty: "Nothing has happened here yet.",
150
- history: "history",
157
+ emptyMessages: "Nobody has written here yet.",
158
+ emptyEvents: "Nothing has been logged yet.",
159
+ title: "Activity",
151
160
  open: "Open the {label}",
152
161
  fold: "Fold the {label}",
153
- tabAll: "all",
154
- tabMessages: "messages",
155
- tabEvents: "events",
162
+ alerts: "Notices",
163
+ conversation: "Conversation",
164
+ history: "History",
165
+ viewTimeline: "Timeline",
166
+ moreAlerts: "Show {count} more",
167
+ fewerAlerts: "Show fewer",
168
+ filters: "Show",
169
+ filterAll: "All",
170
+ filterConversation: "Messages",
171
+ filterNotes: "Notes",
172
+ filterEvents: "Events",
173
+ today: "Today",
174
+ yesterday: "Yesterday",
175
+ emptyFilter: "Nothing of this kind yet.",
176
+ goTo: "Go to {owner}",
156
177
  send: "Send",
157
178
  sending: "Sending…",
158
179
  cancel: "Cancel",
@@ -161,6 +182,8 @@ export const ENGLISH_COPY = {
161
182
  label: "Copy",
162
183
  done: "Copied",
163
184
  failed: "Could not copy",
185
+ field: "Copy «{label}»",
186
+ empty: "Nothing to copy",
164
187
  },
165
188
  sorting: {
166
189
  label: "Sort",
@@ -217,6 +240,7 @@ export const ENGLISH_COPY = {
217
240
  },
218
241
  editable: {
219
242
  add: "Add a line",
243
+ addRow: "Add a row",
220
244
  empty: "No lines yet.",
221
245
  remove: "Remove the line",
222
246
  },
@@ -229,6 +253,7 @@ export const ENGLISH_COPY = {
229
253
  refine: "— narrow the filter",
230
254
  open: "Open",
231
255
  browseAll: "see all…",
256
+ loadingMore: "loading more…",
232
257
  },
233
258
  cell: {
234
259
  missing: "missing",
@@ -240,6 +265,10 @@ export const ENGLISH_COPY = {
240
265
  invalid: "invalid date",
241
266
  noDate: "no date",
242
267
  },
268
+ drill: {
269
+ back: "Back to {name}",
270
+ trail: "Opened from",
271
+ },
243
272
  dialog: {
244
273
  expand: "Expand to a panel",
245
274
  cancel: "Cancel",
@@ -323,6 +352,10 @@ export const ENGLISH_COPY = {
323
352
  remove: "Remove line",
324
353
  empty: "No lines yet.",
325
354
  done: "Done",
355
+ addSection: "Add a section",
356
+ addNote: "Add a note",
357
+ section: "Section",
358
+ note: "Note",
326
359
  },
327
360
  lineItems: {
328
361
  one: "{count} line",
@@ -341,6 +374,15 @@ export const ENGLISH_COPY = {
341
374
  relationCard: {
342
375
  summary: "Summary of {name}",
343
376
  },
377
+ totals: {
378
+ less: "less",
379
+ details: "Details: {label}",
380
+ open: "Open",
381
+ unlink: "Unlink",
382
+ credits: "Outstanding credits",
383
+ apply: "Apply",
384
+ applyCredit: "Apply {label}",
385
+ },
344
386
  inlineEdit: {
345
387
  addTag: "Add…",
346
388
  createTag: "Create «{name}»",
@@ -352,6 +394,9 @@ export const ENGLISH_COPY = {
352
394
  light: "Light",
353
395
  dark: "Dark",
354
396
  },
397
+ tree: {
398
+ empty: "Nothing here yet.",
399
+ },
355
400
  assist: {
356
401
  assistant: "Assistant",
357
402
  suggests: "suggests",
@@ -430,11 +475,12 @@ export function mergeCopy(over) {
430
475
  ops: { ...ENGLISH_COPY.ops, ...over.ops },
431
476
  sorting: { ...ENGLISH_COPY.sorting, ...over.sorting },
432
477
  copyable: { ...ENGLISH_COPY.copyable, ...over.copyable },
433
- chatter: { ...ENGLISH_COPY.chatter, ...over.chatter },
478
+ activity: { ...ENGLISH_COPY.activity, ...over.activity },
434
479
  editable: { ...ENGLISH_COPY.editable, ...over.editable },
435
480
  picker: { ...ENGLISH_COPY.picker, ...over.picker },
436
481
  cell: { ...ENGLISH_COPY.cell, ...over.cell },
437
482
  time: { ...ENGLISH_COPY.time, ...over.time },
483
+ drill: { ...ENGLISH_COPY.drill, ...over.drill },
438
484
  dialog: { ...ENGLISH_COPY.dialog, ...over.dialog },
439
485
  report: { ...ENGLISH_COPY.report, ...over.report },
440
486
  builder: { ...ENGLISH_COPY.builder, ...over.builder },
@@ -444,8 +490,10 @@ export function mergeCopy(over) {
444
490
  lineItems: { ...ENGLISH_COPY.lineItems, ...over.lineItems },
445
491
  relation: { ...ENGLISH_COPY.relation, ...over.relation },
446
492
  relationCard: { ...ENGLISH_COPY.relationCard, ...over.relationCard },
493
+ totals: { ...ENGLISH_COPY.totals, ...over.totals },
447
494
  inlineEdit: { ...ENGLISH_COPY.inlineEdit, ...over.inlineEdit },
448
495
  theme: { ...ENGLISH_COPY.theme, ...over.theme },
496
+ tree: { ...ENGLISH_COPY.tree, ...over.tree },
449
497
  assist: { ...ENGLISH_COPY.assist, ...over.assist },
450
498
  };
451
499
  }
@@ -10,6 +10,9 @@ export type ComboBoxProps = {
10
10
  id?: string;
11
11
  describedBy?: string;
12
12
  invalid?: boolean;
13
+ /** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
14
+ * Inside a field, the field's label names it. */
15
+ "aria-label"?: string;
13
16
  /** Forces the mode. By default it inherits the form's: in read mode it draws the label. */
14
17
  mode?: FormMode;
15
18
  };
@@ -37,14 +37,14 @@ export function ComboBox({ mode, ...props }) {
37
37
  to be ABOVE the component that asks for it. */
38
38
  return (_jsx(AriaLocale, { children: _jsx(EditableComboBox, { ...props }) }));
39
39
  }
40
- function EditableComboBox({ value, onChange, options, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, }) {
40
+ function EditableComboBox({ value, onChange, options, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, "aria-label": ariaLabel, }) {
41
41
  const { id, labelledBy, describedBy, invalid } = useFieldIds({
42
42
  id: ownId,
43
43
  describedBy: ownDescribedBy,
44
44
  invalid: ownInvalid,
45
45
  });
46
46
  const { contains } = useFilter({ sensitivity: "base" });
47
- return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy,
47
+ return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy,
48
48
  /* `defaultItems` and not `items`: with `items` React Aria hands the filtering to the
49
49
  consumer. These are the same options every render — only the filtering is React
50
50
  Aria's. */
@@ -7,6 +7,9 @@ export type SearchFieldProps = {
7
7
  disabled?: boolean;
8
8
  id?: string;
9
9
  describedBy?: string;
10
+ /** The name of the box when it is NOT inside a `Field` — the search of a list bar, of a
11
+ * panel. Inside a field, the field's label names it. Without either, see below. */
12
+ "aria-label"?: string;
10
13
  };
11
14
  /**
12
15
  * A plain search box: a text, a clear button, Enter to submit. To filter the rows of a panel,
@@ -20,5 +23,11 @@ export type SearchFieldProps = {
20
23
  * It does not follow the form's read mode: what is typed here is never data of the record, so
21
24
  * there is nothing to draw as text. The browser's own clear cross is hidden; ours is the one
22
25
  * the keyboard and the screen reader know.
26
+ *
27
+ * It is never nameless. Most search boxes live OUTSIDE a form — the bar of a list, a panel —
28
+ * where there is no `Field` to label them, and a nameless one is read as «edit text» (React
29
+ * Aria warns about it in the console). The name is, in this order: the enclosing `Field`'s
30
+ * label, an explicit `aria-label`, the placeholder (what the eye reads is what the ear
31
+ * should hear), and finally the copy table's «Search».
23
32
  */
24
- export declare function SearchField({ value, onChange, placeholder, onSubmit, disabled, id: ownId, describedBy: ownDescribedBy, }: SearchFieldProps): import("react").JSX.Element;
33
+ export declare function SearchField({ value, onChange, placeholder, onSubmit, disabled, id: ownId, describedBy: ownDescribedBy, "aria-label": ariaLabel, }: SearchFieldProps): import("react").JSX.Element;
@@ -1,10 +1,17 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { AriaLocale } from "../../foundations/aria.js";
4
+ import { useUiCopy } from "../../foundations/copy.js";
4
5
  import { FieldButton, FieldGroup } from "../field/aria.js";
5
6
  import { useFieldIds } from "../field/context.js";
6
7
  import { Search, X } from "lucide-react";
7
8
  import { Input, SearchField as AriaSearchField } from "react-aria-components";
9
+ /** The trailing «…» of a placeholder is a visual cue that typing is expected, not part of
10
+ * the name: «Buscar una cuenta…» is announced as «Buscar una cuenta». */
11
+ function nameFromPlaceholder(placeholder) {
12
+ const name = placeholder?.replace(/[\s.…]+$/u, "").trim();
13
+ return name === undefined || name === "" ? undefined : name;
14
+ }
8
15
  /**
9
16
  * A plain search box: a text, a clear button, Enter to submit. To filter the rows of a panel,
10
17
  * the options of a long settings page, the lines of a document.
@@ -17,11 +24,23 @@ import { Input, SearchField as AriaSearchField } from "react-aria-components";
17
24
  * It does not follow the form's read mode: what is typed here is never data of the record, so
18
25
  * there is nothing to draw as text. The browser's own clear cross is hidden; ours is the one
19
26
  * the keyboard and the screen reader know.
27
+ *
28
+ * It is never nameless. Most search boxes live OUTSIDE a form — the bar of a list, a panel —
29
+ * where there is no `Field` to label them, and a nameless one is read as «edit text» (React
30
+ * Aria warns about it in the console). The name is, in this order: the enclosing `Field`'s
31
+ * label, an explicit `aria-label`, the placeholder (what the eye reads is what the ear
32
+ * should hear), and finally the copy table's «Search».
20
33
  */
21
- export function SearchField({ value, onChange, placeholder, onSubmit, disabled = false, id: ownId, describedBy: ownDescribedBy, }) {
34
+ export function SearchField({ value, onChange, placeholder, onSubmit, disabled = false, id: ownId, describedBy: ownDescribedBy, "aria-label": ariaLabel, }) {
35
+ const copy = useUiCopy();
22
36
  const { id, labelledBy, describedBy } = useFieldIds({
23
37
  id: ownId,
24
38
  describedBy: ownDescribedBy,
25
39
  });
26
- return (_jsx(AriaLocale, { children: _jsx(AriaSearchField, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: value, onChange: (next) => onChange?.(next), onSubmit: onSubmit, isDisabled: disabled, className: "group w-full min-w-0", children: _jsxs(FieldGroup, { children: [_jsx(Search, { "aria-hidden": true, className: "ml-2.5 size-(--size-icon-md) shrink-0 text-muted forced-colors:text-[ButtonText]", strokeWidth: 2 }), _jsx(Input, { placeholder: placeholder, className: "h-full min-w-0 flex-1 bg-transparent px-2 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed [&::-webkit-search-cancel-button]:hidden" }), _jsx(FieldButton, { className: "mr-1 group-data-empty:invisible", children: _jsx(X, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }) }) }));
40
+ /* A `Field` names it through its label; anything else falls back so it always has one. */
41
+ const name = ariaLabel ??
42
+ (labelledBy === undefined
43
+ ? (nameFromPlaceholder(placeholder) ?? copy.choice.search)
44
+ : undefined);
45
+ return (_jsx(AriaLocale, { children: _jsx(AriaSearchField, { id: id, "aria-labelledby": labelledBy, "aria-label": name, "aria-describedby": describedBy, value: value, onChange: (next) => onChange?.(next), onSubmit: onSubmit, isDisabled: disabled, className: "group w-full min-w-0", children: _jsxs(FieldGroup, { children: [_jsx(Search, { "aria-hidden": true, className: "ml-2.5 size-(--size-icon-md) shrink-0 text-muted forced-colors:text-[ButtonText]", strokeWidth: 2 }), _jsx(Input, { placeholder: placeholder, className: "h-full min-w-0 flex-1 bg-transparent px-2 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed [&::-webkit-search-cancel-button]:hidden" }), _jsx(FieldButton, { className: "mr-1 group-data-empty:invisible", children: _jsx(X, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }) }) }));
27
46
  }
@@ -3,8 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { parseDate } from "@internationalized/date";
4
4
  import { AriaLocale } from "../../foundations/aria.js";
5
5
  import { focusRing, tv } from "../../foundations/variants.js";
6
- import { ChevronLeft, ChevronRight } from "lucide-react";
7
- import { Button as AriaButton, Calendar as AriaCalendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarHeading, RangeCalendar as AriaRangeCalendar, useLocale, } from "react-aria-components";
6
+ import { listOptionStyles } from "../field/aria.js";
7
+ import { Popover } from "../overlay/Popover.js";
8
+ import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
9
+ import { Button as AriaButton, Calendar as AriaCalendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarHeading, CalendarMonthPicker, CalendarYearPicker, ListBox, ListBoxItem, RangeCalendar as AriaRangeCalendar, Select as AriaSelect, SelectValue, useLocale, } from "react-aria-components";
8
10
  /**
9
11
  * The month grid — on its own, or inside the popover of a date picker.
10
12
  *
@@ -12,15 +14,37 @@ import { Button as AriaButton, Calendar as AriaCalendar, CalendarCell, CalendarG
12
14
  * implements the grid pattern (arrows move a day, PageUp/PageDown a month, Home/End the
13
15
  * week). The selected day is the system's `primary`, not the gold: the gold is for the ONE
14
16
  * action of a screen, and a calendar is not an action.
17
+ *
18
+ * The month and the year of the heading are two small selects: a birth date in 1987 or a
19
+ * contract signed in 2019 is two picks away, not eighty presses of «previous month». They
20
+ * are ALWAYS there, not behind a prop, because they cost nothing to the look — the words of
21
+ * the heading become the buttons, same weight, same color, the arrows where they were — and
22
+ * a prop is the one every app forgets on the one form (the birth date) that needed it.
15
23
  */
16
24
  const navButton = tv({
17
25
  extend: focusRing,
18
26
  base: "inline-flex size-(--size-control-sm) items-center justify-center rounded-control text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink pressed:bg-ink/10 disabled:text-line-strong",
19
27
  });
20
- function Header() {
28
+ const jumpTrigger = tv({
29
+ extend: focusRing,
30
+ base: "inline-flex h-(--size-control-sm) items-center gap-1 rounded-control px-1.5 text-body font-semibold text-ink transition-colors duration-(--duration-instant) hover:bg-ink/6 pressed:bg-ink/10",
31
+ });
32
+ /* A word of the heading that opens a list. `first-letter` needs a block box: Spanish month
33
+ names are lower case, and a heading that starts «octubre» reads like a typo. */
34
+ function Jump({ "aria-label": label, value, onChange, items }) {
35
+ return (_jsxs(AriaSelect, { "aria-label": label, value: value, onChange: onChange, children: [_jsxs(AriaButton, { className: jumpTrigger, children: [_jsx(SelectValue, { className: "inline-block first-letter:uppercase", children: ({ selectedText }) => selectedText }), _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--size-icon-sm) text-muted", strokeWidth: 2.5 })] }), _jsx(Popover, { placement: "bottom", className: "min-w-(--trigger-width)", children: _jsx(ListBox, { items: items, className: "max-h-[inherit] overflow-auto p-1 outline-0", children: (item) => (_jsx(ListBoxItem, { id: item.id, textValue: item.formatted, className: listOptionStyles, children: _jsx("span", { className: "block tabular-nums first-letter:uppercase", children: item.formatted }) })) }) })] }));
36
+ }
37
+ /* A century around the shown year: wide enough for a birth date or an old contract in one
38
+ pick, and `min`/`max` clip it (a birth date with `max` = today lists back from today).
39
+ The list opens scrolled to the shown year and typing «1987» jumps to it. */
40
+ const YEARS_LISTED = 100;
41
+ /** `jump` turns the heading into the month and year selects. Off only for two months side
42
+ * by side: their heading names a span («octubre – noviembre de 2026») that one month
43
+ * select cannot stand for. */
44
+ function Header({ jump }) {
21
45
  const { direction } = useLocale();
22
46
  const [Prev, Next] = direction === "rtl" ? [ChevronRight, ChevronLeft] : [ChevronLeft, ChevronRight];
23
- return (_jsxs("header", { className: "mb-2 flex items-center gap-1", children: [_jsx(AriaButton, { slot: "previous", className: navButton, children: _jsx(Prev, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) }), _jsx(CalendarHeading, { className: "flex-1 text-center text-body font-semibold text-ink first-letter:uppercase" }), _jsx(AriaButton, { slot: "next", className: navButton, children: _jsx(Next, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }));
47
+ return (_jsxs("header", { className: "mb-2 flex items-center gap-1", children: [_jsx(AriaButton, { slot: "previous", className: navButton, children: _jsx(Prev, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) }), jump ? (_jsxs("div", { className: "flex flex-1 items-center justify-center", children: [_jsx(CalendarMonthPicker, { format: "long", children: (aria) => _jsx(Jump, { ...aria }) }), _jsx(CalendarYearPicker, { visibleYears: YEARS_LISTED, children: (aria) => _jsx(Jump, { ...aria }) })] })) : (_jsx(CalendarHeading, { className: "flex-1 text-center text-body font-semibold text-ink first-letter:uppercase" })), _jsx(AriaButton, { slot: "next", className: navButton, children: _jsx(Next, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }));
24
48
  }
25
49
  function WeekDays() {
26
50
  return (_jsx(CalendarGridHeader, { children: (day) => (_jsx(CalendarHeaderCell, { className: "pb-1 text-micro font-medium text-muted", children: day })) }));
@@ -70,7 +94,7 @@ export function Calendar({ value, onChange, min, max, isUnavailable, ...rest })
70
94
  ? undefined
71
95
  : (date) => isUnavailable(date.toString()),
72
96
  });
73
- return (_jsx(AriaLocale, { children: _jsxs(AriaCalendar, { ...rest, ...props, className: "w-fit", children: [_jsx(Header, {}), _jsxs(CalendarGrid, { className: "border-separate border-spacing-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => _jsx(CalendarCell, { date: date, className: dayCell }) })] })] }) }));
97
+ return (_jsx(AriaLocale, { children: _jsxs(AriaCalendar, { ...rest, ...props, className: "w-fit", children: [_jsx(Header, { jump: true }), _jsxs(CalendarGrid, { className: "border-separate border-spacing-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => _jsx(CalendarCell, { date: date, className: dayCell }) })] })] }) }));
74
98
  }
75
99
  const rangeDay = tv({
76
100
  extend: focusRing,
@@ -98,7 +122,7 @@ export function RangeCalendar({ months = 1, value, onChange, min, max, ...rest }
98
122
  minValue: iso(min) ?? undefined,
99
123
  maxValue: iso(max) ?? undefined,
100
124
  });
101
- return (_jsx(AriaLocale, { children: _jsxs(AriaRangeCalendar, { ...rest, ...props, visibleDuration: { months }, className: "w-fit", children: [_jsx(Header, {}), _jsx("div", { className: "flex gap-4", children: Array.from({ length: months }, (_, i) => (_jsxs(CalendarGrid, { offset: { months: i }, className: "border-separate border-spacing-y-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => (_jsx(CalendarCell, { date: date, className: "group size-(--size-control-md) cursor-default text-meta tabular-nums outside-month:invisible selected:bg-primary/10 selection-start:rounded-s-control selection-end:rounded-e-control", children: ({ formattedDate, isSelected, isSelectionStart, isSelectionEnd, isFocusVisible, isDisabled, }) => (_jsx("span", { className: rangeDay({
125
+ return (_jsx(AriaLocale, { children: _jsxs(AriaRangeCalendar, { ...rest, ...props, visibleDuration: { months }, className: "w-fit", children: [_jsx(Header, { jump: months === 1 }), _jsx("div", { className: "flex gap-4", children: Array.from({ length: months }, (_, i) => (_jsxs(CalendarGrid, { offset: { months: i }, className: "border-separate border-spacing-y-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => (_jsx(CalendarCell, { date: date, className: "group size-(--size-control-md) cursor-default text-meta tabular-nums outside-month:invisible selected:bg-primary/10 selection-start:rounded-s-control selection-end:rounded-e-control", children: ({ formattedDate, isSelected, isSelectionStart, isSelectionEnd, isFocusVisible, isDisabled, }) => (_jsx("span", { className: rangeDay({
102
126
  position: isSelected && (isSelectionStart || isSelectionEnd)
103
127
  ? "cap"
104
128
  : isSelected
@@ -1,4 +1,9 @@
1
1
  import { type FormMode } from "../field/mode.js";
2
+ /** The typed segments inside the field box — shared with `TimeField` and `DateTimePicker`
3
+ * (internal: not in the family barrel). */
4
+ export declare function Segments({ slot }: {
5
+ slot?: "start" | "end";
6
+ }): import("react").JSX.Element;
2
7
  type Shared = {
3
8
  id?: string;
4
9
  describedBy?: string;
@@ -7,17 +12,20 @@ type Shared = {
7
12
  /** The earliest and latest day that can be chosen, as ISO dates. */
8
13
  min?: string;
9
14
  max?: string;
15
+ /** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
16
+ * Inside a field, the field's label names it. */
17
+ "aria-label"?: string;
10
18
  /** Forces the mode. By default it inherits the form's: in read mode it draws the value. */
11
19
  mode?: FormMode;
12
20
  };
13
21
  /** A date typed by segments, without a calendar: for a form where dates are known by heart
14
22
  * (a birth date, a contract date). */
15
- export declare function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, }: Shared & {
23
+ export declare function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, "aria-label": ariaLabel, }: Shared & {
16
24
  value: string | null;
17
25
  onChange?: (next: string | null) => void;
18
26
  }): import("react").JSX.Element;
19
27
  /** A date typed by segments, or picked on a calendar from the button. */
20
- export declare function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, }: Shared & {
28
+ export declare function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, "aria-label": ariaLabel, }: Shared & {
21
29
  value: string | null;
22
30
  onChange?: (next: string | null) => void;
23
31
  }): import("react").JSX.Element;
@@ -27,7 +35,7 @@ export type DateRangeValue = {
27
35
  };
28
36
  /** A span of days — a period of a report, the validity of a contract — typed or picked on
29
37
  * two months at once. */
30
- export declare function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, }: Shared & {
38
+ export declare function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, "aria-label": ariaLabel, }: Shared & {
31
39
  value: DateRangeValue | null;
32
40
  onChange?: (next: DateRangeValue | null) => void;
33
41
  }): import("react").JSX.Element;
@@ -35,7 +35,9 @@ const segment = tv({
35
35
  },
36
36
  },
37
37
  });
38
- function Segments({ slot }) {
38
+ /** The typed segments inside the field box — shared with `TimeField` and `DateTimePicker`
39
+ * (internal: not in the family barrel). */
40
+ export function Segments({ slot }) {
39
41
  return (_jsx(DateInput, { slot: slot, className: "flex min-w-0 items-center px-2.5 whitespace-nowrap", children: (part) => _jsx(DateSegment, { segment: part, className: segment }) }));
40
42
  }
41
43
  function parse(iso) {
@@ -50,7 +52,9 @@ function parse(iso) {
50
52
  }
51
53
  function useReadDate() {
52
54
  const copy = useUiCopy();
53
- const format = new DateFormatter(copy.locale, { dateStyle: "medium" });
55
+ /* `toDate("UTC")` is midnight in UTC, so the formatter has to read it in UTC too: in the
56
+ zone of the machine, anywhere west of Greenwich, the 30th was drawn as the 29th. */
57
+ const format = new DateFormatter(copy.locale, { dateStyle: "medium", timeZone: "UTC" });
54
58
  return (iso) => {
55
59
  const date = parse(iso);
56
60
  return date === null ? "" : format.format(date.toDate("UTC"));
@@ -58,7 +62,7 @@ function useReadDate() {
58
62
  }
59
63
  /** A date typed by segments, without a calendar: for a form where dates are known by heart
60
64
  * (a birth date, a contract date). */
61
- export function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, }) {
65
+ export function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, "aria-label": ariaLabel, }) {
62
66
  const { id, labelledBy, describedBy, invalid } = useFieldIds({
63
67
  id: ownId,
64
68
  describedBy: ownDescribedBy,
@@ -67,10 +71,10 @@ export function DateField({ value, onChange, id: ownId, describedBy: ownDescribe
67
71
  const read = useReadDate();
68
72
  if (useFormMode(mode) === "read")
69
73
  return _jsx(ReadValue, { value: read(value) });
70
- return (_jsx(AriaLocale, { children: _jsx(AriaDateField, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
74
+ return (_jsx(AriaLocale, { children: _jsx(AriaDateField, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
71
75
  }
72
76
  /** A date typed by segments, or picked on a calendar from the button. */
73
- export function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, }) {
77
+ export function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, "aria-label": ariaLabel, }) {
74
78
  const { id, labelledBy, describedBy, invalid } = useFieldIds({
75
79
  id: ownId,
76
80
  describedBy: ownDescribedBy,
@@ -79,11 +83,11 @@ export function DatePicker({ value, onChange, id: ownId, describedBy: ownDescrib
79
83
  const read = useReadDate();
80
84
  if (useFormMode(mode) === "read")
81
85
  return _jsx(ReadValue, { value: read(value) });
82
- return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
86
+ return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
83
87
  }
84
88
  /** A span of days — a period of a report, the validity of a contract — typed or picked on
85
89
  * two months at once. */
86
- export function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, }) {
90
+ export function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, "aria-label": ariaLabel, }) {
87
91
  const { id, labelledBy, describedBy, invalid } = useFieldIds({
88
92
  id: ownId,
89
93
  describedBy: ownDescribedBy,
@@ -95,5 +99,5 @@ export function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDe
95
99
  }
96
100
  const start = parse(value?.start);
97
101
  const end = parse(value?.end);
98
- return (_jsx(AriaLocale, { children: _jsxs(AriaDateRangePicker, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: start === null || end === null ? null : { start, end }, onChange: (next) => onChange?.(next === null ? null : { start: next.start.toString(), end: next.end.toString() }), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, { slot: "start" }), _jsx("span", { "aria-hidden": true, className: "text-muted", children: "\u2013" }), _jsx(Segments, { slot: "end" }), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(RangeCalendar, { months: 2 }) }) })] }) }));
102
+ return (_jsx(AriaLocale, { children: _jsxs(AriaDateRangePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: start === null || end === null ? null : { start, end }, onChange: (next) => onChange?.(next === null ? null : { start: next.start.toString(), end: next.end.toString() }), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, { slot: "start" }), _jsx("span", { "aria-hidden": true, className: "text-muted", children: "\u2013" }), _jsx(Segments, { slot: "end" }), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(RangeCalendar, { months: 2 }) }) })] }) }));
99
103
  }
@@ -0,0 +1,35 @@
1
+ import { type FormMode } from "../field/mode.js";
2
+ type HourCycle = 12 | 24;
3
+ type Shared = {
4
+ id?: string;
5
+ describedBy?: string;
6
+ invalid?: boolean;
7
+ disabled?: boolean;
8
+ /** 12h or 24h. By default, the one of the copy's locale. Only the display: the value is
9
+ * always 24h. */
10
+ hourCycle?: HourCycle;
11
+ /** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
12
+ * Inside a field, the field's label names it. */
13
+ "aria-label"?: string;
14
+ /** Forces the mode. By default it inherits the form's: in read mode it draws the value. */
15
+ mode?: FormMode;
16
+ };
17
+ /** A time of day typed by segments — the hour of an appointment, the opening of a shift.
18
+ * The value is `"HH:MM"` in 24h, or `null`. */
19
+ export declare function TimeField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, hourCycle, mode, "aria-label": ariaLabel, }: Shared & {
20
+ value: string | null;
21
+ onChange?: (next: string | null) => void;
22
+ /** The earliest and latest time that can be typed, as `"HH:MM"`. */
23
+ min?: string;
24
+ max?: string;
25
+ }): import("react").JSX.Element;
26
+ /** A day and its time — a delivery slot, an appointment: the day typed or picked on the
27
+ * calendar, the time typed after it. The value is a local `"YYYY-MM-DDTHH:MM"`, or `null`. */
28
+ export declare function DateTimePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, hourCycle, mode, "aria-label": ariaLabel, }: Shared & {
29
+ value: string | null;
30
+ onChange?: (next: string | null) => void;
31
+ /** The earliest and latest moment, as `"YYYY-MM-DDTHH:MM"` or a bare day `"YYYY-MM-DD"`. */
32
+ min?: string;
33
+ max?: string;
34
+ }): import("react").JSX.Element;
35
+ export {};
@@ -0,0 +1,100 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { DateFormatter, parseDateTime, parseTime, toCalendarDate, } from "@internationalized/date";
4
+ import { AriaLocale } from "../../foundations/aria.js";
5
+ import { useUiCopy } from "../../foundations/copy.js";
6
+ import { Calendar } from "./Calendar.js";
7
+ import { Segments } from "./DatePicker.js";
8
+ import { FieldButton, FieldGroup } from "../field/aria.js";
9
+ import { useFieldIds } from "../field/context.js";
10
+ import { ReadValue } from "../field/Field.js";
11
+ import { useFormMode } from "../field/mode.js";
12
+ import { Popover } from "../overlay/Popover.js";
13
+ import { CalendarDays } from "lucide-react";
14
+ import { DatePicker as AriaDatePicker, Dialog, TimeField as AriaTimeField, } from "react-aria-components";
15
+ /**
16
+ * A time of day, and a day with its time — an appointment, a delivery slot — as an ERP
17
+ * stores them: `"09:05"` and `"2026-10-30T09:05"`.
18
+ *
19
+ * Context: the same contract as `DatePicker` — a string in, a string out, converted here
20
+ * once. The time is 24h and without seconds because that is what a backend column of a
21
+ * time keeps; the time with a date is LOCAL and without a zone, because «the delivery is
22
+ * on the 30th at nine» is a wall-clock time where the warehouse is, not an instant: putting
23
+ * a zone on it is the backend's decision, not the field's.
24
+ *
25
+ * What the person SEES follows the locale of the copy: the hour cycle (12h with «p. m.» or
26
+ * 24h) is the one `Intl` gives that locale through `AriaLocale`. CLDR writes `es-BO` in 12h;
27
+ * a company that works in 24h passes `hourCycle={24}` — the value stays `"15:05"` either way.
28
+ */
29
+ const pad = (n) => String(n).padStart(2, "0");
30
+ const hhmm = (time) => `${pad(time.hour)}:${pad(time.minute)}`;
31
+ /* `"09:05"` or `"09:05:00"` — a column of type time often comes with its seconds. */
32
+ function parseClock(value) {
33
+ if (value === null || value === undefined || value === "")
34
+ return null;
35
+ try {
36
+ return parseTime(value);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /* `"2026-10-30T09:05"`; a bare `"2026-10-30"` is that day at midnight, so a `min` can be a day. */
43
+ function parseStamp(value) {
44
+ if (value === null || value === undefined || value === "")
45
+ return null;
46
+ try {
47
+ return parseDateTime(value);
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /* React Aria's `toString()` adds the seconds; the contract is to the minute. */
54
+ function stampOf(value) {
55
+ const time = "hour" in value ? hhmm(value) : "00:00";
56
+ return `${toCalendarDate(value).toString()}T${time}`;
57
+ }
58
+ /* Read mode draws what the segments would: the locale's format, its hour cycle unless the
59
+ control forces one. Everything is formatted in UTC from UTC parts, so the zone of the
60
+ machine never moves the clock. */
61
+ function useReadClock(options, hourCycle) {
62
+ const copy = useUiCopy();
63
+ return new DateFormatter(copy.locale, {
64
+ ...options,
65
+ timeZone: "UTC",
66
+ ...(hourCycle === undefined ? {} : { hourCycle: hourCycle === 24 ? "h23" : "h12" }),
67
+ });
68
+ }
69
+ /** A time of day typed by segments — the hour of an appointment, the opening of a shift.
70
+ * The value is `"HH:MM"` in 24h, or `null`. */
71
+ export function TimeField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, hourCycle, mode, "aria-label": ariaLabel, }) {
72
+ const { id, labelledBy, describedBy, invalid } = useFieldIds({
73
+ id: ownId,
74
+ describedBy: ownDescribedBy,
75
+ invalid: ownInvalid,
76
+ });
77
+ const format = useReadClock({ timeStyle: "short" }, hourCycle);
78
+ if (useFormMode(mode) === "read") {
79
+ const time = parseClock(value);
80
+ return (_jsx(ReadValue, { value: time === null
81
+ ? ""
82
+ : format.format(new Date(Date.UTC(1970, 0, 1, time.hour, time.minute))) }));
83
+ }
84
+ return (_jsx(AriaLocale, { children: _jsx(AriaTimeField, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parseClock(value), onChange: (next) => onChange?.(next === null ? null : hhmm(next)), minValue: parseClock(min) ?? undefined, maxValue: parseClock(max) ?? undefined, hourCycle: hourCycle, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
85
+ }
86
+ /** A day and its time — a delivery slot, an appointment: the day typed or picked on the
87
+ * calendar, the time typed after it. The value is a local `"YYYY-MM-DDTHH:MM"`, or `null`. */
88
+ export function DateTimePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, hourCycle, mode, "aria-label": ariaLabel, }) {
89
+ const { id, labelledBy, describedBy, invalid } = useFieldIds({
90
+ id: ownId,
91
+ describedBy: ownDescribedBy,
92
+ invalid: ownInvalid,
93
+ });
94
+ const format = useReadClock({ dateStyle: "medium", timeStyle: "short" }, hourCycle);
95
+ if (useFormMode(mode) === "read") {
96
+ const stamp = parseStamp(value);
97
+ return _jsx(ReadValue, { value: stamp === null ? "" : format.format(stamp.toDate("UTC")) });
98
+ }
99
+ return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parseStamp(value), onChange: (next) => onChange?.(next === null ? null : stampOf(next)), minValue: parseStamp(min) ?? undefined, maxValue: parseStamp(max) ?? undefined, granularity: "minute", hourCycle: hourCycle, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
100
+ }
@@ -1,2 +1,3 @@
1
1
  export { Calendar, type CalendarProps, RangeCalendar, type RangeCalendarProps, } from "./Calendar.js";
2
2
  export { DateField, DatePicker, DateRangePicker, type DateRangeValue, } from "./DatePicker.js";
3
+ export { DateTimePicker, TimeField } from "./DateTime.js";
@@ -1,4 +1,6 @@
1
- /* LAS FECHAS — escritas por segmentos o elegidas en un calendario, con el orden del idioma de
2
- la copia. El valor es un ISO (`"2026-10-30"`), lo que manda cualquier backend. */
1
+ /* LAS FECHAS Y LAS HORAS — escritas por segmentos o elegidas en un calendario, con el orden
2
+ del idioma de la copia. El valor es un ISO (`"2026-10-30"`, `"09:05"`,
3
+ `"2026-10-30T09:05"`), lo que manda cualquier backend. */
3
4
  export { Calendar, RangeCalendar, } from "./Calendar.js";
4
5
  export { DateField, DatePicker, DateRangePicker, } from "./DatePicker.js";
6
+ export { DateTimePicker, TimeField } from "./DateTime.js";