@tensor_1/react 2.35.0 → 2.36.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 (35) hide show
  1. package/README.md +141 -15
  2. package/dist/components/Avatar.d.ts +26 -4
  3. package/dist/components/Avatar.js +26 -3
  4. package/dist/components/CommandPalette.d.ts +6 -1
  5. package/dist/components/CommandPalette.js +9 -3
  6. package/dist/components/DateFieldParts.d.ts +61 -0
  7. package/dist/components/DateFieldParts.js +195 -0
  8. package/dist/components/DatePicker.d.ts +129 -0
  9. package/dist/components/DatePicker.js +340 -0
  10. package/dist/components/DateRangePicker.d.ts +34 -3
  11. package/dist/components/DateRangePicker.js +120 -17
  12. package/dist/components/EntityPicker.d.ts +29 -4
  13. package/dist/components/EntityPicker.js +20 -8
  14. package/dist/components/InlineEdit.d.ts +74 -0
  15. package/dist/components/InlineEdit.js +176 -0
  16. package/dist/components/Radio.d.ts +23 -2
  17. package/dist/components/Radio.js +18 -3
  18. package/dist/components/SchedulingTimeline.d.ts +130 -15
  19. package/dist/components/SchedulingTimeline.js +765 -141
  20. package/dist/components/SearchSuggest.d.ts +12 -1
  21. package/dist/components/SearchSuggest.js +7 -2
  22. package/dist/components/Tree.d.ts +30 -2
  23. package/dist/components/Tree.js +135 -10
  24. package/dist/date-field-model.d.ts +40 -0
  25. package/dist/date-field-model.js +79 -0
  26. package/dist/index.cjs +3854 -2068
  27. package/dist/index.d.cts +808 -262
  28. package/dist/index.d.ts +9 -6
  29. package/dist/index.js +40 -4
  30. package/dist/labels-context.d.ts +14 -1
  31. package/dist/scheduling-timeline-model.d.ts +61 -11
  32. package/dist/scheduling-timeline-model.js +85 -26
  33. package/dist/scheduling-timeline-rows.d.ts +40 -0
  34. package/dist/scheduling-timeline-rows.js +42 -0
  35. package/package.json +5 -3
@@ -50,6 +50,17 @@ interface SearchSuggestProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onCha
50
50
  empty?: ReactNode;
51
51
  /** Shown instead of the empty state when suggestions could not be loaded. */
52
52
  error?: ReactNode;
53
+ /**
54
+ * Shown in the open list when there is nothing else to show there: no options, nothing
55
+ * loading, no error and no empty state. For a search that has not started yet, e.g.
56
+ * "Type at least 2 characters." (HAR-1648).
57
+ */
58
+ idle?: ReactNode;
59
+ /**
60
+ * A line under the options that is not an option: arrow keys skip it and it cannot be
61
+ * chosen. For "More matches. Keep typing to narrow the search." when the results were cut off.
62
+ */
63
+ note?: ReactNode;
53
64
  /** A keyboard hint shown in the empty, unfocused field, e.g. "/". */
54
65
  shortcutHint?: string;
55
66
  /** Accessible name of the clear button (default "Clear search"). */
@@ -86,6 +97,6 @@ interface SearchSuggestProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onCha
86
97
  * (`aria-activedescendant`), Enter opens it (the first row when none is active), Escape closes the
87
98
  * list and a second Escape clears the text.
88
99
  */
89
- declare function SearchSuggest({ value, onValueChange, options, onSelect, label, placeholder, highlight, heading, headingAction, footer, loading, loadingLabel, empty, error, shortcutHint, clearLabel: clearLabelProp, status, size, open: openProp, strategy, onOpenChange, inputRef, autoFocus, name, inputId, inputDescribedBy, className, onBlur, ...props }: SearchSuggestProps): react.JSX.Element;
100
+ declare function SearchSuggest({ value, onValueChange, options, onSelect, label, placeholder, highlight, heading, headingAction, footer, loading, loadingLabel, empty, idle, note, error, shortcutHint, clearLabel: clearLabelProp, status, size, open: openProp, strategy, onOpenChange, inputRef, autoFocus, name, inputId, inputDescribedBy, className, onBlur, ...props }: SearchSuggestProps): react.JSX.Element;
90
101
 
91
102
  export { SearchSuggest, type SearchSuggestAction, type SearchSuggestOption, type SearchSuggestProps };
@@ -32,6 +32,8 @@ function SearchSuggest({
32
32
  loading = false,
33
33
  loadingLabel,
34
34
  empty,
35
+ idle,
36
+ note,
35
37
  error,
36
38
  shortcutHint,
37
39
  clearLabel: clearLabelProp,
@@ -72,7 +74,8 @@ function SearchSuggest({
72
74
  const hasText = value.trim().length > 0;
73
75
  const showEmpty = hasText && !loading && !error && options.length === 0 && empty != null;
74
76
  const showLoading = loading && options.length === 0;
75
- const visible = open && (itemCount > 0 || showLoading || showEmpty || error != null);
77
+ const showIdle = idle != null && itemCount === 0 && !loading && error == null && !showEmpty;
78
+ const visible = open && (itemCount > 0 || showLoading || showEmpty || showIdle || error != null);
76
79
  const optionId = (index) => `${id}-option-${index}`;
77
80
  useEffect(() => {
78
81
  if (!visible || active < 0) return;
@@ -294,7 +297,9 @@ function SearchSuggest({
294
297
  loadingLabel && /* @__PURE__ */ jsx("span", { children: loadingLabel })
295
298
  ] }),
296
299
  error != null && options.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: error }),
297
- showEmpty && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: empty })
300
+ showEmpty && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: empty }),
301
+ showIdle && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: idle }),
302
+ note != null && options.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__note", children: note })
298
303
  ] }),
299
304
  /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: status ?? "" })
300
305
  ]
@@ -8,7 +8,23 @@ interface TreeNodeData {
8
8
  typeaheadLabel?: string;
9
9
  icon?: ReactNode;
10
10
  children?: TreeNodeData[];
11
+ /**
12
+ * The node can be expanded although its `children` are not known yet (HAR-1382). With
13
+ * `Tree loadChildren`, expanding it loads them; until then it shows a loading row, and an
14
+ * error row with Retry when the load failed.
15
+ */
16
+ hasChildren?: boolean;
17
+ }
18
+ /** The words of a tree that loads children on expand (HAR-1382). */
19
+ interface TreeLabels {
20
+ /** The row under a node whose children are loading. */
21
+ loading: string;
22
+ /** The row under a node whose children could not be loaded. */
23
+ error: string;
24
+ /** The action in that row. Enter, Space or a click on the row runs it. */
25
+ retry: string;
11
26
  }
27
+ declare const DEFAULT_TREE_LABELS: TreeLabels;
12
28
  interface TreeNodeProps {
13
29
  node: TreeNodeData;
14
30
  expanded: Set<string>;
@@ -21,6 +37,8 @@ interface TreeNodeProps {
21
37
  level?: number;
22
38
  /** Whether a click on the row itself toggles a branch (see {@link TreeProps.toggleOnRowClick}). */
23
39
  rowToggles?: boolean;
40
+ /** Ids of nodes whose children are loading (`aria-busy`). */
41
+ busy?: ReadonlySet<string>;
24
42
  }
25
43
  interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' | 'onChange'> {
26
44
  nodes: TreeNodeData[];
@@ -46,7 +64,17 @@ interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' |
46
64
  * where a row click only selects and the chevron toggles. Enter/Space still does both.
47
65
  */
48
66
  toggleOnRowClick?: boolean;
67
+ /**
68
+ * Loads the children of a node that has `hasChildren` and no `children` yet, the first time
69
+ * it is expanded (HAR-1382; an org chart, a CMDB hierarchy). While it runs the node is
70
+ * `aria-busy` and shows a loading row; a rejection shows an error row whose Enter, Space or
71
+ * click retries. The result is kept for the life of the tree; a node that later arrives with
72
+ * its own `children` uses those.
73
+ */
74
+ loadChildren?: (node: TreeNodeData) => Promise<TreeNodeData[]>;
75
+ /** Words of the loading and error rows. Default: `UixLabelsProvider` `tree`, then English. */
76
+ labels?: Partial<TreeLabels>;
49
77
  }
50
- declare function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect, virtualize, rowHeight, maxHeight, toggleOnRowClick, className, ...props }: TreeProps): react.JSX.Element;
78
+ declare function Tree({ nodes: nodesProp, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect: onSelectProp, virtualize, rowHeight, maxHeight, toggleOnRowClick, loadChildren, labels: labelOverrides, className, ...props }: TreeProps): react.JSX.Element;
51
79
 
52
- export { Tree, type TreeNodeData, type TreeNodeProps, type TreeProps };
80
+ export { DEFAULT_TREE_LABELS, Tree, type TreeLabels, type TreeNodeData, type TreeNodeProps, type TreeProps };
@@ -1,11 +1,21 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
+ import { useUixLabels } from "../labels-context.js";
5
6
  import { flattenTree, treeNav } from "../tree-model.js";
6
7
  import { virtualWindow, shouldVirtualize } from "../table-engine.js";
7
8
  const ChevronIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 4l4 4 4-4" }) });
8
- function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle, onSelect, onFocusNode, level = 1, rowToggles = true }) {
9
+ const DEFAULT_TREE_LABELS = {
10
+ loading: "Loading\u2026",
11
+ error: "Could not load.",
12
+ retry: "Retry"
13
+ };
14
+ const LOADING = "::uix-loading";
15
+ const FAILED = "::uix-error";
16
+ const syntheticOf = (id) => id.endsWith(LOADING) ? { parent: id.slice(0, -LOADING.length), kind: "loading" } : id.endsWith(FAILED) ? { parent: id.slice(0, -FAILED.length), kind: "error" } : null;
17
+ const Ring = () => /* @__PURE__ */ jsx("span", { className: "uix-spinner uix-spinner--sm", "aria-hidden": "true" });
18
+ function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle, onSelect, onFocusNode, level = 1, rowToggles = true, busy }) {
9
19
  const hasChildren = Array.isArray(node.children) && node.children.length > 0;
10
20
  const isExpanded = expanded.has(node.id);
11
21
  return /* @__PURE__ */ jsxs(
@@ -14,7 +24,8 @@ function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle,
14
24
  role: "treeitem",
15
25
  "aria-level": level,
16
26
  "aria-expanded": hasChildren ? isExpanded : void 0,
17
- "aria-selected": selectable ? selected === node.id : void 0,
27
+ "aria-selected": selectable && !syntheticOf(node.id) ? selected === node.id : void 0,
28
+ "aria-busy": busy?.has(node.id) || void 0,
18
29
  "data-id": node.id,
19
30
  tabIndex: tabbableId === node.id ? 0 : -1,
20
31
  className: "uix-tree__item",
@@ -64,7 +75,8 @@ function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle,
64
75
  onSelect,
65
76
  onFocusNode,
66
77
  level: level + 1,
67
- rowToggles
78
+ rowToggles,
79
+ busy
68
80
  },
69
81
  child.id
70
82
  )) })
@@ -73,16 +85,18 @@ function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle,
73
85
  );
74
86
  }
75
87
  function Tree({
76
- nodes,
88
+ nodes: nodesProp,
77
89
  expanded: controlledExpanded,
78
90
  defaultExpanded,
79
91
  selected,
80
92
  onToggle,
81
- onSelect,
93
+ onSelect: onSelectProp,
82
94
  virtualize,
83
95
  rowHeight = 32,
84
96
  maxHeight = 384,
85
97
  toggleOnRowClick,
98
+ loadChildren,
99
+ labels: labelOverrides,
86
100
  className,
87
101
  ...props
88
102
  }) {
@@ -90,8 +104,111 @@ function Tree({
90
104
  const expanded = controlledExpanded ?? internalExpanded;
91
105
  const rootRef = useRef(null);
92
106
  const [focusedId, setFocusedId] = useState(void 0);
93
- const selectable = selected !== void 0 || onSelect != null;
107
+ const selectable = selected !== void 0 || onSelectProp != null;
94
108
  const rowToggles = toggleOnRowClick ?? !selectable;
109
+ const labels = { ...DEFAULT_TREE_LABELS, ...useUixLabels().tree, ...labelOverrides };
110
+ const treeId = useId();
111
+ const [loaded, setLoaded] = useState(() => /* @__PURE__ */ new Map());
112
+ const [failed, setFailed] = useState(() => /* @__PURE__ */ new Set());
113
+ const [loading, setLoading] = useState(() => /* @__PURE__ */ new Set());
114
+ const inFlight = useRef(/* @__PURE__ */ new Set());
115
+ const loadRef = useRef(loadChildren);
116
+ loadRef.current = loadChildren;
117
+ const refocus = useRef(null);
118
+ const nodes = useMemo(() => {
119
+ const row = (parent, kind) => ({
120
+ id: parent.id + (kind === "error" ? FAILED : LOADING),
121
+ typeaheadLabel: "",
122
+ label: kind === "error" ? /* @__PURE__ */ jsxs("span", { className: "uix-tree__status uix-tree__status--error", children: [
123
+ labels.error,
124
+ " ",
125
+ /* @__PURE__ */ jsx("span", { className: "uix-tree__retry", children: labels.retry })
126
+ ] }) : /* @__PURE__ */ jsxs("span", { className: "uix-tree__status", children: [
127
+ /* @__PURE__ */ jsx(Ring, {}),
128
+ labels.loading
129
+ ] })
130
+ });
131
+ const decorate = (list) => list.map((node) => {
132
+ const known = node.children ?? loaded.get(node.id);
133
+ if (known) return { ...node, children: decorate(known) };
134
+ if (!node.hasChildren) return node;
135
+ return { ...node, children: [row(node, failed.has(node.id) ? "error" : "loading")] };
136
+ });
137
+ return loadChildren || nodesProp.some(function lazy(n) {
138
+ return !!n.hasChildren || !!n.children?.some(lazy);
139
+ }) ? decorate(nodesProp) : nodesProp;
140
+ }, [nodesProp, loaded, failed, loadChildren, labels.error, labels.retry, labels.loading]);
141
+ const load = useCallback((node) => {
142
+ const run = loadRef.current;
143
+ if (!run || inFlight.current.has(node.id)) return;
144
+ inFlight.current.add(node.id);
145
+ setFailed((prev) => {
146
+ if (!prev.has(node.id)) return prev;
147
+ const next = new Set(prev);
148
+ next.delete(node.id);
149
+ return next;
150
+ });
151
+ setLoading((prev) => new Set(prev).add(node.id));
152
+ const settle = (apply, focusNext) => {
153
+ inFlight.current.delete(node.id);
154
+ const active = typeof document !== "undefined" ? document.activeElement : null;
155
+ if (active?.dataset?.id === node.id + LOADING || active?.dataset?.id === node.id + FAILED) refocus.current = focusNext;
156
+ setLoading((prev) => {
157
+ const next = new Set(prev);
158
+ next.delete(node.id);
159
+ return next;
160
+ });
161
+ apply();
162
+ };
163
+ run(node).then(
164
+ (children) => settle(() => setLoaded((prev) => new Map(prev).set(node.id, children)), node.id),
165
+ () => settle(() => setFailed((prev) => new Set(prev).add(node.id)), node.id + FAILED)
166
+ );
167
+ }, []);
168
+ useEffect(() => {
169
+ if (!loadChildren) return;
170
+ const visit = (list) => {
171
+ for (const node of list) {
172
+ const known = node.children ?? loaded.get(node.id);
173
+ if (known) visit(known);
174
+ else if (node.hasChildren && expanded.has(node.id) && !failed.has(node.id)) load(node);
175
+ }
176
+ };
177
+ visit(nodesProp);
178
+ }, [nodesProp, expanded, loaded, failed, loadChildren, load]);
179
+ useEffect(() => {
180
+ const id = refocus.current;
181
+ if (id == null) return;
182
+ refocus.current = null;
183
+ setFocusedId(id);
184
+ document.querySelector(`[data-uix-tree="${CSS.escape(treeId)}"] [role="treeitem"][data-id="${CSS.escape(id)}"]`)?.focus();
185
+ }, [loaded, failed, loading, treeId]);
186
+ const findNode = (id, list = nodesProp) => {
187
+ for (const node of list) {
188
+ if (node.id === id) return node;
189
+ const inside = findNode(id, node.children ?? loaded.get(node.id) ?? []);
190
+ if (inside) return inside;
191
+ }
192
+ return void 0;
193
+ };
194
+ const handleSelect = (id) => {
195
+ const synthetic = syntheticOf(id);
196
+ if (!synthetic) {
197
+ onSelectProp?.(id);
198
+ return;
199
+ }
200
+ if (synthetic.kind !== "error") return;
201
+ const parent = findNode(synthetic.parent);
202
+ if (!parent) return;
203
+ refocus.current = parent.id + LOADING;
204
+ setFailed((prev) => {
205
+ const next = new Set(prev);
206
+ next.delete(parent.id);
207
+ return next;
208
+ });
209
+ load(parent);
210
+ };
211
+ const onSelect = selectable || loadChildren ? handleSelect : void 0;
95
212
  const handleToggle = useCallback((id) => {
96
213
  if (!controlledExpanded) {
97
214
  setInternalExpanded((prev) => {
@@ -121,6 +238,8 @@ function Tree({
121
238
  focusedId,
122
239
  setFocusedId,
123
240
  rowToggles,
241
+ busy: loading,
242
+ treeId,
124
243
  className,
125
244
  ...props
126
245
  }
@@ -185,7 +304,7 @@ function Tree({
185
304
  break;
186
305
  }
187
306
  };
188
- return /* @__PURE__ */ jsx("ul", { ref: rootRef, className: cx("uix-tree", className), role: "tree", onKeyDown, ...props, children: nodes.map((node) => /* @__PURE__ */ jsx(
307
+ return /* @__PURE__ */ jsx("ul", { ref: rootRef, className: cx("uix-tree", className), role: "tree", "data-uix-tree": treeId, onKeyDown, ...props, children: nodes.map((node) => /* @__PURE__ */ jsx(
189
308
  TreeNode,
190
309
  {
191
310
  node,
@@ -197,7 +316,8 @@ function Tree({
197
316
  onSelect,
198
317
  onFocusNode: setFocusedId,
199
318
  level: 1,
200
- rowToggles
319
+ rowToggles,
320
+ busy: loading
201
321
  },
202
322
  node.id
203
323
  )) });
@@ -214,6 +334,8 @@ function VirtualTreeView({
214
334
  focusedId,
215
335
  setFocusedId,
216
336
  rowToggles,
337
+ busy,
338
+ treeId,
217
339
  className,
218
340
  ...props
219
341
  }) {
@@ -273,6 +395,7 @@ function VirtualTreeView({
273
395
  ref: scrollRef,
274
396
  className: cx("uix-tree", "uix-tree--virtual", className),
275
397
  role: "tree",
398
+ "data-uix-tree": treeId,
276
399
  tabIndex: -1,
277
400
  style: { maxHeight, overflow: "auto" },
278
401
  onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
@@ -299,7 +422,8 @@ function VirtualTreeView({
299
422
  "aria-setsize": f.setsize,
300
423
  "aria-posinset": f.posinset,
301
424
  "aria-expanded": f.hasChildren ? f.isExpanded : void 0,
302
- "aria-selected": selectable ? selected === f.node.id : void 0,
425
+ "aria-selected": selectable && !syntheticOf(f.node.id) ? selected === f.node.id : void 0,
426
+ "aria-busy": busy.has(f.node.id) || void 0,
303
427
  "data-id": f.node.id,
304
428
  tabIndex: tabbableId === f.node.id || !windowHasTabbable && i === 0 ? 0 : -1,
305
429
  className: "uix-tree__item",
@@ -346,5 +470,6 @@ function VirtualTreeView({
346
470
  );
347
471
  }
348
472
  export {
473
+ DEFAULT_TREE_LABELS,
349
474
  Tree
350
475
  };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * uix date-field model — pure helpers behind `DatePicker`, `DateTimePicker` and the field mode
3
+ * of `DateRangePicker` (HAR-1383): how a locale orders day / month / year, how a typed date is
4
+ * read, and how a date-time value is split and joined. No DOM, no React, no runtime import, so
5
+ * the unit test loads this file directly (date-field-model.test.mjs).
6
+ *
7
+ * A date is an ISO calendar date, `YYYY-MM-DD`. A date-time is a wall-clock value,
8
+ * `YYYY-MM-DDTHH:mm`, as `<input type="datetime-local">` holds it: no zone, no offset. Which
9
+ * zone that wall clock is in is the product's to know and to show.
10
+ */
11
+ type DatePart = 'day' | 'month' | 'year';
12
+ /** Whether `YYYY-MM-DD` names a real calendar day (2026-02-30 does not). */
13
+ declare function isValidDateKey(value: string): boolean;
14
+ /**
15
+ * The order in which `locale` writes a numeric date: `['day', 'month', 'year']` for de or
16
+ * en-GB, `['month', 'day', 'year']` for en-US, `['year', 'month', 'day']` for sv or ja.
17
+ */
18
+ declare function dateOrder(locale?: string | readonly string[]): DatePart[];
19
+ /** A hint for the field, e.g. `DD.MM.YYYY`, `MM/DD/YYYY`, `YYYY-MM-DD`, in the locale's own shape. */
20
+ declare function datePattern(locale?: string | readonly string[]): string;
21
+ /** A numeric date in the locale's shape: 22.11.2026, 11/22/2026, 2026-11-22. */
22
+ declare function formatDateKey(value: string, locale?: string | readonly string[]): string;
23
+ /**
24
+ * Read a typed date. ISO `YYYY-MM-DD` is always accepted; otherwise three numbers separated by
25
+ * `.`, `/`, `-` or spaces are read in the locale's order (so `3.4.2026` is 3 April in German and
26
+ * `3/4/2026` is 4 March in en-US). A two-digit year means 2000–2099. Returns the ISO date, or
27
+ * `null` when the text is not a real date.
28
+ */
29
+ declare function parseDateInput(text: string, locale?: string | readonly string[]): string | null;
30
+ /** Split `YYYY-MM-DDTHH:mm` (seconds are dropped) into its date and time. */
31
+ declare function splitDateTime(value: string | null | undefined): {
32
+ date: string | null;
33
+ time: string;
34
+ };
35
+ /** Join a date and a time into `YYYY-MM-DDTHH:mm`; without a date there is no value. */
36
+ declare function joinDateTime(date: string | null, time: string, fallbackTime?: string): string | null;
37
+ /** The short name of `timeZone` on that day ("CEST", "GMT+2"), for the label beside a time. */
38
+ declare function timeZoneName(timeZone: string, date: string | null, locale?: string | readonly string[]): string;
39
+
40
+ export { type DatePart, dateOrder, datePattern, formatDateKey, isValidDateKey, joinDateTime, parseDateInput, splitDateTime, timeZoneName };
@@ -0,0 +1,79 @@
1
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
2
+ function isValidDateKey(value) {
3
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
4
+ if (!match) return false;
5
+ const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
6
+ if (month < 1 || month > 12 || day < 1) return false;
7
+ return day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
8
+ }
9
+ function dateOrder(locale) {
10
+ try {
11
+ const parts = new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", timeZone: "UTC" }).formatToParts(new Date(Date.UTC(2026, 10, 22)));
12
+ const order = parts.map((part) => part.type).filter((type) => type === "day" || type === "month" || type === "year");
13
+ if (order.length === 3 && new Set(order).size === 3) return order;
14
+ } catch {
15
+ }
16
+ return ["year", "month", "day"];
17
+ }
18
+ function datePattern(locale) {
19
+ const letters = { day: "DD", month: "MM", year: "YYYY" };
20
+ try {
21
+ return new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", timeZone: "UTC" }).formatToParts(new Date(Date.UTC(2026, 10, 22))).map((part) => part.type === "day" || part.type === "month" || part.type === "year" ? letters[part.type] : part.value).join("");
22
+ } catch {
23
+ return "YYYY-MM-DD";
24
+ }
25
+ }
26
+ function formatDateKey(value, locale) {
27
+ if (!isValidDateKey(value)) return value;
28
+ return new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${value}T00:00:00Z`));
29
+ }
30
+ function parseDateInput(text, locale) {
31
+ const trimmed = text.trim();
32
+ if (!trimmed) return null;
33
+ if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(trimmed)) {
34
+ const [year2, month, day] = trimmed.split("-").map(Number);
35
+ const iso2 = `${pad(year2, 4)}-${pad(month)}-${pad(day)}`;
36
+ return isValidDateKey(iso2) ? iso2 : null;
37
+ }
38
+ const numbers = trimmed.split(/[.\/\-\s]+/).filter(Boolean);
39
+ if (numbers.length !== 3 || numbers.some((n) => !/^\d{1,4}$/.test(n))) return null;
40
+ const order = dateOrder(locale);
41
+ const read = {};
42
+ order.forEach((part, index) => {
43
+ read[part] = Number(numbers[index]);
44
+ });
45
+ let year = read.year;
46
+ const yearText = numbers[order.indexOf("year")];
47
+ if (yearText.length <= 2) year += 2e3;
48
+ else if (yearText.length !== 4) return null;
49
+ const iso = `${pad(year, 4)}-${pad(read.month)}-${pad(read.day)}`;
50
+ return isValidDateKey(iso) ? iso : null;
51
+ }
52
+ function splitDateTime(value) {
53
+ const match = value ? /^(\d{4}-\d{2}-\d{2})(?:T(\d{2}:\d{2}))?/.exec(value) : null;
54
+ if (!match || !isValidDateKey(match[1])) return { date: null, time: "" };
55
+ return { date: match[1], time: match[2] ?? "" };
56
+ }
57
+ function joinDateTime(date, time, fallbackTime = "00:00") {
58
+ if (!date) return null;
59
+ return `${date}T${/^\d{2}:\d{2}/.test(time) ? time.slice(0, 5) : fallbackTime}`;
60
+ }
61
+ function timeZoneName(timeZone, date, locale) {
62
+ try {
63
+ const at = date && isValidDateKey(date) ? /* @__PURE__ */ new Date(`${date}T12:00:00Z`) : /* @__PURE__ */ new Date();
64
+ const part = new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName");
65
+ return part?.value ?? timeZone;
66
+ } catch {
67
+ return timeZone;
68
+ }
69
+ }
70
+ export {
71
+ dateOrder,
72
+ datePattern,
73
+ formatDateKey,
74
+ isValidDateKey,
75
+ joinDateTime,
76
+ parseDateInput,
77
+ splitDateTime,
78
+ timeZoneName
79
+ };