From ab92fe89429c5b9ce24e8d71f394550d7f8a8255 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Thu, 9 Jul 2026 21:39:55 +0300 Subject: [PATCH] feat: file upload by agent (first gen) --- .../src/components/FileUpload/FileUpload.mdx | 284 +++++++ .../FileUpload/FileUpload.module.css | 116 +++ .../FileUpload/FileUpload.stories.tsx | 692 ++++++++++++++++++ .../FileUpload/FileUploadContext.ts | 25 + .../FileUpload/MultipleFileUpload.module.css | 73 ++ .../FileUpload/MultipleFileUpload.test.tsx | 173 +++++ .../FileUpload/MultipleFileUpload.tsx | 277 +++++++ .../FileUpload/SingleFileUpload.module.css | 7 + .../FileUpload/SingleFileUpload.test.tsx | 188 +++++ .../FileUpload/SingleFileUpload.tsx | 232 ++++++ .../DropzoneContent.module.css | 8 + .../DropzoneContent/DropzoneContent.tsx | 73 ++ .../components/DropzoneContent/index.ts | 1 + .../components/FileInput/FileInput.tsx | 83 +++ .../FileUpload/components/FileInput/index.ts | 1 + .../FileUploadCaption/FileUploadCaption.tsx | 80 ++ .../components/FileUploadCaption/index.ts | 1 + .../FileUploadItem/FileUploadItem.module.css | 58 ++ .../FileUploadItem/FileUploadItem.tsx | 141 ++++ .../components/FileUploadItem/index.ts | 1 + .../FileUploadProvider/FileUploadProvider.tsx | 33 + .../components/FileUploadProvider/index.ts | 1 + .../FullScreenDropzone.module.css | 13 + .../FullScreenDropzone/FullScreenDropzone.tsx | 53 ++ .../components/FullScreenDropzone/index.ts | 1 + .../LocalDropzone/LocalDropzone.module.css | 15 + .../LocalDropzone/LocalDropzone.tsx | 217 ++++++ .../components/LocalDropzone/index.ts | 1 + .../components/FileUpload/components/index.ts | 5 + .../src/components/FileUpload/constants.ts | 8 + .../components/FileUpload/dropzones.test.tsx | 158 ++++ .../components/FileUpload/errorPredicates.ts | 23 + .../src/components/FileUpload/hooks/index.ts | 7 + .../FileUpload/hooks/useFileDrop/index.ts | 1 + .../hooks/useFileDrop/useFileDrop.ts | 129 ++++ .../FileUpload/hooks/useFileInput/index.ts | 1 + .../hooks/useFileInput/useFileInput.ts | 80 ++ .../FileUpload/hooks/useFileList/index.ts | 1 + .../hooks/useFileList/useFileList.ts | 121 +++ .../hooks/useFileUploadItem/index.ts | 1 + .../useFileUploadItem/useFileUploadItem.ts | 21 + .../hooks/useFileUploadLocale/index.ts | 1 + .../useFileUploadLocale.ts | 131 ++++ .../hooks/useFileUploadState/index.ts | 1 + .../useFileUploadState/useFileUploadState.ts | 60 ++ .../hooks/useFullScreenDropzone/index.ts | 1 + .../useFullScreenDropzone.ts | 144 ++++ .../src/components/FileUpload/index.ts | 17 + .../src/components/FileUpload/intl.json | 22 + .../components/FileUpload/locales.test.tsx | 119 +++ .../src/components/FileUpload/locales.ts | 137 ++++ .../components/FileUpload/primitives.test.tsx | 272 +++++++ .../src/components/FileUpload/types.ts | 254 +++++++ .../src/components/FileUpload/utils.ts | 182 +++++ .../src/components/FileUpload/validators.ts | 41 ++ packages/components/src/components/index.ts | 1 + tools/api-extractor/config.json | 1 + .../components/FileUpload.api.md | 538 ++++++++++++++ 58 files changed, 5326 insertions(+) create mode 100644 packages/components/src/components/FileUpload/FileUpload.mdx create mode 100644 packages/components/src/components/FileUpload/FileUpload.module.css create mode 100644 packages/components/src/components/FileUpload/FileUpload.stories.tsx create mode 100644 packages/components/src/components/FileUpload/FileUploadContext.ts create mode 100644 packages/components/src/components/FileUpload/MultipleFileUpload.module.css create mode 100644 packages/components/src/components/FileUpload/MultipleFileUpload.test.tsx create mode 100644 packages/components/src/components/FileUpload/MultipleFileUpload.tsx create mode 100644 packages/components/src/components/FileUpload/SingleFileUpload.module.css create mode 100644 packages/components/src/components/FileUpload/SingleFileUpload.test.tsx create mode 100644 packages/components/src/components/FileUpload/SingleFileUpload.tsx create mode 100644 packages/components/src/components/FileUpload/components/DropzoneContent/DropzoneContent.module.css create mode 100644 packages/components/src/components/FileUpload/components/DropzoneContent/DropzoneContent.tsx create mode 100644 packages/components/src/components/FileUpload/components/DropzoneContent/index.ts create mode 100644 packages/components/src/components/FileUpload/components/FileInput/FileInput.tsx create mode 100644 packages/components/src/components/FileUpload/components/FileInput/index.ts create mode 100644 packages/components/src/components/FileUpload/components/FileUploadCaption/FileUploadCaption.tsx create mode 100644 packages/components/src/components/FileUpload/components/FileUploadCaption/index.ts create mode 100644 packages/components/src/components/FileUpload/components/FileUploadItem/FileUploadItem.module.css create mode 100644 packages/components/src/components/FileUpload/components/FileUploadItem/FileUploadItem.tsx create mode 100644 packages/components/src/components/FileUpload/components/FileUploadItem/index.ts create mode 100644 packages/components/src/components/FileUpload/components/FileUploadProvider/FileUploadProvider.tsx create mode 100644 packages/components/src/components/FileUpload/components/FileUploadProvider/index.ts create mode 100644 packages/components/src/components/FileUpload/components/FullScreenDropzone/FullScreenDropzone.module.css create mode 100644 packages/components/src/components/FileUpload/components/FullScreenDropzone/FullScreenDropzone.tsx create mode 100644 packages/components/src/components/FileUpload/components/FullScreenDropzone/index.ts create mode 100644 packages/components/src/components/FileUpload/components/LocalDropzone/LocalDropzone.module.css create mode 100644 packages/components/src/components/FileUpload/components/LocalDropzone/LocalDropzone.tsx create mode 100644 packages/components/src/components/FileUpload/components/LocalDropzone/index.ts create mode 100644 packages/components/src/components/FileUpload/components/index.ts create mode 100644 packages/components/src/components/FileUpload/constants.ts create mode 100644 packages/components/src/components/FileUpload/dropzones.test.tsx create mode 100644 packages/components/src/components/FileUpload/errorPredicates.ts create mode 100644 packages/components/src/components/FileUpload/hooks/index.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileDrop/index.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileDrop/useFileDrop.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileInput/index.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileInput/useFileInput.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileList/index.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileList/useFileList.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileUploadItem/index.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileUploadItem/useFileUploadItem.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileUploadLocale/index.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileUploadLocale/useFileUploadLocale.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileUploadState/index.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFileUploadState/useFileUploadState.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFullScreenDropzone/index.ts create mode 100644 packages/components/src/components/FileUpload/hooks/useFullScreenDropzone/useFullScreenDropzone.ts create mode 100644 packages/components/src/components/FileUpload/index.ts create mode 100644 packages/components/src/components/FileUpload/intl.json create mode 100644 packages/components/src/components/FileUpload/locales.test.tsx create mode 100644 packages/components/src/components/FileUpload/locales.ts create mode 100644 packages/components/src/components/FileUpload/primitives.test.tsx create mode 100644 packages/components/src/components/FileUpload/types.ts create mode 100644 packages/components/src/components/FileUpload/utils.ts create mode 100644 packages/components/src/components/FileUpload/validators.ts create mode 100644 tools/public_api_guard/components/FileUpload.api.md diff --git a/packages/components/src/components/FileUpload/FileUpload.mdx b/packages/components/src/components/FileUpload/FileUpload.mdx new file mode 100644 index 00000000..44a45cbe --- /dev/null +++ b/packages/components/src/components/FileUpload/FileUpload.mdx @@ -0,0 +1,284 @@ +import { + Meta, + Story, + Props, + Status, +} from '../../../../../.storybook/components'; + +import * as Stories from './FileUpload.stories'; + + + +# FileUpload + + + +Allows the user to upload files to the product. It ships as two components: +`SingleFileUpload` (holds at most one file) and `MultipleFileUpload` (an ordered +list of files). Files can be dragged into the upload area or picked through the +system dialog; dropped folders are expanded recursively and each file is stamped +with its `fullPath`. + +Validation is **external**: the component never filters, dedupes, or limits +files, and `accept` is only forwarded to the native input as a browse hint — it +is _not_ enforced on drop. Use the shipped helpers (`maxFileSize`, +`isCorrectExtension`) in your `onChange`. + +## Import + +```tsx +import { SingleFileUpload, MultipleFileUpload } from '@koobiq/react-components'; +``` + +## Usage + +Selecting or dropping a file replaces the current one; removing it clears the +value. A custom `icon` can mark the selected file (for example, for images), and +`showFileSize={false}` hides the size for the single uploader. + + + +## Props + + + +## Multiple upload + +Multiple files can be dragged into the upload area or selected via the system +dialog. Files are appended in selection order (duplicates allowed) and removed by +index; the list exposes native `list` / `listitem` semantics. + + + + +### Compact variant + +For the multiple uploader, a compact view is available when it is empty. + + + +## File size + +When uploading multiple files, their size is always shown in the list. For the +single uploader, displaying the file size is configured separately via +`showFileSize` (on by default). Sizes are formatted with SI units, matching the +Angular `KbqDataSizePipe`. + + + +## Icon for selected file + +Selected files can be given a custom icon — a static `icon` for the single +uploader, or a per-row `renderFileIcon` for the multiple one. + + + +## Selecting folders or files + +`allowed` selects the browse links and captions: `file` (default), `folder` +(`webkitdirectory`), or `mixed` (both a file and a folder link). + + + +## Disabled state + +In the disabled state the component does not receive focus and drag-and-drop +does not work. + + + +## Error + +Ship `isInvalid` + `errorMessage`, and optionally a `shouldShowError` predicate +(`showErrorOnTouched` (default), `showErrorOnSubmit`, `showErrorRequiredOnSubmit`, +`showErrorOnDirty`). In the multiple uploader, mark the offending rows with the +item `hasError` flag — the invalid items are highlighted and the error message is +shown below the control. + + + + +## Height of the selected items list + +By default the list grows with the number of files. To cap it — after which the +list scrolls — set the `--fileupload-list-max-block-size` custom property via +`slotProps.dropArea`. + + + +You can also give the uploader a fixed height via `slotProps.dropArea`. + + + +## Full-screen file uploader + +Set `fullScreenDropzone` (a boolean, or a `DropzoneData` object to configure the +title/caption/size). While active, the inline drop area is disabled and a +full-viewport overlay opens whenever a file is dragged over the document; +dropping adds the files and closes the overlay. + + + +## Upload area + +The drop area does not have to be the whole screen or the uploader itself — it +can be a separate region of the page. Give the upload a `dropTargetRef` and +connect a `LocalDropzone` to it; dragging over the host opens an overlay +positioned over it and routes the drop into the connected component. + +```tsx +const target = useRef(null); + + + + +; +``` + + + +## Localization + +Captions ship for `en-US`, `ru-RU`, `pt-BR`, `es-LA` and `tk-TM`. Wrapping a +subtree in an `I18nProvider` changes the language for every uploader inside it at +once. + + + +To override only specific labels for a single instance, pass `localeConfig` +(highest precedence); the rest follow the active language. + + + +## Progress + +While an item's `loading` flag is set, its icon is replaced by a progress +indicator bound to `progress` (0–100). Drive progress by updating the value +**immutably**. + + + +Use `progressMode="indeterminate"` when the progress is unknown. + + + +Because `loading` / `progress` / `hasError` are plain fields, you can model +post-upload processing — for example, flag a file as errored once a background +check completes. + + + +## Forms + +The controlled `value` / `onChange` / `onBlur` API plugs into any form library. +Feed the field's validity into `isInvalid` / `errorMessage` and gate visibility +with `shouldShowError`. + +### Native controlled form + + + +### React Hook Form + +```tsx +import { Controller, useForm } from 'react-hook-form'; +import { SingleFileUpload, type FileItem } from '@koobiq/react-components'; + +const { control } = useForm<{ file: FileItem | null }>(); + + ( + fieldState.isTouched || formState.isSubmitted} + /> + )} +/>; +``` + +### Formik + +```tsx +import { useField } from 'formik'; +import { MultipleFileUpload, type FileItem } from '@koobiq/react-components'; + +const [field, meta, helpers] = useField('files'); + + helpers.setTouched(true)} + isInvalid={Boolean(meta.error)} + errorMessage={meta.error} + shouldShowError={() => meta.touched} +/>; +``` + +## Validation + +Validation is the app's responsibility. Run a validator in `onChange` and feed +the result into `isInvalid` / `errorMessage`. The shipped helpers (`maxFileSize`, +`isCorrectExtension`) mirror the Angular `FileValidators`. + +### Required + + + + +### File size + + + + +### File type or extension + + + + +### Mixed: required and extension + + + + +### Asynchronous + +Await a server-side check and set the error when it resolves; keep the item in +its `loading` state while the check runs. + + + +## Primitives + +Build a fully custom uploader from the exported primitives — `useFileList` +(list state), `useFileDrop` (drag-and-drop), and `FileInput` (the browse link). + + + +## Migrating from Angular `kbq-file-upload` + +- The deprecated `fileQueueChange` / `fileQueueChanged` events are **dropped** — + use `onChange` (plus `onFilesAdded` / `onFileRemoved`). +- `accept` is **not** enforced on drop (it never was); validate in `onChange`. +- `allowed` defaults to `'file'` (the Angular JSDoc said `mixed`, but the code + default was always `File`). +- `KbqFileItem.loading` / `progress` were RxJS `BehaviorSubject`s; in React they + are plain `boolean` / `number` fields — update them immutably to re-render. + +## Accessibility + +- The hidden native file input is tab-reachable; browse links are `