svelte-comp 1.2.5 → 1.2.6
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.
Potentially problematic release.
This version of svelte-comp might be problematic. Click here for more details.
- package/README.md +1 -1
- package/package.json +1 -1
- package/dist/App.svelte +0 -551
- package/dist/App.svelte.d.ts +0 -3
- package/dist/Container.svelte +0 -60
- package/dist/Container.svelte.d.ts +0 -12
- package/dist/app.css +0 -235
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -6
- package/dist/lang.d.ts +0 -1081
- package/dist/lang.js +0 -1096
- package/dist/lib/Accordion.svelte +0 -155
- package/dist/lib/Accordion.svelte.d.ts +0 -40
- package/dist/lib/Button.svelte +0 -170
- package/dist/lib/Button.svelte.d.ts +0 -53
- package/dist/lib/Card.svelte +0 -103
- package/dist/lib/Card.svelte.d.ts +0 -42
- package/dist/lib/Carousel.svelte +0 -293
- package/dist/lib/Carousel.svelte.d.ts +0 -13
- package/dist/lib/CheckBox.svelte +0 -210
- package/dist/lib/CheckBox.svelte.d.ts +0 -53
- package/dist/lib/CodeView.svelte +0 -307
- package/dist/lib/CodeView.svelte.d.ts +0 -64
- package/dist/lib/ColorPicker.svelte +0 -161
- package/dist/lib/ColorPicker.svelte.d.ts +0 -40
- package/dist/lib/DatePicker.svelte +0 -170
- package/dist/lib/DatePicker.svelte.d.ts +0 -53
- package/dist/lib/Dialog.svelte +0 -235
- package/dist/lib/Dialog.svelte.d.ts +0 -58
- package/dist/lib/Field.svelte +0 -299
- package/dist/lib/Field.svelte.d.ts +0 -8
- package/dist/lib/FilePicker.svelte +0 -241
- package/dist/lib/FilePicker.svelte.d.ts +0 -52
- package/dist/lib/Form.svelte +0 -438
- package/dist/lib/Form.svelte.d.ts +0 -20
- package/dist/lib/Hamburger.svelte +0 -211
- package/dist/lib/Hamburger.svelte.d.ts +0 -52
- package/dist/lib/Menu.svelte +0 -623
- package/dist/lib/Menu.svelte.d.ts +0 -33
- package/dist/lib/PaginatedCard.svelte +0 -73
- package/dist/lib/PaginatedCard.svelte.d.ts +0 -11
- package/dist/lib/Pagination.svelte +0 -119
- package/dist/lib/Pagination.svelte.d.ts +0 -9
- package/dist/lib/PrimaryColorSelect.svelte +0 -113
- package/dist/lib/PrimaryColorSelect.svelte.d.ts +0 -9
- package/dist/lib/ProgressBar.svelte +0 -141
- package/dist/lib/ProgressBar.svelte.d.ts +0 -48
- package/dist/lib/ProgressCircle.svelte +0 -192
- package/dist/lib/ProgressCircle.svelte.d.ts +0 -39
- package/dist/lib/Radio.svelte +0 -189
- package/dist/lib/Radio.svelte.d.ts +0 -55
- package/dist/lib/SearchInput.svelte +0 -106
- package/dist/lib/SearchInput.svelte.d.ts +0 -13
- package/dist/lib/Select.svelte +0 -524
- package/dist/lib/Select.svelte.d.ts +0 -21
- package/dist/lib/Slider.svelte +0 -253
- package/dist/lib/Slider.svelte.d.ts +0 -56
- package/dist/lib/Splitter.svelte +0 -150
- package/dist/lib/Splitter.svelte.d.ts +0 -43
- package/dist/lib/Switch.svelte +0 -167
- package/dist/lib/Switch.svelte.d.ts +0 -42
- package/dist/lib/Table.svelte +0 -299
- package/dist/lib/Table.svelte.d.ts +0 -17
- package/dist/lib/Tabs.svelte +0 -213
- package/dist/lib/Tabs.svelte.d.ts +0 -48
- package/dist/lib/ThemeToggle.svelte +0 -127
- package/dist/lib/ThemeToggle.svelte.d.ts +0 -32
- package/dist/lib/TimePicker.svelte +0 -269
- package/dist/lib/TimePicker.svelte.d.ts +0 -48
- package/dist/lib/Toast.svelte +0 -226
- package/dist/lib/Toast.svelte.d.ts +0 -14
- package/dist/lib/Tooltip.svelte +0 -110
- package/dist/lib/Tooltip.svelte.d.ts +0 -40
- package/dist/lib/index.d.ts +0 -32
- package/dist/lib/index.js +0 -33
- package/dist/lib/lang.d.ts +0 -158
- package/dist/lib/lang.js +0 -150
- package/dist/lib/types/index.d.ts +0 -111
- package/dist/lib/types/index.js +0 -26
- package/dist/main.d.ts +0 -3
- package/dist/main.js +0 -7
- package/dist/styles.css +0 -232
- package/dist/utils/index.d.ts +0 -34
- package/dist/utils/index.js +0 -268
package/dist/lib/Field.svelte
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
<!-- src/lib/Field.svelte -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* @component Field
|
|
5
|
-
* @description Unified input/textarea field with label, leading/trailing content, clear button, and validation.
|
|
6
|
-
*
|
|
7
|
-
* @prop as {"input" | "textarea"} - Underlying element to render
|
|
8
|
-
* @default "input"
|
|
9
|
-
*
|
|
10
|
-
* @prop label {string} - Label text rendered above the field
|
|
11
|
-
*
|
|
12
|
-
* @prop sz {SizeKey} - Size preset for spacing and typography
|
|
13
|
-
* @options xs|sm|md|lg|xl
|
|
14
|
-
* @default md
|
|
15
|
-
*
|
|
16
|
-
* @prop variant {FieldVariant} - Visual style variant
|
|
17
|
-
* @options default|filled|neutral
|
|
18
|
-
* @default default
|
|
19
|
-
*
|
|
20
|
-
* @prop clearable {boolean} - Shows a clear button for text inputs
|
|
21
|
-
* @default true
|
|
22
|
-
*
|
|
23
|
-
* @prop rows {number} - Row count for textarea mode
|
|
24
|
-
* @default 3
|
|
25
|
-
*
|
|
26
|
-
* @prop parseNumber {boolean} - Coerces numeric input when possible
|
|
27
|
-
* @default false
|
|
28
|
-
*
|
|
29
|
-
* @prop leading {Snippet | string} - Leading content rendered inside the field
|
|
30
|
-
*
|
|
31
|
-
* @prop trailing {Snippet | string} - Trailing content rendered inside the field
|
|
32
|
-
*
|
|
33
|
-
* @prop onChange {(val: string | number) => void} - Fired when the value changes
|
|
34
|
-
*
|
|
35
|
-
* @prop value {string | number} - Controlled field value (bindable)
|
|
36
|
-
* @default ""
|
|
37
|
-
*
|
|
38
|
-
* @prop class {string} - Additional classes applied to the root label
|
|
39
|
-
* @default ""
|
|
40
|
-
*
|
|
41
|
-
* @prop id {string} - Custom id used for label and input linkage
|
|
42
|
-
*
|
|
43
|
-
* @prop type {string} - Input type when `as="input"`
|
|
44
|
-
*
|
|
45
|
-
* @prop invalid {boolean} - Marks the field invalid and sets `aria-invalid`
|
|
46
|
-
* @default false
|
|
47
|
-
*
|
|
48
|
-
* @prop describedBy {string} - ID of helper or error text for accessibility
|
|
49
|
-
*
|
|
50
|
-
* @note `bind:value` is supported; `onChange` receives cast value (`number` when `parseNumber` succeeds, otherwise `string` or `""`).
|
|
51
|
-
* @note Clear button appears only for text inputs (not for `type="number"`) and sets value to an empty string.
|
|
52
|
-
* @note Automatic padding for leading/trailing content; label is linked via an auto-generated `id`.
|
|
53
|
-
* @note Accessibility: sets `aria-invalid`, `aria-describedby`; number inputs also set `inputmode="decimal"`.
|
|
54
|
-
*/
|
|
55
|
-
import type { Snippet } from "svelte";
|
|
56
|
-
import type {
|
|
57
|
-
HTMLInputAttributes,
|
|
58
|
-
HTMLTextareaAttributes,
|
|
59
|
-
} from "svelte/elements";
|
|
60
|
-
import type { SizeKey, FieldVariant, FieldType } from "./types";
|
|
61
|
-
import { TEXT } from "./types";
|
|
62
|
-
import { uid, cx } from "../utils";
|
|
63
|
-
|
|
64
|
-
type Props = (HTMLInputAttributes & HTMLTextareaAttributes & FieldType) & {
|
|
65
|
-
value?: string | number;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
let {
|
|
69
|
-
as = "input",
|
|
70
|
-
label,
|
|
71
|
-
sz = "md",
|
|
72
|
-
variant = "default",
|
|
73
|
-
clearable = true,
|
|
74
|
-
rows = 3,
|
|
75
|
-
parseNumber = false,
|
|
76
|
-
leading,
|
|
77
|
-
trailing,
|
|
78
|
-
onChange,
|
|
79
|
-
value = $bindable<string | number>(""),
|
|
80
|
-
class: externalClass = "",
|
|
81
|
-
id: externalId,
|
|
82
|
-
type,
|
|
83
|
-
invalid = false,
|
|
84
|
-
describedBy,
|
|
85
|
-
...rest
|
|
86
|
-
}: Props = $props();
|
|
87
|
-
|
|
88
|
-
const base =
|
|
89
|
-
"w-full outline-none transition-colors duration-[var(--transition-fast)] ease-[var(--timing-default)] box-border rounded-[var(--radius-md)] border focus:border-[var(--border-color-focus)] focus:ring-2 focus:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed";
|
|
90
|
-
|
|
91
|
-
const sizes: Record<SizeKey, string> = {
|
|
92
|
-
xs: "px-2 h-6",
|
|
93
|
-
sm: "px-3 h-7",
|
|
94
|
-
md: "px-4 h-8",
|
|
95
|
-
lg: "px-5 h-9",
|
|
96
|
-
xl: "px-6 h-10",
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const variants: Record<FieldVariant, string> = {
|
|
100
|
-
default:
|
|
101
|
-
"bg-[var(--color-bg-surface)] [color:var(--color-text-default)] placeholder:[color:var(--color-text-muted)] border-[var(--border-color-default)]",
|
|
102
|
-
filled:
|
|
103
|
-
"bg-[var(--color-bg-muted)] [color:var(--color-text-default)] placeholder:[color:var(--color-text-muted)] border-[var(--border-color-default)]",
|
|
104
|
-
neutral:
|
|
105
|
-
"bg-transparent [color:var(--color-text-default)] placeholder:[color:var(--color-text-muted)] border-transparent hover:border-[var(--border-color-default)] focus:border-[var(--border-color-focus)]",
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const rootClass = $derived(cx("flex flex-col gap-1", externalClass));
|
|
109
|
-
|
|
110
|
-
const inputClass = $derived(
|
|
111
|
-
as === "textarea"
|
|
112
|
-
? cx(base, variants[variant], TEXT.md, "px-4")
|
|
113
|
-
: cx(base, sizes[sz], TEXT[sz], variants[variant])
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
const containerClass = $derived(
|
|
117
|
-
cx("relative flex items-stretch", as === "textarea" && "items-start")
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const id = $derived(externalId ?? uid("fld-"));
|
|
121
|
-
|
|
122
|
-
const effectiveType = $derived(
|
|
123
|
-
as === "input"
|
|
124
|
-
? type || (rest as HTMLInputAttributes).type || "text"
|
|
125
|
-
: undefined
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const isNumber = $derived(as === "input" && effectiveType === "number");
|
|
129
|
-
const isPassword = $derived(as === "input" && effectiveType === "password");
|
|
130
|
-
|
|
131
|
-
let showPassword = $state(false);
|
|
132
|
-
|
|
133
|
-
function toOutgoing(v: string): string | number {
|
|
134
|
-
if (parseNumber && isNumber) {
|
|
135
|
-
if (v === "" || v === "-" || v === "." || v === "-.") {
|
|
136
|
-
return v;
|
|
137
|
-
}
|
|
138
|
-
const n = Number(v);
|
|
139
|
-
return Number.isNaN(n) ? "" : n;
|
|
140
|
-
}
|
|
141
|
-
return v;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function handleInput(e: Event) {
|
|
145
|
-
const t = e.target as HTMLInputElement | HTMLTextAreaElement;
|
|
146
|
-
const rawValue = t.value;
|
|
147
|
-
const outgoing = toOutgoing(rawValue);
|
|
148
|
-
value = outgoing;
|
|
149
|
-
onChange?.(outgoing);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function clear() {
|
|
153
|
-
const newValue = parseNumber && isNumber ? 0 : "";
|
|
154
|
-
value = newValue;
|
|
155
|
-
onChange?.(newValue);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const displayValue = $derived(String(value ?? ""));
|
|
159
|
-
const showClear = $derived(
|
|
160
|
-
clearable && as === "input" && displayValue.length > 0 && !isNumber
|
|
161
|
-
);
|
|
162
|
-
const needsRightPad = $derived(Boolean(trailing) || showClear || isPassword);
|
|
163
|
-
|
|
164
|
-
const labelColor = "[color:var(--color-text-muted)]";
|
|
165
|
-
</script>
|
|
166
|
-
|
|
167
|
-
<label for={id} class={rootClass}>
|
|
168
|
-
{#if label}
|
|
169
|
-
<span class={cx(TEXT[sz], "font-medium", labelColor)}>{label}</span>
|
|
170
|
-
{/if}
|
|
171
|
-
|
|
172
|
-
<div class={containerClass}>
|
|
173
|
-
{#if leading}
|
|
174
|
-
{@const leadingIsSnippet = typeof leading === "function"}
|
|
175
|
-
<div
|
|
176
|
-
class={cx(
|
|
177
|
-
"absolute inset-y-0 left-2 flex items-center [color:var(--color-text-muted)]"
|
|
178
|
-
)}
|
|
179
|
-
aria-hidden="true"
|
|
180
|
-
>
|
|
181
|
-
{#if leadingIsSnippet}
|
|
182
|
-
{@render (leading as Snippet)()}
|
|
183
|
-
{:else}
|
|
184
|
-
{leading}
|
|
185
|
-
{/if}
|
|
186
|
-
</div>
|
|
187
|
-
{/if}
|
|
188
|
-
|
|
189
|
-
{#if as === "textarea"}
|
|
190
|
-
<textarea
|
|
191
|
-
{...rest}
|
|
192
|
-
{id}
|
|
193
|
-
{rows}
|
|
194
|
-
class={cx(inputClass, needsRightPad && "pr-8", leading && "pl-8")}
|
|
195
|
-
value={displayValue}
|
|
196
|
-
aria-invalid={invalid || undefined}
|
|
197
|
-
aria-describedby={describedBy}
|
|
198
|
-
oninput={handleInput}
|
|
199
|
-
></textarea>
|
|
200
|
-
{:else}
|
|
201
|
-
<input
|
|
202
|
-
{...rest}
|
|
203
|
-
{id}
|
|
204
|
-
type={isPassword ? (showPassword ? "text" : "password") : effectiveType}
|
|
205
|
-
inputmode={isNumber ? "decimal" : undefined}
|
|
206
|
-
class={cx(inputClass, needsRightPad && "pr-8", leading && "pl-8")}
|
|
207
|
-
value={displayValue}
|
|
208
|
-
aria-invalid={invalid || undefined}
|
|
209
|
-
aria-describedby={describedBy}
|
|
210
|
-
oninput={handleInput}
|
|
211
|
-
/>
|
|
212
|
-
{/if}
|
|
213
|
-
|
|
214
|
-
<div class={cx("absolute inset-y-0 right-2 flex items-center gap-1")}>
|
|
215
|
-
{#if trailing}
|
|
216
|
-
{@const trailingIsSnippet = typeof trailing === "function"}
|
|
217
|
-
<span aria-hidden="true">
|
|
218
|
-
{#if trailingIsSnippet}
|
|
219
|
-
{@render (trailing as Snippet)()}
|
|
220
|
-
{:else}
|
|
221
|
-
{trailing}
|
|
222
|
-
{/if}
|
|
223
|
-
</span>
|
|
224
|
-
{/if}
|
|
225
|
-
|
|
226
|
-
{#if isPassword}
|
|
227
|
-
<button
|
|
228
|
-
type="button"
|
|
229
|
-
class={cx("password-toggle [color:var(--color-text-muted)] hover:opacity-[var(--opacity-hover)]")}
|
|
230
|
-
onmousedown={(e) => e.preventDefault()}
|
|
231
|
-
onclick={() => (showPassword = !showPassword)}
|
|
232
|
-
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
233
|
-
>
|
|
234
|
-
{#if showPassword}
|
|
235
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
236
|
-
<path d="M3 3L21 21" stroke="currentColor" stroke-width="2" />
|
|
237
|
-
<path
|
|
238
|
-
d="M10.5 10.677a2 2 0 002.823 2.823"
|
|
239
|
-
stroke="currentColor"
|
|
240
|
-
stroke-width="2"
|
|
241
|
-
/>
|
|
242
|
-
<path
|
|
243
|
-
d="M7.362 7.561C5.68 8.74 4.279 10.42 3 12c2.4 3.6 6 6 9 6 1.4 0 2.8-.5 4.1-1.2"
|
|
244
|
-
stroke="currentColor"
|
|
245
|
-
stroke-width="2"
|
|
246
|
-
/>
|
|
247
|
-
<path
|
|
248
|
-
d="M12 6c3 0 6 2.4 9 6-.6.9-1.3 1.7-2 2.5"
|
|
249
|
-
stroke="currentColor"
|
|
250
|
-
stroke-width="2"
|
|
251
|
-
/>
|
|
252
|
-
</svg>
|
|
253
|
-
{:else}
|
|
254
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
255
|
-
<path
|
|
256
|
-
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
|
257
|
-
stroke="currentColor"
|
|
258
|
-
stroke-width="2"
|
|
259
|
-
/>
|
|
260
|
-
<circle
|
|
261
|
-
cx="12"
|
|
262
|
-
cy="12"
|
|
263
|
-
r="3"
|
|
264
|
-
stroke="currentColor"
|
|
265
|
-
stroke-width="2"
|
|
266
|
-
/>
|
|
267
|
-
</svg>
|
|
268
|
-
{/if}
|
|
269
|
-
</button>
|
|
270
|
-
{/if}
|
|
271
|
-
|
|
272
|
-
{#if showClear}
|
|
273
|
-
<button
|
|
274
|
-
type="button"
|
|
275
|
-
tabindex="-1"
|
|
276
|
-
onmousedown={(e) => e.preventDefault()}
|
|
277
|
-
class={cx("[color:var(--color-text-muted)] hover:opacity-[var(--opacity-hover)]")}
|
|
278
|
-
onclick={clear}
|
|
279
|
-
aria-label="Clear"
|
|
280
|
-
title="Clear"
|
|
281
|
-
>
|
|
282
|
-
<svg
|
|
283
|
-
width="18"
|
|
284
|
-
height="18"
|
|
285
|
-
viewBox="0 0 24 24"
|
|
286
|
-
fill="none"
|
|
287
|
-
stroke="currentColor"
|
|
288
|
-
stroke-width="2"
|
|
289
|
-
stroke-linecap="round"
|
|
290
|
-
stroke-linejoin="round"
|
|
291
|
-
>
|
|
292
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
293
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
294
|
-
</svg>
|
|
295
|
-
</button>
|
|
296
|
-
{/if}
|
|
297
|
-
</div>
|
|
298
|
-
</div>
|
|
299
|
-
</label>
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { HTMLInputAttributes, HTMLTextareaAttributes } from "svelte/elements";
|
|
2
|
-
import type { FieldType } from "./types";
|
|
3
|
-
type Props = (HTMLInputAttributes & HTMLTextareaAttributes & FieldType) & {
|
|
4
|
-
value?: string | number;
|
|
5
|
-
};
|
|
6
|
-
declare const Field: import("svelte").Component<Props, {}, "value">;
|
|
7
|
-
type Field = ReturnType<typeof Field>;
|
|
8
|
-
export default Field;
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
<!-- src/lib/FilePicker.svelte -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* @component FilePicker
|
|
5
|
-
* @description Lightweight file selector with click support and drag-and-drop. Internally uses a hidden `<input type="file">` plus a drop zone.
|
|
6
|
-
*
|
|
7
|
-
* @prop accept {string} - Accepted file types
|
|
8
|
-
* @default "*\\/*"
|
|
9
|
-
*
|
|
10
|
-
* @prop multiple {boolean} - Allows selecting multiple files
|
|
11
|
-
* @default false
|
|
12
|
-
*
|
|
13
|
-
* @prop label {string} - Button label; falls back to localized text
|
|
14
|
-
*
|
|
15
|
-
* @prop disabled {boolean} - Disables all interactions
|
|
16
|
-
* @default false
|
|
17
|
-
*
|
|
18
|
-
* @prop clearable {boolean} - Shows a clear button to reset selection
|
|
19
|
-
* @default true
|
|
20
|
-
*
|
|
21
|
-
* @prop placeholder {string} - Placeholder text for the drop zone
|
|
22
|
-
*
|
|
23
|
-
* @prop value {FileList | null} - Controlled selected files (bindable)
|
|
24
|
-
* @default null
|
|
25
|
-
*
|
|
26
|
-
* @prop onFilesSelected {(files: FileList | null) => void} - Fired when files are chosen
|
|
27
|
-
*
|
|
28
|
-
* @prop onError {(error: string) => void} - Fired on validation errors
|
|
29
|
-
*
|
|
30
|
-
* @prop class {string} - Additional classes for the wrapper
|
|
31
|
-
* @default ""
|
|
32
|
-
*
|
|
33
|
-
* @note The entire area is clickable and supports drag-and-drop.
|
|
34
|
-
* @note After a selection, the underlying input resets its value, so choosing the same file twice still triggers updates.
|
|
35
|
-
* @note `accept` does not apply to dropped files, only to the picker UI; validate files inside `onFilesSelected`.
|
|
36
|
-
* @note When `clearable=true`, the user can clear selected files and the callback receives `null`.
|
|
37
|
-
* @note When `disabled=true`, clicks, drag events, focus, and keyboard input are blocked.
|
|
38
|
-
*/
|
|
39
|
-
import type { HTMLAttributes } from "svelte/elements";
|
|
40
|
-
import { getContext } from "svelte";
|
|
41
|
-
import Button from "./Button.svelte";
|
|
42
|
-
import { cx } from "../utils";
|
|
43
|
-
import { TEXTS } from "./lang";
|
|
44
|
-
|
|
45
|
-
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
46
|
-
accept?: string;
|
|
47
|
-
multiple?: boolean;
|
|
48
|
-
label?: string;
|
|
49
|
-
disabled?: boolean;
|
|
50
|
-
clearable?: boolean;
|
|
51
|
-
placeholder?: string;
|
|
52
|
-
value?: FileList | null;
|
|
53
|
-
onFilesSelected?: (files: FileList | null) => void;
|
|
54
|
-
onError?: (error: string) => void;
|
|
55
|
-
class?: string;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
let {
|
|
59
|
-
accept = "*/*",
|
|
60
|
-
multiple = false,
|
|
61
|
-
label,
|
|
62
|
-
disabled = false,
|
|
63
|
-
clearable = true,
|
|
64
|
-
placeholder,
|
|
65
|
-
value: propValue = null,
|
|
66
|
-
onFilesSelected,
|
|
67
|
-
class: externalClass = "",
|
|
68
|
-
...rest
|
|
69
|
-
}: Props = $props();
|
|
70
|
-
|
|
71
|
-
const lang = getContext<{ value: keyof typeof TEXTS }>("lang");
|
|
72
|
-
const L = $derived(TEXTS[lang.value].components.filePicker);
|
|
73
|
-
|
|
74
|
-
const labelFinal = $derived(label ?? L.text);
|
|
75
|
-
const placeholderFinal = $derived(placeholder ?? L.placeholder);
|
|
76
|
-
|
|
77
|
-
let inputEl: HTMLInputElement;
|
|
78
|
-
let internalValue: FileList | null = $derived(propValue);
|
|
79
|
-
let isDragOver = $state(false);
|
|
80
|
-
|
|
81
|
-
const base = "inline-block w-full";
|
|
82
|
-
const pickerClass = $derived(cx(base, externalClass));
|
|
83
|
-
|
|
84
|
-
const hasValue = $derived(Boolean(internalValue && internalValue.length > 0));
|
|
85
|
-
const fileNames = $derived(
|
|
86
|
-
internalValue
|
|
87
|
-
? Array.from(internalValue)
|
|
88
|
-
.map((file) => file.name)
|
|
89
|
-
.join(", ")
|
|
90
|
-
: ""
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
function handleButtonClick() {
|
|
94
|
-
if (disabled) return;
|
|
95
|
-
inputEl?.click();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function handleFileChange(event: Event) {
|
|
99
|
-
const target = event.target as HTMLInputElement;
|
|
100
|
-
const files = target.files;
|
|
101
|
-
internalValue = files;
|
|
102
|
-
if (files && files.length > 0) {
|
|
103
|
-
onFilesSelected?.(files);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function handleDrop(event: DragEvent) {
|
|
108
|
-
event.preventDefault();
|
|
109
|
-
isDragOver = false;
|
|
110
|
-
if (disabled) return;
|
|
111
|
-
const files = event.dataTransfer?.files;
|
|
112
|
-
internalValue = files || null;
|
|
113
|
-
if (files && files.length > 0) {
|
|
114
|
-
onFilesSelected?.(files);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function handleDragOver(event: DragEvent) {
|
|
119
|
-
event.preventDefault();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function handleDragEnter(event: DragEvent) {
|
|
123
|
-
event.preventDefault();
|
|
124
|
-
if (!disabled) {
|
|
125
|
-
isDragOver = true;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function handleDragLeave(event: DragEvent) {
|
|
130
|
-
event.preventDefault();
|
|
131
|
-
isDragOver = false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function handleKeyDown(event: KeyboardEvent) {
|
|
135
|
-
if (disabled) return;
|
|
136
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
137
|
-
event.preventDefault();
|
|
138
|
-
handleButtonClick();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function clearSelection() {
|
|
143
|
-
if (!clearable) return;
|
|
144
|
-
internalValue = null;
|
|
145
|
-
if (inputEl) {
|
|
146
|
-
inputEl.value = "";
|
|
147
|
-
}
|
|
148
|
-
onFilesSelected?.(null);
|
|
149
|
-
}
|
|
150
|
-
</script>
|
|
151
|
-
|
|
152
|
-
<div class={pickerClass} {...rest}>
|
|
153
|
-
<input
|
|
154
|
-
bind:this={inputEl}
|
|
155
|
-
type="file"
|
|
156
|
-
{accept}
|
|
157
|
-
{multiple}
|
|
158
|
-
class="hidden"
|
|
159
|
-
onchange={handleFileChange}
|
|
160
|
-
/>
|
|
161
|
-
|
|
162
|
-
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
163
|
-
<Button {disabled} onClick={handleButtonClick} class="relative" sz="xs">
|
|
164
|
-
{labelFinal}
|
|
165
|
-
</Button>
|
|
166
|
-
|
|
167
|
-
{#if clearable}
|
|
168
|
-
<Button
|
|
169
|
-
onClick={clearSelection}
|
|
170
|
-
variant="danger"
|
|
171
|
-
disabled={!hasValue || disabled}
|
|
172
|
-
sz="xs"
|
|
173
|
-
>
|
|
174
|
-
{L.clear}
|
|
175
|
-
</Button>
|
|
176
|
-
{/if}
|
|
177
|
-
</div>
|
|
178
|
-
|
|
179
|
-
<div
|
|
180
|
-
class="mt-2 p-4 border-2 border-dashed rounded-[var(--radius-md)] text-center transition-colors duration-200"
|
|
181
|
-
class:border-[var(--color-primary)]={isDragOver && !disabled}
|
|
182
|
-
class:border-[var(--border-color-default)]={!isDragOver || disabled}
|
|
183
|
-
class:bg-[var(--color-bg-surface-hover)]={isDragOver && !disabled}
|
|
184
|
-
class:cursor-pointer={!disabled}
|
|
185
|
-
class:opacity-[var(--opacity-disabled)]={disabled}
|
|
186
|
-
class:cursor-not-allowed={disabled}
|
|
187
|
-
class:cursor-copy={isDragOver && !disabled}
|
|
188
|
-
role="button"
|
|
189
|
-
tabindex={disabled ? -1 : 0}
|
|
190
|
-
aria-disabled={disabled}
|
|
191
|
-
ondrop={handleDrop}
|
|
192
|
-
ondragover={handleDragOver}
|
|
193
|
-
ondragenter={handleDragEnter}
|
|
194
|
-
ondragleave={handleDragLeave}
|
|
195
|
-
onclick={handleButtonClick}
|
|
196
|
-
onkeydown={handleKeyDown}
|
|
197
|
-
>
|
|
198
|
-
<p class="text-sm [color:var(--color-text-muted)]">
|
|
199
|
-
{L.dragDrop}
|
|
200
|
-
</p>
|
|
201
|
-
{#if accept !== "*/*"}
|
|
202
|
-
<p class="text-xs mt-1 [color:var(--color-text-muted)]">
|
|
203
|
-
{L.accepted}: {accept}
|
|
204
|
-
</p>
|
|
205
|
-
{/if}
|
|
206
|
-
</div>
|
|
207
|
-
|
|
208
|
-
<div
|
|
209
|
-
class="mt-3 p-4 bg-[var(--color-bg-surface)] text-center"
|
|
210
|
-
aria-live="polite"
|
|
211
|
-
>
|
|
212
|
-
<p class="text-xs uppercase tracking-wide [color:var(--color-text-muted)]">
|
|
213
|
-
{L.selectedFiles}
|
|
214
|
-
</p>
|
|
215
|
-
<p
|
|
216
|
-
class="text-sm font-semibold mt-1 [color:var(--color-text-default)] break-words"
|
|
217
|
-
>
|
|
218
|
-
{#if hasValue}
|
|
219
|
-
{fileNames}
|
|
220
|
-
{:else}
|
|
221
|
-
{placeholderFinal}
|
|
222
|
-
{/if}
|
|
223
|
-
</p>
|
|
224
|
-
{#if hasValue && internalValue}
|
|
225
|
-
<p class="text-sm mt-1 [color:var(--color-text-muted)]">
|
|
226
|
-
{L.fileCount.replace("{n}", String(internalValue.length))}
|
|
227
|
-
|
|
228
|
-
{#if multiple && internalValue.length > 1}
|
|
229
|
-
• {L.totalSize}: {(
|
|
230
|
-
Array.from(internalValue).reduce(
|
|
231
|
-
(acc, file) => acc + file.size,
|
|
232
|
-
0
|
|
233
|
-
) /
|
|
234
|
-
1024 /
|
|
235
|
-
1024
|
|
236
|
-
).toFixed(2)} MB
|
|
237
|
-
{/if}
|
|
238
|
-
</p>
|
|
239
|
-
{/if}
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @component FilePicker
|
|
3
|
-
* @description Lightweight file selector with click support and drag-and-drop. Internally uses a hidden `<input type="file">` plus a drop zone.
|
|
4
|
-
*
|
|
5
|
-
* @prop accept {string} - Accepted file types
|
|
6
|
-
* @default "*\\/*"
|
|
7
|
-
*
|
|
8
|
-
* @prop multiple {boolean} - Allows selecting multiple files
|
|
9
|
-
* @default false
|
|
10
|
-
*
|
|
11
|
-
* @prop label {string} - Button label; falls back to localized text
|
|
12
|
-
*
|
|
13
|
-
* @prop disabled {boolean} - Disables all interactions
|
|
14
|
-
* @default false
|
|
15
|
-
*
|
|
16
|
-
* @prop clearable {boolean} - Shows a clear button to reset selection
|
|
17
|
-
* @default true
|
|
18
|
-
*
|
|
19
|
-
* @prop placeholder {string} - Placeholder text for the drop zone
|
|
20
|
-
*
|
|
21
|
-
* @prop value {FileList | null} - Controlled selected files (bindable)
|
|
22
|
-
* @default null
|
|
23
|
-
*
|
|
24
|
-
* @prop onFilesSelected {(files: FileList | null) => void} - Fired when files are chosen
|
|
25
|
-
*
|
|
26
|
-
* @prop onError {(error: string) => void} - Fired on validation errors
|
|
27
|
-
*
|
|
28
|
-
* @prop class {string} - Additional classes for the wrapper
|
|
29
|
-
* @default ""
|
|
30
|
-
*
|
|
31
|
-
* @note The entire area is clickable and supports drag-and-drop.
|
|
32
|
-
* @note After a selection, the underlying input resets its value, so choosing the same file twice still triggers updates.
|
|
33
|
-
* @note `accept` does not apply to dropped files, only to the picker UI; validate files inside `onFilesSelected`.
|
|
34
|
-
* @note When `clearable=true`, the user can clear selected files and the callback receives `null`.
|
|
35
|
-
* @note When `disabled=true`, clicks, drag events, focus, and keyboard input are blocked.
|
|
36
|
-
*/
|
|
37
|
-
import type { HTMLAttributes } from "svelte/elements";
|
|
38
|
-
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
39
|
-
accept?: string;
|
|
40
|
-
multiple?: boolean;
|
|
41
|
-
label?: string;
|
|
42
|
-
disabled?: boolean;
|
|
43
|
-
clearable?: boolean;
|
|
44
|
-
placeholder?: string;
|
|
45
|
-
value?: FileList | null;
|
|
46
|
-
onFilesSelected?: (files: FileList | null) => void;
|
|
47
|
-
onError?: (error: string) => void;
|
|
48
|
-
class?: string;
|
|
49
|
-
};
|
|
50
|
-
declare const FilePicker: import("svelte").Component<Props, {}, "">;
|
|
51
|
-
type FilePicker = ReturnType<typeof FilePicker>;
|
|
52
|
-
export default FilePicker;
|