@recursica/mantine-adapter 0.40.0 → 0.41.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.
@@ -1,18 +1,310 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
+ import React, { useState } from "react";
2
3
  import { FileInput } from "./FileInput";
3
- import { ComingSoon } from "@recursica/storybook-template";
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 FileInput> = {
6
- title: "UI-Kit/🚧 FileInput",
12
+ title: "UI-Kit/FileInput",
7
13
  component: FileInput,
8
14
  tags: ["autodocs"],
9
- argTypes: {},
15
+ parameters: {
16
+ docs: {
17
+ description: {
18
+ component: `
19
+ The \`FileInput\` component is a single-line, \`TextField\`-shaped control for choosing files, integrated directly into the \`FormControlWrapper\` architecture. It shares \`FileUpload\`'s selection/validation interface (\`accept\`/\`maxSize\`/\`maxFiles\`, \`readOnly\`) behind a different presentation — every selected file renders as a removable chip in a horizontally scrollable row, and a trailing \`Button\` clears the current selection.
20
+
21
+ ### Examples
22
+ \`\`\`tsx
23
+ <FileInput
24
+ label="Resume"
25
+ files={files}
26
+ onFilesAdded={(added) => setFiles(added.map((file) => ({ file })))}
27
+ onFileRemove={() => setFiles([])}
28
+ />
29
+ \`\`\`
30
+ `,
31
+ },
32
+ },
33
+ },
34
+ argTypes: {
35
+ ...formControlArgTypes,
36
+ accept: {
37
+ control: "text",
38
+ description:
39
+ "Native `accept` attribute for the file picker (e.g. `.pdf,.png`).",
40
+ },
41
+ multiple: {
42
+ control: "boolean",
43
+ description:
44
+ "Whether more than one file can be selected/dropped at once. Defaults to `false`.",
45
+ },
46
+ maxSize: {
47
+ control: "number",
48
+ description: "Maximum size per file, in bytes.",
49
+ },
50
+ maxFiles: {
51
+ control: "number",
52
+ description:
53
+ "Maximum total number of files allowed. Only meaningful when `multiple` is `true`.",
54
+ },
55
+ readOnly: {
56
+ control: "boolean",
57
+ description:
58
+ "Renders `files` as a static, non-interactive display with no clear/remove icons.",
59
+ },
60
+ placeholder: {
61
+ control: "text",
62
+ },
63
+ icon: {
64
+ table: { disable: true },
65
+ },
66
+ files: {
67
+ table: { disable: true },
68
+ },
69
+ onFilesAdded: {
70
+ table: { disable: true },
71
+ },
72
+ onFileRemove: {
73
+ table: { disable: true },
74
+ },
75
+ onFilesRejected: {
76
+ table: { disable: true },
77
+ },
78
+ },
10
79
  };
11
80
 
12
81
  export default meta;
13
82
 
14
83
  type Story = StoryObj<typeof FileInput>;
84
+ type FileInputStoryArgs = React.ComponentProps<typeof FileInput> & {
85
+ withLayer?: boolean;
86
+ layer?: number;
87
+ };
15
88
 
16
89
  export const Default: Story = {
17
- render: () => <ComingSoon componentName="FileInput" />,
90
+ args: {
91
+ label: "Resume",
92
+ },
93
+ /* eslint-disable @typescript-eslint/no-unused-vars */
94
+ render: function FileInputStory({
95
+ withLayer,
96
+ layer,
97
+ ...args
98
+ }: FileInputStoryArgs) {
99
+ /* eslint-enable @typescript-eslint/no-unused-vars */
100
+ const [files, setFiles] = useState<RecursicaFileUploadItem[]>([]);
101
+ return (
102
+ <FileInput
103
+ {...args}
104
+ files={files}
105
+ onFilesAdded={(added) => setFiles(added.map((file) => ({ file })))}
106
+ onFileRemove={() => setFiles([])}
107
+ />
108
+ );
109
+ },
110
+ };
111
+
112
+ export const WithFile: Story = {
113
+ args: {
114
+ label: "Resume",
115
+ },
116
+ /* eslint-disable @typescript-eslint/no-unused-vars */
117
+ render: function FileInputStory({
118
+ withLayer,
119
+ layer,
120
+ ...args
121
+ }: FileInputStoryArgs) {
122
+ /* eslint-enable @typescript-eslint/no-unused-vars */
123
+ const [files, setFiles] = useState<RecursicaFileUploadItem[]>([
124
+ { file: mockFile("resume.pdf") },
125
+ ]);
126
+ return (
127
+ <FileInput
128
+ {...args}
129
+ files={files}
130
+ onFilesAdded={(added) => setFiles(added.map((file) => ({ file })))}
131
+ onFileRemove={() => setFiles([])}
132
+ />
133
+ );
134
+ },
135
+ };
136
+
137
+ export const MultipleFiles: Story = {
138
+ args: {
139
+ label: "Attachments",
140
+ assistiveText: "Up to 5 files",
141
+ multiple: true,
142
+ },
143
+ /* eslint-disable @typescript-eslint/no-unused-vars */
144
+ render: function FileInputStory({
145
+ withLayer,
146
+ layer,
147
+ ...args
148
+ }: FileInputStoryArgs) {
149
+ /* eslint-enable @typescript-eslint/no-unused-vars */
150
+ const [files, setFiles] = useState<RecursicaFileUploadItem[]>([
151
+ { file: mockFile("document.pdf") },
152
+ { file: mockFile("image.png") },
153
+ { file: mockFile("spreadsheet.xlsx") },
154
+ ]);
155
+ return (
156
+ <FileInput
157
+ {...args}
158
+ files={files}
159
+ onFilesAdded={(added) =>
160
+ setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
161
+ }
162
+ onFileRemove={(id) =>
163
+ setFiles((prev) =>
164
+ prev.filter((item) => (item.id ?? item.file.name) !== id),
165
+ )
166
+ }
167
+ />
168
+ );
169
+ },
170
+ };
171
+
172
+ export const SideBySide: Story = {
173
+ args: {
174
+ label: "Resume",
175
+ assistiveText: "PDF or Word document",
176
+ formLayout: "side-by-side",
177
+ },
178
+ /* eslint-disable @typescript-eslint/no-unused-vars */
179
+ render: function FileInputStory({
180
+ withLayer,
181
+ layer,
182
+ ...args
183
+ }: FileInputStoryArgs) {
184
+ /* eslint-enable @typescript-eslint/no-unused-vars */
185
+ const [files, setFiles] = useState<RecursicaFileUploadItem[]>([]);
186
+ return (
187
+ <FileInput
188
+ {...args}
189
+ files={files}
190
+ onFilesAdded={(added) => setFiles(added.map((file) => ({ file })))}
191
+ onFileRemove={() => setFiles([])}
192
+ />
193
+ );
194
+ },
195
+ };
196
+
197
+ export const Disabled: Story = {
198
+ args: {
199
+ label: "Resume",
200
+ disabled: true,
201
+ },
202
+ /* eslint-disable @typescript-eslint/no-unused-vars */
203
+ render: function FileInputStory({
204
+ withLayer,
205
+ layer,
206
+ ...args
207
+ }: FileInputStoryArgs) {
208
+ /* eslint-enable @typescript-eslint/no-unused-vars */
209
+ const [files] = useState<RecursicaFileUploadItem[]>([
210
+ { file: mockFile("resume.pdf") },
211
+ ]);
212
+ return <FileInput {...args} files={files} />;
213
+ },
214
+ };
215
+
216
+ export const ErrorState: Story = {
217
+ args: {
218
+ label: "Resume",
219
+ error: "A file is required.",
220
+ },
221
+ };
222
+
223
+ export const ReadOnly: Story = {
224
+ args: {
225
+ label: "Attachments",
226
+ assistiveText: "Submitted files cannot be changed",
227
+ multiple: true,
228
+ readOnly: true,
229
+ },
230
+ /* eslint-disable @typescript-eslint/no-unused-vars */
231
+ render: function FileInputStory({
232
+ withLayer,
233
+ layer,
234
+ ...args
235
+ }: FileInputStoryArgs) {
236
+ /* eslint-enable @typescript-eslint/no-unused-vars */
237
+ const [files] = useState<RecursicaFileUploadItem[]>([
238
+ { file: mockFile("document.pdf") },
239
+ { file: mockFile("image.png") },
240
+ ]);
241
+ return <FileInput {...args} files={files} />;
242
+ },
243
+ };
244
+
245
+ // Demonstrates that `accept` isn't just cosmetic on the native file-picker dialog — files that
246
+ // don't match it are rejected the same way whether picked or dropped directly (drag-and-drop
247
+ // bypasses the native `accept` filtering entirely, so this only works because FileInput
248
+ // re-validates it itself, same as FileUpload). Drag a non-.pdf file onto the control to see it
249
+ // switch into its error state with the default `invalidFileTypeMessage`.
250
+ export const AcceptRestriction: Story = {
251
+ args: {
252
+ label: "Resume",
253
+ assistiveText: "Only .pdf files are accepted",
254
+ accept: ".pdf",
255
+ },
256
+ /* eslint-disable @typescript-eslint/no-unused-vars */
257
+ render: function FileInputStory({
258
+ withLayer,
259
+ layer,
260
+ ...args
261
+ }: FileInputStoryArgs) {
262
+ /* eslint-enable @typescript-eslint/no-unused-vars */
263
+ const [files, setFiles] = useState<RecursicaFileUploadItem[]>([]);
264
+ return (
265
+ <FileInput
266
+ {...args}
267
+ files={files}
268
+ onFilesAdded={(added) => setFiles(added.map((file) => ({ file })))}
269
+ onFileRemove={() => setFiles([])}
270
+ />
271
+ );
272
+ },
273
+ };
274
+
275
+ // Demonstrates `maxFiles` in multiple-file mode: already at the 2-file cap below, so
276
+ // dropping/picking another file is rejected with the default `maxFilesMessage`.
277
+ export const MaxFilesRestriction: Story = {
278
+ args: {
279
+ label: "Attachments",
280
+ assistiveText: "Up to 2 files allowed",
281
+ multiple: true,
282
+ maxFiles: 2,
283
+ },
284
+ /* eslint-disable @typescript-eslint/no-unused-vars */
285
+ render: function FileInputStory({
286
+ withLayer,
287
+ layer,
288
+ ...args
289
+ }: FileInputStoryArgs) {
290
+ /* eslint-enable @typescript-eslint/no-unused-vars */
291
+ const [files, setFiles] = useState<RecursicaFileUploadItem[]>([
292
+ { file: mockFile("document.pdf") },
293
+ { file: mockFile("image.png") },
294
+ ]);
295
+ return (
296
+ <FileInput
297
+ {...args}
298
+ files={files}
299
+ onFilesAdded={(added) =>
300
+ setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
301
+ }
302
+ onFileRemove={(id) =>
303
+ setFiles((prev) =>
304
+ prev.filter((item) => (item.id ?? item.file.name) !== id),
305
+ )
306
+ }
307
+ />
308
+ );
309
+ },
18
310
  };