@recursica/mui-adapter 0.22.0 → 0.23.0
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.
- package/CHANGELOG.md +23 -0
- package/dist/index.d.ts +157 -6
- package/dist/mui-adapter.cjs +76 -76
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +5885 -5650
- package/dist/mui-adapter.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Button/Button.module.css +13 -0
- package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +102 -0
- package/src/components/Chip/Chip.module.css +30 -4
- package/src/components/Chip/Chip.tsx +40 -4
- package/src/components/Chip/USAGE.md +19 -0
- package/src/components/FileUpload/FILEUPLOAD_IMPLEMENTATION_NOTES.md +249 -0
- package/src/components/FileUpload/FileUpload.module.css +203 -38
- package/src/components/FileUpload/FileUpload.stories.tsx +348 -4
- package/src/components/FileUpload/FileUpload.tsx +350 -6
- package/src/components/FileUpload/USAGE.md +163 -5
- package/src/index.ts +3 -1
|
@@ -1,40 +1,205 @@
|
|
|
1
1
|
/* EXEMPTIONS:
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
- border-style has no usable var(...) form: the Figma-exported token's value is the literal
|
|
3
|
+
string `"dashed"` (quotes included), which is not a valid CSS <line-style> keyword — the same
|
|
4
|
+
class of "structural, not tokenized" property the canonical guide already treats border-style
|
|
5
|
+
as (see .root's own HARDCODED VALUES note below and TimePicker/DatePicker's identical
|
|
6
|
+
hardcoded `border-style: solid`). Hardcoded to the keyword `dashed` instead.
|
|
7
|
+
- properties_icon-size has no equivalent: unlike TextField/DatePicker, file-upload has no
|
|
8
|
+
dedicated icon-size token. Sized to visually match the Figma reference instead (see
|
|
9
|
+
HARDCODED VALUES below). */
|
|
6
10
|
/* recursica-ignore: --recursica_ui-kit_components_file-upload_properties_border-style */
|
|
7
|
-
|
|
8
|
-
/*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
11
|
+
|
|
12
|
+
/* HARDCODED VALUES:
|
|
13
|
+
- .root: display: flex; flex-direction: column; width: 100% — structural layout, not a design
|
|
14
|
+
token concern (matches how other block-level components like Layer are laid out).
|
|
15
|
+
- .dropzone: border-style: dashed — see EXEMPTIONS above.
|
|
16
|
+
- .uploadIcon: width/height: 2rem — no icon-size token exists for file-upload (unlike
|
|
17
|
+
TextField/DatePicker); sized to visually match the Figma reference. Revisit if a dedicated
|
|
18
|
+
token is ever added. */
|
|
19
|
+
|
|
20
|
+
/* LAYOUT SPACING OVERRIDES:
|
|
21
|
+
- Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens. */
|
|
22
|
+
.layoutOverride {
|
|
23
|
+
--form-control-margin-bottom: var(
|
|
24
|
+
--recursica_ui-kit_components_file-upload_variants_layouts_stacked_properties_top-bottom-margin
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.layoutOverride[data-form-layout="side-by-side"] {
|
|
29
|
+
--form-control-margin-bottom: var(
|
|
30
|
+
--recursica_ui-kit_components_file-upload_variants_layouts_side-by-side_properties_top-bottom-margin
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.root {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
width: 100%;
|
|
38
|
+
gap: var(--recursica_ui-kit_components_file-upload_properties_list-spacing);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dropzone {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
position: relative; /* HARDCODE: anchors the drag-over overlay below */
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
width: 100%;
|
|
49
|
+
border-style: dashed; /* HARDCODE: see EXEMPTIONS above */
|
|
50
|
+
border-width: var(
|
|
51
|
+
--recursica_ui-kit_components_file-upload_properties_border-size
|
|
52
|
+
);
|
|
53
|
+
border-radius: var(
|
|
54
|
+
--recursica_ui-kit_components_file-upload_properties_border-radius
|
|
55
|
+
);
|
|
56
|
+
border-color: var(
|
|
57
|
+
--recursica_ui-kit_components_file-upload_properties_colors_border-color
|
|
58
|
+
);
|
|
59
|
+
background-color: var(
|
|
60
|
+
--recursica_ui-kit_components_file-upload_properties_colors_background-color
|
|
61
|
+
);
|
|
62
|
+
padding: var(--recursica_ui-kit_components_file-upload_properties_padding);
|
|
63
|
+
gap: var(
|
|
64
|
+
--recursica_ui-kit_components_file-upload_properties_vertical-element-gap
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Dragging-over state: reuses the generic hover overlay recipe (see Button.module.css) for the
|
|
69
|
+
background, and the shared "selected" border bridge var FormControlWrapper already exposes
|
|
70
|
+
(see FormControlWrapper.module.css) for the border — file-upload has no color tokens of its own
|
|
71
|
+
for this state. See "Dragging-over visual state" in FILEUPLOAD_IMPLEMENTATION_NOTES.md. */
|
|
72
|
+
.dropzone > * {
|
|
73
|
+
position: relative;
|
|
74
|
+
z-index: 1;
|
|
75
|
+
}
|
|
76
|
+
.dropzone::after {
|
|
77
|
+
content: "";
|
|
78
|
+
position: absolute;
|
|
79
|
+
inset: 0;
|
|
80
|
+
border-radius: inherit;
|
|
81
|
+
z-index: 0;
|
|
82
|
+
pointer-events: none;
|
|
83
|
+
transition: opacity 150ms ease;
|
|
84
|
+
opacity: 0;
|
|
85
|
+
background-color: var(--recursica_brand_states_hover_color);
|
|
86
|
+
}
|
|
87
|
+
.dropzone[data-dragging="true"] {
|
|
88
|
+
border-color: var(--form-field-border-selected);
|
|
89
|
+
}
|
|
90
|
+
.dropzone[data-dragging="true"]::after {
|
|
91
|
+
opacity: var(--recursica_brand_states_hover_opacity);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.uploadIcon {
|
|
95
|
+
display: flex;
|
|
96
|
+
width: 2rem; /* HARDCODE: see EXEMPTIONS above */
|
|
97
|
+
height: 2rem; /* HARDCODE: see EXEMPTIONS above */
|
|
98
|
+
color: var(
|
|
99
|
+
--recursica_ui-kit_components_file-upload_properties_colors_upload-icon
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.uploadIcon svg {
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: 100%;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.dropzoneText {
|
|
109
|
+
font-family: var(
|
|
110
|
+
--recursica_ui-kit_components_file-upload_properties_text_font-family
|
|
111
|
+
);
|
|
112
|
+
font-size: var(
|
|
113
|
+
--recursica_ui-kit_components_file-upload_properties_text_font-size
|
|
114
|
+
);
|
|
115
|
+
font-style: var(
|
|
116
|
+
--recursica_ui-kit_components_file-upload_properties_text_font-style
|
|
117
|
+
);
|
|
118
|
+
font-weight: var(
|
|
119
|
+
--recursica_ui-kit_components_file-upload_properties_text_font-weight
|
|
120
|
+
);
|
|
121
|
+
letter-spacing: var(
|
|
122
|
+
--recursica_ui-kit_components_file-upload_properties_text_letter-spacing
|
|
123
|
+
);
|
|
124
|
+
line-height: var(
|
|
125
|
+
--recursica_ui-kit_components_file-upload_properties_text_line-height
|
|
126
|
+
);
|
|
127
|
+
text-decoration: var(
|
|
128
|
+
--recursica_ui-kit_components_file-upload_properties_text_text-decoration
|
|
129
|
+
);
|
|
130
|
+
text-transform: var(
|
|
131
|
+
--recursica_ui-kit_components_file-upload_properties_text_text-transform
|
|
132
|
+
);
|
|
133
|
+
color: var(
|
|
134
|
+
--recursica_ui-kit_components_file-upload_properties_colors_text-color
|
|
135
|
+
);
|
|
136
|
+
text-align: center;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.fileList {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-wrap: wrap;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: var(--recursica_ui-kit_components_file-upload_properties_item-gap);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* -------------------------------------
|
|
147
|
+
STATE CASCADE ARCHITECTURE
|
|
148
|
+
-------------------------------------- */
|
|
149
|
+
|
|
150
|
+
/* Error State Mapping (Propagated strictly down from the wrapper DOM context) */
|
|
151
|
+
.root[data-error="true"] .dropzone {
|
|
152
|
+
border-width: var(
|
|
153
|
+
--recursica_ui-kit_components_file-upload_variants_states_error_properties_border-size
|
|
154
|
+
);
|
|
155
|
+
border-color: var(
|
|
156
|
+
--recursica_ui-kit_components_file-upload_variants_states_error_properties_colors_border-color
|
|
157
|
+
);
|
|
158
|
+
background-color: var(
|
|
159
|
+
--recursica_ui-kit_components_file-upload_variants_states_error_properties_colors_background-color
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.root[data-error="true"] .dropzoneText {
|
|
164
|
+
color: var(
|
|
165
|
+
--recursica_ui-kit_components_file-upload_variants_states_error_properties_colors_text-color
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.root[data-error="true"] .uploadIcon {
|
|
170
|
+
color: var(
|
|
171
|
+
--recursica_ui-kit_components_file-upload_variants_states_error_properties_colors_upload-icon
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Disabled State Mapping (Propagated strictly down from the wrapper DOM context) */
|
|
176
|
+
.root[data-disabled="true"] .dropzone {
|
|
177
|
+
border-width: var(
|
|
178
|
+
--recursica_ui-kit_components_file-upload_variants_states_disabled_properties_border-size
|
|
179
|
+
);
|
|
180
|
+
border-color: var(
|
|
181
|
+
--recursica_ui-kit_components_file-upload_variants_states_disabled_properties_colors_border-color
|
|
182
|
+
);
|
|
183
|
+
background-color: var(
|
|
184
|
+
--recursica_ui-kit_components_file-upload_variants_states_disabled_properties_colors_background-color
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.root[data-disabled="true"] .dropzoneText {
|
|
189
|
+
color: var(
|
|
190
|
+
--recursica_ui-kit_components_file-upload_variants_states_disabled_properties_colors_text-color
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.root[data-disabled="true"] .uploadIcon {
|
|
195
|
+
color: var(
|
|
196
|
+
--recursica_ui-kit_components_file-upload_variants_states_disabled_properties_colors_upload-icon
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.root[data-disabled="true"] {
|
|
201
|
+
opacity: var(
|
|
202
|
+
--recursica_ui-kit_components_file-upload_variants_states_disabled_properties_opacity
|
|
203
|
+
);
|
|
204
|
+
cursor: not-allowed;
|
|
205
|
+
}
|
|
@@ -1,18 +1,362 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import React, { useState } from "react";
|
|
2
3
|
import { FileUpload } from "./FileUpload";
|
|
3
|
-
import {
|
|
4
|
+
import { type RecursicaFileUploadItem } from "@recursica/adapter-common";
|
|
5
|
+
import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
|
|
6
|
+
|
|
7
|
+
function mockFile(name: string, size = 1024) {
|
|
8
|
+
return new File([new Uint8Array(size)], name);
|
|
9
|
+
}
|
|
4
10
|
|
|
5
11
|
const meta: Meta<typeof FileUpload> = {
|
|
6
|
-
title: "UI-Kit
|
|
12
|
+
title: "UI-Kit/FileUpload",
|
|
7
13
|
component: FileUpload,
|
|
8
14
|
tags: ["autodocs"],
|
|
9
|
-
|
|
15
|
+
parameters: {
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component: `
|
|
19
|
+
The \`FileUpload\` component is a drag-and-drop dropzone with a native browse-button fallback, integrated directly into the \`FormControlWrapper\` architecture. Selected files render as a removable-chip list below the dropzone.
|
|
20
|
+
|
|
21
|
+
### Examples
|
|
22
|
+
\`\`\`tsx
|
|
23
|
+
<FileUpload
|
|
24
|
+
label="Upload Files"
|
|
25
|
+
assistiveText="Max file size 5MB"
|
|
26
|
+
files={files}
|
|
27
|
+
onFilesAdded={(added) =>
|
|
28
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
29
|
+
}
|
|
30
|
+
onFileRemove={(id) =>
|
|
31
|
+
setFiles((prev) => prev.filter((item) => (item.id ?? item.file.name) !== id))
|
|
32
|
+
}
|
|
33
|
+
/>
|
|
34
|
+
\`\`\`
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
...formControlArgTypes,
|
|
41
|
+
accept: {
|
|
42
|
+
control: "text",
|
|
43
|
+
description:
|
|
44
|
+
"Native `accept` attribute for the file picker (e.g. `.pdf,.png`).",
|
|
45
|
+
},
|
|
46
|
+
multiple: {
|
|
47
|
+
control: "boolean",
|
|
48
|
+
description: "Whether multiple files can be selected/dropped at once.",
|
|
49
|
+
},
|
|
50
|
+
maxSize: {
|
|
51
|
+
control: "number",
|
|
52
|
+
description: "Maximum size per file, in bytes.",
|
|
53
|
+
},
|
|
54
|
+
maxFiles: {
|
|
55
|
+
control: "number",
|
|
56
|
+
description: "Maximum total number of files allowed.",
|
|
57
|
+
},
|
|
58
|
+
readOnly: {
|
|
59
|
+
control: "boolean",
|
|
60
|
+
description:
|
|
61
|
+
"Renders `files` as a static chip list with no remove icon, and hides the dropzone.",
|
|
62
|
+
},
|
|
63
|
+
dropzoneLabel: {
|
|
64
|
+
control: "text",
|
|
65
|
+
},
|
|
66
|
+
browseButtonLabel: {
|
|
67
|
+
control: "text",
|
|
68
|
+
},
|
|
69
|
+
icon: {
|
|
70
|
+
table: { disable: true },
|
|
71
|
+
},
|
|
72
|
+
files: {
|
|
73
|
+
table: { disable: true },
|
|
74
|
+
},
|
|
75
|
+
onFilesAdded: {
|
|
76
|
+
table: { disable: true },
|
|
77
|
+
},
|
|
78
|
+
onFileRemove: {
|
|
79
|
+
table: { disable: true },
|
|
80
|
+
},
|
|
81
|
+
onFilesRejected: {
|
|
82
|
+
table: { disable: true },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
10
85
|
};
|
|
11
86
|
|
|
12
87
|
export default meta;
|
|
13
88
|
|
|
14
89
|
type Story = StoryObj<typeof FileUpload>;
|
|
90
|
+
type FileUploadStoryArgs = React.ComponentProps<typeof FileUpload> & {
|
|
91
|
+
withLayer?: boolean;
|
|
92
|
+
layer?: number;
|
|
93
|
+
};
|
|
15
94
|
|
|
16
95
|
export const Default: Story = {
|
|
17
|
-
|
|
96
|
+
args: {
|
|
97
|
+
label: "Upload Files",
|
|
98
|
+
assistiveText: "Max file size 5MB",
|
|
99
|
+
},
|
|
100
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
101
|
+
render: function FileUploadStory({
|
|
102
|
+
withLayer,
|
|
103
|
+
layer,
|
|
104
|
+
...args
|
|
105
|
+
}: FileUploadStoryArgs) {
|
|
106
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
107
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([]);
|
|
108
|
+
return (
|
|
109
|
+
<FileUpload
|
|
110
|
+
{...args}
|
|
111
|
+
files={files}
|
|
112
|
+
onFilesAdded={(added) =>
|
|
113
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
114
|
+
}
|
|
115
|
+
onFileRemove={(id) =>
|
|
116
|
+
setFiles((prev) =>
|
|
117
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const WithFiles: Story = {
|
|
126
|
+
args: {
|
|
127
|
+
label: "Upload Files",
|
|
128
|
+
assistiveText: "Max file size 5MB",
|
|
129
|
+
},
|
|
130
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
131
|
+
render: function FileUploadStory({
|
|
132
|
+
withLayer,
|
|
133
|
+
layer,
|
|
134
|
+
...args
|
|
135
|
+
}: FileUploadStoryArgs) {
|
|
136
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
137
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([
|
|
138
|
+
{ file: mockFile("document.pdf") },
|
|
139
|
+
{ file: mockFile("image.png") },
|
|
140
|
+
{ file: mockFile("spreadsheet.xlsx") },
|
|
141
|
+
]);
|
|
142
|
+
return (
|
|
143
|
+
<FileUpload
|
|
144
|
+
{...args}
|
|
145
|
+
files={files}
|
|
146
|
+
onFilesAdded={(added) =>
|
|
147
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
148
|
+
}
|
|
149
|
+
onFileRemove={(id) =>
|
|
150
|
+
setFiles((prev) =>
|
|
151
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const EmptyState: Story = {
|
|
160
|
+
args: {
|
|
161
|
+
label: "Upload Files",
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const Disabled: Story = {
|
|
166
|
+
args: {
|
|
167
|
+
label: "Upload Files",
|
|
168
|
+
disabled: true,
|
|
169
|
+
},
|
|
170
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
171
|
+
render: function FileUploadStory({
|
|
172
|
+
withLayer,
|
|
173
|
+
layer,
|
|
174
|
+
...args
|
|
175
|
+
}: FileUploadStoryArgs) {
|
|
176
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
177
|
+
const [files] = useState<RecursicaFileUploadItem[]>([
|
|
178
|
+
{ file: mockFile("document.pdf") },
|
|
179
|
+
]);
|
|
180
|
+
return <FileUpload {...args} files={files} />;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const ErrorState: Story = {
|
|
185
|
+
args: {
|
|
186
|
+
label: "Upload Files",
|
|
187
|
+
error: "File upload failed. Please try again.",
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function StarIcon() {
|
|
192
|
+
return (
|
|
193
|
+
<svg
|
|
194
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
195
|
+
viewBox="0 0 24 24"
|
|
196
|
+
fill="currentColor"
|
|
197
|
+
aria-hidden
|
|
198
|
+
>
|
|
199
|
+
<path d="M12 2l2.9 6.6 7.1.6-5.4 4.7 1.6 7-6.2-3.8-6.2 3.8 1.6-7L2 9.2l7.1-.6z" />
|
|
200
|
+
</svg>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const CustomIcon: Story = {
|
|
205
|
+
args: {
|
|
206
|
+
label: "Upload Files",
|
|
207
|
+
icon: <StarIcon />,
|
|
208
|
+
},
|
|
209
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
210
|
+
render: function FileUploadStory({
|
|
211
|
+
withLayer,
|
|
212
|
+
layer,
|
|
213
|
+
...args
|
|
214
|
+
}: FileUploadStoryArgs) {
|
|
215
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
216
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([]);
|
|
217
|
+
return (
|
|
218
|
+
<FileUpload
|
|
219
|
+
{...args}
|
|
220
|
+
files={files}
|
|
221
|
+
onFilesAdded={(added) =>
|
|
222
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
223
|
+
}
|
|
224
|
+
onFileRemove={(id) =>
|
|
225
|
+
setFiles((prev) =>
|
|
226
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const LongFilenames: Story = {
|
|
235
|
+
args: {
|
|
236
|
+
label: "Upload Files",
|
|
237
|
+
},
|
|
238
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
239
|
+
render: function FileUploadStory({
|
|
240
|
+
withLayer,
|
|
241
|
+
layer,
|
|
242
|
+
...args
|
|
243
|
+
}: FileUploadStoryArgs) {
|
|
244
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
245
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([
|
|
246
|
+
{
|
|
247
|
+
file: mockFile("quarterly-financial-report-final-version-approved.pdf"),
|
|
248
|
+
},
|
|
249
|
+
{ file: mockFile("2026-08-team-offsite-photos-and-notes.zip") },
|
|
250
|
+
{ file: mockFile("resume.docx") },
|
|
251
|
+
]);
|
|
252
|
+
return (
|
|
253
|
+
<FileUpload
|
|
254
|
+
{...args}
|
|
255
|
+
files={files}
|
|
256
|
+
onFilesAdded={(added) =>
|
|
257
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
258
|
+
}
|
|
259
|
+
onFileRemove={(id) =>
|
|
260
|
+
setFiles((prev) =>
|
|
261
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const ReadOnly: Story = {
|
|
270
|
+
args: {
|
|
271
|
+
label: "Upload Files",
|
|
272
|
+
assistiveText: "Submitted files cannot be changed",
|
|
273
|
+
readOnly: true,
|
|
274
|
+
},
|
|
275
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
276
|
+
render: function FileUploadStory({
|
|
277
|
+
withLayer,
|
|
278
|
+
layer,
|
|
279
|
+
...args
|
|
280
|
+
}: FileUploadStoryArgs) {
|
|
281
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
282
|
+
const [files] = useState<RecursicaFileUploadItem[]>([
|
|
283
|
+
{ file: mockFile("document.pdf") },
|
|
284
|
+
{ file: mockFile("image.png") },
|
|
285
|
+
]);
|
|
286
|
+
return <FileUpload {...args} files={files} />;
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Demonstrates that `accept` isn't just cosmetic on the browse-button dialog — files that don't
|
|
291
|
+
// match it are rejected the same way whether picked via Browse or dropped directly (drag-and-drop
|
|
292
|
+
// bypasses the native `accept` filtering entirely, so this only works because FileUpload
|
|
293
|
+
// re-validates it itself). Drag a non-.pdf/.png file onto the dropzone (or edit `accept` in the
|
|
294
|
+
// controls) to see the control switch into its error state with the default
|
|
295
|
+
// `invalidFileTypeMessage` ("File type not accepted") instead of accepting the file.
|
|
296
|
+
export const AcceptRestriction: Story = {
|
|
297
|
+
args: {
|
|
298
|
+
label: "Upload Files",
|
|
299
|
+
assistiveText: "Only .pdf and .png files are accepted",
|
|
300
|
+
accept: ".pdf,.png",
|
|
301
|
+
},
|
|
302
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
303
|
+
render: function FileUploadStory({
|
|
304
|
+
withLayer,
|
|
305
|
+
layer,
|
|
306
|
+
...args
|
|
307
|
+
}: FileUploadStoryArgs) {
|
|
308
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
309
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([]);
|
|
310
|
+
return (
|
|
311
|
+
<FileUpload
|
|
312
|
+
{...args}
|
|
313
|
+
files={files}
|
|
314
|
+
onFilesAdded={(added) =>
|
|
315
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
316
|
+
}
|
|
317
|
+
onFileRemove={(id) =>
|
|
318
|
+
setFiles((prev) =>
|
|
319
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
/>
|
|
323
|
+
);
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Demonstrates `maxFiles`: already at the 2-file cap below, so dropping/picking another file is
|
|
328
|
+
// rejected the same way an `accept` mismatch is — the control switches into its error state with
|
|
329
|
+
// the default `maxFilesMessage` ("Maximum of 2 files allowed") instead of accepting the file.
|
|
330
|
+
export const MaxFilesRestriction: Story = {
|
|
331
|
+
args: {
|
|
332
|
+
label: "Upload Files",
|
|
333
|
+
assistiveText: "Up to 2 files allowed",
|
|
334
|
+
maxFiles: 2,
|
|
335
|
+
},
|
|
336
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
337
|
+
render: function FileUploadStory({
|
|
338
|
+
withLayer,
|
|
339
|
+
layer,
|
|
340
|
+
...args
|
|
341
|
+
}: FileUploadStoryArgs) {
|
|
342
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
343
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([
|
|
344
|
+
{ file: mockFile("document.pdf") },
|
|
345
|
+
{ file: mockFile("image.png") },
|
|
346
|
+
]);
|
|
347
|
+
return (
|
|
348
|
+
<FileUpload
|
|
349
|
+
{...args}
|
|
350
|
+
files={files}
|
|
351
|
+
onFilesAdded={(added) =>
|
|
352
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
353
|
+
}
|
|
354
|
+
onFileRemove={(id) =>
|
|
355
|
+
setFiles((prev) =>
|
|
356
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
/>
|
|
360
|
+
);
|
|
361
|
+
},
|
|
18
362
|
};
|