@recursica/mui-adapter 0.22.0 → 0.24.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,47 +1,272 @@
1
1
  /* EXEMPTIONS:
2
- This component is a placeholder stub. Once fully implemented, its specific
3
- tokens will be fully wired up to direct styles. */
4
-
5
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_border-radius */
6
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_horizontal-padding */
7
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_icon-size */
8
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_icon-text-gap */
9
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_max-width */
10
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_min-height */
11
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_min-width */
12
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_placeholder-opacity */
13
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_text_font-family */
14
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_text_font-size */
15
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_text_font-style */
16
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_text_font-weight */
17
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_text_letter-spacing */
18
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_text_line-height */
19
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_text_text-decoration */
20
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_text_text-transform */
21
- /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_vertical-padding */
22
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_layouts_side-by-side_properties_top-bottom-margin */
23
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_layouts_stacked_properties_top-bottom-margin */
24
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_default_properties_border-size */
25
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_default_properties_colors_background */
26
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_default_properties_colors_border-color */
27
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_default_properties_colors_leading-icon */
28
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_default_properties_colors_text */
29
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_default_properties_colors_trailing-icon */
2
+ - The disabled/error states' own border-size variables are ignored so the border never changes
3
+ thickness across states and shifts the layout — same house policy as TextField/DatePicker.
4
+ The flat, state-agnostic properties_border-size token below is applied uniformly instead.
5
+ - There is no forge-defined focus state for file-input (no `states.focus` axis, unlike its
6
+ disabled/error siblings) — the generic recursica_brand_states_focus_* ring is used instead,
7
+ same fallback TextField/DatePicker use for the same reason.
8
+ - properties_icon-text-gap doubles as the gap between chips: file-input has no dedicated
9
+ chip-gap token (unlike file-upload's properties_item-gap).
10
+ - The trailing clear-all affordance is now a real shared `Button` (icon-only, "text" variant)
11
+ rather than a bespoke span, so it gets its own real button semantics/keyboard handling —
12
+ same pattern as Tree's expand/collapse button. It renders through Button's own tokened
13
+ color states (including disabled), so file-input's own trailing-icon color tokens — which
14
+ have no "clear button is disabled/errored" equivalent state of their own anyway — are no
15
+ longer consumed here. */
30
16
  /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_border-size */
31
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_background */
32
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_border-color */
33
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_leading-icon */
34
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_text */
35
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_trailing-icon */
36
17
  /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_border-size */
37
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_background */
38
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_border-color */
39
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_leading-icon */
40
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_text */
18
+ /* recursica-ignore: --recursica_ui-kit_components_file-input_properties_colors_trailing-icon */
41
19
  /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_trailing-icon */
42
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_focus_properties_border-size */
43
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_focus_properties_colors_background */
44
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_focus_properties_colors_border-color */
45
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_focus_properties_colors_leading-icon */
46
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_focus_properties_colors_text */
47
- /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_focus_properties_colors_trailing-icon */
20
+ /* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_trailing-icon */
21
+
22
+ /* LAYOUT SPACING OVERRIDES:
23
+ - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
24
+ - Also sets the --file-input-control-{max,min}-width hooks consumed inline in FileInput.tsx,
25
+ since max-width/min-width are split per layout variant (same shape as TextField). */
26
+ .layoutOverride {
27
+ --form-control-margin-bottom: var(
28
+ --recursica_ui-kit_components_file-input_variants_layouts_stacked_properties_top-bottom-margin
29
+ );
30
+ --file-input-control-max-width: var(
31
+ --recursica_ui-kit_components_file-input_variants_layouts_stacked_properties_max-width
32
+ );
33
+ --file-input-control-min-width: var(
34
+ --recursica_ui-kit_components_file-input_variants_layouts_stacked_properties_min-width
35
+ );
36
+ }
37
+
38
+ .layoutOverride[data-form-layout="side-by-side"] {
39
+ --form-control-margin-bottom: var(
40
+ --recursica_ui-kit_components_file-input_variants_layouts_side-by-side_properties_top-bottom-margin
41
+ );
42
+ --file-input-control-max-width: var(
43
+ --recursica_ui-kit_components_file-input_variants_layouts_side-by-side_properties_max-width
44
+ );
45
+ --file-input-control-min-width: var(
46
+ --recursica_ui-kit_components_file-input_variants_layouts_side-by-side_properties_min-width
47
+ );
48
+ }
49
+
50
+ /* HARDCODED VALUES:
51
+ - border-style: solid. Native structural rendering rule (matches TextField).
52
+ - outline: none. Bypassing the browser focus ring to rely strictly on the state cascade below.
53
+ - .root's cursor/position/overflow: structural, not a design-token concern. */
54
+
55
+ .root {
56
+ display: flex;
57
+ align-items: center;
58
+ position: relative;
59
+ box-sizing: border-box;
60
+ width: 100%;
61
+ max-width: var(--file-input-control-max-width);
62
+ min-width: var(--file-input-control-min-width);
63
+ min-height: var(
64
+ --recursica_ui-kit_components_file-input_properties_min-height
65
+ );
66
+ gap: var(--recursica_ui-kit_components_file-input_properties_icon-text-gap);
67
+ padding-top: var(
68
+ --recursica_ui-kit_components_file-input_properties_vertical-padding
69
+ );
70
+ padding-bottom: var(
71
+ --recursica_ui-kit_components_file-input_properties_vertical-padding
72
+ );
73
+ padding-left: var(
74
+ --recursica_ui-kit_components_file-input_properties_horizontal-padding
75
+ );
76
+ padding-right: var(
77
+ --recursica_ui-kit_components_file-input_properties_horizontal-padding
78
+ );
79
+ border-width: var(
80
+ --recursica_ui-kit_components_file-input_properties_border-size
81
+ );
82
+ border-style: solid;
83
+ border-radius: var(
84
+ --recursica_ui-kit_components_file-input_properties_border-radius
85
+ );
86
+ background-color: var(
87
+ --recursica_ui-kit_components_file-input_properties_colors_background-color
88
+ );
89
+ border-color: var(
90
+ --recursica_ui-kit_components_file-input_properties_colors_border-color
91
+ );
92
+ color: var(
93
+ --recursica_ui-kit_components_file-input_properties_colors_text-color
94
+ );
95
+ cursor: pointer;
96
+ outline: none;
97
+ }
98
+
99
+ .root[data-disabled="true"],
100
+ .root[data-readonly="true"] {
101
+ cursor: default;
102
+ }
103
+
104
+ .root > * {
105
+ position: relative;
106
+ }
107
+
108
+ /* Dragging-over state: reuses the generic hover overlay recipe (see Button.module.css) for the
109
+ background, and the shared "selected" border bridge var FormControlWrapper already exposes —
110
+ file-input has no color tokens of its own for this state. Same recipe as FileUpload's
111
+ dropzone; see "Dragging-over visual state" in FILEINPUT_IMPLEMENTATION_NOTES.md. */
112
+ .root::after {
113
+ content: "";
114
+ position: absolute;
115
+ inset: 0;
116
+ border-radius: inherit;
117
+ z-index: 0;
118
+ pointer-events: none;
119
+ transition: opacity 150ms ease;
120
+ opacity: 0;
121
+ background-color: var(--recursica_brand_states_hover_color);
122
+ }
123
+ .root[data-dragging="true"] {
124
+ border-color: var(--form-field-border-selected);
125
+ }
126
+ .root[data-dragging="true"]::after {
127
+ opacity: var(--recursica_brand_states_hover_opacity);
128
+ }
129
+
130
+ .leadingIcon {
131
+ display: flex;
132
+ flex: none;
133
+ width: var(--recursica_ui-kit_components_file-input_properties_icon-size);
134
+ height: var(--recursica_ui-kit_components_file-input_properties_icon-size);
135
+ color: var(
136
+ --recursica_ui-kit_components_file-input_properties_colors_leading-icon
137
+ );
138
+ }
139
+ .leadingIcon svg {
140
+ width: 100%;
141
+ height: 100%;
142
+ }
143
+
144
+ .content {
145
+ display: flex;
146
+ align-items: center;
147
+ flex: 1 1 auto;
148
+ min-width: 0; /* HARDCODE: lets the value/chip row actually shrink and ellipsize */
149
+ overflow: hidden;
150
+ }
151
+
152
+ .value {
153
+ font-family: var(
154
+ --recursica_ui-kit_components_file-input_properties_text_font-family
155
+ );
156
+ font-size: var(
157
+ --recursica_ui-kit_components_file-input_properties_text_font-size
158
+ );
159
+ font-style: var(
160
+ --recursica_ui-kit_components_file-input_properties_text_font-style
161
+ );
162
+ font-weight: var(
163
+ --recursica_ui-kit_components_file-input_properties_text_font-weight
164
+ );
165
+ letter-spacing: var(
166
+ --recursica_ui-kit_components_file-input_properties_text_letter-spacing
167
+ );
168
+ line-height: var(
169
+ --recursica_ui-kit_components_file-input_properties_text_line-height
170
+ );
171
+ text-decoration: var(
172
+ --recursica_ui-kit_components_file-input_properties_text_text-decoration
173
+ );
174
+ text-transform: var(
175
+ --recursica_ui-kit_components_file-input_properties_text_text-transform
176
+ );
177
+ width: 100%;
178
+ white-space: nowrap;
179
+ text-overflow: ellipsis;
180
+ /* HARDCODE: x-only clipping — clipping y too (plain overflow: hidden) cuts off descenders
181
+ (e.g. the "g" in "image.png"); same fix as Chip.module.css's .children. */
182
+ overflow-x: clip;
183
+ overflow-y: visible;
184
+ }
185
+
186
+ .value[data-placeholder="true"] {
187
+ opacity: var(
188
+ --recursica_ui-kit_components_file-input_properties_placeholder-opacity
189
+ );
190
+ }
191
+
192
+ .chipRow {
193
+ display: flex;
194
+ align-items: center;
195
+ flex-wrap: nowrap;
196
+ /* HARDCODE: x-only scroll — the chip list can exceed the control's own width (more files than
197
+ fit on one line), so it scrolls horizontally within the control instead of wrapping/growing
198
+ it or clipping the overflow away. */
199
+ overflow-x: auto;
200
+ overflow-y: hidden;
201
+ gap: var(--recursica_ui-kit_components_file-input_properties_icon-text-gap);
202
+ }
203
+
204
+ .chipWrapper {
205
+ display: flex;
206
+ min-width: 0;
207
+ flex: none;
208
+ }
209
+
210
+ /* Structural placement only — the button's own "text" variant/size="small" tokens (including its
211
+ own disabled state) own its visual appearance, same as Tree's .expandButton. */
212
+ .trailingIcon {
213
+ position: relative;
214
+ z-index: 1;
215
+ flex: none;
216
+ }
217
+
218
+ /* -------------------------------------
219
+ STATE CASCADE ARCHITECTURE
220
+ -------------------------------------- */
221
+
222
+ /* Focus State Mapping (Triggered internally via browser pseudo-class on the root control) */
223
+ /* file-input has no per-component focus colors (see EXEMPTIONS above); apply the generic focus
224
+ ring tokens as a box-shadow ring instead of a color swap (matches TextField/DatePicker). */
225
+ .root:focus-visible {
226
+ box-shadow:
227
+ 0 0 0 var(--recursica_brand_states_focus_border-size)
228
+ var(--recursica_brand_states_focus_color),
229
+ 0 0 var(--recursica_brand_states_focus_blur)
230
+ var(--recursica_brand_states_focus_margin)
231
+ var(--recursica_brand_states_focus_color);
232
+ }
233
+
234
+ /* Error State Mapping (Propagated strictly down from the wrapper DOM context) */
235
+ .root[data-error="true"] {
236
+ border-color: var(
237
+ --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_border-color
238
+ ) !important;
239
+ background-color: var(
240
+ --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_background-color
241
+ ) !important;
242
+ color: var(
243
+ --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_text-color
244
+ ) !important;
245
+ }
246
+ .root[data-error="true"] .leadingIcon {
247
+ color: var(
248
+ --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_leading-icon
249
+ ) !important;
250
+ }
251
+
252
+ /* Disabled State Mapping (Propagated strictly down from the wrapper DOM context) */
253
+ .root[data-disabled="true"] {
254
+ border-color: var(
255
+ --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_border-color
256
+ ) !important;
257
+ background-color: var(
258
+ --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_background-color
259
+ ) !important;
260
+ color: var(
261
+ --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_text-color
262
+ ) !important;
263
+ opacity: var(
264
+ --recursica_ui-kit_components_file-input_variants_states_disabled_properties_opacity
265
+ ) !important;
266
+ cursor: not-allowed;
267
+ }
268
+ .root[data-disabled="true"] .leadingIcon {
269
+ color: var(
270
+ --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_leading-icon
271
+ ) !important;
272
+ }
@@ -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
  };