@dreamtree-org/twreact-ui 1.3.9 → 1.3.11

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 (128) hide show
  1. package/README.md +32 -0
  2. package/ai-skills/dreamtree-ui.md +12 -7
  3. package/dist/ai-schema.json +156 -29
  4. package/dist/charts/index.js +195 -66
  5. package/dist/components/blocks/AppShell.d.ts.map +1 -1
  6. package/dist/components/charts/CartesianChart.d.ts.map +1 -1
  7. package/dist/components/charts/ChartFrame.d.ts.map +1 -1
  8. package/dist/components/charts/useChartLayout.d.ts +25 -0
  9. package/dist/components/charts/useChartLayout.d.ts.map +1 -0
  10. package/dist/components/core/Accordion.d.ts.map +1 -1
  11. package/dist/components/core/Button.d.ts.map +1 -1
  12. package/dist/components/core/Checkbox.d.ts.map +1 -1
  13. package/dist/components/core/ColorPicker.d.ts.map +1 -1
  14. package/dist/components/core/DatePicker.d.ts.map +1 -1
  15. package/dist/components/core/Form.d.ts.map +1 -1
  16. package/dist/components/core/Input.d.ts.map +1 -1
  17. package/dist/components/core/Loader.d.ts.map +1 -1
  18. package/dist/components/core/LocationPicker.d.ts.map +1 -1
  19. package/dist/components/core/PriceRangePicker.d.ts +0 -29
  20. package/dist/components/core/PriceRangePicker.d.ts.map +1 -1
  21. package/dist/components/core/ProgressBar.d.ts.map +1 -1
  22. package/dist/components/core/Radio.d.ts.map +1 -1
  23. package/dist/components/core/Rate.d.ts.map +1 -1
  24. package/dist/components/core/RoundedTag.d.ts.map +1 -1
  25. package/dist/components/core/Skeleton.d.ts.map +1 -1
  26. package/dist/components/core/Slider.d.ts.map +1 -1
  27. package/dist/components/core/SpeechToText.d.ts.map +1 -1
  28. package/dist/components/core/Switch.d.ts.map +1 -1
  29. package/dist/components/core/Table/ColumnMenu.d.ts.map +1 -1
  30. package/dist/components/core/Table/Pagination.d.ts.map +1 -1
  31. package/dist/components/core/Table/TableRow.d.ts.map +1 -1
  32. package/dist/components/core/Table/helpers.d.ts +2 -0
  33. package/dist/components/core/Table/helpers.d.ts.map +1 -1
  34. package/dist/components/core/Tabs.d.ts +2 -5
  35. package/dist/components/core/Tabs.d.ts.map +1 -1
  36. package/dist/components/core/TextToSpeech.d.ts +2 -7
  37. package/dist/components/core/TextToSpeech.d.ts.map +1 -1
  38. package/dist/components/core/Textarea.d.ts.map +1 -1
  39. package/dist/components/core/Tooltip.d.ts.map +1 -1
  40. package/dist/components/feedback/Alert.d.ts.map +1 -1
  41. package/dist/components/feedback/Dialog.d.ts.map +1 -1
  42. package/dist/components/feedback/Toast.d.ts.map +1 -1
  43. package/dist/components/navigation/Breadcrumbs.d.ts.map +1 -1
  44. package/dist/components/navigation/FootNav.d.ts.map +1 -1
  45. package/dist/components/navigation/Sidebar.d.ts.map +1 -1
  46. package/dist/components/utility/Avatar.d.ts +8 -0
  47. package/dist/components/utility/Avatar.d.ts.map +1 -1
  48. package/dist/components/utility/AvatarGroup.d.ts +4 -0
  49. package/dist/components/utility/AvatarGroup.d.ts.map +1 -0
  50. package/dist/components/utility/Badge.d.ts.map +1 -1
  51. package/dist/components/utility/Card.d.ts.map +1 -1
  52. package/dist/components/utility/Carousel.d.ts.map +1 -1
  53. package/dist/components/utility/FileUpload.d.ts.map +1 -1
  54. package/dist/components/utility/Pagination.d.ts.map +1 -1
  55. package/dist/components/utility/Sortable.d.ts.map +1 -1
  56. package/dist/components/utility/Stepper.d.ts.map +1 -1
  57. package/dist/esm/components/blocks/AppShell.js +24 -18
  58. package/dist/esm/components/blocks/EmptyState.js +1 -1
  59. package/dist/esm/components/blocks/ErrorState.js +1 -1
  60. package/dist/esm/components/blocks/LoadingState.js +4 -4
  61. package/dist/esm/components/blocks/StatCard.js +6 -6
  62. package/dist/esm/components/blocks/StatusPill.js +5 -8
  63. package/dist/esm/components/charts/CartesianChart.js +85 -62
  64. package/dist/esm/components/charts/ChartFrame.js +12 -5
  65. package/dist/esm/components/charts/useChartLayout.js +105 -0
  66. package/dist/esm/components/core/Accordion.js +40 -9
  67. package/dist/esm/components/core/Button.js +49 -25
  68. package/dist/esm/components/core/Checkbox.js +71 -45
  69. package/dist/esm/components/core/ColorPicker.js +49 -48
  70. package/dist/esm/components/core/DatePicker.js +97 -58
  71. package/dist/esm/components/core/Form.js +35 -8
  72. package/dist/esm/components/core/Input.js +89 -20
  73. package/dist/esm/components/core/Loader.js +30 -44
  74. package/dist/esm/components/core/LocationPicker.js +82 -16
  75. package/dist/esm/components/core/PriceRangePicker.js +205 -218
  76. package/dist/esm/components/core/ProgressBar.js +8 -12
  77. package/dist/esm/components/core/Radio.js +43 -15
  78. package/dist/esm/components/core/Rate.js +109 -50
  79. package/dist/esm/components/core/RoundedTag.js +27 -10
  80. package/dist/esm/components/core/Skeleton.js +13 -14
  81. package/dist/esm/components/core/Slider.js +24 -6
  82. package/dist/esm/components/core/SpeechToText.js +68 -16
  83. package/dist/esm/components/core/Switch.js +88 -16
  84. package/dist/esm/components/core/Table/ColumnMenu.js +2 -4
  85. package/dist/esm/components/core/Table/Pagination.js +15 -18
  86. package/dist/esm/components/core/Table/TableRow.js +2 -2
  87. package/dist/esm/components/core/Table/helpers.js +33 -1
  88. package/dist/esm/components/core/Tabs.js +185 -37
  89. package/dist/esm/components/core/TextToSpeech.js +121 -28
  90. package/dist/esm/components/core/Textarea.js +10 -2
  91. package/dist/esm/components/core/Tooltip.js +100 -28
  92. package/dist/esm/components/feedback/Alert.js +21 -11
  93. package/dist/esm/components/feedback/Dialog.js +44 -100
  94. package/dist/esm/components/feedback/Toast.js +24 -5
  95. package/dist/esm/components/navigation/Breadcrumbs.js +97 -35
  96. package/dist/esm/components/navigation/FootNav.js +102 -13
  97. package/dist/esm/components/navigation/Sidebar.js +13 -61
  98. package/dist/esm/components/utility/Avatar.js +37 -22
  99. package/dist/esm/components/utility/AvatarGroup.js +56 -0
  100. package/dist/esm/components/utility/Badge.js +43 -23
  101. package/dist/esm/components/utility/Card.js +32 -8
  102. package/dist/esm/components/utility/Carousel.js +165 -38
  103. package/dist/esm/components/utility/FileUpload.js +106 -31
  104. package/dist/esm/components/utility/Pagination.js +33 -15
  105. package/dist/esm/components/utility/Sortable.js +6 -2
  106. package/dist/esm/components/utility/Stepper.js +110 -47
  107. package/dist/esm/hooks/useFocusTrap.js +10 -2
  108. package/dist/esm/hooks/useSliderValues.js +8 -2
  109. package/dist/esm/hooks/useSortable.js +107 -5
  110. package/dist/esm/index.js +1 -0
  111. package/dist/esm/utils/touchTarget.js +4 -1
  112. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  113. package/dist/hooks/useSliderValues.d.ts +6 -2
  114. package/dist/hooks/useSliderValues.d.ts.map +1 -1
  115. package/dist/hooks/useSortable.d.ts +11 -1
  116. package/dist/hooks/useSortable.d.ts.map +1 -1
  117. package/dist/index.css +1 -1
  118. package/dist/index.d.ts +3 -2
  119. package/dist/index.esm.css +1 -1
  120. package/dist/index.esm.js +3734 -2174
  121. package/dist/index.js +3732 -2171
  122. package/dist/types/charts.d.ts +3 -1
  123. package/dist/types/charts.d.ts.map +1 -1
  124. package/dist/types/index.d.ts +64 -9
  125. package/dist/types/index.d.ts.map +1 -1
  126. package/doc/README.md +1 -0
  127. package/mcp/catalog.snapshot.json +350 -121
  128. package/package.json +1 -1
@@ -3,9 +3,10 @@ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _
3
3
  import { jsxs, jsx } from 'react/jsx-runtime';
4
4
  import React, { useId, useMemo, useState, useRef, useImperativeHandle, useEffect, useLayoutEffect } from 'react';
5
5
  import ReactDOM from 'react-dom';
6
- import { parseISO, startOfMonth, startOfWeek, endOfMonth, endOfWeek, eachDayOfInterval, startOfDay, subMonths, format, addMonths, isSameDay, isSameMonth, isToday, isBefore, isAfter } from 'date-fns';
6
+ import { parseISO, startOfMonth, startOfWeek, endOfMonth, endOfWeek, eachDayOfInterval, startOfDay, isSameDay, subMonths, format, addMonths, isSameMonth, isToday, isBefore, isAfter, addDays } from 'date-fns';
7
7
  import { cn } from '../../utils/cn.js';
8
- import { ChevronLeft, ChevronRight, X, Calendar } from 'lucide-react';
8
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
9
+ import { ChevronLeft, ChevronRight, Calendar, X } from 'lucide-react';
9
10
 
10
11
  var _excluded = ["value", "onChange", "placeholder", "label", "error", "helperText", "disabled", "required", "minDate", "maxDate", "className", "weekStartsOn", "portal", "displayFormat", "locale", "showClear", "closeOnSelect"];
11
12
  var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
@@ -64,19 +65,27 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
64
65
  setCurrentMonth = _useState4[1];
65
66
  var datePickerRef = useRef(null);
66
67
  var popupRef = useRef(null);
68
+ var triggerRef = useRef(null);
67
69
  var gridButtonRefs = useRef([]);
68
- var _useState5 = useState({}),
70
+ // Roving tabindex: the day the user moved to with the keyboard (or focused).
71
+ var _useState5 = useState(null),
69
72
  _useState6 = _slicedToArray(_useState5, 2),
70
- popupStyle = _useState6[0],
71
- setPopupStyle = _useState6[1];
73
+ focusedDate = _useState6[0],
74
+ setFocusedDate = _useState6[1];
75
+ // Set when a keyboard action / open should move DOM focus to the active day.
76
+ var pendingFocusRef = useRef(false);
77
+ var _useState7 = useState({}),
78
+ _useState8 = _slicedToArray(_useState7, 2),
79
+ popupStyle = _useState8[0],
80
+ setPopupStyle = _useState8[1];
72
81
  useImperativeHandle(forwardedRef, function () {
73
82
  return datePickerRef.current;
74
83
  }, []);
75
84
  // Keep currentMonth in sync with external value changes
76
- var _useState7 = useState(selected),
77
- _useState8 = _slicedToArray(_useState7, 2),
78
- prevSelected = _useState8[0],
79
- setPrevSelected = _useState8[1];
85
+ var _useState9 = useState(selected),
86
+ _useState0 = _slicedToArray(_useState9, 2),
87
+ prevSelected = _useState0[0],
88
+ setPrevSelected = _useState0[1];
80
89
  if (selected !== prevSelected) {
81
90
  setPrevSelected(selected);
82
91
  if (selected) setCurrentMonth(selected);
@@ -98,7 +107,10 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
98
107
  var handleKey = function handleKey(e) {
99
108
  if (!isOpen) return;
100
109
  if (e.key === "Escape") {
110
+ var _triggerRef$current;
111
+ var hadFocusInside = popupRef.current && popupRef.current.contains(document.activeElement);
101
112
  setIsOpen(false);
113
+ if (hadFocusInside) (_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 || _triggerRef$current.focus();
102
114
  }
103
115
  };
104
116
  document.addEventListener("keydown", handleKey);
@@ -139,62 +151,81 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
139
151
  var handleDateSelect = function handleDateSelect(date) {
140
152
  if (isDateDisabled(date)) return;
141
153
  onChange === null || onChange === void 0 || onChange(date);
142
- if (closeOnSelect) setIsOpen(false);
154
+ if (closeOnSelect) {
155
+ var _triggerRef$current2;
156
+ setIsOpen(false);
157
+ // The focused day unmounts with the popup; keep focus on the field.
158
+ (_triggerRef$current2 = triggerRef.current) === null || _triggerRef$current2 === void 0 || _triggerRef$current2.focus();
159
+ }
143
160
  };
144
- // keyboard navigation inside the calendar grid
145
- var focusDayAtIndex = function focusDayAtIndex(index) {
146
- var btn = gridButtonRefs.current[index];
161
+ // Roving tabindex target: exactly one enabled day in the grid is tabbable.
162
+ // Priority: keyboard-focused day ?? selected ?? today (if shown & enabled)
163
+ // ?? first enabled day of the visible month ?? first enabled grid day.
164
+ var isUsable = function isUsable(d) {
165
+ return d && isSameMonth(d, currentMonth) && !isDateDisabled(d);
166
+ };
167
+ var activeDate = null;
168
+ if (isUsable(focusedDate)) activeDate = focusedDate;else if (isUsable(selected)) activeDate = selected;else if (isUsable(new Date())) activeDate = new Date();else activeDate = days.find(function (d) {
169
+ return isUsable(d);
170
+ }) || days.find(function (d) {
171
+ return !isDateDisabled(d);
172
+ }) || null;
173
+ var activeIndex = activeDate ? days.findIndex(function (d) {
174
+ return isSameDay(d, activeDate);
175
+ }) : -1;
176
+ // Move DOM focus to the active day after a keyboard action / open.
177
+ useEffect(function () {
178
+ if (!isOpen || !pendingFocusRef.current) return;
179
+ pendingFocusRef.current = false;
180
+ var btn = gridButtonRefs.current[activeIndex];
147
181
  if (btn && typeof btn.focus === "function") btn.focus();
182
+ });
183
+ var moveFocusTo = function moveFocusTo(date) {
184
+ if (!date || isDateDisabled(date)) return;
185
+ if (!isSameMonth(date, currentMonth)) setCurrentMonth(date);
186
+ setFocusedDate(date);
187
+ pendingFocusRef.current = true;
148
188
  };
149
189
  var handleGridKeyDown = function handleGridKeyDown(e, idx) {
190
+ var day = days[idx];
150
191
  if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", "Enter"].includes(e.key)) {
151
192
  e.preventDefault();
152
193
  }
153
194
  switch (e.key) {
154
195
  case "ArrowLeft":
155
- focusDayAtIndex(Math.max(0, idx - 1));
196
+ moveFocusTo(addDays(day, -1));
156
197
  break;
157
198
  case "ArrowRight":
158
- focusDayAtIndex(Math.min(days.length - 1, idx + 1));
199
+ moveFocusTo(addDays(day, 1));
159
200
  break;
160
201
  case "ArrowUp":
161
- focusDayAtIndex(Math.max(0, idx - 7));
202
+ moveFocusTo(addDays(day, -7));
162
203
  break;
163
204
  case "ArrowDown":
164
- focusDayAtIndex(Math.min(days.length - 1, idx + 7));
205
+ moveFocusTo(addDays(day, 7));
165
206
  break;
166
207
  case "PageUp":
167
- setCurrentMonth(function (m) {
168
- return subMonths(m, 1);
169
- });
208
+ moveFocusTo(subMonths(day, 1));
170
209
  break;
171
210
  case "PageDown":
172
- setCurrentMonth(function (m) {
173
- return addMonths(m, 1);
174
- });
211
+ moveFocusTo(addMonths(day, 1));
175
212
  break;
176
213
  case "Home":
177
- focusDayAtIndex(Math.floor(idx / 7) * 7);
214
+ moveFocusTo(days[Math.floor(idx / 7) * 7]);
178
215
  break;
179
216
  case "End":
180
- focusDayAtIndex(Math.min(days.length - 1, Math.floor(idx / 7) * 7 + 6));
217
+ moveFocusTo(days[Math.min(days.length - 1, Math.floor(idx / 7) * 7 + 6)]);
181
218
  break;
182
219
  case "Enter":
183
- if (!isDateDisabled(days[idx])) handleDateSelect(days[idx]);
220
+ if (!isDateDisabled(day)) handleDateSelect(day);
184
221
  break;
185
222
  }
186
223
  };
187
224
  var openAndFocusSelected = function openAndFocusSelected() {
225
+ setFocusedDate(null);
188
226
  setIsOpen(true);
189
- // focus after opening on next tick
190
- setTimeout(function () {
191
- var idx = selected ? days.findIndex(function (d) {
192
- return isSameDay(d, selected);
193
- }) : days.findIndex(function (d) {
194
- return isToday(d);
195
- });
196
- if (idx >= 0) focusDayAtIndex(idx);
197
- }, 0);
227
+ // the active-day effect moves focus once the grid has rendered
228
+ pendingFocusRef.current = true;
198
229
  };
199
230
  // compute popup position so it's aware of viewport edges (top/bottom/left/right)
200
231
  var computePopupPosition = function computePopupPosition() {
@@ -266,7 +297,7 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
266
297
  className: "mb-4 flex items-center justify-between",
267
298
  children: [jsx("button", {
268
299
  type: "button",
269
- className: "rounded-md p-1 hover:bg-gray-100",
300
+ className: cn("inline-flex items-center justify-center rounded-md p-1 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500", TOUCH_TARGET),
270
301
  onClick: function onClick() {
271
302
  return setCurrentMonth(subMonths(currentMonth, 1));
272
303
  },
@@ -279,7 +310,7 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
279
310
  children: format(currentMonth, "MMMM yyyy")
280
311
  }), jsx("button", {
281
312
  type: "button",
282
- className: "rounded-md p-1 hover:bg-gray-100",
313
+ className: cn("inline-flex items-center justify-center rounded-md p-1 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500", TOUCH_TARGET),
283
314
  onClick: function onClick() {
284
315
  return setCurrentMonth(addMonths(currentMonth, 1));
285
316
  },
@@ -321,12 +352,15 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
321
352
  return gridButtonRefs.current[idx] = el;
322
353
  },
323
354
  role: "gridcell",
324
- tabIndex: isSelected ? 0 : -1,
325
- "aria-selected": isSelected,
355
+ tabIndex: idx === activeIndex ? 0 : -1,
356
+ "aria-selected": Boolean(isSelected),
326
357
  onKeyDown: function onKeyDown(e) {
327
358
  return handleGridKeyDown(e, idx);
328
359
  },
329
- className: cn("h-8 w-8 rounded-md text-sm transition-colors focus:outline-2 focus:outline-offset-1", {
360
+ onFocus: function onFocus() {
361
+ if (!focusedDate || !isSameDay(focusedDate, day)) setFocusedDate(day);
362
+ },
363
+ className: cn("h-8 w-8 rounded-md text-sm transition-colors motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-1", {
330
364
  "text-primary-400": !isCurrentMonth,
331
365
  "bg-primary-600 text-white": isSelected,
332
366
  "bg-primary-100 text-primary-600": isTodayDate && !isSelected,
@@ -382,6 +416,7 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
382
416
  ref: datePickerRef,
383
417
  className: "relative",
384
418
  children: [jsxs("button", _objectSpread2(_objectSpread2({
419
+ ref: triggerRef,
385
420
  type: "button",
386
421
  id: fieldId,
387
422
  "aria-labelledby": label ? "".concat(labelId, " ").concat(valueId) : valueId,
@@ -389,7 +424,9 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
389
424
  "aria-invalid": error ? true : undefined,
390
425
  className: cn("flex w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50", {
391
426
  "border-error-500 focus:ring-error-500": error,
392
- "text-primary-500": !selected
427
+ "text-primary-500": !selected,
428
+ // leave room for the sibling clear button overlaid on the field
429
+ "pr-16": showClear && selected
393
430
  }, className),
394
431
  onClick: function onClick() {
395
432
  return isOpen ? setIsOpen(false) : openAndFocusSelected();
@@ -402,24 +439,26 @@ var DatePicker = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
402
439
  className: "truncate",
403
440
  id: valueId,
404
441
  children: formatDisplayValue()
405
- }), jsxs("div", {
406
- className: "flex items-center gap-2",
407
- children: [showClear && selected && jsx("div", {
408
- type: "button",
409
- onClick: function onClick(e) {
410
- e.stopPropagation();
411
- onChange === null || onChange === void 0 || onChange(null);
412
- },
413
- "aria-label": "Clear date",
414
- className: "p-1",
415
- children: jsx(X, {
416
- className: "h-4 w-4 text-primary-400"
417
- })
418
- }), jsx(Calendar, {
419
- className: "h-4 w-4 text-primary-400"
420
- })]
442
+ }), jsx(Calendar, {
443
+ className: "h-4 w-4 shrink-0 text-primary-400",
444
+ "aria-hidden": "true"
421
445
  })]
422
- })), isOpen && (portal ? /*#__PURE__*/ReactDOM.createPortal(calendarPopup, document.body) : calendarPopup)]
446
+ })), showClear && selected && jsx("button", {
447
+ type: "button",
448
+ "aria-label": "Clear date",
449
+ disabled: disabled,
450
+ onClick: function onClick() {
451
+ var _triggerRef$current3;
452
+ onChange === null || onChange === void 0 || onChange(null);
453
+ // this button unmounts once the value clears
454
+ (_triggerRef$current3 = triggerRef.current) === null || _triggerRef$current3 === void 0 || _triggerRef$current3.focus();
455
+ },
456
+ className: "absolute right-9 top-1/2 inline-flex -translate-y-1/2 items-center justify-center rounded-md p-1 text-primary-400 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50",
457
+ children: jsx(X, {
458
+ className: "h-4 w-4",
459
+ "aria-hidden": "true"
460
+ })
461
+ }), isOpen && (portal ? /*#__PURE__*/ReactDOM.createPortal(calendarPopup, document.body) : calendarPopup)]
423
462
  }), error ? jsx("p", {
424
463
  id: errorId,
425
464
  className: "mt-1 text-sm text-error-500",
@@ -40,6 +40,19 @@ var FieldMessage = function FieldMessage(_ref) {
40
40
  }
41
41
  return null;
42
42
  };
43
+ // Static class map for field groups (DTUI-19). A dynamic `grid-cols-${n}`
44
+ // string is invisible to Tailwind's content scan and gets purged, so every
45
+ // layout is spelled out here. All layouts collapse to one column below `sm`.
46
+ var GRID_COLS = {
47
+ 1: "grid-cols-1",
48
+ 2: "grid-cols-1 sm:grid-cols-2",
49
+ 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
50
+ 4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
51
+ };
52
+ var gridColsClass = function gridColsClass(n) {
53
+ var cols = Math.min(4, Math.max(1, Math.trunc(Number(n)) || 1));
54
+ return GRID_COLS[cols];
55
+ };
43
56
  var describedByFor = function describedByFor(fieldId, error, helperText) {
44
57
  return error ? "".concat(fieldId, "-error") : helperText ? "".concat(fieldId, "-helper") : undefined;
45
58
  };
@@ -72,8 +85,12 @@ var Form = /*#__PURE__*/forwardRef(function Form(_ref2, ref) {
72
85
  _methods$formState = methods.formState,
73
86
  errors = _methods$formState.errors,
74
87
  isSubmitting = _methods$formState.isSubmitting;
75
- var handleFormSubmit = useCallback(function (data) {
76
- onSubmit === null || onSubmit === void 0 || onSubmit(data);
88
+ var handleFormSubmit = useCallback(
89
+ // Return the handler's result so react-hook-form awaits an async
90
+ // onSubmit and `isSubmitting` (the submit button's loading state)
91
+ // reflects the real work (DTUI-19).
92
+ function (data) {
93
+ return onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(data);
77
94
  }, [onSubmit]);
78
95
  var handleReset = useCallback(function () {
79
96
  reset(defaultValues);
@@ -259,7 +276,8 @@ var Form = /*#__PURE__*/forwardRef(function Form(_ref2, ref) {
259
276
  ref = _ref8$field.ref;
260
277
  return jsxs("div", {
261
278
  className: "w-full",
262
- children: [label && jsxs("label", {
279
+ children: [label && jsxs("span", {
280
+ id: "".concat(fieldId, "-label"),
263
281
  className: "block text-sm font-medium mb-2",
264
282
  children: [label, required && jsx("span", {
265
283
  className: "text-error-500 ml-1",
@@ -268,20 +286,29 @@ var Form = /*#__PURE__*/forwardRef(function Form(_ref2, ref) {
268
286
  }), jsx("div", {
269
287
  className: "space-y-2",
270
288
  role: "radiogroup",
271
- "aria-label": typeof label === "string" ? label : undefined,
289
+ "aria-labelledby": label ? "".concat(fieldId, "-label") : undefined,
290
+ "aria-required": required ? "true" : undefined,
272
291
  "aria-describedby": describedBy,
273
- children: options === null || options === void 0 ? void 0 : options.map(function (option) {
292
+ children: options === null || options === void 0 ? void 0 : options.map(function (option, index) {
293
+ // Shared `name` makes the options one native group
294
+ // (arrow keys, single tab stop); id/htmlFor makes each
295
+ // option label clickable and its accessible name.
296
+ var optionId = "".concat(fieldId, "-option-").concat(index);
274
297
  return jsxs("div", {
275
298
  className: "flex items-center",
276
299
  children: [jsx("input", {
277
- ref: ref,
300
+ ref: index === 0 ? ref : undefined,
301
+ id: optionId,
278
302
  type: "radio",
303
+ name: name,
304
+ value: String(option.value),
279
305
  checked: value === option.value,
280
306
  onChange: function onChange() {
281
307
  return _onChange2(option.value);
282
308
  },
283
- className: "text-primary-600 focus:ring-primary-500"
309
+ className: "text-primary-600 focus:ring-primary-500 focus-visible:ring-2"
284
310
  }), jsx("label", {
311
+ htmlFor: optionId,
285
312
  className: "ml-2 text-sm",
286
313
  children: option.label
287
314
  })]
@@ -484,7 +511,7 @@ var Form = /*#__PURE__*/forwardRef(function Form(_ref2, ref) {
484
511
  className: "text-sm text-primary-600",
485
512
  children: group.description
486
513
  }), jsx("div", {
487
- className: cn("grid gap-4", group.gridCols ? "grid-cols-".concat(group.gridCols) : "grid-cols-1"),
514
+ className: cn("grid gap-4", gridColsClass(group.gridCols)),
488
515
  children: group.fields.map(renderField)
489
516
  })]
490
517
  }, group.name);
@@ -1,11 +1,52 @@
1
1
  'use client';
2
2
  import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
- import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
4
4
  import { forwardRef, useId, useRef, useCallback, useState } from 'react';
5
5
  import { Search, EyeOff, Eye, X } from 'lucide-react';
6
6
  import { cn } from '../../utils/cn.js';
7
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
7
8
 
8
- var _excluded = ["id", "className", "type", "label", "placeholder", "error", "success", "helperText", "disabled", "required", "clearable", "icon", "iconPosition", "onClear", "showCount", "maxLength", "readOnly", "onChange", "value", "defaultValue"];
9
+ var _excluded = ["id", "className", "type", "label", "placeholder", "error", "success", "helperText", "disabled", "required", "clearable", "icon", "iconPosition", "onClear", "showCount", "maxLength", "readOnly", "onChange", "value", "defaultValue", "size", "floatingLabel", "otp"];
10
+ // Field heights. `md` keeps its 40px desktop look and grows to the 44px touch
11
+ // target on coarse pointers (same rule as Select); `lg` is 44px everywhere.
12
+ var SIZE_CLASSES = {
13
+ sm: "h-8 text-xs",
14
+ md: "h-10 text-sm leading-10 [@media(pointer:coarse)]:h-11",
15
+ lg: "h-11 text-base"
16
+ };
17
+ // Taller fields for `floatingLabel`, with top padding so the value sits below
18
+ // the floated label.
19
+ var FLOATING_SIZE_CLASSES = {
20
+ sm: "h-10 pt-3 text-sm",
21
+ md: "h-12 pt-4 text-sm",
22
+ lg: "h-14 pt-5 text-base"
23
+ };
24
+ // Mobile keyboard / autofill hints for native types. Applied only when the
25
+ // caller did not pass the attribute themselves.
26
+ var TYPE_HINTS = {
27
+ tel: {
28
+ inputMode: "tel",
29
+ autoComplete: "tel"
30
+ },
31
+ email: {
32
+ inputMode: "email",
33
+ autoComplete: "email"
34
+ },
35
+ url: {
36
+ inputMode: "url",
37
+ autoComplete: "url"
38
+ },
39
+ search: {
40
+ inputMode: "search",
41
+ enterKeyHint: "search"
42
+ }
43
+ };
44
+ var OTP_HINTS = {
45
+ inputMode: "numeric",
46
+ autoComplete: "one-time-code"
47
+ };
48
+ // Password toggle / clear: real buttons that reach 44×44 on coarse pointers.
49
+ var ADORNMENT_BUTTON = cn("absolute right-1 top-1/2 -translate-y-1/2 inline-flex h-8 w-8 items-center justify-center rounded-md", "text-primary-500 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300", "transition-colors duration-fast ease-standard motion-reduce:transition-none", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500", "disabled:cursor-not-allowed disabled:opacity-50", TOUCH_TARGET);
9
50
  var Input = /*#__PURE__*/forwardRef(function (props, forwardedRef) {
10
51
  var id = props.id,
11
52
  className = props.className,
@@ -34,7 +75,25 @@ var Input = /*#__PURE__*/forwardRef(function (props, forwardedRef) {
34
75
  onChange = props.onChange,
35
76
  value = props.value,
36
77
  defaultValue = props.defaultValue,
78
+ _props$size = props.size,
79
+ sizeProp = _props$size === void 0 ? "md" : _props$size,
80
+ _props$floatingLabel = props.floatingLabel,
81
+ floatingLabel = _props$floatingLabel === void 0 ? false : _props$floatingLabel,
82
+ _props$otp = props.otp,
83
+ otp = _props$otp === void 0 ? false : _props$otp,
37
84
  rest = _objectWithoutProperties(props, _excluded);
85
+ // `size` is the field size (sm|md|lg). A numeric value is the native
86
+ // `<input size>` attribute and is passed through unchanged.
87
+ var size = SIZE_CLASSES[sizeProp] ? sizeProp : "md";
88
+ var nativeSize = SIZE_CLASSES[sizeProp] ? undefined : sizeProp;
89
+ var hints = otp ? OTP_HINTS : TYPE_HINTS[type];
90
+ var hintProps = {};
91
+ if (hints) {
92
+ Object.keys(hints).forEach(function (key) {
93
+ if (!(key in rest)) hintProps[key] = hints[key];
94
+ });
95
+ }
96
+ var isFloating = Boolean(floatingLabel && label);
38
97
  var internalId = useId();
39
98
  var inputId = id || "input-".concat(internalId);
40
99
  var isSearch = type === "search";
@@ -88,19 +147,22 @@ var Input = /*#__PURE__*/forwardRef(function (props, forwardedRef) {
88
147
  var leftHasIcon = Boolean(icon && iconPosition === "left") || isSearch;
89
148
  var rightHasIcon = Boolean(icon && iconPosition === "right") || isPassword || clearable && currentLength > 0;
90
149
  var paddingClass = cn(leftHasIcon ? "pl-10" : "pl-3", rightHasIcon ? "pr-12" : "pr-3");
91
- var baseClasses = cn("w-full rounded-md border bg-white text-sm placeholder-gray-400 disabled:opacity-50", "h-10 leading-10",
92
- // consistent height
93
- paddingClass, error ? "border-red-500 focus:ring-red-500" : "border-gray-300", success ? "border-green-500 focus:ring-green-500" : "", className);
150
+ var baseClasses = cn("w-full rounded-md border bg-surface text-foreground placeholder-gray-400 dark:placeholder-gray-500", "transition-colors duration-fast ease-standard motion-reduce:transition-none", "disabled:cursor-not-allowed disabled:opacity-50", isFloating ? FLOATING_SIZE_CLASSES[size] : SIZE_CLASSES[size],
151
+ // The floated label replaces the placeholder until the field is focused.
152
+ isFloating && "peer placeholder-transparent focus:placeholder-gray-400 dark:focus:placeholder-gray-500", paddingClass, "focus:outline-none focus-visible:ring-2", error ? "border-error-500 focus-visible:ring-error-500 dark:border-error-400" : success ? "border-success-500 focus-visible:ring-success-500 dark:border-success-400" : "border-gray-300 focus-visible:ring-primary-500 dark:border-gray-600", className);
153
+ var labelContent = jsxs(Fragment, {
154
+ children: [label, required && jsx("span", {
155
+ className: "text-error-500 ml-1",
156
+ children: "*"
157
+ })]
158
+ });
94
159
  var inputType = isPassword && showPassword ? "text" : type;
95
160
  return jsxs("div", {
96
161
  className: "w-full",
97
- children: [label && jsxs("label", {
162
+ children: [label && !isFloating && jsx("label", {
98
163
  htmlFor: inputId,
99
- className: "block text-sm font-medium mb-1",
100
- children: [label, required && jsx("span", {
101
- className: "text-error-500 ml-1",
102
- children: "*"
103
- })]
164
+ className: "block text-sm font-medium mb-1 text-foreground",
165
+ children: labelContent
104
166
  }), jsxs("div", {
105
167
  className: "relative",
106
168
  children: [leftHasIcon && jsx("div", {
@@ -111,12 +173,13 @@ var Input = /*#__PURE__*/forwardRef(function (props, forwardedRef) {
111
173
  className: "text-primary-400",
112
174
  children: icon
113
175
  })
114
- }), jsx("input", _objectSpread2({
176
+ }), jsx("input", _objectSpread2(_objectSpread2({
115
177
  id: inputId,
116
178
  ref: setRefs,
117
179
  type: inputType,
118
180
  className: baseClasses,
119
- placeholder: placeholder,
181
+ // :placeholder-shown drives the floating label, so it needs a placeholder.
182
+ placeholder: isFloating ? placeholder || " " : placeholder,
120
183
  disabled: disabled,
121
184
  readOnly: readOnly,
122
185
  "aria-invalid": !!error,
@@ -124,13 +187,19 @@ var Input = /*#__PURE__*/forwardRef(function (props, forwardedRef) {
124
187
  maxLength: maxLength,
125
188
  onChange: handleChange,
126
189
  value: value,
127
- defaultValue: defaultValue
128
- }, rest)), icon && iconPosition === "right" && !isPassword && !(clearable && currentLength > 0) && jsx("div", {
190
+ defaultValue: defaultValue,
191
+ size: nativeSize
192
+ }, hintProps), rest)), isFloating && jsx("label", {
193
+ htmlFor: inputId,
194
+ "data-floating-label": "",
195
+ className: cn("pointer-events-none absolute top-1 origin-left text-xs font-medium text-muted", leftHasIcon ? "left-10" : "left-3", "transition-[top,transform,font-size,color] duration-fast ease-standard motion-reduce:transition-none", "peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:text-sm", "peer-focus:top-1 peer-focus:translate-y-0 peer-focus:text-xs peer-focus:text-primary-600 dark:peer-focus:text-primary-400"),
196
+ children: labelContent
197
+ }), icon && iconPosition === "right" && !isPassword && !(clearable && currentLength > 0) && jsx("div", {
129
198
  className: "absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none text-primary-400",
130
199
  children: icon
131
200
  }), isPassword && jsx("button", {
132
201
  type: "button",
133
- className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-primary-500",
202
+ className: ADORNMENT_BUTTON,
134
203
  onClick: function onClick() {
135
204
  return setShowPassword(function (s) {
136
205
  return !s;
@@ -145,7 +214,7 @@ var Input = /*#__PURE__*/forwardRef(function (props, forwardedRef) {
145
214
  })
146
215
  }), clearable && currentLength > 0 && !isPassword && jsx("button", {
147
216
  type: "button",
148
- className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-primary-500",
217
+ className: ADORNMENT_BUTTON,
149
218
  onClick: handleClear,
150
219
  "aria-label": "Clear input",
151
220
  disabled: disabled,
@@ -155,12 +224,12 @@ var Input = /*#__PURE__*/forwardRef(function (props, forwardedRef) {
155
224
  })]
156
225
  }), error && jsx("p", {
157
226
  id: "".concat(inputId, "-error"),
158
- className: "mt-1 text-sm text-error-600",
227
+ className: "mt-1 text-sm text-error-600 dark:text-error-400",
159
228
  "aria-live": "polite",
160
229
  children: error
161
230
  }), !error && success && jsx("p", {
162
231
  id: "".concat(inputId, "-success"),
163
- className: "mt-1 text-sm text-success-600",
232
+ className: "mt-1 text-sm text-success-600 dark:text-success-400",
164
233
  "aria-live": "polite",
165
234
  children: success
166
235
  }), !error && !success && helperText && jsx("p", {
@@ -168,7 +237,7 @@ var Input = /*#__PURE__*/forwardRef(function (props, forwardedRef) {
168
237
  className: "mt-1 text-sm text-muted",
169
238
  children: helperText
170
239
  }), showCount && maxLength && jsxs("div", {
171
- className: "mt-1 text-right text-xs text-primary-500",
240
+ className: "mt-1 text-right text-xs text-primary-500 dark:text-primary-400",
172
241
  "aria-hidden": true,
173
242
  children: [currentLength, "/", maxLength]
174
243
  })]