@exsto/ds 0.0.1-rc.19 → 0.0.1-rc.21

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 (45) hide show
  1. package/README.md +13 -1
  2. package/dist/source-hash.txt +1 -1
  3. package/dist/styles.css +21 -139
  4. package/dist/ui/src/components/edyn/date-picker/date-picker.d.ts +13 -4
  5. package/dist/ui/src/components/edyn/date-picker/date-picker.js +36 -4
  6. package/dist/ui/src/components/edyn/date-picker/date-picker.js.map +1 -1
  7. package/dist/ui/src/components/edyn/file-input/file-input.js +1 -1
  8. package/dist/ui/src/components/edyn/file-input/file-input.js.map +1 -1
  9. package/dist/ui/src/components/edyn/multi-select/multi-select.js +1 -1
  10. package/dist/ui/src/components/edyn/multi-select/multi-select.js.map +1 -1
  11. package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.js +1 -1
  12. package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.js.map +1 -1
  13. package/dist/ui/src/components/ui/badge/badge.js +5 -5
  14. package/dist/ui/src/components/ui/badge/badge.js.map +1 -1
  15. package/dist/ui/src/components/ui/bubble/bubble.js +1 -1
  16. package/dist/ui/src/components/ui/bubble/bubble.js.map +1 -1
  17. package/dist/ui/src/components/ui/button/button.js +2 -2
  18. package/dist/ui/src/components/ui/button/button.js.map +1 -1
  19. package/dist/ui/src/components/ui/checkbox/checkbox.js +1 -1
  20. package/dist/ui/src/components/ui/checkbox/checkbox.js.map +1 -1
  21. package/dist/ui/src/components/ui/item/item.js +1 -1
  22. package/dist/ui/src/components/ui/item/item.js.map +1 -1
  23. package/dist/ui/src/components/ui/native-select/native-select.js +1 -1
  24. package/dist/ui/src/components/ui/native-select/native-select.js.map +1 -1
  25. package/dist/ui/src/components/ui/navigation-menu/navigation-menu.js +2 -2
  26. package/dist/ui/src/components/ui/navigation-menu/navigation-menu.js.map +1 -1
  27. package/dist/ui/src/components/ui/questionnaire/questionnaire.js +1 -1
  28. package/dist/ui/src/components/ui/questionnaire/questionnaire.js.map +1 -1
  29. package/dist/ui/src/components/ui/radio-group/radio-group.js +1 -1
  30. package/dist/ui/src/components/ui/radio-group/radio-group.js.map +1 -1
  31. package/dist/ui/src/components/ui/scroll-area/scroll-area.js +15 -2
  32. package/dist/ui/src/components/ui/scroll-area/scroll-area.js.map +1 -1
  33. package/dist/ui/src/components/ui/select/select.js +1 -1
  34. package/dist/ui/src/components/ui/select/select.js.map +1 -1
  35. package/dist/ui/src/components/ui/slider/slider.js +1 -1
  36. package/dist/ui/src/components/ui/slider/slider.js.map +1 -1
  37. package/dist/ui/src/components/ui/switch/switch.js +1 -1
  38. package/dist/ui/src/components/ui/switch/switch.js.map +1 -1
  39. package/dist/ui/src/components/ui/tabs/tabs.js +1 -1
  40. package/dist/ui/src/components/ui/tabs/tabs.js.map +1 -1
  41. package/dist/ui/src/components/ui/toast/toast.js +1 -1
  42. package/dist/ui/src/components/ui/toast/toast.js.map +1 -1
  43. package/dist/ui/src/components/ui/toggle/toggle.js +1 -1
  44. package/dist/ui/src/components/ui/toggle/toggle.js.map +1 -1
  45. package/package.json +1 -1
package/README.md CHANGED
@@ -129,7 +129,7 @@ import { DatePicker } from '@exsto/ds/date-picker';
129
129
  <DatePicker aria-label="Go-live date" value={day} onValueChange={setDay} min="2026-01-01" locale="en-GB" />;
130
130
  ```
131
131
 
132
- `onValueChange(day, state)` gets `undefined` as the day when the field is emptied or its text is not a usable day. The second argument says which: `{ kind: 'day' }`, `{ kind: 'empty' }`, or `{ kind: 'refused'; text; problem }`, where `text` is what was typed and `problem` the message the picker shows under the field. A caller that ignores the second argument keeps working. A required field uses it to show its own message only when the field is empty, so the person never reads two:
132
+ `onValueChange(day, state)` gets `undefined` as the day when the field is emptied or its text is not a day. The second argument says which: `{ kind: 'day' }`, `{ kind: 'empty' }`, `{ kind: 'refused'; text; problem }` for text that is not a day, where `text` is what was typed and `problem` the message the picker shows under the field, or `{ kind: 'out-of-range'; day; text; problem }` for a real day that `min`, `max` or `disabledDates` refuse. **For an out-of-range day the day IS reported** (as the first argument too), the typed text stays in the field and the field is marked `aria-invalid`, so your form's own rule can judge it and show its message; check `state.kind` before treating the day as a usable one. A caller that ignores the second argument keeps working. A required field uses it to show its own message only when the field is empty, so the person never reads two:
133
133
 
134
134
  ```tsx
135
135
  import { Field, FieldError, FieldLabel } from '@exsto/ds';
@@ -190,6 +190,18 @@ import { cn, POPUP_FRAME_CLASS, POPUP_LIST_HEIGHT_CLASS, POPUP_LIST_WIDTH_CLASS,
190
190
  - `POPUP_GROUP_LABEL_CLASS` is a group's heading. `ComboboxGroup`, `ComboboxLabel`, `ComboboxCollection` and `ComboboxSeparator` give a Combobox its groups, drawn the same way Select draws its own.
191
191
  - `POPUP_FRAME_CLASS` is the frame (a 10px radius and a 1px ring, no border). `POPUP_LIST_WIDTH_CLASS` is its width: at least the control and 8rem, as wide as the longest row, 28rem at most. `POPUP_LIST_HEIGHT_CLASS` caps the scrolling part: eight rows, or as many whole rows as the room allows, so a list never ends on half a row. Put it on a `ScrollArea` inside a popup that is a flex column with `max-h-(--available-height)`; the class carries the 4px of air above and below the rows as a margin, so the list inside takes `px-1` and no vertical padding (padding there scrolls with the rows and leaves a strip of the next row at the foot). `POPUP_LAYER_CLASS` (`z-60`) goes on the popup's positioner, so it is drawn above the Sheet, Dialog or Drawer (`z-50`) it is opened from.
192
192
 
193
+ ## Changes in 0.0.1-rc.21
194
+
195
+ - **Migration: check `state.kind` where you store the day a DatePicker reports.** A typed day outside `min`, `max` or `disabledDates` used to arrive as `onValueChange(undefined, { kind: 'refused', ... })`; it now arrives as `onValueChange(thatDay, { kind: 'out-of-range', day, text, problem })`. `day ?? ''` now stores the out-of-range day, which is what lets your form's rule see it and show its message; guard with `state.kind === 'day'` where an out-of-range day must not be kept. Text that is not a day is still `refused` with `undefined`.
196
+ - **The DatePicker keeps the text a person typed** and marks the field invalid (`aria-invalid`) when the day is out of range; the calendar still greys out the days outside the limits.
197
+ - **One message for a field.** Inside a DS `Field` that renders a `FieldError`, the FieldError is the only message: the picker shows none of its own and `aria-describedby` points at the FieldError. A bare picker keeps its own message.
198
+
199
+ ## Changes in 0.0.1-rc.20 (never published on its own; part of rc.21)
200
+
201
+ - **One focus style for every control.** A focused control shows the 2px outline in the focus colour and nothing else: no border colour change, no ring. That now holds for Button (and its destructive variant), Select, NativeSelect, MultiSelect, Toggle, Tabs, Switch, Checkbox, RadioGroup, Slider, Badge, Item, Bubble, Questionnaire, Toast, the NavigationMenu triggers, an open Select, MultiSelect and Combobox, the FileInput drop area and the RichTextEditor frame, as it did for Input and Textarea in rc.19. An app that added its own focus border or ring to any of them can drop it.
202
+ - **The outline of a bordered control sits on its border** (offset -1px, or -2px inside a clipping container): a focused field is one 2px line that follows its radius, not a ring standing off a grey border. A control with no border keeps the 2px gap.
203
+ - **A block exactly as tall as its ScrollArea no longer scrolls for the horizontal bar**; the bar overlays its last 8px. A taller block keeps the bar's room.
204
+
193
205
  ## Changes in 0.0.1-rc.19
194
206
 
195
207
  Sizes and behaviours that moved. An app that patched one of these locally can drop its patch.
@@ -1 +1 @@
1
- 33fc5eaaa57e82c0a535b01a4df1d62dc40ad18e49d0bc7e149e3c0ef4b073f2
1
+ b6e20e2fd0fc13137dbb1e22fd43f3b275379358cbaa4f31e197195843317de9
package/dist/styles.css CHANGED
@@ -3956,20 +3956,11 @@
3956
3956
  padding-block: 0;
3957
3957
  }
3958
3958
 
3959
- .focus-within\:border-ring:focus-within {
3960
- border-color: var(--ring);
3961
- }
3962
-
3963
3959
  .focus-within\:ring-1:focus-within {
3964
3960
  --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
3965
3961
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
3966
3962
  }
3967
3963
 
3968
- .focus-within\:ring-3:focus-within {
3969
- --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
3970
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
3971
- }
3972
-
3973
3964
  .focus-within\:ring-ring\/50:focus-within {
3974
3965
  --tw-ring-color: var(--ring);
3975
3966
  }
@@ -3980,6 +3971,11 @@
3980
3971
  }
3981
3972
  }
3982
3973
 
3974
+ .focus-within\:outline-solid:focus-within {
3975
+ --tw-outline-style: solid;
3976
+ outline-style: solid;
3977
+ }
3978
+
3983
3979
  @media (hover: hover) {
3984
3980
  .hover\:bg-\[color-mix\(in_oklch\,var\(--secondary\)\,var\(--foreground\)_5\%\)\]:hover {
3985
3981
  background-color: var(--secondary);
@@ -4120,20 +4116,6 @@
4120
4116
  z-index: 10;
4121
4117
  }
4122
4118
 
4123
- .focus-visible\:border-destructive\/40:focus-visible {
4124
- border-color: var(--destructive);
4125
- }
4126
-
4127
- @supports (color: color-mix(in lab, red, red)) {
4128
- .focus-visible\:border-destructive\/40:focus-visible {
4129
- border-color: color-mix(in oklab, var(--destructive) 40%, transparent);
4130
- }
4131
- }
4132
-
4133
- .focus-visible\:border-ring:focus-visible {
4134
- border-color: var(--ring);
4135
- }
4136
-
4137
4119
  .focus-visible\:ring-0:focus-visible {
4138
4120
  --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
4139
4121
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -4149,61 +4131,10 @@
4149
4131
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
4150
4132
  }
4151
4133
 
4152
- .focus-visible\:ring-3:focus-visible, .focus-visible\:ring-\[3px\]:focus-visible {
4153
- --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
4154
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
4155
- }
4156
-
4157
- .focus-visible\:ring-destructive\/20:focus-visible {
4158
- --tw-ring-color: var(--destructive);
4159
- }
4160
-
4161
- @supports (color: color-mix(in lab, red, red)) {
4162
- .focus-visible\:ring-destructive\/20:focus-visible {
4163
- --tw-ring-color: color-mix(in oklab, var(--destructive) 20%, transparent);
4164
- }
4165
- }
4166
-
4167
- .focus-visible\:ring-info\/20:focus-visible {
4168
- --tw-ring-color: var(--info);
4169
- }
4170
-
4171
- @supports (color: color-mix(in lab, red, red)) {
4172
- .focus-visible\:ring-info\/20:focus-visible {
4173
- --tw-ring-color: color-mix(in oklab, var(--info) 20%, transparent);
4174
- }
4175
- }
4176
-
4177
- .focus-visible\:ring-ring:focus-visible, .focus-visible\:ring-ring\/50:focus-visible {
4134
+ .focus-visible\:ring-ring:focus-visible {
4178
4135
  --tw-ring-color: var(--ring);
4179
4136
  }
4180
4137
 
4181
- @supports (color: color-mix(in lab, red, red)) {
4182
- .focus-visible\:ring-ring\/50:focus-visible {
4183
- --tw-ring-color: color-mix(in oklab, var(--ring) 50%, transparent);
4184
- }
4185
- }
4186
-
4187
- .focus-visible\:ring-success\/20:focus-visible {
4188
- --tw-ring-color: var(--success);
4189
- }
4190
-
4191
- @supports (color: color-mix(in lab, red, red)) {
4192
- .focus-visible\:ring-success\/20:focus-visible {
4193
- --tw-ring-color: color-mix(in oklab, var(--success) 20%, transparent);
4194
- }
4195
- }
4196
-
4197
- .focus-visible\:ring-warning\/20:focus-visible {
4198
- --tw-ring-color: var(--warning);
4199
- }
4200
-
4201
- @supports (color: color-mix(in lab, red, red)) {
4202
- .focus-visible\:ring-warning\/20:focus-visible {
4203
- --tw-ring-color: color-mix(in oklab, var(--warning) 20%, transparent);
4204
- }
4205
- }
4206
-
4207
4138
  .focus-visible\:outline-hidden:focus-visible {
4208
4139
  --tw-outline-style: none;
4209
4140
  outline-style: none;
@@ -4518,23 +4449,9 @@
4518
4449
  padding: 0;
4519
4450
  }
4520
4451
 
4521
- .has-\[input\:focus-visible\]\:border-ring:has(:is(input:focus-visible)) {
4522
- border-color: var(--ring);
4523
- }
4524
-
4525
- .has-\[input\:focus-visible\]\:ring-3:has(:is(input:focus-visible)) {
4526
- --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
4527
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
4528
- }
4529
-
4530
- .has-\[input\:focus-visible\]\:ring-ring\/50:has(:is(input:focus-visible)) {
4531
- --tw-ring-color: var(--ring);
4532
- }
4533
-
4534
- @supports (color: color-mix(in lab, red, red)) {
4535
- .has-\[input\:focus-visible\]\:ring-ring\/50:has(:is(input:focus-visible)) {
4536
- --tw-ring-color: color-mix(in oklab, var(--ring) 50%, transparent);
4537
- }
4452
+ .has-\[input\:focus-visible\]\:outline-solid:has(:is(input:focus-visible)) {
4453
+ --tw-outline-style: solid;
4454
+ outline-style: solid;
4538
4455
  }
4539
4456
 
4540
4457
  .has-\[select\:disabled\]\:opacity-50:has(:is(select:disabled)) {
@@ -4817,7 +4734,7 @@
4817
4734
  opacity: 1;
4818
4735
  }
4819
4736
 
4820
- .data-has-overflow-x\:pb-\(--scroll-bar-reserve\)[data-has-overflow-x] {
4737
+ .data-has-overflow-x\:not-data-bar-over\:pb-\(--scroll-bar-reserve\)[data-has-overflow-x]:not([data-bar-over]) {
4821
4738
  padding-bottom: var(--scroll-bar-reserve);
4822
4739
  }
4823
4740
 
@@ -4887,10 +4804,6 @@
4887
4804
  color: var(--muted-foreground);
4888
4805
  }
4889
4806
 
4890
- .data-popup-open\:border-ring[data-popup-open] {
4891
- border-color: var(--ring);
4892
- }
4893
-
4894
4807
  .data-popup-open\:bg-foreground\/6[data-popup-open] {
4895
4808
  background-color: var(--foreground);
4896
4809
  }
@@ -4911,19 +4824,9 @@
4911
4824
  }
4912
4825
  }
4913
4826
 
4914
- .data-popup-open\:ring-3[data-popup-open] {
4915
- --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
4916
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
4917
- }
4918
-
4919
- .data-popup-open\:ring-ring\/50[data-popup-open] {
4920
- --tw-ring-color: var(--ring);
4921
- }
4922
-
4923
- @supports (color: color-mix(in lab, red, red)) {
4924
- .data-popup-open\:ring-ring\/50[data-popup-open] {
4925
- --tw-ring-color: color-mix(in oklab, var(--ring) 50%, transparent);
4926
- }
4827
+ .data-popup-open\:outline-solid[data-popup-open] {
4828
+ --tw-outline-style: solid;
4829
+ outline-style: solid;
4927
4830
  }
4928
4831
 
4929
4832
  @media (hover: hover) {
@@ -6110,16 +6013,6 @@
6110
6013
  }
6111
6014
  }
6112
6015
 
6113
- .dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible {
6114
- --tw-ring-color: var(--destructive);
6115
- }
6116
-
6117
- @supports (color: color-mix(in lab, red, red)) {
6118
- .dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible {
6119
- --tw-ring-color: color-mix(in oklab, var(--destructive) 40%, transparent);
6120
- }
6121
- }
6122
-
6123
6016
  .dark\:disabled\:border-transparent:is(.dark *):disabled {
6124
6017
  border-color: #0000;
6125
6018
  }
@@ -7038,25 +6931,6 @@
7038
6931
  outline-style: none;
7039
6932
  }
7040
6933
 
7041
- .\[button\,a\]\:focus-visible\:border-ring:is(button, a):focus-visible {
7042
- border-color: var(--ring);
7043
- }
7044
-
7045
- .\[button\,a\]\:focus-visible\:ring-3:is(button, a):focus-visible {
7046
- --tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
7047
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
7048
- }
7049
-
7050
- .\[button\,a\]\:focus-visible\:ring-ring\/50:is(button, a):focus-visible {
7051
- --tw-ring-color: var(--ring);
7052
- }
7053
-
7054
- @supports (color: color-mix(in lab, red, red)) {
7055
- .\[button\,a\]\:focus-visible\:ring-ring\/50:is(button, a):focus-visible {
7056
- --tw-ring-color: color-mix(in oklab, var(--ring) 50%, transparent);
7057
- }
7058
- }
7059
-
7060
6934
  :is(.\*\:\[img\]\:aspect-square > *):is(img) {
7061
6935
  aspect-ratio: 1;
7062
6936
  }
@@ -7855,6 +7729,14 @@
7855
7729
  outline-width: 1px;
7856
7730
  }
7857
7731
 
7732
+ .border-control {
7733
+ outline-offset: -1px;
7734
+ }
7735
+
7736
+ [data-focus="inset"] .border-control {
7737
+ outline-offset: -2px;
7738
+ }
7739
+
7858
7740
  :root {
7859
7741
  --motion-expand: .2s;
7860
7742
  --motion-collapse: .16s;
@@ -14,12 +14,16 @@ export interface IDatePickerLabels {
14
14
  }
15
15
  export declare const DEFAULT_DATE_PICKER_LABELS: IDatePickerLabels;
16
16
  /**
17
- * What the field holds when it reports, so a parent can tell three things apart that all arrive as `undefined`:
17
+ * What the field holds when it reports, so a parent can tell apart things that would otherwise look alike:
18
18
  * - `day`: a usable day, typed or picked.
19
19
  * - `empty`: the field was emptied, or nothing has been put in it. A required field is right to ask for a date.
20
- * - `refused`: the field holds text it will not take (not a day, not a real one, or outside `min`, `max` or
21
- * `disabledDates`). `text` is what was typed and `problem` the message the picker shows under the field, already
22
- * in the picker's `labels`. The picker is saying so itself, so the page should not add a second message.
20
+ * - `refused`: the field holds text that is not a day (not a day, or not a real one). The day reported is `undefined`.
21
+ * `text` is what was typed and `problem` the message the picker shows under the field, in the picker's `labels`.
22
+ * - `out-of-range`: the field holds a real day that `min`, `max` or `disabledDates` refuse. The day IS reported (as the
23
+ * first argument and as `day`), so a form's own rule can judge it and show its message; the field is marked invalid.
24
+ * Check `kind` before treating the day as a usable one.
25
+ *
26
+ * Inside a Field that renders a FieldError, that message is the only one: the picker does not show its own.
23
27
  */
24
28
  export type TDatePickerState = {
25
29
  kind: 'day';
@@ -29,6 +33,11 @@ export type TDatePickerState = {
29
33
  kind: 'refused';
30
34
  text: string;
31
35
  problem: string;
36
+ } | {
37
+ kind: 'out-of-range';
38
+ day: string;
39
+ text: string;
40
+ problem: string;
32
41
  };
33
42
  export interface IDatePickerProps {
34
43
  /** For a Label's htmlFor; inside a Field the Field's label is already wired. */
@@ -51,6 +51,23 @@ var DatePicker = ({ id, value, onValueChange, min, max, disabledDates, disabled
51
51
  const [text, setText] = useState(current === void 0 ? "" : formatDay(current, locale));
52
52
  const [problem, setProblem] = useState(null);
53
53
  const [open, setOpen] = useState(false);
54
+ const [formError, setFormError] = useState(null);
55
+ useEffect(() => {
56
+ const field = refField.current?.closest("[data-slot=\"field\"]");
57
+ if (!field) return;
58
+ const read = () => setFormError(field.querySelector("[data-slot=\"field-error\"]")?.id || null);
59
+ read();
60
+ const observer = new MutationObserver(read);
61
+ observer.observe(field, {
62
+ childList: true,
63
+ subtree: true,
64
+ attributes: true,
65
+ attributeFilter: ["id"]
66
+ });
67
+ return () => {
68
+ observer.disconnect();
69
+ };
70
+ }, []);
54
71
  useEffect(() => {
55
72
  if (current !== refKnown.current) {
56
73
  refKnown.current = current;
@@ -61,7 +78,7 @@ var DatePicker = ({ id, value, onValueChange, min, max, disabledDates, disabled
61
78
  }, [current, locale]);
62
79
  const report = (day, state) => {
63
80
  refKnown.current = day;
64
- refRefused.current = state.kind === "refused" ? `${state.text}\u0000${state.problem}` : null;
81
+ refRefused.current = state.kind === "refused" || state.kind === "out-of-range" ? `${state.text}\u0000${state.problem}` : null;
65
82
  onValueChange(day, state);
66
83
  };
67
84
  /**
@@ -86,7 +103,17 @@ var DatePicker = ({ id, value, onValueChange, min, max, disabledDates, disabled
86
103
  min,
87
104
  max
88
105
  }, pattern);
89
- if (current !== void 0 || refRefused.current !== `${typed}\u0000${message}`) report(void 0, {
106
+ const seen = refRefused.current === `${typed}\u0000${message}`;
107
+ if (parsed.kind === "day") {
108
+ if (parsed.day !== current || !seen) report(parsed.day, {
109
+ kind: "out-of-range",
110
+ day: parsed.day,
111
+ text: typed,
112
+ problem: message
113
+ });
114
+ return;
115
+ }
116
+ if (current !== void 0 || !seen) report(void 0, {
90
117
  kind: "refused",
91
118
  text: typed,
92
119
  problem: message
@@ -168,7 +195,12 @@ var DatePicker = ({ id, value, onValueChange, min, max, disabledDates, disabled
168
195
  max
169
196
  };
170
197
  const invalid = problem !== null || ariaInvalid === true || ariaInvalid === "true";
171
- const describedBy = [ariaDescribedBy, problem === null ? void 0 : errorId].filter(Boolean).join(" ") || void 0;
198
+ const ownMessage = problem !== null && formError === null;
199
+ const describedBy = [...new Set([
200
+ ariaDescribedBy,
201
+ ownMessage ? errorId : void 0,
202
+ formError ?? void 0
203
+ ].filter(Boolean))].join(" ") || void 0;
172
204
  const isOff = (date) => {
173
205
  const day = dateToDay(date);
174
206
  return day !== void 0 && dayProblem(day, {
@@ -255,7 +287,7 @@ var DatePicker = ({ id, value, onValueChange, min, max, disabledDates, disabled
255
287
  })
256
288
  })]
257
289
  })]
258
- }), problem !== null && /* @__PURE__ */ jsx("p", {
290
+ }), ownMessage && /* @__PURE__ */ jsx("p", {
259
291
  id: errorId,
260
292
  "data-slot": "date-picker-error",
261
293
  role: "alert",
@@ -1 +1 @@
1
- {"version":3,"file":"date-picker.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/date-picker/date-picker.tsx"],"sourcesContent":["import { dateToDay, dayToDate } from '@edyn/ui/components/edyn/date-range-picker';\nimport { Button } from '@edyn/ui/components/ui/button';\nimport { Calendar } from '@edyn/ui/components/ui/calendar';\nimport { Input } from '@edyn/ui/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { firstWeekdayOf } from '@edyn/ui/lib/first-weekday';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCalendar } from '@tabler/icons-react';\nimport { type ChangeEvent, type FocusEvent, type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from 'react';\nimport type { Locale } from 'react-day-picker';\nimport { datePatternOf, dayProblem, formatDay, parseDateText, type TDateProblem, type TDisabledDays } from './date-text';\n\n/** Every word the component prints, so a product can translate it. */\nexport interface IDatePickerLabels {\n /** The calendar button's accessible name. */\n choose: string;\n /** Said when the text is not a date; `pattern` is what to type, written the locale's way (`DD/MM/YYYY`). */\n format: (pattern: string) => string;\n notReal: string;\n /** `min` and `max` are written the locale's way. */\n beforeMin: (min: string) => string;\n afterMax: (max: string) => string;\n unavailable: string;\n}\n\nexport const DEFAULT_DATE_PICKER_LABELS: IDatePickerLabels = {\n choose: 'Choose date',\n format: (pattern) => `Enter a date as ${pattern}.`,\n notReal: 'That date does not exist.',\n beforeMin: (min) => `Choose ${min} or later.`,\n afterMax: (max) => `Choose ${max} or earlier.`,\n unavailable: 'That date is not available.',\n};\n\n/**\n * What the field holds when it reports, so a parent can tell three things apart that all arrive as `undefined`:\n * - `day`: a usable day, typed or picked.\n * - `empty`: the field was emptied, or nothing has been put in it. A required field is right to ask for a date.\n * - `refused`: the field holds text it will not take (not a day, not a real one, or outside `min`, `max` or\n * `disabledDates`). `text` is what was typed and `problem` the message the picker shows under the field, already\n * in the picker's `labels`. The picker is saying so itself, so the page should not add a second message.\n */\nexport type TDatePickerState = { kind: 'day' } | { kind: 'empty' } | { kind: 'refused'; text: string; problem: string };\n\nexport interface IDatePickerProps {\n /** For a Label's htmlFor; inside a Field the Field's label is already wired. */\n id?: string;\n /**\n * The chosen day as `yyyy-mm-dd`, or `undefined` for none (an empty string counts as none, so a value kept for an\n * `<input type=\"date\">` drops in). A string and not a Date, on purpose: a Date is an instant, and the instant for\n * \"5 March\" is another calendar day in another time zone, so a Date serialised for an API drifts a day. The text is\n * what `<input type=\"date\">`, JSON and a .NET `DateOnly` use, and it compares as a day.\n */\n value: string | undefined;\n /**\n * Called with a valid day, or with `undefined` when the field is emptied or its text is not a usable day. The second\n * argument says which of the two it was (see `TDatePickerState`); a caller that has no use for it can ignore it.\n * Text that is refused is reported each time it changes, also when the field held no day before, so a form that\n * tracks the state hears about it. Emptying the field after that reports `empty`.\n */\n onValueChange: (value: string | undefined, state: TDatePickerState) => void;\n /** The earliest and latest day that can be chosen, as `yyyy-mm-dd`, both included. */\n min?: string;\n max?: string;\n /** Days that cannot be chosen: a list of `yyyy-mm-dd`, or a function that says so for one (weekends, say). */\n disabledDates?: TDisabledDays;\n disabled?: boolean;\n /** How a day is written and in which order it is typed (BCP 47); en-GB reads 05/03/2026. */\n locale?: string;\n /** The calendar's own words (month and weekday names), as a date-fns locale; English by default. The first weekday follows `locale`. */\n calendarLocale?: Locale;\n /** What the empty field shows; the pattern for the locale (`DD/MM/YYYY`) by default. */\n placeholder?: string;\n /** An accessible name, when there is no visible label to point at. */\n 'aria-label'?: string;\n /** Marks the field invalid (the kit's destructive border), as a Field's invalid state does. */\n 'aria-invalid'?: boolean | 'true' | 'false';\n /** The id of the text that explains it. Added to what a Field names already, and to the picker's own error. */\n 'aria-describedby'?: string;\n labels?: Partial<IDatePickerLabels>;\n /** Layout only (a width); the look is the kit's. */\n className?: string;\n}\n\nconst problemText = (\n problem: TDateProblem,\n labels: IDatePickerLabels,\n locale: string,\n limits: { min?: string; max?: string },\n pattern: string,\n): string => {\n switch (problem) {\n case 'format':\n return labels.format(pattern);\n case 'notReal':\n return labels.notReal;\n case 'beforeMin':\n return labels.beforeMin(limits.min === undefined ? '' : formatDay(limits.min, locale));\n case 'afterMax':\n return labels.afterMax(limits.max === undefined ? '' : formatDay(limits.max, locale));\n default:\n return labels.unavailable;\n }\n};\n\n/**\n * One calendar day, typed or picked. The field takes the day in the locale's order (05/03/2026 in en-GB) and reads it\n * when it loses focus, saying inline what is wrong with it; the button, or Alt+ArrowDown in the field, opens a calendar.\n * Escape closes the calendar and gives the focus back to whichever of the two opened it; a day picked puts it on the field.\n */\nexport const DatePicker = ({\n id,\n value,\n onValueChange,\n min,\n max,\n disabledDates,\n disabled = false,\n locale = 'en-GB',\n calendarLocale,\n placeholder,\n 'aria-label': ariaLabel,\n 'aria-invalid': ariaInvalid,\n 'aria-describedby': ariaDescribedBy,\n labels: labelOverrides,\n className,\n}: IDatePickerProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_DATE_PICKER_LABELS, ...labelOverrides }), [labelOverrides]);\n const pattern = useMemo(() => datePatternOf(locale), [locale]);\n const current = value === '' ? undefined : value;\n const errorId = useId();\n const refField = useRef<HTMLDivElement | null>(null);\n const refInput = useRef<HTMLInputElement | null>(null);\n const refButton = useRef<HTMLButtonElement | null>(null);\n // What opened the calendar, so that Escape gives the focus back to it: the button when the button did it, the field\n // when Alt+ArrowDown did. Choosing a day is different (see `pick`): the field then shows the day, so it gets the focus.\n const refOpener = useRef<'button' | 'field'>('button');\n const refPicked = useRef(false);\n // The mouse is down inside the picker, so a blur of the field is a click on something in it, not a Tab.\n const refPointer = useRef(false);\n // The day the field last agreed on with the page, so a change from outside is told from our own report of it.\n const refKnown = useRef<string | undefined>(current);\n // The refused text last reported (with its message), or null: told apart from a fresh refusal so it is reported once,\n // and so that emptying a field that held a refused text is reported although it held no day.\n const refRefused = useRef<string | null>(null);\n const [text, setText] = useState(current === undefined ? '' : formatDay(current, locale));\n const [problem, setProblem] = useState<TDateProblem | null>(null);\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n if (current !== refKnown.current) {\n refKnown.current = current;\n refRefused.current = null;\n setText(current === undefined ? '' : formatDay(current, locale));\n setProblem(null);\n }\n }, [current, locale]);\n\n const report = (day: string | undefined, state: TDatePickerState) => {\n refKnown.current = day;\n refRefused.current = state.kind === 'refused' ? `${state.text}\\u0000${state.problem}` : null;\n onValueChange(day, state);\n };\n\n /**\n * Reads the text: a day is reported and written back in full, anything else is said inline and reported as no day,\n * with the state saying whether the field is empty or holds text it refused.\n */\n const commit = (typed: string) => {\n const parsed = parseDateText(typed, locale);\n if (parsed.kind === 'empty') {\n setProblem(null);\n if (current !== undefined || refRefused.current !== null) {\n report(undefined, { kind: 'empty' });\n }\n return;\n }\n const found = parsed.kind === 'problem' ? parsed.problem : dayProblem(parsed.day, { min, max, disabledDates });\n if (found !== undefined) {\n setProblem(found);\n const message = problemText(found, labels, locale, { min, max }, pattern);\n if (current !== undefined || refRefused.current !== `${typed}\\u0000${message}`) {\n report(undefined, { kind: 'refused', text: typed, problem: message });\n }\n return;\n }\n if (parsed.kind === 'day') {\n setProblem(null);\n setText(formatDay(parsed.day, locale));\n if (parsed.day !== current || refRefused.current !== null) {\n report(parsed.day, { kind: 'day' });\n }\n }\n };\n\n const pick = (date: Date | undefined) => {\n const day = dateToDay(date);\n if (day === undefined) {\n return;\n }\n setProblem(null);\n setText(formatDay(day, locale));\n if (day !== current || refRefused.current !== null) {\n report(day, { kind: 'day' });\n }\n refPicked.current = true;\n setOpen(false);\n };\n\n const openFrom = (opener: 'button' | 'field') => {\n refOpener.current = opener;\n refPicked.current = false;\n setOpen(true);\n };\n\n /** Where the focus goes when the calendar closes: the field after a day is chosen, else whatever opened it. */\n const focusAfterClose = (): HTMLElement | null => (refPicked.current || refOpener.current === 'field' ? refInput.current : refButton.current);\n\n /**\n * Text is read as the person leaves the field. A day (or nothing) is taken at once; a message about text that is not a\n * day is shown too, unless they are only on the way to the calendar: a press of the mouse on the button, or focus\n * going into the calendar that Alt+ArrowDown opened, with the text half typed. Leaving the picker for anywhere else\n * reads whatever is typed.\n */\n const handleBlur = (event: FocusEvent<HTMLElement>) => {\n const next = event.relatedTarget as Element | null;\n const intoCalendar = next?.closest('[data-slot=\"popover-content\"]') != null;\n const intoButton = next?.closest('[data-slot=\"date-picker-trigger\"]') != null;\n const typed = refInput.current?.value ?? '';\n if (!(intoCalendar || intoButton || (next !== null && refField.current?.contains(next) === true))) {\n commit(typed);\n return;\n }\n if (event.target !== refInput.current) {\n return;\n }\n const onTheWay = intoCalendar || (intoButton && refPointer.current);\n const parsed = parseDateText(typed, locale);\n const usable = parsed.kind === 'empty' || (parsed.kind === 'day' && dayProblem(parsed.day, { min, max, disabledDates }) === undefined);\n if (usable || !onTheWay) {\n commit(typed);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'ArrowDown' && event.altKey && !disabled) {\n event.preventDefault();\n openFrom('field');\n return;\n }\n // The field has no key handler of Base UI's to close the calendar, so the key would reach the Sheet or Dialog around\n // it and close that too. While the calendar is open it is this field's key; when it is closed it is the page's.\n if (event.key === 'Escape' && open) {\n event.preventDefault();\n event.stopPropagation();\n // The focus is on the field already, whatever opened the calendar: it stays there.\n refOpener.current = 'field';\n setOpen(false);\n return;\n }\n if (event.key === 'Enter') {\n const parsed = parseDateText(event.currentTarget.value, locale);\n if (parsed.kind !== 'empty' && (parsed.kind === 'problem' || dayProblem(parsed.day, { min, max, disabledDates }) !== undefined)) {\n event.preventDefault();\n }\n commit(event.currentTarget.value);\n }\n };\n\n const selected = dayToDate(current);\n const limits = { min, max };\n const invalid = problem !== null || ariaInvalid === true || ariaInvalid === 'true';\n const describedBy = [ariaDescribedBy, problem === null ? undefined : errorId].filter(Boolean).join(' ') || undefined;\n const isOff = (date: Date): boolean => {\n const day = dateToDay(date);\n return day !== undefined && dayProblem(day, { min, max, disabledDates }) !== undefined;\n };\n const today = new Date();\n const start = dayToDate(min);\n const end = dayToDate(max);\n const month = selected ?? (start !== undefined && today < start ? start : end !== undefined && today > end ? end : today);\n\n return (\n <div data-slot=\"date-picker\" className={cn('flex flex-col gap-1.5', className)}>\n <div\n ref={refField}\n className=\"relative\"\n onPointerDownCapture={() => {\n refPointer.current = true;\n }}\n onPointerUpCapture={() => {\n refPointer.current = false;\n }}\n >\n <Input\n ref={refInput}\n id={id}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"off\"\n data-slot=\"date-picker-input\"\n value={text}\n placeholder={placeholder ?? pattern}\n disabled={disabled}\n aria-label={ariaLabel}\n aria-invalid={invalid ? true : undefined}\n aria-describedby={describedBy}\n className=\"pr-9\"\n onChange={(event: ChangeEvent<HTMLInputElement>) => {\n setText(event.target.value);\n setProblem(null);\n }}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n />\n <Popover\n open={open}\n onOpenChange={(next) => {\n // Only the button opens it from here (Alt+ArrowDown in the field sets the state itself, through `openFrom`).\n if (next) {\n openFrom('button');\n return;\n }\n setOpen(false);\n }}\n >\n <PopoverTrigger\n render={\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n ref={refButton}\n data-slot=\"date-picker-trigger\"\n aria-label={labels.choose}\n disabled={disabled}\n className=\"absolute top-0.5 right-0.5\"\n onBlur={handleBlur}\n />\n }\n >\n <IconCalendar aria-hidden=\"true\" />\n </PopoverTrigger>\n <PopoverContent anchor={refField} align=\"start\" className=\"w-auto p-0\" finalFocus={focusAfterClose} onBlur={handleBlur}>\n <Calendar\n mode=\"single\"\n required\n autoFocus\n locale={calendarLocale}\n weekStartsOn={firstWeekdayOf(locale)}\n defaultMonth={month}\n startMonth={start}\n endMonth={end}\n selected={selected}\n disabled={isOff}\n onSelect={pick}\n />\n </PopoverContent>\n </Popover>\n </div>\n {problem !== null && (\n <p id={errorId} data-slot=\"date-picker-error\" role=\"alert\" className=\"text-sm text-destructive\">\n {problemText(problem, labels, locale, limits, pattern)}\n </p>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"date-picker.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/date-picker/date-picker.tsx"],"sourcesContent":["import { dateToDay, dayToDate } from '@edyn/ui/components/edyn/date-range-picker';\nimport { Button } from '@edyn/ui/components/ui/button';\nimport { Calendar } from '@edyn/ui/components/ui/calendar';\nimport { Input } from '@edyn/ui/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { firstWeekdayOf } from '@edyn/ui/lib/first-weekday';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCalendar } from '@tabler/icons-react';\nimport { type ChangeEvent, type FocusEvent, type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from 'react';\nimport type { Locale } from 'react-day-picker';\nimport { datePatternOf, dayProblem, formatDay, parseDateText, type TDateProblem, type TDisabledDays } from './date-text';\n\n/** Every word the component prints, so a product can translate it. */\nexport interface IDatePickerLabels {\n /** The calendar button's accessible name. */\n choose: string;\n /** Said when the text is not a date; `pattern` is what to type, written the locale's way (`DD/MM/YYYY`). */\n format: (pattern: string) => string;\n notReal: string;\n /** `min` and `max` are written the locale's way. */\n beforeMin: (min: string) => string;\n afterMax: (max: string) => string;\n unavailable: string;\n}\n\nexport const DEFAULT_DATE_PICKER_LABELS: IDatePickerLabels = {\n choose: 'Choose date',\n format: (pattern) => `Enter a date as ${pattern}.`,\n notReal: 'That date does not exist.',\n beforeMin: (min) => `Choose ${min} or later.`,\n afterMax: (max) => `Choose ${max} or earlier.`,\n unavailable: 'That date is not available.',\n};\n\n/**\n * What the field holds when it reports, so a parent can tell apart things that would otherwise look alike:\n * - `day`: a usable day, typed or picked.\n * - `empty`: the field was emptied, or nothing has been put in it. A required field is right to ask for a date.\n * - `refused`: the field holds text that is not a day (not a day, or not a real one). The day reported is `undefined`.\n * `text` is what was typed and `problem` the message the picker shows under the field, in the picker's `labels`.\n * - `out-of-range`: the field holds a real day that `min`, `max` or `disabledDates` refuse. The day IS reported (as the\n * first argument and as `day`), so a form's own rule can judge it and show its message; the field is marked invalid.\n * Check `kind` before treating the day as a usable one.\n *\n * Inside a Field that renders a FieldError, that message is the only one: the picker does not show its own.\n */\nexport type TDatePickerState =\n | { kind: 'day' }\n | { kind: 'empty' }\n | { kind: 'refused'; text: string; problem: string }\n | { kind: 'out-of-range'; day: string; text: string; problem: string };\n\nexport interface IDatePickerProps {\n /** For a Label's htmlFor; inside a Field the Field's label is already wired. */\n id?: string;\n /**\n * The chosen day as `yyyy-mm-dd`, or `undefined` for none (an empty string counts as none, so a value kept for an\n * `<input type=\"date\">` drops in). A string and not a Date, on purpose: a Date is an instant, and the instant for\n * \"5 March\" is another calendar day in another time zone, so a Date serialised for an API drifts a day. The text is\n * what `<input type=\"date\">`, JSON and a .NET `DateOnly` use, and it compares as a day.\n */\n value: string | undefined;\n /**\n * Called with a valid day, or with `undefined` when the field is emptied or its text is not a usable day. The second\n * argument says which of the two it was (see `TDatePickerState`); a caller that has no use for it can ignore it.\n * Text that is refused is reported each time it changes, also when the field held no day before, so a form that\n * tracks the state hears about it. Emptying the field after that reports `empty`.\n */\n onValueChange: (value: string | undefined, state: TDatePickerState) => void;\n /** The earliest and latest day that can be chosen, as `yyyy-mm-dd`, both included. */\n min?: string;\n max?: string;\n /** Days that cannot be chosen: a list of `yyyy-mm-dd`, or a function that says so for one (weekends, say). */\n disabledDates?: TDisabledDays;\n disabled?: boolean;\n /** How a day is written and in which order it is typed (BCP 47); en-GB reads 05/03/2026. */\n locale?: string;\n /** The calendar's own words (month and weekday names), as a date-fns locale; English by default. The first weekday follows `locale`. */\n calendarLocale?: Locale;\n /** What the empty field shows; the pattern for the locale (`DD/MM/YYYY`) by default. */\n placeholder?: string;\n /** An accessible name, when there is no visible label to point at. */\n 'aria-label'?: string;\n /** Marks the field invalid (the kit's destructive border), as a Field's invalid state does. */\n 'aria-invalid'?: boolean | 'true' | 'false';\n /** The id of the text that explains it. Added to what a Field names already, and to the picker's own error. */\n 'aria-describedby'?: string;\n labels?: Partial<IDatePickerLabels>;\n /** Layout only (a width); the look is the kit's. */\n className?: string;\n}\n\nconst problemText = (\n problem: TDateProblem,\n labels: IDatePickerLabels,\n locale: string,\n limits: { min?: string; max?: string },\n pattern: string,\n): string => {\n switch (problem) {\n case 'format':\n return labels.format(pattern);\n case 'notReal':\n return labels.notReal;\n case 'beforeMin':\n return labels.beforeMin(limits.min === undefined ? '' : formatDay(limits.min, locale));\n case 'afterMax':\n return labels.afterMax(limits.max === undefined ? '' : formatDay(limits.max, locale));\n default:\n return labels.unavailable;\n }\n};\n\n/**\n * One calendar day, typed or picked. The field takes the day in the locale's order (05/03/2026 in en-GB) and reads it\n * when it loses focus, saying inline what is wrong with it; the button, or Alt+ArrowDown in the field, opens a calendar.\n * Escape closes the calendar and gives the focus back to whichever of the two opened it; a day picked puts it on the field.\n */\nexport const DatePicker = ({\n id,\n value,\n onValueChange,\n min,\n max,\n disabledDates,\n disabled = false,\n locale = 'en-GB',\n calendarLocale,\n placeholder,\n 'aria-label': ariaLabel,\n 'aria-invalid': ariaInvalid,\n 'aria-describedby': ariaDescribedBy,\n labels: labelOverrides,\n className,\n}: IDatePickerProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_DATE_PICKER_LABELS, ...labelOverrides }), [labelOverrides]);\n const pattern = useMemo(() => datePatternOf(locale), [locale]);\n const current = value === '' ? undefined : value;\n const errorId = useId();\n const refField = useRef<HTMLDivElement | null>(null);\n const refInput = useRef<HTMLInputElement | null>(null);\n const refButton = useRef<HTMLButtonElement | null>(null);\n // What opened the calendar, so that Escape gives the focus back to it: the button when the button did it, the field\n // when Alt+ArrowDown did. Choosing a day is different (see `pick`): the field then shows the day, so it gets the focus.\n const refOpener = useRef<'button' | 'field'>('button');\n const refPicked = useRef(false);\n // The mouse is down inside the picker, so a blur of the field is a click on something in it, not a Tab.\n const refPointer = useRef(false);\n // The day the field last agreed on with the page, so a change from outside is told from our own report of it.\n const refKnown = useRef<string | undefined>(current);\n // The refused text last reported (with its message), or null: told apart from a fresh refusal so it is reported once,\n // and so that emptying a field that held a refused text is reported although it held no day.\n const refRefused = useRef<string | null>(null);\n const [text, setText] = useState(current === undefined ? '' : formatDay(current, locale));\n const [problem, setProblem] = useState<TDateProblem | null>(null);\n const [open, setOpen] = useState(false);\n // The id of the FieldError of the Field around the picker, when it renders one: that message is the only one for the field.\n const [formError, setFormError] = useState<string | null>(null);\n\n useEffect(() => {\n const field = refField.current?.closest('[data-slot=\"field\"]');\n if (!field) {\n return;\n }\n const read = () => setFormError(field.querySelector('[data-slot=\"field-error\"]')?.id || null);\n read();\n const observer = new MutationObserver(read);\n observer.observe(field, { childList: true, subtree: true, attributes: true, attributeFilter: ['id'] });\n return () => {\n observer.disconnect();\n };\n }, []);\n\n useEffect(() => {\n if (current !== refKnown.current) {\n refKnown.current = current;\n refRefused.current = null;\n setText(current === undefined ? '' : formatDay(current, locale));\n setProblem(null);\n }\n }, [current, locale]);\n\n const report = (day: string | undefined, state: TDatePickerState) => {\n refKnown.current = day;\n refRefused.current = state.kind === 'refused' || state.kind === 'out-of-range' ? `${state.text}\\u0000${state.problem}` : null;\n onValueChange(day, state);\n };\n\n /**\n * Reads the text: a day is reported and written back in full, anything else is said inline and reported as no day,\n * with the state saying whether the field is empty or holds text it refused.\n */\n const commit = (typed: string) => {\n const parsed = parseDateText(typed, locale);\n if (parsed.kind === 'empty') {\n setProblem(null);\n if (current !== undefined || refRefused.current !== null) {\n report(undefined, { kind: 'empty' });\n }\n return;\n }\n const found = parsed.kind === 'problem' ? parsed.problem : dayProblem(parsed.day, { min, max, disabledDates });\n if (found !== undefined) {\n setProblem(found);\n const message = problemText(found, labels, locale, { min, max }, pattern);\n const seen = refRefused.current === `${typed}\\u0000${message}`;\n if (parsed.kind === 'day') {\n // A real day that the limits refuse: the text stays as typed and the day is reported, flagged, so that the form's own\n // rule can show its message instead of seeing an empty field.\n if (parsed.day !== current || !seen) {\n report(parsed.day, { kind: 'out-of-range', day: parsed.day, text: typed, problem: message });\n }\n return;\n }\n if (current !== undefined || !seen) {\n report(undefined, { kind: 'refused', text: typed, problem: message });\n }\n return;\n }\n if (parsed.kind === 'day') {\n setProblem(null);\n setText(formatDay(parsed.day, locale));\n if (parsed.day !== current || refRefused.current !== null) {\n report(parsed.day, { kind: 'day' });\n }\n }\n };\n\n const pick = (date: Date | undefined) => {\n const day = dateToDay(date);\n if (day === undefined) {\n return;\n }\n setProblem(null);\n setText(formatDay(day, locale));\n if (day !== current || refRefused.current !== null) {\n report(day, { kind: 'day' });\n }\n refPicked.current = true;\n setOpen(false);\n };\n\n const openFrom = (opener: 'button' | 'field') => {\n refOpener.current = opener;\n refPicked.current = false;\n setOpen(true);\n };\n\n /** Where the focus goes when the calendar closes: the field after a day is chosen, else whatever opened it. */\n const focusAfterClose = (): HTMLElement | null => (refPicked.current || refOpener.current === 'field' ? refInput.current : refButton.current);\n\n /**\n * Text is read as the person leaves the field. A day (or nothing) is taken at once; a message about text that is not a\n * day is shown too, unless they are only on the way to the calendar: a press of the mouse on the button, or focus\n * going into the calendar that Alt+ArrowDown opened, with the text half typed. Leaving the picker for anywhere else\n * reads whatever is typed.\n */\n const handleBlur = (event: FocusEvent<HTMLElement>) => {\n const next = event.relatedTarget as Element | null;\n const intoCalendar = next?.closest('[data-slot=\"popover-content\"]') != null;\n const intoButton = next?.closest('[data-slot=\"date-picker-trigger\"]') != null;\n const typed = refInput.current?.value ?? '';\n if (!(intoCalendar || intoButton || (next !== null && refField.current?.contains(next) === true))) {\n commit(typed);\n return;\n }\n if (event.target !== refInput.current) {\n return;\n }\n const onTheWay = intoCalendar || (intoButton && refPointer.current);\n const parsed = parseDateText(typed, locale);\n const usable = parsed.kind === 'empty' || (parsed.kind === 'day' && dayProblem(parsed.day, { min, max, disabledDates }) === undefined);\n if (usable || !onTheWay) {\n commit(typed);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'ArrowDown' && event.altKey && !disabled) {\n event.preventDefault();\n openFrom('field');\n return;\n }\n // The field has no key handler of Base UI's to close the calendar, so the key would reach the Sheet or Dialog around\n // it and close that too. While the calendar is open it is this field's key; when it is closed it is the page's.\n if (event.key === 'Escape' && open) {\n event.preventDefault();\n event.stopPropagation();\n // The focus is on the field already, whatever opened the calendar: it stays there.\n refOpener.current = 'field';\n setOpen(false);\n return;\n }\n if (event.key === 'Enter') {\n const parsed = parseDateText(event.currentTarget.value, locale);\n if (parsed.kind !== 'empty' && (parsed.kind === 'problem' || dayProblem(parsed.day, { min, max, disabledDates }) !== undefined)) {\n event.preventDefault();\n }\n commit(event.currentTarget.value);\n }\n };\n\n const selected = dayToDate(current);\n const limits = { min, max };\n const invalid = problem !== null || ariaInvalid === true || ariaInvalid === 'true';\n const ownMessage = problem !== null && formError === null;\n const describedBy =\n [...new Set([ariaDescribedBy, ownMessage ? errorId : undefined, formError ?? undefined].filter(Boolean))].join(' ') || undefined;\n const isOff = (date: Date): boolean => {\n const day = dateToDay(date);\n return day !== undefined && dayProblem(day, { min, max, disabledDates }) !== undefined;\n };\n const today = new Date();\n const start = dayToDate(min);\n const end = dayToDate(max);\n const month = selected ?? (start !== undefined && today < start ? start : end !== undefined && today > end ? end : today);\n\n return (\n <div data-slot=\"date-picker\" className={cn('flex flex-col gap-1.5', className)}>\n <div\n ref={refField}\n className=\"relative\"\n onPointerDownCapture={() => {\n refPointer.current = true;\n }}\n onPointerUpCapture={() => {\n refPointer.current = false;\n }}\n >\n <Input\n ref={refInput}\n id={id}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"off\"\n data-slot=\"date-picker-input\"\n value={text}\n placeholder={placeholder ?? pattern}\n disabled={disabled}\n aria-label={ariaLabel}\n aria-invalid={invalid ? true : undefined}\n aria-describedby={describedBy}\n className=\"pr-9\"\n onChange={(event: ChangeEvent<HTMLInputElement>) => {\n setText(event.target.value);\n setProblem(null);\n }}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n />\n <Popover\n open={open}\n onOpenChange={(next) => {\n // Only the button opens it from here (Alt+ArrowDown in the field sets the state itself, through `openFrom`).\n if (next) {\n openFrom('button');\n return;\n }\n setOpen(false);\n }}\n >\n <PopoverTrigger\n render={\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n ref={refButton}\n data-slot=\"date-picker-trigger\"\n aria-label={labels.choose}\n disabled={disabled}\n className=\"absolute top-0.5 right-0.5\"\n onBlur={handleBlur}\n />\n }\n >\n <IconCalendar aria-hidden=\"true\" />\n </PopoverTrigger>\n <PopoverContent anchor={refField} align=\"start\" className=\"w-auto p-0\" finalFocus={focusAfterClose} onBlur={handleBlur}>\n <Calendar\n mode=\"single\"\n required\n autoFocus\n locale={calendarLocale}\n weekStartsOn={firstWeekdayOf(locale)}\n defaultMonth={month}\n startMonth={start}\n endMonth={end}\n selected={selected}\n disabled={isOff}\n onSelect={pick}\n />\n </PopoverContent>\n </Popover>\n </div>\n {ownMessage && (\n <p id={errorId} data-slot=\"date-picker-error\" role=\"alert\" className=\"text-sm text-destructive\">\n {problemText(problem, labels, locale, limits, pattern)}\n </p>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -163,7 +163,7 @@ var FileInput = ({ value, defaultValue = [], onValueChange, accept, multiple = f
163
163
  "data-slot": "file-input-zone",
164
164
  role: "presentation",
165
165
  "data-dragging": dragging || void 0,
166
- className: cn("relative flex flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-control px-4 py-6 text-center text-sm transition-colors dark:bg-input/30", "hover:bg-foreground/4 has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-ring/50", "data-dragging:border-ring data-dragging:bg-foreground/6", invalid && "border-destructive ring-3 ring-destructive/20 dark:border-destructive/50 dark:ring-destructive/40", disabled && "pointer-events-none cursor-not-allowed border-border"),
166
+ className: cn("relative flex flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-control px-4 py-6 text-center text-sm transition-colors dark:bg-input/30", "hover:bg-foreground/4 has-[input:focus-visible]:outline-solid", "data-dragging:border-ring data-dragging:bg-foreground/6", invalid && "border-destructive ring-3 ring-destructive/20 dark:border-destructive/50 dark:ring-destructive/40", disabled && "pointer-events-none cursor-not-allowed border-border"),
167
167
  onDragEnter: onDrag,
168
168
  onDragOver: onDrag,
169
169
  onDragLeave: (event) => {
@@ -1 +1 @@
1
- {"version":3,"file":"file-input.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/file-input/file-input.tsx"],"sourcesContent":["import { Button } from '@edyn/ui/components/ui/button';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconFile, IconUpload, IconX } from '@tabler/icons-react';\nimport { type ChangeEvent, type ComponentPropsWithoutRef, type DragEvent, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';\n\nexport type TFileRejectReason = 'type' | 'size' | 'count';\n\nexport interface IFileRejection {\n file: File;\n reason: TFileRejectReason;\n}\n\n/** The words a FileInput says and announces. Pass the ones to change. */\nexport interface IFileInputLabels {\n /** The prompt in the drop area when it takes one file, before the \"browse\" link. */\n dropOne: string;\n /** The same, when it takes several. */\n dropMany: string;\n browse: string;\n /** The accessible name of the file chooser itself, when the caller gives it no name of its own. */\n chooseOne: string;\n chooseMany: string;\n /** The accessible name of the list of chosen files. */\n list: string;\n remove: (name: string) => string;\n /** Announced when files are added. */\n added: (count: number) => string;\n /** Announced when a file is taken out. */\n removed: (name: string) => string;\n /** Said, and shown, for a file that was refused. */\n rejection: (name: string, reason: TFileRejectReason, limit: string) => string;\n}\n\nexport const DEFAULT_FILE_INPUT_LABELS: IFileInputLabels = {\n dropOne: 'Drag a file here or',\n dropMany: 'Drag files here or',\n browse: 'browse',\n chooseOne: 'Choose a file',\n chooseMany: 'Choose files',\n list: 'Chosen files',\n remove: (name) => `Remove ${name}`,\n added: (count) => (count === 1 ? '1 file added' : `${count} files added`),\n removed: (name) => `${name} removed`,\n rejection: (name, reason, limit) => {\n if (reason === 'type') {\n return `${name} was not added: this type is not accepted`;\n }\n if (reason === 'size') {\n return `${name} was not added: it is larger than ${limit}`;\n }\n return `${name} was not added: ${limit === '1' ? '1 file is' : `${limit} files are`} the most`;\n },\n};\n\nconst UNITS = ['B', 'KB', 'MB', 'GB'] as const;\n\n/** 1,536 bytes as \"1.5 KB\": the size a person reads next to a file's name. */\nexport const formatBytes = (bytes: number): string => {\n let size = bytes;\n let unit = 0;\n while (size >= 1024 && unit < UNITS.length - 1) {\n size /= 1024;\n unit += 1;\n }\n const text = unit === 0 || size >= 10 ? String(Math.round(size)) : size.toFixed(1).replace(/\\.0$/, '');\n return `${text} ${UNITS[unit]}`;\n};\n\n/** Whether a file is one `accept` allows: an extension (`.pdf`), a type (`application/pdf`) or a family of types (`image/*`). No `accept` allows all. */\nexport const matchesAccept = (file: { name: string; type: string }, accept: string | undefined): boolean => {\n const tokens = (accept ?? '')\n .split(',')\n .map((token) => token.trim().toLowerCase())\n .filter((token) => token !== '');\n if (tokens.length === 0) {\n return true;\n }\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return tokens.some((token) => {\n if (token.startsWith('.')) {\n return name.endsWith(token);\n }\n if (token.endsWith('/*')) {\n return type.startsWith(token.slice(0, -1));\n }\n return type === token;\n });\n};\n\nconst sameFile = (a: File, b: File): boolean => a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;\n\nexport interface ISortFilesInput {\n incoming: readonly File[];\n current: readonly File[];\n multiple: boolean;\n accept?: string;\n maxSize?: number;\n maxFiles?: number;\n}\n\n/**\n * Which of the files just offered are taken, and which are refused and why. A file already chosen is skipped without a word. With one\n * file allowed, the first acceptable one replaces what was chosen and the rest are refused as too many.\n */\nexport const sortFiles = ({\n incoming,\n current,\n multiple,\n accept,\n maxSize,\n maxFiles,\n}: ISortFilesInput): { accepted: File[]; rejected: IFileRejection[] } => {\n const accepted: File[] = [];\n const rejected: IFileRejection[] = [];\n const limit = multiple ? maxFiles : 1;\n for (const file of incoming) {\n if (!matchesAccept(file, accept)) {\n rejected.push({ file, reason: 'type' });\n } else if (maxSize !== undefined && file.size > maxSize) {\n rejected.push({ file, reason: 'size' });\n } else if (current.some((chosen) => sameFile(chosen, file)) || accepted.some((taken) => sameFile(taken, file))) {\n } else if (limit !== undefined && (multiple ? current.length : 0) + accepted.length >= limit) {\n rejected.push({ file, reason: 'count' });\n } else {\n accepted.push(file);\n }\n }\n return { accepted, rejected };\n};\n\nexport interface IFileInputProps extends Omit<ComponentPropsWithoutRef<'div'>, 'defaultValue' | 'onChange' | 'children' | 'id'> {\n /** The chosen files. Leave it out and the field keeps them itself. */\n value?: readonly File[];\n defaultValue?: readonly File[];\n onValueChange?: (files: File[]) => void;\n /** What the picker offers, and what a drop is held to: extensions (`.pdf`), types (`application/pdf`) or families (`image/*`). */\n accept?: string;\n /** Several files, added to each other, instead of one that is replaced. */\n multiple?: boolean;\n /** The most a file may weigh, in bytes. */\n maxSize?: number;\n /** The most files that may be chosen. Several-file fields only. */\n maxFiles?: number;\n /** Called with the files that were refused, each with the reason. They are also listed under the field. */\n onReject?: (rejections: IFileRejection[]) => void;\n disabled?: boolean;\n invalid?: boolean;\n required?: boolean;\n /** Submits the chosen files with a form, under this name. */\n name?: string;\n /** For a `<Label htmlFor>` to name the field. */\n id?: string;\n /** What the field accepts, in words (\"PDF or PNG, up to 5 MB\"), read with the field. */\n hint?: ReactNode;\n labels?: Partial<IFileInputLabels>;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\nconst limitText = (reason: TFileRejectReason, maxSize: number | undefined, maxFiles: number | undefined, multiple: boolean): string => {\n if (reason === 'size') {\n return formatBytes(maxSize ?? 0);\n }\n return String(multiple ? (maxFiles ?? 0) : 1);\n};\n\n/**\n * Chooses files by picking them or by dropping them on the field, in the kit's look. It exists so an app does not fall back to\n * `<input type=\"file\">`, which cannot be styled and has no list of what was chosen.\n *\n * The control is a real file input laid over the whole drop area, so Tab, Enter and Space, the system's file dialog, a drop and a\n * screen reader's name for it all come from the browser. What the browser lacks is added around it: the list of chosen files with a\n * button to take each out, the checks against `accept`, `maxSize` and `maxFiles` (a drop skips the dialog's own filter), and a\n * live region that says what was added, removed or refused.\n *\n * Name the field with `aria-label`, `aria-labelledby` or a `<Label htmlFor={id}>`; with none of those it is named \"Choose files\".\n */\nexport const FileInput = ({\n value,\n defaultValue = [],\n onValueChange,\n accept,\n multiple = false,\n maxSize,\n maxFiles,\n onReject,\n disabled = false,\n invalid = false,\n required = false,\n name,\n id,\n hint,\n labels: labelOverrides,\n className,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n ...rest\n}: IFileInputProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_FILE_INPUT_LABELS, ...labelOverrides }), [labelOverrides]);\n const [inner, setInner] = useState<readonly File[]>(defaultValue);\n const files = value ?? inner;\n const [dragging, setDragging] = useState(false);\n const [rejections, setRejections] = useState<IFileRejection[]>([]);\n const [announcement, setAnnouncement] = useState('');\n const refInput = useRef<HTMLInputElement | null>(null);\n const hintId = `${id ?? 'file-input'}-hint`;\n\n // The input's own list follows the chosen files, so a form submits exactly what the list shows.\n useEffect(() => {\n const input = refInput.current;\n if (input === null || typeof DataTransfer === 'undefined') {\n return;\n }\n const transfer = new DataTransfer();\n for (const file of files) {\n transfer.items.add(file);\n }\n input.files = transfer.files;\n }, [files]);\n\n const commit = (next: File[]): void => {\n setInner(next);\n onValueChange?.(next);\n };\n\n const take = (incoming: File[]): void => {\n if (incoming.length === 0) {\n return;\n }\n const { accepted, rejected } = sortFiles({ incoming, current: files, multiple, accept, maxSize, maxFiles });\n setRejections(rejected);\n if (rejected.length > 0) {\n onReject?.(rejected);\n }\n if (accepted.length > 0) {\n commit(multiple ? [...files, ...accepted] : accepted);\n setAnnouncement(labels.added(accepted.length));\n } else {\n setAnnouncement('');\n }\n };\n\n const remove = (index: number): void => {\n const gone = files[index];\n if (gone === undefined) {\n return;\n }\n setRejections([]);\n commit(files.filter((_, at) => at !== index));\n setAnnouncement(labels.removed(gone.name));\n refInput.current?.focus();\n };\n\n const onChange = (event: ChangeEvent<HTMLInputElement>): void => {\n take([...(event.target.files ?? [])]);\n };\n\n const carriesFiles = (event: DragEvent): boolean => event.dataTransfer.types.includes('Files');\n\n const onDrag = (event: DragEvent<HTMLDivElement>): void => {\n if (disabled || !carriesFiles(event)) {\n return;\n }\n event.preventDefault();\n setDragging(true);\n };\n\n const onDrop = (event: DragEvent<HTMLDivElement>): void => {\n if (disabled || !carriesFiles(event)) {\n return;\n }\n // The drop is handled here, once, and not again by the input underneath.\n event.preventDefault();\n setDragging(false);\n take([...event.dataTransfer.files]);\n };\n\n const named = ariaLabel !== undefined || ariaLabelledBy !== undefined || id !== undefined;\n\n return (\n <div data-slot=\"file-input\" className={cn('flex w-full flex-col gap-2', className)} {...rest}>\n {/* biome-ignore lint/a11y/noStaticElementInteractions: a drop is only the pointer's way onto the file input laid over this area, which stays the keyboard and screen-reader way. */}\n <div\n data-slot=\"file-input-zone\"\n role=\"presentation\"\n data-dragging={dragging || undefined}\n className={cn(\n 'relative flex flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-control px-4 py-6 text-center text-sm transition-colors dark:bg-input/30',\n 'hover:bg-foreground/4 has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-ring/50',\n 'data-dragging:border-ring data-dragging:bg-foreground/6',\n invalid && 'border-destructive ring-3 ring-destructive/20 dark:border-destructive/50 dark:ring-destructive/40',\n // The prompt stays readable (4.5:1) the way a disabled Button's text does: no opacity on the text, a muted colour instead.\n disabled && 'pointer-events-none cursor-not-allowed border-border',\n )}\n onDragEnter={onDrag}\n onDragOver={onDrag}\n onDragLeave={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n setDragging(false);\n }\n }}\n onDrop={onDrop}\n >\n <IconUpload aria-hidden=\"true\" className=\"size-5 text-muted-foreground\" />\n <p className={disabled ? 'text-muted-foreground' : 'text-foreground'}>\n {multiple ? labels.dropMany : labels.dropOne} <span className=\"font-medium underline underline-offset-3\">{labels.browse}</span>\n </p>\n {hint !== undefined && (\n <p id={hintId} className=\"text-xs text-muted-foreground\">\n {hint}\n </p>\n )}\n <input\n ref={refInput}\n id={id}\n type=\"file\"\n name={name}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n required={required && files.length === 0}\n aria-label={named ? ariaLabel : multiple ? labels.chooseMany : labels.chooseOne}\n aria-labelledby={ariaLabelledBy}\n aria-describedby={hint === undefined ? undefined : hintId}\n aria-invalid={invalid || undefined}\n className=\"absolute inset-0 size-full cursor-pointer opacity-0 disabled:cursor-not-allowed\"\n onChange={onChange}\n />\n </div>\n {files.length > 0 && (\n <ul data-slot=\"file-input-list\" aria-label={labels.list} className=\"flex flex-col gap-1\">\n {files.map((file, index) => (\n <li\n key={`${file.name}-${file.size}-${file.lastModified}`}\n className=\"flex items-center gap-2 rounded-lg border border-control py-1 pr-1 pl-2 text-sm\"\n >\n <IconFile aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate\">{file.name}</span>\n <span className=\"shrink-0 text-xs text-muted-foreground tabular-nums\">{formatBytes(file.size)}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n disabled={disabled}\n aria-label={labels.remove(file.name)}\n onClick={() => remove(index)}\n >\n <IconX aria-hidden=\"true\" />\n </Button>\n </li>\n ))}\n </ul>\n )}\n {/* One live region: it stays mounted and only its words change, which is what makes a screen reader say them. The refused files are\n the visible part; what was added or removed is for the ear. */}\n <div role=\"status\" data-slot=\"file-input-status\" className=\"text-sm empty:hidden\">\n {rejections.map(({ file, reason }) => (\n <p key={`${file.name}-${reason}`} className=\"text-destructive\">\n {labels.rejection(file.name, reason, limitText(reason, maxSize, maxFiles, multiple))}\n </p>\n ))}\n {announcement !== '' && <span className=\"sr-only\">{announcement}</span>}\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;;AAiCA,IAAa,4BAA8C;CACzD,SAAS;CACT,UAAU;CACV,QAAQ;CACR,WAAW;CACX,YAAY;CACZ,MAAM;CACN,SAAS,SAAS,UAAU;CAC5B,QAAQ,UAAW,UAAU,IAAI,iBAAiB,GAAG,MAAM;CAC3D,UAAU,SAAS,GAAG,KAAK;CAC3B,YAAY,MAAM,QAAQ,UAAU;EAClC,IAAI,WAAW,QACb,OAAO,GAAG,KAAK;EAEjB,IAAI,WAAW,QACb,OAAO,GAAG,KAAK,oCAAoC;EAErD,OAAO,GAAG,KAAK,kBAAkB,UAAU,MAAM,cAAc,GAAG,MAAM,YAAY;CACtF;AACF;AAEA,IAAM,QAAQ;CAAC;CAAK;CAAM;CAAM;AAAI;;AAGpC,IAAa,eAAe,UAA0B;CACpD,IAAI,OAAO;CACX,IAAI,OAAO;CACX,OAAO,QAAQ,QAAQ,OAAO,MAAM,SAAS,GAAG;EAC9C,QAAQ;EACR,QAAQ;CACV;CAEA,OAAO,GADM,SAAS,KAAK,QAAQ,KAAK,OAAO,KAAK,MAAM,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,QAAQ,EAAE,EACtF,GAAG,MAAM;AAC1B;;AAGA,IAAa,iBAAiB,MAAsC,WAAwC;CAC1G,MAAM,UAAU,UAAU,GAAA,CACvB,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAC1C,QAAQ,UAAU,UAAU,EAAE;CACjC,IAAI,OAAO,WAAW,GACpB,OAAO;CAET,MAAM,OAAO,KAAK,KAAK,YAAY;CACnC,MAAM,OAAO,KAAK,KAAK,YAAY;CACnC,OAAO,OAAO,MAAM,UAAU;EAC5B,IAAI,MAAM,WAAW,GAAG,GACtB,OAAO,KAAK,SAAS,KAAK;EAE5B,IAAI,MAAM,SAAS,IAAI,GACrB,OAAO,KAAK,WAAW,MAAM,MAAM,GAAG,EAAE,CAAC;EAE3C,OAAO,SAAS;CAClB,CAAC;AACH;AAEA,IAAM,YAAY,GAAS,MAAqB,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,iBAAiB,EAAE;;;;;AAe/G,IAAa,aAAa,EACxB,UACA,SACA,UACA,QACA,SACA,eACuE;CACvE,MAAM,WAAmB,CAAC;CAC1B,MAAM,WAA6B,CAAC;CACpC,MAAM,QAAQ,WAAW,WAAW;CACpC,KAAK,MAAM,QAAQ,UACjB,IAAI,CAAC,cAAc,MAAM,MAAM,GAC7B,SAAS,KAAK;EAAE;EAAM,QAAQ;CAAO,CAAC;MACjC,IAAI,YAAY,KAAA,KAAa,KAAK,OAAO,SAC9C,SAAS,KAAK;EAAE;EAAM,QAAQ;CAAO,CAAC;MACjC,IAAI,QAAQ,MAAM,WAAW,SAAS,QAAQ,IAAI,CAAC,KAAK,SAAS,MAAM,UAAU,SAAS,OAAO,IAAI,CAAC,GAAG,CAChH,OAAO,IAAI,UAAU,KAAA,MAAc,WAAW,QAAQ,SAAS,KAAK,SAAS,UAAU,OACrF,SAAS,KAAK;EAAE;EAAM,QAAQ;CAAQ,CAAC;MAEvC,SAAS,KAAK,IAAI;CAGtB,OAAO;EAAE;EAAU;CAAS;AAC9B;AA+BA,IAAM,aAAa,QAA2B,SAA6B,UAA8B,aAA8B;CACrI,IAAI,WAAW,QACb,OAAO,YAAY,WAAW,CAAC;CAEjC,OAAO,OAAO,WAAY,YAAY,IAAK,CAAC;AAC9C;;;;;;;;;;;;AAaA,IAAa,aAAa,EACxB,OACA,eAAe,CAAC,GAChB,eACA,QACA,WAAW,OACX,SACA,UACA,UACA,WAAW,OACX,UAAU,OACV,WAAW,OACX,MACA,IACA,MACA,QAAQ,gBACR,WACA,cAAc,WACd,mBAAmB,gBACnB,GAAG,WACkB;CACrB,MAAM,SAAS,eAAe;EAAE,GAAG;EAA2B,GAAG;CAAe,IAAI,CAAC,cAAc,CAAC;CACpG,MAAM,CAAC,OAAO,YAAY,SAA0B,YAAY;CAChE,MAAM,QAAQ,SAAS;CACvB,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,CAAC,YAAY,iBAAiB,SAA2B,CAAC,CAAC;CACjE,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CACnD,MAAM,WAAW,OAAgC,IAAI;CACrD,MAAM,SAAS,GAAG,MAAM,aAAa;CAGrC,gBAAgB;EACd,MAAM,QAAQ,SAAS;EACvB,IAAI,UAAU,QAAQ,OAAO,iBAAiB,aAC5C;EAEF,MAAM,WAAW,IAAI,aAAa;EAClC,KAAK,MAAM,QAAQ,OACjB,SAAS,MAAM,IAAI,IAAI;EAEzB,MAAM,QAAQ,SAAS;CACzB,GAAG,CAAC,KAAK,CAAC;CAEV,MAAM,UAAU,SAAuB;EACrC,SAAS,IAAI;EACb,gBAAgB,IAAI;CACtB;CAEA,MAAM,QAAQ,aAA2B;EACvC,IAAI,SAAS,WAAW,GACtB;EAEF,MAAM,EAAE,UAAU,aAAa,UAAU;GAAE;GAAU,SAAS;GAAO;GAAU;GAAQ;GAAS;EAAS,CAAC;EAC1G,cAAc,QAAQ;EACtB,IAAI,SAAS,SAAS,GACpB,WAAW,QAAQ;EAErB,IAAI,SAAS,SAAS,GAAG;GACvB,OAAO,WAAW,CAAC,GAAG,OAAO,GAAG,QAAQ,IAAI,QAAQ;GACpD,gBAAgB,OAAO,MAAM,SAAS,MAAM,CAAC;EAC/C,OACE,gBAAgB,EAAE;CAEtB;CAEA,MAAM,UAAU,UAAwB;EACtC,MAAM,OAAO,MAAM;EACnB,IAAI,SAAS,KAAA,GACX;EAEF,cAAc,CAAC,CAAC;EAChB,OAAO,MAAM,QAAQ,GAAG,OAAO,OAAO,KAAK,CAAC;EAC5C,gBAAgB,OAAO,QAAQ,KAAK,IAAI,CAAC;EACzC,SAAS,SAAS,MAAM;CAC1B;CAEA,MAAM,YAAY,UAA+C;EAC/D,KAAK,CAAC,GAAI,MAAM,OAAO,SAAS,CAAC,CAAE,CAAC;CACtC;CAEA,MAAM,gBAAgB,UAA8B,MAAM,aAAa,MAAM,SAAS,OAAO;CAE7F,MAAM,UAAU,UAA2C;EACzD,IAAI,YAAY,CAAC,aAAa,KAAK,GACjC;EAEF,MAAM,eAAe;EACrB,YAAY,IAAI;CAClB;CAEA,MAAM,UAAU,UAA2C;EACzD,IAAI,YAAY,CAAC,aAAa,KAAK,GACjC;EAGF,MAAM,eAAe;EACrB,YAAY,KAAK;EACjB,KAAK,CAAC,GAAG,MAAM,aAAa,KAAK,CAAC;CACpC;CAEA,MAAM,QAAQ,cAAc,KAAA,KAAa,mBAAmB,KAAA,KAAa,OAAO,KAAA;CAEhF,OACE,qBAAC,OAAD;EAAK,aAAU;EAAa,WAAW,GAAG,8BAA8B,SAAS;EAAG,GAAI;EAAxF,UAAA;GAEE,qBAAC,OAAD;IACE,aAAU;IACV,MAAK;IACL,iBAAe,YAAY,KAAA;IAC3B,WAAW,GACT,4KACA,uIACA,2DACA,WAAW,qGAEX,YAAY,sDACd;IACA,aAAa;IACb,YAAY;IACZ,cAAc,UAAU;KACtB,IAAI,CAAC,MAAM,cAAc,SAAS,MAAM,aAA4B,GAClE,YAAY,KAAK;IAErB;IACQ;IAnBV,UAAA;KAqBE,oBAAC,YAAD;MAAY,eAAY;MAAO,WAAU;KAAgC,CAAA;KACzE,qBAAC,KAAD;MAAG,WAAW,WAAW,0BAA0B;MAAnD,UAAA;OACG,WAAW,OAAO,WAAW,OAAO;OAAQ;OAAC,oBAAC,QAAD;QAAM,WAAU;QAA4C,UAAA,OAAO;OAAa,CAAA;MAC7H;;KACF,SAAS,KAAA,KACR,oBAAC,KAAD;MAAG,IAAI;MAAQ,WAAU;MACtB,UAAA;KACA,CAAA;KAEL,oBAAC,SAAD;MACE,KAAK;MACD;MACJ,MAAK;MACC;MACE;MACE;MACA;MACV,UAAU,YAAY,MAAM,WAAW;MACvC,cAAY,QAAQ,YAAY,WAAW,OAAO,aAAa,OAAO;MACtE,mBAAiB;MACjB,oBAAkB,SAAS,KAAA,IAAY,KAAA,IAAY;MACnD,gBAAc,WAAW,KAAA;MACzB,WAAU;MACA;KACX,CAAA;IACE;;GACJ,MAAM,SAAS,KACd,oBAAC,MAAD;IAAI,aAAU;IAAkB,cAAY,OAAO;IAAM,WAAU;IAChE,UAAA,MAAM,KAAK,MAAM,UAChB,qBAAC,MAAD;KAEE,WAAU;KAFZ,UAAA;MAIE,oBAAC,UAAD;OAAU,eAAY;OAAO,WAAU;MAAyC,CAAA;MAChF,oBAAC,QAAD;OAAM,WAAU;OAA2B,UAAA,KAAK;MAAW,CAAA;MAC3D,oBAAC,QAAD;OAAM,WAAU;OAAuD,UAAA,YAAY,KAAK,IAAI;MAAQ,CAAA;MACpG,oBAAC,QAAD;OACE,MAAK;OACL,SAAQ;OACR,MAAK;OACK;OACV,cAAY,OAAO,OAAO,KAAK,IAAI;OACnC,eAAe,OAAO,KAAK;OAE3B,UAAA,oBAAC,OAAD,EAAO,eAAY,OAAQ,CAAA;MACrB,CAAA;KACN;IAhBG,GAAA,GAAG,KAAK,KAAK,GAAG,KAAK,KAAK,GAAG,KAAK,cAgBrC,CACL;GACC,CAAA;GAIN,qBAAC,OAAD;IAAK,MAAK;IAAS,aAAU;IAAoB,WAAU;IAA3D,UAAA,CACG,WAAW,KAAK,EAAE,MAAM,aACvB,oBAAC,KAAD;KAAkC,WAAU;KACzC,UAAA,OAAO,UAAU,KAAK,MAAM,QAAQ,UAAU,QAAQ,SAAS,UAAU,QAAQ,CAAC;IAClF,GAFK,GAAG,KAAK,KAAK,GAAG,QAErB,CACJ,GACA,iBAAiB,MAAM,oBAAC,QAAD;KAAM,WAAU;KAAW,UAAA;IAAmB,CAAA,CACnE;;EACF;;AAET"}
1
+ {"version":3,"file":"file-input.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/file-input/file-input.tsx"],"sourcesContent":["import { Button } from '@edyn/ui/components/ui/button';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconFile, IconUpload, IconX } from '@tabler/icons-react';\nimport { type ChangeEvent, type ComponentPropsWithoutRef, type DragEvent, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';\n\nexport type TFileRejectReason = 'type' | 'size' | 'count';\n\nexport interface IFileRejection {\n file: File;\n reason: TFileRejectReason;\n}\n\n/** The words a FileInput says and announces. Pass the ones to change. */\nexport interface IFileInputLabels {\n /** The prompt in the drop area when it takes one file, before the \"browse\" link. */\n dropOne: string;\n /** The same, when it takes several. */\n dropMany: string;\n browse: string;\n /** The accessible name of the file chooser itself, when the caller gives it no name of its own. */\n chooseOne: string;\n chooseMany: string;\n /** The accessible name of the list of chosen files. */\n list: string;\n remove: (name: string) => string;\n /** Announced when files are added. */\n added: (count: number) => string;\n /** Announced when a file is taken out. */\n removed: (name: string) => string;\n /** Said, and shown, for a file that was refused. */\n rejection: (name: string, reason: TFileRejectReason, limit: string) => string;\n}\n\nexport const DEFAULT_FILE_INPUT_LABELS: IFileInputLabels = {\n dropOne: 'Drag a file here or',\n dropMany: 'Drag files here or',\n browse: 'browse',\n chooseOne: 'Choose a file',\n chooseMany: 'Choose files',\n list: 'Chosen files',\n remove: (name) => `Remove ${name}`,\n added: (count) => (count === 1 ? '1 file added' : `${count} files added`),\n removed: (name) => `${name} removed`,\n rejection: (name, reason, limit) => {\n if (reason === 'type') {\n return `${name} was not added: this type is not accepted`;\n }\n if (reason === 'size') {\n return `${name} was not added: it is larger than ${limit}`;\n }\n return `${name} was not added: ${limit === '1' ? '1 file is' : `${limit} files are`} the most`;\n },\n};\n\nconst UNITS = ['B', 'KB', 'MB', 'GB'] as const;\n\n/** 1,536 bytes as \"1.5 KB\": the size a person reads next to a file's name. */\nexport const formatBytes = (bytes: number): string => {\n let size = bytes;\n let unit = 0;\n while (size >= 1024 && unit < UNITS.length - 1) {\n size /= 1024;\n unit += 1;\n }\n const text = unit === 0 || size >= 10 ? String(Math.round(size)) : size.toFixed(1).replace(/\\.0$/, '');\n return `${text} ${UNITS[unit]}`;\n};\n\n/** Whether a file is one `accept` allows: an extension (`.pdf`), a type (`application/pdf`) or a family of types (`image/*`). No `accept` allows all. */\nexport const matchesAccept = (file: { name: string; type: string }, accept: string | undefined): boolean => {\n const tokens = (accept ?? '')\n .split(',')\n .map((token) => token.trim().toLowerCase())\n .filter((token) => token !== '');\n if (tokens.length === 0) {\n return true;\n }\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return tokens.some((token) => {\n if (token.startsWith('.')) {\n return name.endsWith(token);\n }\n if (token.endsWith('/*')) {\n return type.startsWith(token.slice(0, -1));\n }\n return type === token;\n });\n};\n\nconst sameFile = (a: File, b: File): boolean => a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;\n\nexport interface ISortFilesInput {\n incoming: readonly File[];\n current: readonly File[];\n multiple: boolean;\n accept?: string;\n maxSize?: number;\n maxFiles?: number;\n}\n\n/**\n * Which of the files just offered are taken, and which are refused and why. A file already chosen is skipped without a word. With one\n * file allowed, the first acceptable one replaces what was chosen and the rest are refused as too many.\n */\nexport const sortFiles = ({\n incoming,\n current,\n multiple,\n accept,\n maxSize,\n maxFiles,\n}: ISortFilesInput): { accepted: File[]; rejected: IFileRejection[] } => {\n const accepted: File[] = [];\n const rejected: IFileRejection[] = [];\n const limit = multiple ? maxFiles : 1;\n for (const file of incoming) {\n if (!matchesAccept(file, accept)) {\n rejected.push({ file, reason: 'type' });\n } else if (maxSize !== undefined && file.size > maxSize) {\n rejected.push({ file, reason: 'size' });\n } else if (current.some((chosen) => sameFile(chosen, file)) || accepted.some((taken) => sameFile(taken, file))) {\n } else if (limit !== undefined && (multiple ? current.length : 0) + accepted.length >= limit) {\n rejected.push({ file, reason: 'count' });\n } else {\n accepted.push(file);\n }\n }\n return { accepted, rejected };\n};\n\nexport interface IFileInputProps extends Omit<ComponentPropsWithoutRef<'div'>, 'defaultValue' | 'onChange' | 'children' | 'id'> {\n /** The chosen files. Leave it out and the field keeps them itself. */\n value?: readonly File[];\n defaultValue?: readonly File[];\n onValueChange?: (files: File[]) => void;\n /** What the picker offers, and what a drop is held to: extensions (`.pdf`), types (`application/pdf`) or families (`image/*`). */\n accept?: string;\n /** Several files, added to each other, instead of one that is replaced. */\n multiple?: boolean;\n /** The most a file may weigh, in bytes. */\n maxSize?: number;\n /** The most files that may be chosen. Several-file fields only. */\n maxFiles?: number;\n /** Called with the files that were refused, each with the reason. They are also listed under the field. */\n onReject?: (rejections: IFileRejection[]) => void;\n disabled?: boolean;\n invalid?: boolean;\n required?: boolean;\n /** Submits the chosen files with a form, under this name. */\n name?: string;\n /** For a `<Label htmlFor>` to name the field. */\n id?: string;\n /** What the field accepts, in words (\"PDF or PNG, up to 5 MB\"), read with the field. */\n hint?: ReactNode;\n labels?: Partial<IFileInputLabels>;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\nconst limitText = (reason: TFileRejectReason, maxSize: number | undefined, maxFiles: number | undefined, multiple: boolean): string => {\n if (reason === 'size') {\n return formatBytes(maxSize ?? 0);\n }\n return String(multiple ? (maxFiles ?? 0) : 1);\n};\n\n/**\n * Chooses files by picking them or by dropping them on the field, in the kit's look. It exists so an app does not fall back to\n * `<input type=\"file\">`, which cannot be styled and has no list of what was chosen.\n *\n * The control is a real file input laid over the whole drop area, so Tab, Enter and Space, the system's file dialog, a drop and a\n * screen reader's name for it all come from the browser. What the browser lacks is added around it: the list of chosen files with a\n * button to take each out, the checks against `accept`, `maxSize` and `maxFiles` (a drop skips the dialog's own filter), and a\n * live region that says what was added, removed or refused.\n *\n * Name the field with `aria-label`, `aria-labelledby` or a `<Label htmlFor={id}>`; with none of those it is named \"Choose files\".\n */\nexport const FileInput = ({\n value,\n defaultValue = [],\n onValueChange,\n accept,\n multiple = false,\n maxSize,\n maxFiles,\n onReject,\n disabled = false,\n invalid = false,\n required = false,\n name,\n id,\n hint,\n labels: labelOverrides,\n className,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n ...rest\n}: IFileInputProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_FILE_INPUT_LABELS, ...labelOverrides }), [labelOverrides]);\n const [inner, setInner] = useState<readonly File[]>(defaultValue);\n const files = value ?? inner;\n const [dragging, setDragging] = useState(false);\n const [rejections, setRejections] = useState<IFileRejection[]>([]);\n const [announcement, setAnnouncement] = useState('');\n const refInput = useRef<HTMLInputElement | null>(null);\n const hintId = `${id ?? 'file-input'}-hint`;\n\n // The input's own list follows the chosen files, so a form submits exactly what the list shows.\n useEffect(() => {\n const input = refInput.current;\n if (input === null || typeof DataTransfer === 'undefined') {\n return;\n }\n const transfer = new DataTransfer();\n for (const file of files) {\n transfer.items.add(file);\n }\n input.files = transfer.files;\n }, [files]);\n\n const commit = (next: File[]): void => {\n setInner(next);\n onValueChange?.(next);\n };\n\n const take = (incoming: File[]): void => {\n if (incoming.length === 0) {\n return;\n }\n const { accepted, rejected } = sortFiles({ incoming, current: files, multiple, accept, maxSize, maxFiles });\n setRejections(rejected);\n if (rejected.length > 0) {\n onReject?.(rejected);\n }\n if (accepted.length > 0) {\n commit(multiple ? [...files, ...accepted] : accepted);\n setAnnouncement(labels.added(accepted.length));\n } else {\n setAnnouncement('');\n }\n };\n\n const remove = (index: number): void => {\n const gone = files[index];\n if (gone === undefined) {\n return;\n }\n setRejections([]);\n commit(files.filter((_, at) => at !== index));\n setAnnouncement(labels.removed(gone.name));\n refInput.current?.focus();\n };\n\n const onChange = (event: ChangeEvent<HTMLInputElement>): void => {\n take([...(event.target.files ?? [])]);\n };\n\n const carriesFiles = (event: DragEvent): boolean => event.dataTransfer.types.includes('Files');\n\n const onDrag = (event: DragEvent<HTMLDivElement>): void => {\n if (disabled || !carriesFiles(event)) {\n return;\n }\n event.preventDefault();\n setDragging(true);\n };\n\n const onDrop = (event: DragEvent<HTMLDivElement>): void => {\n if (disabled || !carriesFiles(event)) {\n return;\n }\n // The drop is handled here, once, and not again by the input underneath.\n event.preventDefault();\n setDragging(false);\n take([...event.dataTransfer.files]);\n };\n\n const named = ariaLabel !== undefined || ariaLabelledBy !== undefined || id !== undefined;\n\n return (\n <div data-slot=\"file-input\" className={cn('flex w-full flex-col gap-2', className)} {...rest}>\n {/* biome-ignore lint/a11y/noStaticElementInteractions: a drop is only the pointer's way onto the file input laid over this area, which stays the keyboard and screen-reader way. */}\n <div\n data-slot=\"file-input-zone\"\n role=\"presentation\"\n data-dragging={dragging || undefined}\n className={cn(\n 'relative flex flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-control px-4 py-6 text-center text-sm transition-colors dark:bg-input/30',\n 'hover:bg-foreground/4 has-[input:focus-visible]:outline-solid',\n 'data-dragging:border-ring data-dragging:bg-foreground/6',\n invalid && 'border-destructive ring-3 ring-destructive/20 dark:border-destructive/50 dark:ring-destructive/40',\n // The prompt stays readable (4.5:1) the way a disabled Button's text does: no opacity on the text, a muted colour instead.\n disabled && 'pointer-events-none cursor-not-allowed border-border',\n )}\n onDragEnter={onDrag}\n onDragOver={onDrag}\n onDragLeave={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n setDragging(false);\n }\n }}\n onDrop={onDrop}\n >\n <IconUpload aria-hidden=\"true\" className=\"size-5 text-muted-foreground\" />\n <p className={disabled ? 'text-muted-foreground' : 'text-foreground'}>\n {multiple ? labels.dropMany : labels.dropOne} <span className=\"font-medium underline underline-offset-3\">{labels.browse}</span>\n </p>\n {hint !== undefined && (\n <p id={hintId} className=\"text-xs text-muted-foreground\">\n {hint}\n </p>\n )}\n <input\n ref={refInput}\n id={id}\n type=\"file\"\n name={name}\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n required={required && files.length === 0}\n aria-label={named ? ariaLabel : multiple ? labels.chooseMany : labels.chooseOne}\n aria-labelledby={ariaLabelledBy}\n aria-describedby={hint === undefined ? undefined : hintId}\n aria-invalid={invalid || undefined}\n className=\"absolute inset-0 size-full cursor-pointer opacity-0 disabled:cursor-not-allowed\"\n onChange={onChange}\n />\n </div>\n {files.length > 0 && (\n <ul data-slot=\"file-input-list\" aria-label={labels.list} className=\"flex flex-col gap-1\">\n {files.map((file, index) => (\n <li\n key={`${file.name}-${file.size}-${file.lastModified}`}\n className=\"flex items-center gap-2 rounded-lg border border-control py-1 pr-1 pl-2 text-sm\"\n >\n <IconFile aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate\">{file.name}</span>\n <span className=\"shrink-0 text-xs text-muted-foreground tabular-nums\">{formatBytes(file.size)}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n disabled={disabled}\n aria-label={labels.remove(file.name)}\n onClick={() => remove(index)}\n >\n <IconX aria-hidden=\"true\" />\n </Button>\n </li>\n ))}\n </ul>\n )}\n {/* One live region: it stays mounted and only its words change, which is what makes a screen reader say them. The refused files are\n the visible part; what was added or removed is for the ear. */}\n <div role=\"status\" data-slot=\"file-input-status\" className=\"text-sm empty:hidden\">\n {rejections.map(({ file, reason }) => (\n <p key={`${file.name}-${reason}`} className=\"text-destructive\">\n {labels.rejection(file.name, reason, limitText(reason, maxSize, maxFiles, multiple))}\n </p>\n ))}\n {announcement !== '' && <span className=\"sr-only\">{announcement}</span>}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -46,7 +46,7 @@ var summarize = (chosen, placeholder, labels, summary = "selection") => {
46
46
  /** Every chosen name, for a tooltip on a trigger whose text is cut. With `label` the names are not shown, so it is the text itself. */
47
47
  var fullSelection = (chosen, placeholder, labels, summary = "selection") => summary === "selection" && chosen.length > 0 ? chosen.map((option) => option.label).join(", ") : summarize(chosen, placeholder, labels, summary);
48
48
  var POPUP = cn("flex max-h-(--available-height) origin-(--transform-origin) flex-col overflow-hidden duration-100", POPUP_FRAME_CLASS, POPUP_LIST_WIDTH_CLASS, "motion-reduce:animate-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95");
49
- var TRIGGER = "flex h-8 w-full min-w-0 cursor-pointer items-center justify-between gap-2 rounded-lg border border-control bg-transparent px-2.5 text-sm text-foreground transition-colors outline-none focus-visible:border-ring disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive data-popup-open:border-ring dark:bg-input/30";
49
+ var TRIGGER = "flex h-8 w-full min-w-0 cursor-pointer items-center justify-between gap-2 rounded-lg border border-control bg-transparent px-2.5 text-sm text-foreground transition-colors outline-none disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive data-popup-open:outline-solid dark:bg-input/30";
50
50
  var Option = ({ option, index, style, measure, highlighted }) => {
51
51
  const [cut, setCut] = useState(false);
52
52
  const measureLabel = useCallback((element) => {