Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
982163d
chore(FileUpload): checkpoint current implementation
KamilEmeleev Jul 8, 2026
cc3ac25
chore(FileUpload): update implementation after review
KamilEmeleev Jul 8, 2026
4561ea0
chore(FileUpload): update implementation after review (round 2)
KamilEmeleev Jul 8, 2026
33c6ad1
chore(FileUpload): add validation example (story + docs)
KamilEmeleev Jul 8, 2026
4fee52d
chore(FileUpload): add hover/focus/invalid states, progress spinner, …
KamilEmeleev Jul 8, 2026
0647581
chore(FileUpload): add compact size for the empty drop zone
KamilEmeleev Jul 8, 2026
ca6e5c4
chore(FileUpload): update empty dropzone and item layout
KamilEmeleev Jul 8, 2026
e658550
chore(FileUpload): align layout with reference
KamilEmeleev Jul 8, 2026
a915b1b
chore(FileUpload): improve layout and locale
KamilEmeleev Jul 9, 2026
08f9014
chore(FileUpload): add `isInvalid` prop
KamilEmeleev Jul 9, 2026
e897b63
chore(FileUpload): rework API and component structure
KamilEmeleev Jul 9, 2026
465d8a3
chore(FileUpload): redesign API around composable collections
KamilEmeleev Jul 10, 2026
8f29c49
chore(FileUpload): add composable API and custom trigger support
KamilEmeleev Jul 10, 2026
d84ccb0
chore(FileUpload): folder selection support
KamilEmeleev Jul 10, 2026
965870c
chore(FileUpload): add `dropzoneTarget` prop
KamilEmeleev Jul 11, 2026
84169e1
chore(FileUpload): improve file handling and usage examples
KamilEmeleev Jul 13, 2026
2533de2
chore(FileUpload): simplify customization and localization
KamilEmeleev Jul 13, 2026
e37e073
chore(FileUpload): integrate with FormField
KamilEmeleev Jul 14, 2026
8f7c461
Merge branch 'main' into feat/ds-5195
KamilEmeleev Jul 14, 2026
7e1bd79
chore(FileUpload): add tooltip for truncated names and keep size unit…
KamilEmeleev Jul 14, 2026
aa706b5
chore(FileUpload): refine list composition, layout, and filename trun…
KamilEmeleev Jul 14, 2026
79fdb65
Merge branch 'main' into feat/ds-5195
KamilEmeleev Jul 15, 2026
5e198b3
chore(FileUpload): approve api
KamilEmeleev Jul 15, 2026
1d340a2
chore(FileUpload): add per-file validation
KamilEmeleev Jul 17, 2026
de322ae
chore(FileUpload): cover accept validation by extension
KamilEmeleev Jul 17, 2026
ff07acf
chore(FileUpload): improve css-layout
KamilEmeleev Jul 17, 2026
34e6659
chore(FileUpload): address review feedback
KamilEmeleev Jul 17, 2026
7023569
Merge branch 'main' into feat/ds-5195
KamilEmeleev Jul 18, 2026
9e6f375
chore(FileUpload): normalize file size formatting
KamilEmeleev Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .storybook/public/files/security-scan-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"scanId": "demo-scan-001",
"status": "completed",
"findings": 3
}
143 changes: 143 additions & 0 deletions packages/components/src/components/FileUpload/FileUpload.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
Meta,
Story,
Props,
Status,
} from '../../../../../.storybook/components';

import * as Stories from './FileUpload.stories';

<Meta of={Stories} />

# FileUpload

<Status variant="draft" />

FileUpload lets users select, display, and remove files. It reports user actions
but does not upload or store files.

## Import

```tsx
import { FileUpload } from '@koobiq/react-components';
```

## Usage

<Story of={Stories.Base} />

## Props

<Props of={Stories.Base} />

## Data model

Pass `items` and a render function for a dynamic collection. Pass
`FileUpload.Item` elements as children for a static collection. Keep the native
file on each item and pass it to `FileUpload.Item` to enable validation.

## Long file names

Long item names are truncated in the middle, keeping the end of the name and
its extension visible. Their full text is shown in a tooltip.

<Story of={Stories.LongFileName} />

## Label and help text

Use `label` and `caption` to describe the field.

<Story of={Stories.WithLabel} />

## Single file

FileUpload accepts one file by default.

<Story of={Stories.Single} />

## Multiple files

Use `allowsMultiple` to add several files.

<Story of={Stories.Multiple} />

## Duplicate files

Filter duplicate files in `onAdd` when the collection requires it.

<Story of={Stories.WithoutDuplicates} />

## Selecting a folder or files

Use `allowed` to accept files, folders, or both. Folder files include their
`relativePath` and can be grouped into one collection item.

<Story of={Stories.Allowed} />

## Compact

Use `size="compact"` for a smaller empty state.

<Story of={Stories.Compact} />

## Scrolling

Use `slotProps` to limit the list height.

<Story of={Stories.Scrollable} />

## Progress

Render upload progress inside the item row.

<Story of={Stories.UploadProgress} />

## Validation

Use `accept` and `maxFileSize` for standard checks, and `validate` for custom
synchronous checks. Invalid files remain in the collection.

Pass text or JSX to `errorMessage` to replace generated errors. Pass a function
to format the `ValidationResult`, or `null` to hide the error text.

<Story of={Stories.Validation} />

## Disabled

Use `isDisabled` to disable the whole component.

<Story of={Stories.Disabled} />

## Invalid

Use `isInvalid` with `errorMessage` for a field-level validation error.

<Story of={Stories.Invalid} />

## Drop zone target

By default, the drop area is inside the component.
Pass an element ref to `dropzoneTarget` to use another container,
or `"fullscreen"` to make the whole screen the drop area.

<Story of={Stories.DropzoneTarget} />

## Server file

Keep the native `File` in local items and append it to `FormData` on submit.

<Story of={Stories.ServerFile} />

## Customization

Use `messages` to change standard text. Use `slotProps` for field text, list
parts, and the drop overlay. Use
`renderEmptyState` to replace the large empty state.

<Story of={Stories.CustomContent} />

## Accessibility

Provide `label`, `aria-label`, or `aria-labelledby` and a `textValue` for each
item. Label, caption, and error text are associated with the upload area. After
removal, focus moves to the next file or the browse trigger.
103 changes: 103 additions & 0 deletions packages/components/src/components/FileUpload/FileUpload.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
.base {
--file-upload-container-border-color: var(--kbq-line-contrast-fade);
--file-upload-container-background: var(--kbq-background-bg);
--file-upload-dropzone-icon-color: var(--kbq-icon-contrast-fade);
--file-upload-upload-icon-color: var(--kbq-icon-contrast);
--file-upload-left-icon-color: var(--kbq-icon-contrast-fade);
--file-upload-text-color: var(--kbq-foreground-contrast);
--file-upload-caption-color: var(--file-upload-text-color);
--file-upload-file-size-color: var(--kbq-foreground-contrast-secondary);
--file-upload-footer-border-color: var(--file-upload-container-border-color);
--file-upload-border-width: var(--kbq-size-border-width);
--file-upload-border-radius: var(--kbq-size-s);
--file-upload-single-min-block-size: 44px;
--file-upload-single-padding-block: var(--kbq-size-m);
--file-upload-single-padding-inline: var(--kbq-size-l);
--file-upload-list-min-block-size: 120px;
--file-upload-multiple-min-block-size: 192px;
--file-upload-multiple-min-inline-size: 320px;
--file-upload-grid-cell-padding-inline: var(--kbq-size-s);
--file-upload-grid-cell-gap: var(--kbq-size-s);
--file-upload-file-size-inline-size: var(--kbq-size-7xl);
--file-upload-compact-padding-block: var(--kbq-size-m);
--file-upload-compact-padding-inline: var(--kbq-size-l);

min-inline-size: 0;
inline-size: 100%;
}

.base[data-invalid][data-empty]:not([data-disabled]) {
--file-upload-container-border-color: var(--kbq-line-error);
--file-upload-container-background: var(--kbq-background-error-less);
--file-upload-dropzone-icon-color: var(--kbq-icon-error);
--file-upload-upload-icon-color: var(--kbq-icon-error);
--file-upload-caption-color: var(--kbq-foreground-error);
}

.base[data-invalid][data-empty]:not([data-disabled], [data-multiple]) {
--file-upload-left-icon-color: var(--kbq-icon-error);
--file-upload-text-color: var(--kbq-foreground-error);
--file-upload-file-size-color: var(--kbq-foreground-error);
}

.base[data-disabled] {
--file-upload-container-border-color: var(--kbq-states-line-disabled);
--file-upload-container-background: var(--kbq-states-background-disabled);
--file-upload-dropzone-icon-color: var(--kbq-states-icon-disabled);
--file-upload-upload-icon-color: var(--kbq-states-icon-disabled);
--file-upload-left-icon-color: var(--kbq-states-icon-disabled);
--file-upload-text-color: var(--kbq-states-foreground-disabled);
--file-upload-file-size-color: var(--kbq-states-foreground-disabled);
--file-upload-footer-border-color: var(--kbq-states-line-disabled);
}

.base:not([data-disabled], [data-invalid][data-empty])
[data-slot='file-upload-control'][data-drop-target] {
--file-upload-container-border-color: var(--kbq-line-theme-fade);
--file-upload-container-background: var(--kbq-background-theme-fade);
--file-upload-footer-border-color: var(--kbq-line-theme-fade);
}

.base[data-disabled] [data-slot='file-upload-control'] {
pointer-events: none;
}

.base:not([data-multiple]) [data-slot='file-upload-control'] {
min-block-size: var(--file-upload-single-min-block-size);
}

.base:not([data-multiple])[data-empty] [data-slot='file-upload-control'] {
padding-block: var(--file-upload-single-padding-block);
padding-inline: var(--file-upload-single-padding-inline);
}

.base[data-multiple]:not([data-list-empty]) [data-slot='file-upload-control'] {
min-block-size: var(--file-upload-multiple-min-block-size);
}

.base[data-multiple][data-empty]:not([data-list-empty])[data-size='default']
[data-slot='file-upload-control'] {
align-items: center;
justify-content: center;
min-inline-size: var(--file-upload-multiple-min-inline-size);
}

.base[data-multiple][data-list-empty] [data-slot='file-upload-control'] {
padding-block: var(--file-upload-compact-padding-block);
padding-inline: var(--file-upload-compact-padding-inline);
}

.body {
min-inline-size: 0;
inline-size: 100%;
}

.body > :first-child + * {
margin: 0;
margin-block-start: var(--kbq-size-xs);
}

.body > :first-child + * + * {
margin: 0;
margin-block-start: var(--kbq-size-xxs);
}
Loading
Loading