zabi-components 5.0.11 → 5.0.13

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,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import Dropdown from "../molecules/Dropdown.svelte";
3
+ import { ChevronDown, CheckCircle, AlertTriangle, AlertCircle } from "@lucide/svelte";
4
+
2
5
  // SSR-safe ID generation
3
6
  function generateId(prefix: string = "id"): string {
4
7
  if (typeof window !== "undefined") {
@@ -19,6 +22,8 @@
19
22
  label?: string;
20
23
  disabled?: boolean;
21
24
  size?: "sm" | "md" | "lg";
25
+ variant?: "default" | "success" | "warning" | "error";
26
+ message?: string;
22
27
  onchange?: (event: Event) => void;
23
28
  }
24
29
 
@@ -29,27 +34,55 @@
29
34
  label = "",
30
35
  disabled = false,
31
36
  size = "md",
37
+ variant = "default",
38
+ message = "",
32
39
  ...restProps
33
40
  }: Props = $props();
34
41
 
35
- // Generate unique ID - SSR safe (call directly, not in $state)
36
- const selectId = generateId("select");
42
+ let isOpen = $state(false);
37
43
 
38
- // Size classes using full class names
44
+ // Size classes matching M3 design specifications
39
45
  const sizeClass = $derived(() => {
40
- return size === "sm"
41
- ? "px-3 py-1.5 text-sm"
42
- : size === "lg"
43
- ? "px-5 py-3 text-base"
44
- : "px-4 py-2 text-sm"; // default md
46
+ if (size === "sm") {
47
+ return {
48
+ padding: "px-4 py-2",
49
+ text: "text-sm",
50
+ leading: "leading-5"
51
+ };
52
+ } else if (size === "lg") {
53
+ return {
54
+ padding: "px-4 py-3",
55
+ text: "text-base",
56
+ leading: "leading-6"
57
+ };
58
+ } else {
59
+ // default md
60
+ return {
61
+ padding: "px-4 py-2.5",
62
+ text: "text-base",
63
+ leading: "leading-6"
64
+ };
65
+ }
45
66
  });
46
67
 
47
- // Select classes using Badge pattern
48
- const selectClasses = $derived(() => {
68
+ // Variant classes using semantic colors
69
+ const variantClass = $derived(() => {
70
+ return variant === "success"
71
+ ? "border-success focus:border-success focus:ring-success"
72
+ : variant === "warning"
73
+ ? "border-warning focus:border-warning focus:ring-warning"
74
+ : variant === "error"
75
+ ? "border-error focus:border-error focus:ring-error"
76
+ : "border-0 focus:ring-2 focus:ring-brand-500"; // default - no border
77
+ });
78
+
79
+ // Trigger button classes matching M3 design
80
+ const triggerClasses = $derived(() => {
81
+ const sizeStyles = sizeClass();
49
82
  const baseClasses =
50
- "w-full border border-border rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-surface focus:ring-focus focus:border-focus disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-surface-disabled";
83
+ "w-full bg-brand-100 rounded-lg transition-all duration-200 text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between";
51
84
 
52
- return `${baseClasses} ${sizeClass()}`.trim();
85
+ return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.leading} ${variantClass()}`.trim();
53
86
  });
54
87
 
55
88
  // Label classes using semantic text colors
@@ -57,32 +90,130 @@
57
90
  () => "block text-sm font-medium text-label mb-1",
58
91
  );
59
92
 
60
- function handleChange(event: Event) {
61
- const target = event.target as HTMLSelectElement;
62
- value = target.value;
93
+ // Message classes based on variant
94
+ const messageClasses = $derived(() => {
95
+ if (variant === "error") {
96
+ return "text-error text-sm mt-1 flex items-center gap-1.5";
97
+ } else if (variant === "success") {
98
+ return "text-success text-sm mt-1 flex items-center gap-1.5";
99
+ } else if (variant === "warning") {
100
+ return "text-warning text-sm mt-1 flex items-center gap-1.5";
101
+ }
102
+ return "text-description text-sm mt-1 flex items-center gap-1.5";
103
+ });
104
+
105
+ // Get icon component based on variant
106
+ const getIcon = $derived(() => {
107
+ if (variant === "error") return AlertCircle;
108
+ if (variant === "success") return CheckCircle;
109
+ if (variant === "warning") return AlertTriangle;
110
+ return null;
111
+ });
112
+
113
+ // Generate unique ID - SSR safe
114
+ const selectId = generateId("select");
115
+
116
+ // Get selected option label
117
+ const selectedLabel = $derived(() => {
118
+ if (isEmpty()) {
119
+ return String(placeholder || "Select an option");
120
+ }
121
+ const selected = options.find((opt) => opt.value === value);
122
+ return selected?.label ? String(selected.label) : String(placeholder || "Select an option");
123
+ });
124
+
125
+ // Check if value is empty
126
+ const isEmpty = $derived(() => {
127
+ return value === undefined || value === null || value === "";
128
+ });
129
+
130
+ function handleOptionClick(optionValue: string | number) {
131
+ if (disabled) return;
132
+ value = optionValue;
133
+ isOpen = false;
134
+
135
+ // Create a synthetic event for onchange
136
+ if (onchange) {
137
+ const syntheticEvent = new Event("change", { bubbles: true });
138
+ Object.defineProperty(syntheticEvent, "target", {
139
+ value: { value: optionValue },
140
+ enumerable: true
141
+ });
142
+ (onchange as (event: Event) => void)(syntheticEvent);
143
+ }
144
+ }
145
+
146
+ function handleTriggerClick(event: MouseEvent) {
147
+ if (disabled) return;
148
+ event.stopPropagation();
149
+ isOpen = !isOpen;
150
+ }
151
+
152
+ // Close dropdown when clicking outside
153
+ function handleClickOutside(event: MouseEvent) {
154
+ if (isOpen && !(event.target as HTMLElement).closest('.select-container')) {
155
+ isOpen = false;
156
+ }
157
+ }
158
+
159
+ // Handle escape key
160
+ function handleKeydown(event: KeyboardEvent) {
161
+ if (event.key === "Escape" && isOpen) {
162
+ isOpen = false;
163
+ }
63
164
  }
64
165
  </script>
65
166
 
66
- <div>
167
+ <svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
168
+
169
+ <div class="w-full select-container">
67
170
  {#if label}
68
171
  <label for={selectId} class={labelClasses()}>{label}</label>
69
172
  {/if}
70
173
 
71
- <select
72
- id={selectId}
73
- {value}
74
- {disabled}
75
- class={selectClasses()}
76
- onchange={handleChange}
77
- {...restProps}
78
- >
79
- {#if placeholder && !value}
80
- <option value="" disabled>{placeholder}</option>
81
- {/if}
82
- {#each options as option (option.value)}
83
- <option value={option.value} disabled={option.disabled}>
84
- {option.label}
85
- </option>
86
- {/each}
87
- </select>
88
- </div>
174
+ <Dropdown isOpen={isOpen} placement="bottom-start">
175
+ {#snippet trigger()}
176
+ <button
177
+ type="button"
178
+ id={selectId}
179
+ class={triggerClasses()}
180
+ {disabled}
181
+ onclick={handleTriggerClick}
182
+ aria-haspopup="listbox"
183
+ aria-expanded={isOpen}
184
+ aria-describedby={message ? `${selectId}-message` : undefined}
185
+ >
186
+ <span class="text-left flex-1 {isEmpty() ? 'text-description' : 'text-body'}">
187
+ {isEmpty() ? placeholder : (options.find((opt) => opt.value === value)?.label || placeholder)}
188
+ </span>
189
+ <ChevronDown
190
+ size={20}
191
+ class="text-description transition-transform duration-200 {isOpen ? 'rotate-180' : ''}"
192
+ />
193
+ </button>
194
+ {/snippet}
195
+ {#snippet children()}
196
+ <div class="px-2 py-2">
197
+ {#each options as option (option.value)}
198
+ <button
199
+ type="button"
200
+ class="w-full text-left px-4 py-2 text-body hover:bg-brand-50 transition-colors rounded-md my-0.5 {option.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} {value === option.value ? 'bg-brand-50' : ''}"
201
+ onclick={() => handleOptionClick(option.value)}
202
+ disabled={option.disabled}
203
+ >
204
+ {option.label}
205
+ </button>
206
+ {/each}
207
+ </div>
208
+ {/snippet}
209
+ </Dropdown>
210
+ {#if message && variant !== "default"}
211
+ <p id={`${selectId}-message`} class={messageClasses()} role="alert">
212
+ {#if getIcon()}
213
+ {@const Icon = getIcon()}
214
+ <Icon size={14} class="shrink-0" />
215
+ {/if}
216
+ <span>{message}</span>
217
+ </p>
218
+ {/if}
219
+ </div>
@@ -9,6 +9,8 @@ interface Props {
9
9
  label?: string;
10
10
  disabled?: boolean;
11
11
  size?: "sm" | "md" | "lg";
12
+ variant?: "default" | "success" | "warning" | "error";
13
+ message?: string;
12
14
  onchange?: (event: Event) => void;
13
15
  }
14
16
  declare const Select: import("svelte").Component<Props, {}, "">;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from "svelte";
3
3
  import { Sun, Moon } from "@lucide/svelte";
4
+
4
5
  // SSR-safe utilities
5
6
  function safeLocalStorage(): Storage | undefined {
6
7
  return typeof window !== "undefined" ? localStorage : undefined;
@@ -11,12 +12,18 @@
11
12
  }
12
13
 
13
14
  interface Props {
14
- isDark?: boolean;
15
+ size?: "sm" | "md" | "lg";
16
+ variant?: "default" | "ghost" | "outline";
15
17
  onclick?: (event: Event) => void;
16
18
  }
17
19
 
18
- let { isDark = false, ...restProps }: Props = $props();
20
+ let {
21
+ size = "md",
22
+ variant = "default",
23
+ ...restProps
24
+ }: Props = $props();
19
25
 
26
+ let isDark = $state(false);
20
27
  let mounted = $state(false);
21
28
 
22
29
  onMount(() => {
@@ -27,14 +34,10 @@
27
34
  const savedTheme = storage.getItem("theme");
28
35
  let prefersDark = false;
29
36
 
30
- // Only check media query when mounted and in browser
31
- if (mounted && typeof window !== "undefined" && window.matchMedia) {
37
+ if (window.matchMedia) {
32
38
  try {
33
- prefersDark = window.matchMedia(
34
- "(prefers-color-scheme: dark)",
35
- ).matches;
39
+ prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
36
40
  } catch (e) {
37
- // Fallback if matchMedia fails
38
41
  prefersDark = false;
39
42
  }
40
43
  }
@@ -51,6 +54,10 @@
51
54
  if (mounted && storage) {
52
55
  storage.setItem("theme", isDark ? "dark" : "light");
53
56
  }
57
+
58
+ if (onclick) {
59
+ (onclick as (event: Event) => void)(event);
60
+ }
54
61
  }
55
62
 
56
63
  function updateTheme() {
@@ -63,26 +70,82 @@
63
70
  }
64
71
  }
65
72
  }
73
+
74
+ // Size classes matching M3 design
75
+ const sizeClass = $derived(() => {
76
+ if (size === "sm") {
77
+ return {
78
+ button: "w-8 h-8",
79
+ icon: 16
80
+ };
81
+ } else if (size === "lg") {
82
+ return {
83
+ button: "w-12 h-12",
84
+ icon: 24
85
+ };
86
+ } else {
87
+ // default md
88
+ return {
89
+ button: "w-10 h-10",
90
+ icon: 20
91
+ };
92
+ }
93
+ });
94
+
95
+ // Variant classes using semantic colors
96
+ const variantClass = $derived(() => {
97
+ if (variant === "ghost") {
98
+ return "bg-transparent hover:bg-surface-hover border-0";
99
+ } else if (variant === "outline") {
100
+ return "bg-surface-elevated hover:bg-surface-hover border border-border";
101
+ } else {
102
+ // default
103
+ return "bg-surface-elevated hover:bg-surface-hover border-0";
104
+ }
105
+ });
106
+
107
+ const buttonClasses = $derived(() => {
108
+ const sizeStyles = sizeClass();
109
+ return `
110
+ ${sizeStyles.button}
111
+ ${variantClass()}
112
+ rounded-lg
113
+ flex
114
+ items-center
115
+ justify-center
116
+ text-label
117
+ cursor-pointer
118
+ transition-colors
119
+ duration-200
120
+ focus:outline-none
121
+ focus:ring-2
122
+ focus:ring-brand-500
123
+ focus:ring-offset-2
124
+ focus:ring-offset-surface
125
+ `.trim().replace(/\s+/g, " ");
126
+ });
66
127
  </script>
67
128
 
68
129
  {#if mounted}
69
130
  <button
70
131
  onclick={toggleTheme}
71
- class="w-10 h-10 bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-lg flex items-center justify-center text-gray-700 cursor-pointer focus:outline-none focus:ring-2 focus:ring-brand-500"
132
+ class={buttonClasses()}
72
133
  aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
134
+ type="button"
73
135
  {...restProps}
74
136
  >
75
137
  {#if isDark}
76
- <Moon size={20} class="text-label" />
138
+ <Moon size={sizeClass().icon} class="text-label" />
77
139
  {:else}
78
- <Sun size={20} class="text-label" />
140
+ <Sun size={sizeClass().icon} class="text-label" />
79
141
  {/if}
80
142
  </button>
81
143
  {:else}
82
144
  <!-- SSR fallback -->
83
145
  <button
84
- class="w-10 h-10 bg-gray-100 border border-gray-300 rounded-lg flex items-center justify-center text-gray-700 cursor-pointer"
146
+ class="w-10 h-10 bg-surface-elevated rounded-lg flex items-center justify-center text-label cursor-pointer"
85
147
  aria-label="Theme toggle"
148
+ type="button"
86
149
  {...restProps}
87
150
  >
88
151
  <Sun size={20} class="text-label" />
@@ -1,5 +1,6 @@
1
1
  interface Props {
2
- isDark?: boolean;
2
+ size?: "sm" | "md" | "lg";
3
+ variant?: "default" | "ghost" | "outline";
3
4
  onclick?: (event: Event) => void;
4
5
  }
5
6
  declare const ThemeToggle: import("svelte").Component<Props, {}, "">;
@@ -39,21 +39,21 @@
39
39
  if (onchange) onchange({ checked });
40
40
  }
41
41
 
42
- // Toggle button classes using full class names
42
+ // Toggle button classes matching M3 design
43
43
  const toggleButtonClasses = $derived(() =>
44
44
  [
45
- "relative inline-flex w-10 h-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2",
46
- checked ? "bg-brand-600" : "bg-gray-200",
45
+ "relative inline-flex w-10 h-6 flex-shrink-0 cursor-pointer rounded-full border-0 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2",
46
+ checked ? "bg-brand-600" : "bg-stone-300",
47
47
  disabled && "opacity-50 cursor-not-allowed",
48
48
  ]
49
49
  .filter(Boolean)
50
50
  .join(" "),
51
51
  );
52
52
 
53
- // Toggle thumb classes using full class names
53
+ // Toggle thumb classes matching M3 design
54
54
  const toggleThumbClasses = $derived(() => {
55
55
  const baseClasses =
56
- "pointer-events-none inline-block w-5 h-5 transform rounded-full bg-white shadow-lg transition duration-200 ease-in-out";
56
+ "pointer-events-none absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white transition-transform duration-200 ease-in-out";
57
57
  const positionClasses = checked ? "translate-x-4" : "translate-x-0";
58
58
 
59
59
  return `${baseClasses} ${positionClasses}`.trim();
@@ -11,118 +11,56 @@
11
11
  trigger,
12
12
  ...restProps
13
13
  }: Props & { children?: any; trigger?: any } = $props();
14
+
15
+ // Get positioning classes based on placement
16
+ const placementClasses = $derived(() => {
17
+ const base = "absolute z-dropdown min-w-[12rem]";
18
+ const positioning = {
19
+ "bottom-start": "top-full left-0 mt-2",
20
+ "bottom-end": "top-full right-0 mt-2",
21
+ "top-start": "bottom-full left-0 mb-2",
22
+ "top-end": "bottom-full right-0 mb-2",
23
+ };
24
+ return `${base} ${positioning[placement]}`;
25
+ });
26
+
27
+ // Get transform classes based on placement and open state
28
+ const transformClasses = $derived(() => {
29
+ if (!isOpen) {
30
+ const hiddenTransform = {
31
+ "bottom-start": "translate-y-1",
32
+ "bottom-end": "translate-y-1",
33
+ "top-start": "-translate-y-1",
34
+ "top-end": "-translate-y-1",
35
+ };
36
+ return `opacity-0 invisible ${hiddenTransform[placement]}`;
37
+ }
38
+ return "opacity-100 visible translate-y-0";
39
+ });
40
+
41
+ // Get dropdown content classes
42
+ const dropdownContentClasses = $derived(() => {
43
+ return `
44
+ ${placementClasses()}
45
+ bg-brand-100
46
+ rounded-lg
47
+ shadow-lg
48
+ border-0
49
+ py-2
50
+ transition-all
51
+ duration-200
52
+ ease-in-out
53
+ ${transformClasses()}
54
+ `.trim().replace(/\s+/g, " ");
55
+ });
14
56
  </script>
15
57
 
16
- <div
17
- class="dropdown-container group relative inline-block"
18
- data-placement={placement}
19
- >
58
+ <div class="relative inline-block" data-placement={placement}>
20
59
  {@render trigger?.()}
21
60
 
22
61
  {#if isOpen}
23
- <div
24
- class="dropdown-content opacity-100 visible transform-none group-hover:opacity-100 group-hover:visible group-focus-within:opacity-100 group-focus-within:visible"
25
- >
62
+ <div class={dropdownContentClasses()}>
26
63
  {@render children?.()}
27
64
  </div>
28
65
  {/if}
29
66
  </div>
30
-
31
- <style>
32
- .dropdown-container {
33
- position: relative;
34
- display: inline-block;
35
- }
36
-
37
- .dropdown-content {
38
- position: absolute;
39
- z-index: 50;
40
- background-color: white;
41
- border: 1px solid #e5e7eb;
42
- border-radius: 0.5rem;
43
- box-shadow:
44
- 0 10px 15px -3px rgba(0, 0, 0, 0.1),
45
- 0 4px 6px -2px rgba(0, 0, 0, 0.05);
46
- min-width: 12rem;
47
- opacity: 0;
48
- visibility: hidden;
49
- transition:
50
- opacity 0.2s ease-in-out,
51
- visibility 0.2s ease-in-out,
52
- transform 0.2s ease-in-out;
53
- }
54
-
55
- /* Positioning based on data-placement */
56
- .dropdown-container[data-placement="bottom-start"] .dropdown-content {
57
- top: calc(100% + 8px);
58
- left: 0;
59
- transform: translateY(4px);
60
- }
61
-
62
- .dropdown-container[data-placement="bottom-end"] .dropdown-content {
63
- top: calc(100% + 8px);
64
- right: 0;
65
- transform: translateY(4px);
66
- }
67
-
68
- .dropdown-container[data-placement="top-start"] .dropdown-content {
69
- bottom: calc(100% + 8px);
70
- left: 0;
71
- transform: translateY(-4px);
72
- }
73
-
74
- .dropdown-container[data-placement="top-end"] .dropdown-content {
75
- bottom: calc(100% + 8px);
76
- right: 0;
77
- transform: translateY(-4px);
78
- }
79
-
80
- /* Show on hover/focus */
81
- .dropdown-container:hover .dropdown-content,
82
- .dropdown-container:focus-within .dropdown-content {
83
- opacity: 1;
84
- visibility: visible;
85
- transform: translateY(0);
86
- }
87
-
88
- /* Arrow styling */
89
- .dropdown-content::before {
90
- content: "";
91
- position: absolute;
92
- width: 0;
93
- height: 0;
94
- border-style: solid;
95
- border-width: 6px;
96
- border-color: transparent;
97
- }
98
-
99
- .dropdown-container[data-placement="bottom-start"]
100
- .dropdown-content::before,
101
- .dropdown-container[data-placement="bottom-end"] .dropdown-content::before {
102
- top: -6px;
103
- border-bottom-color: white;
104
- }
105
-
106
- .dropdown-container[data-placement="top-start"] .dropdown-content::before,
107
- .dropdown-container[data-placement="top-end"] .dropdown-content::before {
108
- bottom: -6px;
109
- border-top-color: white;
110
- }
111
-
112
- .dropdown-container[data-placement="bottom-start"]
113
- .dropdown-content::before {
114
- left: 1rem;
115
- }
116
-
117
- .dropdown-container[data-placement="bottom-end"] .dropdown-content::before {
118
- right: 1rem;
119
- }
120
-
121
- .dropdown-container[data-placement="top-start"] .dropdown-content::before {
122
- left: 1rem;
123
- }
124
-
125
- .dropdown-container[data-placement="top-end"] .dropdown-content::before {
126
- right: 1rem;
127
- }
128
- </style>
@@ -23,6 +23,8 @@
23
23
  let fileInput = $state<HTMLInputElement>();
24
24
 
25
25
  function handleFileSelect(event: Event) {
26
+ if (disabled) return;
27
+
26
28
  const input = event.target as HTMLInputElement;
27
29
  if (!input.files || input.files.length === 0) return;
28
30
 
@@ -36,6 +38,8 @@
36
38
  }
37
39
 
38
40
  function removeImage() {
41
+ if (disabled) return;
42
+
39
43
  if (value && typeof URL !== "undefined" && URL.revokeObjectURL) {
40
44
  URL.revokeObjectURL(value);
41
45
  }
@@ -43,6 +47,7 @@
43
47
  }
44
48
 
45
49
  function triggerFileSelect() {
50
+ if (disabled) return;
46
51
  fileInput?.click();
47
52
  }
48
53
  </script>
@@ -54,10 +59,10 @@
54
59
  <img
55
60
  src={value}
56
61
  alt=""
57
- class="w-full h-32 object-cover rounded-lg border border-border"
62
+ class="w-full h-32 object-cover rounded-lg border-0"
58
63
  />
59
64
  <div
60
- class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center"
65
+ class="absolute inset-0 bg-black/50 dark:bg-black/70 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center"
61
66
  >
62
67
  <div class="flex gap-2">
63
68
  <Button
@@ -82,11 +87,15 @@
82
87
  {:else}
83
88
  <!-- Empty State -->
84
89
  <div
85
- class="border-2 border-dashed border-border rounded-lg p-6 text-center hover:border-border-strong transition-colors cursor-pointer"
90
+ class="border-2 border-dashed border-stone-200 rounded-lg p-6 text-center hover:border-brand-500 transition-colors {disabled
91
+ ? 'cursor-not-allowed opacity-50'
92
+ : 'cursor-pointer'}"
86
93
  onclick={triggerFileSelect}
87
94
  role="button"
88
- tabindex="0"
89
- onkeydown={(e) => e.key === "Enter" && triggerFileSelect()}
95
+ tabindex={disabled ? -1 : 0}
96
+ onkeydown={(e) =>
97
+ e.key === "Enter" && !disabled && triggerFileSelect()}
98
+ aria-disabled={disabled}
90
99
  >
91
100
  <div class="space-y-3">
92
101
  <div
@@ -110,6 +119,7 @@
110
119
  type="file"
111
120
  {accept}
112
121
  onchange={handleFileSelect}
122
+ {disabled}
113
123
  class="hidden"
114
124
  />
115
125
  </div>