@lerianstudio/sindarian-ui 2.0.0-beta.1 → 2.0.0-beta.3

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 (42) hide show
  1. package/dist/components/form/file-upload-field/index.d.ts +11 -2
  2. package/dist/components/form/file-upload-field/index.d.ts.map +1 -1
  3. package/dist/components/form/file-upload-field/index.js +8 -3
  4. package/dist/components/form/input-field/index.d.ts +41 -2
  5. package/dist/components/form/input-field/index.d.ts.map +1 -1
  6. package/dist/components/form/input-field/index.js +73 -15
  7. package/dist/components/form/select-field/index.d.ts +9 -1
  8. package/dist/components/form/select-field/index.d.ts.map +1 -1
  9. package/dist/components/form/select-field/index.js +2 -2
  10. package/dist/components/ui/button/styles.css +31 -1
  11. package/dist/components/ui/calendar/index.d.ts.map +1 -1
  12. package/dist/components/ui/calendar/index.js +7 -1
  13. package/dist/components/ui/file-upload/index.d.ts +235 -9
  14. package/dist/components/ui/file-upload/index.d.ts.map +1 -1
  15. package/dist/components/ui/file-upload/index.js +328 -13
  16. package/dist/components/ui/form.d.ts +4 -0
  17. package/dist/components/ui/form.d.ts.map +1 -1
  18. package/dist/components/ui/form.js +48 -6
  19. package/dist/components/ui/toggle-group/index.d.ts +1 -1
  20. package/dist/components/ui/toggle-group/index.d.ts.map +1 -1
  21. package/dist/components/ui/toggle-group/index.js +73 -4
  22. package/dist/esm/components/form/file-upload-field/index.d.ts +11 -2
  23. package/dist/esm/components/form/file-upload-field/index.d.ts.map +1 -1
  24. package/dist/esm/components/form/file-upload-field/index.js +8 -3
  25. package/dist/esm/components/form/input-field/index.d.ts +41 -2
  26. package/dist/esm/components/form/input-field/index.d.ts.map +1 -1
  27. package/dist/esm/components/form/input-field/index.js +73 -15
  28. package/dist/esm/components/form/select-field/index.d.ts +9 -1
  29. package/dist/esm/components/form/select-field/index.d.ts.map +1 -1
  30. package/dist/esm/components/form/select-field/index.js +2 -2
  31. package/dist/esm/components/ui/calendar/index.d.ts.map +1 -1
  32. package/dist/esm/components/ui/calendar/index.js +8 -2
  33. package/dist/esm/components/ui/file-upload/index.d.ts +235 -9
  34. package/dist/esm/components/ui/file-upload/index.d.ts.map +1 -1
  35. package/dist/esm/components/ui/file-upload/index.js +327 -13
  36. package/dist/esm/components/ui/form.d.ts +4 -0
  37. package/dist/esm/components/ui/form.d.ts.map +1 -1
  38. package/dist/esm/components/ui/form.js +48 -6
  39. package/dist/esm/components/ui/toggle-group/index.d.ts +1 -1
  40. package/dist/esm/components/ui/toggle-group/index.d.ts.map +1 -1
  41. package/dist/esm/components/ui/toggle-group/index.js +73 -4
  42. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { Control, FieldPathByValue, FieldValues } from 'react-hook-form';
3
- import { type FileUploadResult } from '../../../components/ui/file-upload/index.js';
3
+ import { type FileUploadError, type FileUploadLabels, type FileUploadResult } from '../../../components/ui/file-upload/index.js';
4
4
  import { type RenderableLabel } from '../../../components/form/renderable-label.js';
5
5
  type FileUploadFieldOwnProps<T extends FieldValues = FieldValues> = {
6
6
  control: Control<T>;
@@ -17,6 +17,15 @@ type FileUploadFieldOwnProps<T extends FieldValues = FieldValues> = {
17
17
  maxSizeBytes?: number;
18
18
  disabled?: boolean;
19
19
  className?: string;
20
+ /**
21
+ * Override the picker's user-visible copy. Threaded straight to FileUpload;
22
+ * omitted fields keep the English defaults. Note `labels.error` governs the
23
+ * PICKER's own size/type/read refusal only — resolver messages still come
24
+ * from the schema through FormMessage.
25
+ */
26
+ labels?: FileUploadLabels;
27
+ /** Observe size, type, and read failures after the form value is cleared. */
28
+ onError?: (error: FileUploadError) => void;
20
29
  /** Optional escape hatch to also observe the picked File/clear alongside the form's text value. */
21
30
  onSelect?: (result: FileUploadResult | null) => void;
22
31
  };
@@ -47,6 +56,6 @@ export type FileUploadFieldProps<T extends FieldValues = FieldValues> = FileUplo
47
56
  * rejection; on resolver failure the FormMessage announces. Both surfaces are
48
57
  * wired.
49
58
  */
50
- export declare const FileUploadField: <T extends FieldValues = FieldValues>({ control, name, label, description, tooltip, required, accept, maxSizeBytes, disabled, className, onSelect, "aria-label": ariaLabelProp }: FileUploadFieldProps<T>) => import("react").JSX.Element;
59
+ export declare const FileUploadField: <T extends FieldValues = FieldValues>({ control, name, label, description, tooltip, required, accept, maxSizeBytes, disabled, className, labels, onError, onSelect, "aria-label": ariaLabelProp }: FileUploadFieldProps<T>) => import("react").JSX.Element;
51
60
  export {};
52
61
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/file-upload-field/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAY,MAAM,OAAO,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAWxE,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAC/E,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,oCAAoC,CAAA;AAE3C,KAAK,uBAAuB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAAI;IAClE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACnB;;;;OAIG;IACH,IAAI,EAAE,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC7C,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mGAAmG;IACnG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,CAAA;CACrD,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAClE,uBAAuB,CAAC,CAAC,CAAC,GACxB,CACI;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,KAAK,CAAC,EAAE,KAAK,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAC1C,CAAA;AAEL;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,4IAalE,oBAAoB,CAAC,CAAC,CAAC,gCA8EzB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/file-upload-field/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAY,MAAM,OAAO,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAWxE,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACtB,MAAM,6BAA6B,CAAA;AACpC,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,oCAAoC,CAAA;AAE3C,KAAK,uBAAuB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAAI;IAClE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACnB;;;;OAIG;IACH,IAAI,EAAE,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC7C,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IAC1C,mGAAmG;IACnG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,CAAA;CACrD,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAClE,uBAAuB,CAAC,CAAC,CAAC,GACxB,CACI;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,KAAK,CAAC,EAAE,KAAK,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAC1C,CAAA;AAEL;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,6JAelE,oBAAoB,CAAC,CAAC,CAAC,gCAoFzB,CAAA"}
@@ -19,7 +19,7 @@ import { hasRenderableLabel } from '../../../components/form/renderable-label.js
19
19
  * rejection; on resolver failure the FormMessage announces. Both surfaces are
20
20
  * wired.
21
21
  */
22
- export const FileUploadField = ({ control, name, label, description, tooltip, required, accept, maxSizeBytes, disabled, className, onSelect, 'aria-label': ariaLabelProp }) => {
22
+ export const FileUploadField = ({ control, name, label, description, tooltip, required, accept, maxSizeBytes, disabled, className, labels, onError, onSelect, 'aria-label': ariaLabelProp }) => {
23
23
  const [localValue, setLocalValue] = useState(null);
24
24
  const showLabel = hasRenderableLabel(label);
25
25
  // An empty or whitespace-only aria-label is worse than none: it names the
@@ -51,10 +51,15 @@ export const FileUploadField = ({ control, name, label, description, tooltip, re
51
51
  // value carries nothing else to tell them apart. Fix needs a reset epoch
52
52
  // from react-hook-form, or a form value richer than the file text.
53
53
  const shown = localValue && field.value === localValue.text ? localValue : null;
54
- return (_jsxs(FormItem, { required: required, children: [showLabel && (_jsx(FormLabel, { extra: tooltip ? _jsx(FormTooltip, { children: tooltip }) : undefined, children: label })), _jsx(FormControl, { children: _jsx(FileUpload, { ref: field.ref, name: field.name, onBlur: field.onBlur, "aria-label": ariaLabel, accept: accept, maxSizeBytes: maxSizeBytes, disabled: disabled, className: className, value: shown, onSelect: (result) => {
54
+ return (_jsxs(FormItem, { required: required, children: [showLabel && (_jsx(FormLabel, { extra: tooltip ? _jsx(FormTooltip, { children: tooltip }) : undefined, children: label })), _jsx(FormControl, { children: _jsx(FileUpload, { ref: field.ref, name: field.name, onBlur: field.onBlur, "aria-label": ariaLabel, labels: labels, accept: accept, maxSizeBytes: maxSizeBytes, disabled: disabled, className: className, value: shown, onSelect: (result) => {
55
55
  setLocalValue(result);
56
56
  field.onChange(result?.text ?? '');
57
57
  onSelect?.(result);
58
- }, onError: () => field.onChange('') }) }), _jsx(FormMessage, {}), description && _jsx(FormDescription, { children: description })] }));
58
+ }, onError: (error) => {
59
+ setLocalValue(null);
60
+ field.onChange('');
61
+ onSelect?.(null);
62
+ onError?.(error);
63
+ } }) }), _jsx(FormMessage, {}), description && _jsx(FormDescription, { children: description })] }));
59
64
  } }));
60
65
  };
@@ -1,9 +1,48 @@
1
- import { HTMLInputTypeAttribute, ReactNode } from 'react';
1
+ import { type InputRef } from '../../../components/ui/input/index.js';
2
+ import { HTMLInputTypeAttribute, ReactNode, Ref } from 'react';
2
3
  import { Control, FieldValues } from 'react-hook-form';
3
4
  export type InputFieldProps<T extends FieldValues = FieldValues> = {
4
5
  className?: string;
5
6
  name: string;
6
7
  type?: HTMLInputTypeAttribute;
8
+ /**
9
+ * Announce the field's own invalidity. Reaches the rendered control on BOTH
10
+ * branches, so a field with no `control` — and therefore no react-hook-form
11
+ * error state — can still tell a screen reader it is invalid.
12
+ *
13
+ * ORed with the form's error, never overriding it: `false` here cannot
14
+ * suppress a live resolver error. Nobody asked to mute a real validation
15
+ * failure, and on a financial console a muted one is a wrong number reaching
16
+ * a money path.
17
+ *
18
+ * TypeScript exempts hyphenated JSX attributes from excess-property
19
+ * checking, so passing this used to compile and then vanish.
20
+ */
21
+ 'aria-invalid'?: boolean;
22
+ /** Lower bound, forwarded to the single-line control on both branches. */
23
+ min?: number | string;
24
+ /** Upper bound, forwarded to the single-line control on both branches. */
25
+ max?: number | string;
26
+ /** Increment, forwarded to the single-line control on both branches. */
27
+ step?: number | string;
28
+ /** Character ceiling, forwarded on both branches (single-line and textarea). */
29
+ maxLength?: number;
30
+ /** Browser autofill hint. `"off"` is the one a money field usually wants. */
31
+ autoComplete?: React.HTMLInputAutoCompleteAttribute;
32
+ /** Virtual-keyboard hint, e.g. `"numeric"` or `"decimal"`. Both branches. */
33
+ inputMode?: React.HTMLAttributes<HTMLElement>['inputMode'];
34
+ /** Native validation regex. Single-line only — a textarea has no `pattern`. */
35
+ pattern?: string;
36
+ /** Focus the control on mount. React focuses the node rather than emitting an attribute. */
37
+ autoFocus?: boolean;
38
+ /**
39
+ * Imperative handle on the rendered control: `focus()`/`blur()` on the
40
+ * single-line branch, the textarea node on the `textArea` branch (which
41
+ * satisfies the same shape). COMPOSED with react-hook-form's own ref when
42
+ * `control` is present, never replacing it — the form keeps the node it
43
+ * needs for `shouldFocusError`.
44
+ */
45
+ ref?: Ref<InputRef>;
7
46
  label?: ReactNode;
8
47
  tooltip?: string;
9
48
  labelExtra?: ReactNode;
@@ -32,5 +71,5 @@ export type InputFieldProps<T extends FieldValues = FieldValues> = {
32
71
  'data-testid'?: string;
33
72
  onChange?: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
34
73
  };
35
- export declare const InputField: <T extends FieldValues = FieldValues>({ className, type, label, tooltip, labelExtra, placeholder, description, startAdornment, endAdornment, required, readOnly, minHeight, maxHeight, textArea, defaultValue, value, onChange, ...others }: InputFieldProps<T>) => import("react").JSX.Element;
74
+ export declare const InputField: <T extends FieldValues = FieldValues>({ className, type, label, tooltip, labelExtra, placeholder, description, startAdornment, endAdornment, required, readOnly, minHeight, maxHeight, textArea, defaultValue, value, onChange, min, max, step, maxLength, autoComplete, inputMode, pattern, autoFocus, ref, "aria-invalid": ariaInvalid, ...others }: InputFieldProps<T>) => import("react").JSX.Element;
36
75
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/input-field/index.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAE,sBAAsB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,EAAE,OAAO,EAAkB,WAAW,EAAQ,MAAM,iBAAiB,CAAA;AAE5E,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAAI;IACjE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,cAAc,CAAC,EAAE,SAAS,CAAA;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB;oDACgD;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACpB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,CACT,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KACzD,IAAI,CAAA;CACV,CAAA;AAeD,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,uMAmB7D,eAAe,CAAC,CAAC,CAAC,gCA2EpB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/input-field/index.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,sBAAsB,EAAE,SAAS,EAAE,GAAG,EAAmB,MAAM,OAAO,CAAA;AAC/E,OAAO,EAAE,OAAO,EAAkB,WAAW,EAAQ,MAAM,iBAAiB,CAAA;AAE5E,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAAI;IACjE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACrB,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACrB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,KAAK,CAAC,8BAA8B,CAAA;IACnD,6EAA6E;IAC7E,SAAS,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,WAAW,CAAC,CAAA;IAC1D,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IACnB,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,cAAc,CAAC,EAAE,SAAS,CAAA;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB;oDACgD;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACpB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,CACT,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KACzD,IAAI,CAAA;CACV,CAAA;AA4CD,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,iTA6B7D,eAAe,CAAC,CAAC,CAAC,gCAyHpB,CAAA"}
@@ -2,8 +2,59 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { AutosizeTextarea } from '../../../components/ui/autosize-textarea/index.js';
3
3
  import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, FormTooltip } from '../../../components/ui/form.js';
4
4
  import { Input } from '../../../components/ui/input/index.js';
5
- export const InputField = ({ className, type, label, tooltip, labelExtra, placeholder, description, startAdornment, endAdornment, required, readOnly, minHeight, maxHeight, textArea, defaultValue, value, onChange, ...others }) => {
6
- const renderItem = (binding) => (_jsxs(FormItem, { required: required, children: [label && (_jsx(FormLabel, { extra: tooltip ? _jsx(FormTooltip, { children: tooltip }) : labelExtra, children: label })), _jsx(FormControl, { children: textArea ? (_jsx(AutosizeTextarea, { className: className, placeholder: placeholder, readOnly: readOnly, minHeight: minHeight, maxHeight: maxHeight, "data-testid": others['data-testid'], ...binding })) : (_jsx(Input, { className: className, type: type, placeholder: placeholder, readOnly: readOnly, startAdornment: startAdornment, endAdornment: endAdornment, "data-testid": others['data-testid'], ...binding })) }), _jsx(FormMessage, {}), description && _jsx(FormDescription, { children: description })] }));
5
+ import { useMemo, useRef } from 'react';
6
+ /**
7
+ * Fan one node out to several refs. React hands a node to ONE ref, so a
8
+ * consumer ref and react-hook-form's `field.ref` cannot both be passed
9
+ * directly — one silently wins. Only used when the consumer actually supplied
10
+ * a ref, so a field without one keeps `field.ref`'s own identity and React
11
+ * never re-attaches it.
12
+ */
13
+ function fanOutRef(...refs) {
14
+ return (node) => {
15
+ const cleanups = [];
16
+ for (const ref of refs) {
17
+ if (typeof ref === 'function') {
18
+ const cleanup = ref(node);
19
+ cleanups.push(typeof cleanup === 'function' ? cleanup : () => ref(null));
20
+ }
21
+ else if (ref) {
22
+ ;
23
+ ref.current = node;
24
+ cleanups.push(() => {
25
+ ;
26
+ ref.current = null;
27
+ });
28
+ }
29
+ }
30
+ return () => cleanups.forEach((cleanup) => cleanup());
31
+ };
32
+ }
33
+ export const InputField = ({ className, type, label, tooltip, labelExtra, placeholder, description, startAdornment, endAdornment, required, readOnly, minHeight, maxHeight, textArea, defaultValue, value, onChange, min, max, step, maxLength, autoComplete, inputMode, pattern, autoFocus, ref, 'aria-invalid': ariaInvalid, ...others }) => {
34
+ // OR with the form's error, expressed through the Slot merge rather than by
35
+ // reading the form: FormControl already injects `aria-invalid={!!error}`, and
36
+ // a child prop only wins that merge when the KEY is present. So emit the key
37
+ // only for an explicit `true`, and let FormControl answer for every other
38
+ // case. `true || error` is true; `false || error` is whatever the form says,
39
+ // which is the whole point — an explicit `false` cannot mute a live error.
40
+ // With no `control` there is no error to OR against, so FormControl injects
41
+ // `false` and today's behaviour is unchanged.
42
+ const ariaInvalidProp = ariaInvalid ? { 'aria-invalid': true } : undefined;
43
+ const formRef = useRef(undefined);
44
+ const composedRef = useMemo(() => ref ? fanOutRef((node) => formRef.current?.(node), ref) : ref,
45
+ // `others.name` belongs here, and leaving it out is not a lint nit.
46
+ // react-hook-form hands a fresh `field.ref` for every name, and that ref
47
+ // only receives the element when React reattaches, which it does only when
48
+ // this callback's identity changes. Memoised on `ref` alone, one identity
49
+ // survived a name change, the new field never got its element, and
50
+ // `form.setFocus(newName)` then found nothing to focus and said nothing.
51
+ [ref, others.name]);
52
+ const renderItem = (binding) => (_jsxs(FormItem, { required: required, children: [label && (_jsx(FormLabel, { extra: tooltip ? _jsx(FormTooltip, { children: tooltip }) : labelExtra, children: label })), _jsx(FormControl, { children: textArea ? (_jsx(AutosizeTextarea, { className: className, placeholder: placeholder, readOnly: readOnly, minHeight: minHeight, maxHeight: maxHeight, maxLength: maxLength, autoComplete: autoComplete, inputMode: inputMode, autoFocus: autoFocus, "data-testid": others['data-testid'], ...ariaInvalidProp, ...binding })) : (_jsx(Input, { className: className, type: type, placeholder: placeholder, readOnly: readOnly, startAdornment: startAdornment, endAdornment: endAdornment,
53
+ // Bounds belong to the single-line control only: `min`/`max`/`step`
54
+ // mean nothing on a textarea and would render as invalid markup.
55
+ min: min, max: max, step: step, maxLength: maxLength, autoComplete: autoComplete, inputMode: inputMode,
56
+ // Single-line only: `<textarea pattern>` is not a thing.
57
+ pattern: pattern, autoFocus: autoFocus, "data-testid": others['data-testid'], ...ariaInvalidProp, ...binding })) }), _jsx(FormMessage, {}), description && _jsx(FormDescription, { children: description })] }));
7
58
  if (!others.control) {
8
59
  return renderItem({
9
60
  name: others.name,
@@ -11,7 +62,10 @@ export const InputField = ({ className, type, label, tooltip, labelExtra, placeh
11
62
  // Controlled when the caller holds the value, uncontrolled when it only
12
63
  // seeds one — supplying both is what React warns about.
13
64
  ...(value !== undefined ? { value } : { defaultValue }),
14
- onChange: (e) => onChange?.(e)
65
+ onChange: (e) => onChange?.(e),
66
+ // Nothing else claims the node on this branch, so the consumer ref goes
67
+ // straight through. Without it there was no node to focus at all.
68
+ ref: ref
15
69
  });
16
70
  }
17
71
  return (_jsx(FormField, { ...others, name: others.name,
@@ -19,16 +73,20 @@ export const InputField = ({ className, type, label, tooltip, labelExtra, placeh
19
73
  // value painted on the box only makes the field LOOK filled while the
20
74
  // form still submits nothing for that name. Controller seeds its own
21
75
  // state from this, so `field.value` carries the seed on both branches.
22
- defaultValue: defaultValue, render: ({ field }) => renderItem({
23
- ...field,
24
- // Controlled from mount. react-hook-form hands `undefined` for any
25
- // name the form declares no default for; React then mounts the box
26
- // uncontrolled and flips it on the first keystroke, losing that
27
- // character.
28
- value: field.value ?? '',
29
- onChange: (e) => {
30
- field.onChange(e);
31
- onChange?.(e);
32
- }
33
- }) }));
76
+ defaultValue: defaultValue, render: ({ field }) => {
77
+ formRef.current = field.ref;
78
+ return renderItem({
79
+ ...field,
80
+ // Controlled from mount. react-hook-form hands `undefined` for any
81
+ // name the form declares no default for; React then mounts the box
82
+ // uncontrolled and flips it on the first keystroke, losing that
83
+ // character.
84
+ value: field.value ?? '',
85
+ onChange: (e) => {
86
+ field.onChange(e);
87
+ onChange?.(e);
88
+ },
89
+ ref: (ref ? composedRef : field.ref)
90
+ });
91
+ } }));
34
92
  };
@@ -13,7 +13,15 @@ type SelectFieldSharedProps<T extends FieldValues = FieldValues> = PropsWithChil
13
13
  * with `value` + `onChange`. */
14
14
  control?: Control<T>;
15
15
  required?: boolean;
16
+ /** Copy for the "nothing to pick" row. Defaults to "No options found." */
16
17
  emptyMessage?: string;
18
+ /**
19
+ * Placed on the field's own root box, merged with its spacing rather than
20
+ * replacing it. The root is the element a parent grid positions, so
21
+ * without this a consumer had to wrap the field in a spare div just to
22
+ * span a column.
23
+ */
24
+ className?: string;
17
25
  'data-testid'?: string;
18
26
  };
19
27
  /**
@@ -39,6 +47,6 @@ type SelectFieldModeProps = {
39
47
  onChange?: (value: string) => void;
40
48
  };
41
49
  export type SelectFieldProps<T extends FieldValues = FieldValues> = SelectFieldSharedProps<T> & SelectFieldModeProps;
42
- export declare const SelectField: <T extends FieldValues = FieldValues>({ name, label, tooltip, labelExtra, required, placeholder, description, disabled, readOnly, multi, control, value, children, emptyMessage, onChange, ...others }: SelectFieldProps<T>) => React.JSX.Element;
50
+ export declare const SelectField: <T extends FieldValues = FieldValues>({ name, label, tooltip, labelExtra, required, placeholder, description, disabled, readOnly, multi, control, value, children, emptyMessage, className, onChange, ...others }: SelectFieldProps<T>) => React.JSX.Element;
43
51
  export {};
44
52
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/select-field/index.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAC3D,OAAO,EACL,OAAO,EAEP,WAAW,EAEZ,MAAM,iBAAiB,CAAA;AAExB,KAAK,sBAAsB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAC7D,iBAAiB,GAAG;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;qCACiC;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAEH;;;;;;;;GAQG;AACH,KAAK,oBAAoB,GACrB;IACE,KAAK,EAAE,IAAI,CAAA;IACX;yEACqE;IACrE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;CACrC,GACD;IACE,KAAK,CAAC,EAAE,KAAK,CAAA;IACb;yEACqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACnC,CAAA;AAEL,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAC9D,sBAAsB,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAA;AAWlD,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,kKAiB9D,gBAAgB,CAAC,CAAC,CAAC,sBA6FrB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/select-field/index.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAC3D,OAAO,EACL,OAAO,EAEP,WAAW,EAEZ,MAAM,iBAAiB,CAAA;AAExB,KAAK,sBAAsB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAC7D,iBAAiB,GAAG;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;qCACiC;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAEH;;;;;;;;GAQG;AACH,KAAK,oBAAoB,GACrB;IACE,KAAK,EAAE,IAAI,CAAA;IACX;yEACqE;IACrE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;CACrC,GACD;IACE,KAAK,CAAC,EAAE,KAAK,CAAA;IACb;yEACqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACnC,CAAA;AAEL,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAC9D,sBAAsB,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAA;AAWlD,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,6KAkB9D,gBAAgB,CAAC,CAAC,CAAC,sBA6FrB,CAAA"}
@@ -3,14 +3,14 @@ import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessa
3
3
  import { MultipleSelect, MultipleSelectContent, MultipleSelectTrigger, MultipleSelectValue } from '../../../components/ui/multiple-select/index.js';
4
4
  import { Select, SelectContent, SelectEmpty, SelectTrigger, SelectValue } from '../../../components/ui/select/index.js';
5
5
  import { cn } from '../../../lib/utils.js';
6
- export const SelectField = ({ name, label, tooltip, labelExtra, required, placeholder, description, disabled, readOnly, multi, control, value, children, emptyMessage = 'No options found.', onChange, ...others }) => {
6
+ export const SelectField = ({ name, label, tooltip, labelExtra, required, placeholder, description, disabled, readOnly, multi, control, value, children, emptyMessage = 'No options found.', className, onChange, ...others }) => {
7
7
  // The props union already pairs `multi` with the callback shape that branch
8
8
  // emits, but destructuring an intersection loses that correlation — TS sees
9
9
  // two unrelated callbacks. Both branches report through this one seam, each
10
10
  // with the payload its own union member declared.
11
11
  const emitChange = onChange;
12
12
  const renderItem = (field) => {
13
- return (_jsxs(FormItem, { required: required, children: [label && (_jsx(FormLabel, { extra: tooltip ? _jsx(FormTooltip, { children: tooltip }) : labelExtra, children: label })), multi ? (_jsxs(MultipleSelect, { onValueChange: (value) => {
13
+ return (_jsxs(FormItem, { required: required, className: className, children: [label && (_jsx(FormLabel, { extra: tooltip ? _jsx(FormTooltip, { children: tooltip }) : labelExtra, children: label })), multi ? (_jsxs(MultipleSelect, { onValueChange: (value) => {
14
14
  field.onChange(value);
15
15
  emitChange?.(value);
16
16
  }, disabled: disabled, ...field, children: [_jsx(MultipleSelectTrigger, { readOnly: readOnly, children: _jsx(MultipleSelectValue, { placeholder: placeholder }) }), _jsx(MultipleSelectContent, { children: children })] })) : (_jsxs(Select, { onValueChange: (value) => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/calendar/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EACL,SAAS,EAET,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,MAAM,EAAkB,MAAM,wBAAwB,CAAA;AAO/D,iBAAS,QAAQ,CAAC,EAChB,SAAS,EACT,UAAU,EACV,eAAsB,EACtB,aAAuB,EACvB,aAAuB,EACvB,UAAU,EACV,UAAU,EACV,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,SAAS,CAAC,GAAG;IAC1C,aAAa,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,CAAC,SAAS,CAAC,CAAA;CAC/D,qBA0JA;AAED,iBAAS,iBAAiB,CAAC,EACzB,SAAS,EACT,GAAG,EACH,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,SAAS,CAAC,qBAqCxC;AAED,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/calendar/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EACL,SAAS,EAGT,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,MAAM,EAAkB,MAAM,wBAAwB,CAAA;AAO/D,iBAAS,QAAQ,CAAC,EAChB,SAAS,EACT,UAAU,EACV,eAAsB,EACtB,aAAuB,EACvB,aAAuB,EACvB,UAAU,EACV,UAAU,EACV,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,SAAS,CAAC,GAAG;IAC1C,aAAa,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,CAAC,SAAS,CAAC,CAAA;CAC/D,qBAgKA;AAED,iBAAS,iBAAiB,CAAC,EACzB,SAAS,EACT,GAAG,EACH,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,SAAS,CAAC,qBAqCxC;AAED,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAA"}
@@ -1,14 +1,20 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from 'react';
4
- import { DayPicker, getDefaultClassNames } from 'react-day-picker';
4
+ import { DayPicker, defaultDateLib, getDefaultClassNames } from 'react-day-picker';
5
5
  import { cn } from '../../../lib/utils.js';
6
6
  import { Button, buttonVariants } from '../../../components/ui/button/index.js';
7
7
  import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from 'lucide-react';
8
8
  function Calendar({ className, classNames, showOutsideDays = true, captionLayout = 'label', buttonVariant = 'plain', formatters, components, ...props }) {
9
9
  const defaultClassNames = getDefaultClassNames();
10
10
  return (_jsx(DayPicker, { showOutsideDays: showOutsideDays, className: cn('bg-background group/calendar p-2 [--cell-radius:var(--radius-md)] [--cell-size:34px] [--nav-size:28px] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent', String.raw `rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw `rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className), captionLayout: captionLayout, formatters: {
11
- formatMonthDropdown: (date) => date.toLocaleString('default', { month: 'short' }),
11
+ // react-day-picker's own default is dateLib.format(month, 'LLLL'), the
12
+ // full month name in the locale it was given. Shortening it has to go
13
+ // through the same dateLib: Date.toLocaleString('default') reads the
14
+ // runtime locale instead, so the dropdown fell back to English while
15
+ // the weekdays and the caption around it stayed in the consumer's
16
+ // language. 'LLL' is the abbreviated form of the same standalone month.
17
+ formatMonthDropdown: (month, dateLib = defaultDateLib) => dateLib.format(month, 'LLL'),
12
18
  ...formatters
13
19
  }, classNames: {
14
20
  root: cn('w-fit', defaultClassNames.root),
@@ -1,9 +1,16 @@
1
1
  /**
2
- * FileUpload — a controlled "pick a file, read it to text, hand back the text"
3
- * primitive. The defining job: select one file (by click, keyboard, or
4
- * drag-and-drop), validate it against an accept filter + a byte ceiling, read
5
- * it as UTF-8 via FileReader.readAsText, and emit `{ file, text }`. Binary/DER
6
- * (.pfx/.p12) is explicitly OUT of scope — readAsText only.
2
+ * FileUpload — a controlled "pick a file, hand it back" primitive. The defining
3
+ * job: select one file (by click, keyboard, or drag-and-drop), validate it
4
+ * against an accept filter + a byte ceiling, and emit `{ file, text }`.
5
+ *
6
+ * `readAs` decides whether the bytes are decoded on the way through. `'text'`
7
+ * (the default) reads UTF-8 via FileReader.readAsText, which is what a PEM/CSV
8
+ * host wants. `'none'` hands the `File` over untouched and is the only mode a
9
+ * BINARY file can use: a PDF, XLSX or PFX put through readAsText decodes into
10
+ * replacement-character garbage, and that garbage string is then retained for
11
+ * as long as the host holds the value — 20 MiB of PDF became a useless 20 MiB
12
+ * string, and the host still had to read the file a second time to get at the
13
+ * real bytes.
7
14
  *
8
15
  * The real `<input type="file">` IS the accessible control: it is `sr-only`
9
16
  * (visually hidden) but focusable and labelable — never `aria-hidden`, never
@@ -24,6 +31,11 @@
24
31
  * a value, so it cannot masquerade as valid. `accept` is validated against
25
32
  * BOTH extension and MIME because the native `accept` attribute is only a
26
33
  * browser hint and is bypassable via drag-drop.
34
+ *
35
+ * A host that already announces the rejection itself — a toast in its own
36
+ * locale, driven off `onError` — silences this one by returning nothing from
37
+ * `labels.error`. Two announcements for one event, in two languages, is the
38
+ * accessibility defect; the wording is not.
27
39
  */
28
40
  import * as React from 'react';
29
41
  export type FileUploadResult = {
@@ -42,6 +54,33 @@ export type FileUploadError = {
42
54
  kind: 'read-failed';
43
55
  file: File;
44
56
  };
57
+ /** How the picked file's bytes are handled on the way to `onSelect`. */
58
+ export type FileUploadReadAs = 'text' | 'none';
59
+ /**
60
+ * Overrides for the fixed English copy. Every field is optional and defaults to
61
+ * the current value, so a consumer that passes nothing renders exactly as
62
+ * before. These four strings were the whole of the component's user-visible
63
+ * text, and none of them was reachable — a pt-BR console rendered an English
64
+ * affordance and handed a screen reader an English accessible name.
65
+ */
66
+ export interface FileUploadLabels {
67
+ /** Emphasised call to action in the empty zone. Defaults to "Choose a file". */
68
+ action?: string;
69
+ /** Trailing hint after the action. Defaults to "or drag and drop". */
70
+ hint?: string;
71
+ /** Accessible name of the clear button. Defaults to "Remove file". */
72
+ remove?: string;
73
+ /**
74
+ * Copy for the component's own `role="alert"` refusal. Receives the rejection
75
+ * so the message can interpolate the cap or the accept filter; use the
76
+ * exported `humanizeSize` to format `maxSizeBytes` the way the chip does.
77
+ *
78
+ * Return `null`, `undefined` or `''` to stay SILENT and leave the
79
+ * announcement to the host's own `onError` handling. Defaults to the
80
+ * built-in English messages.
81
+ */
82
+ error?: (error: FileUploadError) => string | null | undefined;
83
+ }
45
84
  export type FileUploadProps = {
46
85
  /** Comma-separated accept filter, e.g. ".pem,.key" or "text/plain". Mirrors the native input accept. */
47
86
  accept?: string;
@@ -49,7 +88,18 @@ export type FileUploadProps = {
49
88
  maxSizeBytes?: number;
50
89
  /** Controlled selection. `null` = empty. The host owns state. */
51
90
  value?: FileUploadResult | null;
52
- /** Fires on accept (with {file,text}) or clear (null). Reads UTF-8 via FileReader.readAsText. */
91
+ /**
92
+ * How the accepted file is handed over.
93
+ * - `'text'` (default): decode as UTF-8 via FileReader.readAsText and emit
94
+ * `{ file, text }`. Unchanged behaviour for every existing consumer.
95
+ * - `'none'`: skip decoding entirely and emit `{ file, text: '' }`. The mode
96
+ * for BINARY files (PDF, XLSX, PFX/DER): the host gets the `File` intact
97
+ * and `text` carries nothing, so do not read it in this mode.
98
+ */
99
+ readAs?: FileUploadReadAs;
100
+ /** Override the fixed English copy. Omitted fields keep their defaults. */
101
+ labels?: FileUploadLabels;
102
+ /** Fires on accept (with the result) or clear (null). See `readAs` for whether `text` is populated. */
53
103
  onSelect: (result: FileUploadResult | null) => void;
54
104
  /** Fires when a pick is rejected (size/type/read). Optional — the component also shows its own inline error. */
55
105
  onError?: (error: FileUploadError) => void;
@@ -60,7 +110,7 @@ export type FileUploadProps = {
60
110
  'aria-required'?: boolean;
61
111
  'aria-describedby'?: string;
62
112
  'aria-label'?: string;
63
- } & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'accept' | 'value' | 'disabled' | 'onChange' | 'onSelect' | 'className' | 'multiple'>;
113
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'accept' | 'value' | 'disabled' | 'onChange' | 'onSelect' | 'className' | 'onError' | 'multiple'>;
64
114
  /**
65
115
  * Validate a chosen file against an accept filter and a byte ceiling. Exported
66
116
  * so hosts can pre-validate before handing a file over. `accept` is matched
@@ -72,6 +122,12 @@ export declare function validateFile(file: File, opts: {
72
122
  accept?: string;
73
123
  maxSizeBytes?: number;
74
124
  }): FileUploadError | null;
125
+ /**
126
+ * Humanize a byte count for the selected-file chip. Binary units, 1 decimal.
127
+ * Exported so a `labels.error` override can format `maxSizeBytes` exactly the
128
+ * way the chip and the default message do, instead of re-deriving binary units.
129
+ */
130
+ export declare function humanizeSize(bytes: number): string;
75
131
  export declare const FileUpload: React.ForwardRefExoticComponent<{
76
132
  /** Comma-separated accept filter, e.g. ".pem,.key" or "text/plain". Mirrors the native input accept. */
77
133
  accept?: string;
@@ -79,7 +135,18 @@ export declare const FileUpload: React.ForwardRefExoticComponent<{
79
135
  maxSizeBytes?: number;
80
136
  /** Controlled selection. `null` = empty. The host owns state. */
81
137
  value?: FileUploadResult | null;
82
- /** Fires on accept (with {file,text}) or clear (null). Reads UTF-8 via FileReader.readAsText. */
138
+ /**
139
+ * How the accepted file is handed over.
140
+ * - `'text'` (default): decode as UTF-8 via FileReader.readAsText and emit
141
+ * `{ file, text }`. Unchanged behaviour for every existing consumer.
142
+ * - `'none'`: skip decoding entirely and emit `{ file, text: '' }`. The mode
143
+ * for BINARY files (PDF, XLSX, PFX/DER): the host gets the `File` intact
144
+ * and `text` carries nothing, so do not read it in this mode.
145
+ */
146
+ readAs?: FileUploadReadAs;
147
+ /** Override the fixed English copy. Omitted fields keep their defaults. */
148
+ labels?: FileUploadLabels;
149
+ /** Fires on accept (with the result) or clear (null). See `readAs` for whether `text` is populated. */
83
150
  onSelect: (result: FileUploadResult | null) => void;
84
151
  /** Fires when a pick is rejected (size/type/read). Optional — the component also shows its own inline error. */
85
152
  onError?: (error: FileUploadError) => void;
@@ -90,5 +157,164 @@ export declare const FileUpload: React.ForwardRefExoticComponent<{
90
157
  'aria-required'?: boolean;
91
158
  'aria-describedby'?: string;
92
159
  'aria-label'?: string;
93
- } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "className" | "disabled" | "type" | "value" | "onChange" | "onSelect" | "accept" | "multiple"> & React.RefAttributes<HTMLInputElement>>;
160
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "className" | "disabled" | "type" | "value" | "onChange" | "onError" | "onSelect" | "accept" | "multiple"> & React.RefAttributes<HTMLInputElement>>;
161
+ /**
162
+ * A rejection from `MultipleFileUpload`: every rejection the single-file
163
+ * sibling can produce, plus the one only a plural selection has — the cap.
164
+ */
165
+ export type MultipleFileUploadError = FileUploadError | {
166
+ kind: 'too-many';
167
+ file: File;
168
+ maxFiles: number;
169
+ };
170
+ /** Overrides for the fixed English copy. Every field is optional. */
171
+ export interface MultipleFileUploadLabels {
172
+ /** Emphasised call to action in the empty zone. Defaults to "Choose files". */
173
+ action?: string;
174
+ /** Trailing hint after the action. Defaults to "or drag and drop". */
175
+ hint?: string;
176
+ /** Zone copy once `maxFiles` is reached. Receives the cap to interpolate. */
177
+ full?: (maxFiles: number) => string;
178
+ /**
179
+ * Accessible name of a row's remove control. Receives that row's file, so the
180
+ * name identifies it. Defaults to `Remove <filename>`.
181
+ */
182
+ remove?: (file: File) => string;
183
+ /**
184
+ * Copy for one rejection. Called once per rejection in a batch; use the
185
+ * exported `humanizeSize` to format `maxSizeBytes`. Return `null`,
186
+ * `undefined` or `''` to stay SILENT and leave the announcement to the host's
187
+ * own `onError` handling. A batch whose every message is silent renders no
188
+ * alert at all.
189
+ */
190
+ error?: (error: MultipleFileUploadError) => string | null | undefined;
191
+ }
192
+ export type MultipleFileUploadProps = {
193
+ /** Comma-separated accept filter. Applied to every file, extension or MIME. */
194
+ accept?: string;
195
+ /** Inclusive per-file byte ceiling. Applied to each file independently. */
196
+ maxSizeBytes?: number;
197
+ /** Ceiling on the TOTAL selection. Omitted means unbounded. */
198
+ maxFiles?: number;
199
+ /** Controlled selection. The host owns state; defaults to empty. */
200
+ value?: FileUploadResult[];
201
+ /**
202
+ * How each accepted file is handed over. `'text'` (default) decodes UTF-8
203
+ * and populates `text`; `'none'` skips decoding entirely and is the mode
204
+ * BINARY files need. See FileUpload's `readAs` for why.
205
+ */
206
+ readAs?: FileUploadReadAs;
207
+ /** Override the fixed English copy. Omitted fields keep their defaults. */
208
+ labels?: MultipleFileUploadLabels;
209
+ /** Fires with the WHOLE next selection whenever files are added or removed. */
210
+ onValueChange: (values: FileUploadResult[]) => void;
211
+ /** Fires once per rejected file. A batch can produce several. */
212
+ onError?: (error: MultipleFileUploadError) => void;
213
+ disabled?: boolean;
214
+ id?: string;
215
+ className?: string;
216
+ 'aria-invalid'?: boolean;
217
+ 'aria-required'?: boolean;
218
+ 'aria-describedby'?: string;
219
+ 'aria-label'?: string;
220
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'accept' | 'value' | 'disabled' | 'onChange' | 'onSelect' | 'className' | 'multiple' | 'onError'>;
221
+ /**
222
+ * MultipleFileUpload: the plural sibling of FileUpload. Pick SEVERAL files,
223
+ * accumulate them across repeated picks, validate each one, cap the total, and
224
+ * hand back `FileUploadResult[]`.
225
+ *
226
+ * A SIBLING COMPONENT, not a `multiple` flag. This library already answers
227
+ * "this one takes many" that way (`Select` / `MultipleSelect`), and FileUpload
228
+ * strips `'multiple'` from its props on purpose: single-file is its contract,
229
+ * not a default it happens to have. A boolean would have forced every prop
230
+ * here into a union that means one thing when the flag is set and another when
231
+ * it is not: `value` as `Result | Result[] | null`, a `maxFiles` that is
232
+ * meaningless in half the configurations, and a remove control whose
233
+ * accessible name is fixed copy in one mode and per-file in the other. The
234
+ * plural props follow the house shape for plural components: `value?: T[]`
235
+ * with `onValueChange?: (values: T[]) => void`.
236
+ *
237
+ * WHERE THE LINE SITS: this component owns SELECTION and nothing after it.
238
+ * Choosing, validating, capping, listing and removing are its job; uploading
239
+ * is not. That split is not squeamishness about scope, it is where the
240
+ * knowledge actually lives. An upload needs an endpoint, an auth scheme, a
241
+ * concurrency policy, a retry policy and, very often, a parent id that does
242
+ * not exist yet when the files are chosen: the motivating host stages evidence
243
+ * files while a form is being filled and can only upload them against the id
244
+ * that its create call returns afterwards. None of that is knowable from
245
+ * inside a library primitive, and a component that guessed would have to be
246
+ * fought rather than used. So the host keeps its own per-file record with
247
+ * status and retry, and this component keeps the part a form can hold and
248
+ * validate: the chosen files. `FileUploadResult[]` is a value; an upload state
249
+ * machine is not.
250
+ *
251
+ * ACCUMULATION is the defining behaviour. A second pick ADDS to the selection
252
+ * rather than replacing it, because a user assembling five documents does it
253
+ * in two or three trips to the file dialog, not one. Everything else follows
254
+ * from that: room is measured against what is already selected, and the batch
255
+ * that overflows the cap still contributes the files that fit.
256
+ *
257
+ * A BATCH SURVIVES ITS OWN CASUALTIES. One file rejected for type, size or a
258
+ * failed read does not discard the rest of the batch, and it does not consume
259
+ * a slot either: validation runs over the WHOLE batch before the cap is
260
+ * applied, so a file that was never eligible cannot cost an eligible one its
261
+ * place, and `'too-many'` always names a file a slot would genuinely have
262
+ * taken. The alternative punishes
263
+ * a user for a mistake in one file by throwing away four good ones, and hands
264
+ * back no way to tell which was which. Every rejection is reported through
265
+ * `onError` and announced together in one `role="alert"`.
266
+ *
267
+ * Accessibility follows the sibling BELOW THE CAP: the real `<input
268
+ * type="file">` is `sr-only` but focusable and labelable, and never
269
+ * `aria-hidden`, so FormControl-injected ARIA and react-hook-form's focus on
270
+ * error both work while the picker is enabled. At the cap that changes, and
271
+ * the paragraph below says how.
272
+ * The file list sits OUTSIDE the click zone, so activating a remove control
273
+ * cannot also reopen the picker, and each remove control is named after its
274
+ * own file: a column of identical "Remove file" buttons tells a screen-reader
275
+ * user nothing about which row they are on.
276
+ *
277
+ * AT THE CAP the picker takes the native `disabled` attribute, and that DOES
278
+ * take it out of the tab order. This is the one state in which the input is
279
+ * not a focus target, and the one state in which focus-on-error cannot land on
280
+ * it, so it is a real cost rather than a free win. The cap has no counterpart
281
+ * in the single-file sibling, so the precedent followed here is
282
+ * `DateRangePicker`'s trigger: a control whose only job is to open a dialog has
283
+ * no state worth keeping focusable, and native `disabled` is what both removes
284
+ * it from the tab order and keeps the dialog shut. The alternative, an enabled
285
+ * picker that opens the file dialog and then refuses every file with
286
+ * `too-many`, is a control that lies about being available. What keeps the cap
287
+ * from being a dead end is the escape hatch: the remove controls answer to
288
+ * `disabled` alone and NEVER to the cap, so they stay focusable and removing
289
+ * one file reopens the picker.
290
+ */
291
+ export declare const MultipleFileUpload: React.ForwardRefExoticComponent<{
292
+ /** Comma-separated accept filter. Applied to every file, extension or MIME. */
293
+ accept?: string;
294
+ /** Inclusive per-file byte ceiling. Applied to each file independently. */
295
+ maxSizeBytes?: number;
296
+ /** Ceiling on the TOTAL selection. Omitted means unbounded. */
297
+ maxFiles?: number;
298
+ /** Controlled selection. The host owns state; defaults to empty. */
299
+ value?: FileUploadResult[];
300
+ /**
301
+ * How each accepted file is handed over. `'text'` (default) decodes UTF-8
302
+ * and populates `text`; `'none'` skips decoding entirely and is the mode
303
+ * BINARY files need. See FileUpload's `readAs` for why.
304
+ */
305
+ readAs?: FileUploadReadAs;
306
+ /** Override the fixed English copy. Omitted fields keep their defaults. */
307
+ labels?: MultipleFileUploadLabels;
308
+ /** Fires with the WHOLE next selection whenever files are added or removed. */
309
+ onValueChange: (values: FileUploadResult[]) => void;
310
+ /** Fires once per rejected file. A batch can produce several. */
311
+ onError?: (error: MultipleFileUploadError) => void;
312
+ disabled?: boolean;
313
+ id?: string;
314
+ className?: string;
315
+ 'aria-invalid'?: boolean;
316
+ 'aria-required'?: boolean;
317
+ 'aria-describedby'?: string;
318
+ 'aria-label'?: string;
319
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "className" | "disabled" | "type" | "value" | "onChange" | "onError" | "onSelect" | "accept" | "multiple"> & React.RefAttributes<HTMLInputElement>>;
94
320
  //# sourceMappingURL=index.d.ts.map