@aiaiai-pt/design-system 0.1.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/components/Alert.svelte +100 -0
- package/components/Badge.svelte +108 -0
- package/components/BottomNav.svelte +37 -0
- package/components/BottomNavItem.svelte +121 -0
- package/components/Button.svelte +269 -0
- package/components/Card.svelte +108 -0
- package/components/Checkbox.svelte +138 -0
- package/components/CodeBlock.svelte +187 -0
- package/components/CodeEditor.svelte +221 -0
- package/components/CollapsibleSection.svelte +160 -0
- package/components/Combobox.svelte +396 -0
- package/components/EmptyState.svelte +148 -0
- package/components/FileUpload.svelte +280 -0
- package/components/FileUploadItem.svelte +222 -0
- package/components/Input.svelte +222 -0
- package/components/KeyValue.svelte +79 -0
- package/components/Label.svelte +49 -0
- package/components/List.svelte +70 -0
- package/components/ListItem.svelte +125 -0
- package/components/Menu.svelte +161 -0
- package/components/MenuItem.svelte +120 -0
- package/components/MenuSeparator.svelte +34 -0
- package/components/Modal.svelte +260 -0
- package/components/OptionGrid.svelte +195 -0
- package/components/Panel.svelte +256 -0
- package/components/Popover.svelte +194 -0
- package/components/Progress.svelte +78 -0
- package/components/Select.svelte +182 -0
- package/components/Separator.svelte +47 -0
- package/components/Sidebar.svelte +106 -0
- package/components/SidebarItem.svelte +154 -0
- package/components/SidebarSection.svelte +43 -0
- package/components/Skeleton.svelte +79 -0
- package/components/Status.svelte +104 -0
- package/components/Stepper.svelte +142 -0
- package/components/Tab.svelte +94 -0
- package/components/TabList.svelte +36 -0
- package/components/TabPanel.svelte +45 -0
- package/components/Tabs.svelte +46 -0
- package/components/Tag.svelte +96 -0
- package/components/Textarea.svelte +143 -0
- package/components/Toast.svelte +114 -0
- package/components/Toggle.svelte +132 -0
- package/components/index.js +70 -0
- package/package.json +45 -0
- package/tokens/base.css +175 -0
- package/tokens/components.css +530 -0
- package/tokens/semantic.css +211 -0
- package/tokens/themes/aiaiai.css +53 -0
- package/tokens/themes/bespoke-example.css +148 -0
- package/tokens/themes/branded-example.css +55 -0
- package/tokens/utilities.css +1865 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component FileUpload
|
|
3
|
+
|
|
4
|
+
Drag-and-drop file upload zone. Does NOT handle uploads — emits
|
|
5
|
+
validated File[] to parent via callback. Parent owns upload logic.
|
|
6
|
+
|
|
7
|
+
Consumes --fileupload-* tokens from components.css.
|
|
8
|
+
|
|
9
|
+
@example
|
|
10
|
+
<FileUpload accept=".pdf,.docx" maxSize={52_428_800} onfiles={(files) => handleFiles(files)}>
|
|
11
|
+
{#snippet icon()}<PhCloudArrowUp size={32} />{/snippet}
|
|
12
|
+
</FileUpload>
|
|
13
|
+
|
|
14
|
+
@example Custom content
|
|
15
|
+
<FileUpload onfiles={handleFiles}>
|
|
16
|
+
{#snippet children()}
|
|
17
|
+
<p>Custom drop zone content</p>
|
|
18
|
+
{/snippet}
|
|
19
|
+
</FileUpload>
|
|
20
|
+
-->
|
|
21
|
+
<script>
|
|
22
|
+
let {
|
|
23
|
+
/** @type {string} Comma-separated MIME types or extensions */
|
|
24
|
+
accept = '',
|
|
25
|
+
/** @type {number} Max bytes per file. 0 = no limit */
|
|
26
|
+
maxSize = 0,
|
|
27
|
+
/** @type {boolean} */
|
|
28
|
+
multiple = true,
|
|
29
|
+
/** @type {boolean} */
|
|
30
|
+
disabled = false,
|
|
31
|
+
/** @type {((files: File[]) => void) | undefined} */
|
|
32
|
+
onfiles = undefined,
|
|
33
|
+
/** @type {((files: File[]) => void) | undefined} Called with files that failed validation */
|
|
34
|
+
onreject = undefined,
|
|
35
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
36
|
+
icon = undefined,
|
|
37
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
38
|
+
children = undefined,
|
|
39
|
+
/** @type {string} */
|
|
40
|
+
class: className = '',
|
|
41
|
+
...rest
|
|
42
|
+
} = $props();
|
|
43
|
+
|
|
44
|
+
let dragging = $state(false);
|
|
45
|
+
/** @type {HTMLInputElement | undefined} */
|
|
46
|
+
let inputEl;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format bytes to human-readable size.
|
|
50
|
+
* @param {number} bytes
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function formatSize(bytes) {
|
|
54
|
+
if (bytes === 0) return '0 B';
|
|
55
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
56
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
57
|
+
const size = bytes / Math.pow(1024, i);
|
|
58
|
+
return `${size % 1 === 0 ? size : size.toFixed(1)} ${units[i]}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse accept string into a set of lowercase extensions and MIME types.
|
|
63
|
+
* @param {string} acceptStr
|
|
64
|
+
* @returns {{ extensions: Set<string>, mimeTypes: Set<string> }}
|
|
65
|
+
*/
|
|
66
|
+
function parseAccept(acceptStr) {
|
|
67
|
+
const extensions = new Set();
|
|
68
|
+
const mimeTypes = new Set();
|
|
69
|
+
if (!acceptStr) return { extensions, mimeTypes };
|
|
70
|
+
for (const part of acceptStr.split(',')) {
|
|
71
|
+
const trimmed = part.trim().toLowerCase();
|
|
72
|
+
if (trimmed.startsWith('.')) {
|
|
73
|
+
extensions.add(trimmed);
|
|
74
|
+
} else if (trimmed.includes('/')) {
|
|
75
|
+
mimeTypes.add(trimmed);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { extensions, mimeTypes };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if a file matches the accept constraint.
|
|
83
|
+
* @param {File} file
|
|
84
|
+
* @param {{ extensions: Set<string>, mimeTypes: Set<string> }} parsed
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function matchesAccept(file, parsed) {
|
|
88
|
+
if (parsed.extensions.size === 0 && parsed.mimeTypes.size === 0) return true;
|
|
89
|
+
const name = file.name.toLowerCase();
|
|
90
|
+
for (const ext of parsed.extensions) {
|
|
91
|
+
if (name.endsWith(ext)) return true;
|
|
92
|
+
}
|
|
93
|
+
const type = file.type.toLowerCase();
|
|
94
|
+
for (const mime of parsed.mimeTypes) {
|
|
95
|
+
if (mime.endsWith('/*')) {
|
|
96
|
+
if (type.startsWith(mime.slice(0, -1))) return true;
|
|
97
|
+
} else if (type === mime) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Filter files by accept + maxSize + non-empty.
|
|
106
|
+
* @param {FileList | File[]} fileList
|
|
107
|
+
* @returns {{ valid: File[], rejected: File[] }}
|
|
108
|
+
*/
|
|
109
|
+
function validateFiles(fileList) {
|
|
110
|
+
const parsed = parseAccept(accept);
|
|
111
|
+
const valid = [];
|
|
112
|
+
const rejected = [];
|
|
113
|
+
for (const file of fileList) {
|
|
114
|
+
if (file.size === 0 || !matchesAccept(file, parsed) || (maxSize > 0 && file.size > maxSize)) {
|
|
115
|
+
rejected.push(file);
|
|
116
|
+
} else {
|
|
117
|
+
valid.push(file);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!multiple && valid.length > 1) {
|
|
121
|
+
rejected.push(...valid.slice(1));
|
|
122
|
+
return { valid: [valid[0]], rejected };
|
|
123
|
+
}
|
|
124
|
+
return { valid, rejected };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @param {DragEvent} e */
|
|
128
|
+
function handleDrop(e) {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
dragging = false;
|
|
131
|
+
if (disabled) return;
|
|
132
|
+
const { valid, rejected } = validateFiles(e.dataTransfer?.files ?? []);
|
|
133
|
+
if (valid.length > 0) onfiles?.(valid);
|
|
134
|
+
if (rejected.length > 0) onreject?.(rejected);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @param {DragEvent} e */
|
|
138
|
+
function handleDragOver(e) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
if (!disabled) dragging = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handleDragLeave() {
|
|
144
|
+
dragging = false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function handleClick() {
|
|
148
|
+
if (!disabled) inputEl?.click();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @param {Event} e */
|
|
152
|
+
function handleInputChange(e) {
|
|
153
|
+
const target = /** @type {HTMLInputElement} */ (e.target);
|
|
154
|
+
const { valid, rejected } = validateFiles(target.files ?? []);
|
|
155
|
+
if (valid.length > 0) onfiles?.(valid);
|
|
156
|
+
if (rejected.length > 0) onreject?.(rejected);
|
|
157
|
+
target.value = '';
|
|
158
|
+
}
|
|
159
|
+
</script>
|
|
160
|
+
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
class="fileupload {className}"
|
|
164
|
+
class:fileupload-dragging={dragging}
|
|
165
|
+
class:fileupload-disabled={disabled}
|
|
166
|
+
{disabled}
|
|
167
|
+
aria-label="Upload files"
|
|
168
|
+
ondrop={handleDrop}
|
|
169
|
+
ondragover={handleDragOver}
|
|
170
|
+
ondragleave={handleDragLeave}
|
|
171
|
+
{...rest}
|
|
172
|
+
onclick={handleClick}
|
|
173
|
+
>
|
|
174
|
+
<input
|
|
175
|
+
bind:this={inputEl}
|
|
176
|
+
type="file"
|
|
177
|
+
{accept}
|
|
178
|
+
{multiple}
|
|
179
|
+
class="fileupload-input"
|
|
180
|
+
onchange={handleInputChange}
|
|
181
|
+
tabindex={-1}
|
|
182
|
+
aria-hidden="true"
|
|
183
|
+
/>
|
|
184
|
+
{#if children}
|
|
185
|
+
{@render children()}
|
|
186
|
+
{:else}
|
|
187
|
+
<div class="fileupload-content">
|
|
188
|
+
{#if icon}
|
|
189
|
+
<span class="fileupload-icon">{@render icon()}</span>
|
|
190
|
+
{/if}
|
|
191
|
+
<span class="fileupload-text">Drop files here or click to browse</span>
|
|
192
|
+
{#if accept || maxSize}
|
|
193
|
+
<span class="fileupload-hint">
|
|
194
|
+
{#if accept}{accept}{/if}
|
|
195
|
+
{#if accept && maxSize} · {/if}
|
|
196
|
+
{#if maxSize}Max {formatSize(maxSize)}{/if}
|
|
197
|
+
</span>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
{/if}
|
|
201
|
+
</button>
|
|
202
|
+
|
|
203
|
+
<style>
|
|
204
|
+
.fileupload {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
width: 100%;
|
|
209
|
+
padding: var(--fileupload-padding);
|
|
210
|
+
border: var(--fileupload-border);
|
|
211
|
+
border-radius: var(--fileupload-radius);
|
|
212
|
+
background: var(--fileupload-bg);
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
transition: border var(--fileupload-transition), background var(--fileupload-transition);
|
|
215
|
+
font: inherit;
|
|
216
|
+
color: inherit;
|
|
217
|
+
text-align: center;
|
|
218
|
+
position: relative;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.fileupload:focus-visible {
|
|
222
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
223
|
+
outline-offset: var(--focus-ring-offset);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.fileupload-dragging {
|
|
227
|
+
border: var(--fileupload-border-dragging);
|
|
228
|
+
background: var(--fileupload-bg-dragging);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.fileupload-disabled {
|
|
232
|
+
opacity: 0.5;
|
|
233
|
+
cursor: not-allowed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.fileupload-input {
|
|
237
|
+
position: absolute;
|
|
238
|
+
width: 0;
|
|
239
|
+
height: 0;
|
|
240
|
+
overflow: hidden;
|
|
241
|
+
opacity: 0;
|
|
242
|
+
pointer-events: none;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.fileupload-content {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
align-items: center;
|
|
249
|
+
gap: var(--space-xs);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.fileupload-icon {
|
|
253
|
+
display: flex;
|
|
254
|
+
width: var(--fileupload-icon-size);
|
|
255
|
+
height: var(--fileupload-icon-size);
|
|
256
|
+
color: var(--fileupload-icon-color);
|
|
257
|
+
transition: color var(--fileupload-transition);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.fileupload-dragging .fileupload-icon {
|
|
261
|
+
color: var(--fileupload-icon-color-dragging);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.fileupload-icon :global(svg) {
|
|
265
|
+
width: 100%;
|
|
266
|
+
height: 100%;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.fileupload-text {
|
|
270
|
+
font-family: var(--fileupload-text-font);
|
|
271
|
+
font-size: var(--fileupload-text-size);
|
|
272
|
+
color: var(--fileupload-text-color);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.fileupload-hint {
|
|
276
|
+
font-family: var(--fileupload-hint-font);
|
|
277
|
+
font-size: var(--fileupload-hint-size);
|
|
278
|
+
color: var(--fileupload-hint-color);
|
|
279
|
+
}
|
|
280
|
+
</style>
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component FileUploadItem
|
|
3
|
+
|
|
4
|
+
Per-file upload/processing status row. Parent controls all state —
|
|
5
|
+
this is a pure display component. Uses DS Progress internally.
|
|
6
|
+
|
|
7
|
+
Consumes --fileupload-item-* tokens from components.css.
|
|
8
|
+
|
|
9
|
+
@example Uploading
|
|
10
|
+
<FileUploadItem name="report.pdf" size={2_400_000} status="uploading" progress={65} />
|
|
11
|
+
|
|
12
|
+
@example Error
|
|
13
|
+
<FileUploadItem name="data.csv" status="error" error="File exceeds 50 MB limit" onremove={() => {}} />
|
|
14
|
+
|
|
15
|
+
@example Complete
|
|
16
|
+
<FileUploadItem name="notes.txt" size={1_200} status="complete" />
|
|
17
|
+
-->
|
|
18
|
+
<script>
|
|
19
|
+
import Progress from './Progress.svelte';
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
/** @type {string} */
|
|
23
|
+
name = '',
|
|
24
|
+
/** @type {number} File size in bytes */
|
|
25
|
+
size = 0,
|
|
26
|
+
/** @type {'pending' | 'uploading' | 'complete' | 'error'} */
|
|
27
|
+
status = 'pending',
|
|
28
|
+
/** @type {number} 0-100 */
|
|
29
|
+
progress = 0,
|
|
30
|
+
/** @type {string} */
|
|
31
|
+
error = '',
|
|
32
|
+
/** @type {(() => void) | undefined} */
|
|
33
|
+
onremove = undefined,
|
|
34
|
+
/** @type {string} */
|
|
35
|
+
class: className = '',
|
|
36
|
+
...rest
|
|
37
|
+
} = $props();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Format bytes to human-readable size.
|
|
41
|
+
* @param {number} bytes
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function formatSize(bytes) {
|
|
45
|
+
if (bytes === 0) return '0 B';
|
|
46
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
47
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
48
|
+
const val = bytes / Math.pow(1024, i);
|
|
49
|
+
return `${val % 1 === 0 ? val : val.toFixed(1)} ${units[i]}`;
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<div class="fileupload-item fileupload-item-{status} {className}" {...rest}>
|
|
54
|
+
<div class="fileupload-item-icon">
|
|
55
|
+
{#if status === 'complete'}
|
|
56
|
+
<!-- Checkmark -->
|
|
57
|
+
<svg viewBox="0 0 20 20" fill="none">
|
|
58
|
+
<circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.5"/>
|
|
59
|
+
<path d="M7 10l2 2 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
60
|
+
</svg>
|
|
61
|
+
{:else if status === 'error'}
|
|
62
|
+
<!-- X circle -->
|
|
63
|
+
<svg viewBox="0 0 20 20" fill="none">
|
|
64
|
+
<circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.5"/>
|
|
65
|
+
<path d="M7.5 7.5l5 5M12.5 7.5l-5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
66
|
+
</svg>
|
|
67
|
+
{:else if status === 'uploading'}
|
|
68
|
+
<!-- Arrow up -->
|
|
69
|
+
<svg viewBox="0 0 20 20" fill="none">
|
|
70
|
+
<path d="M10 15V5M10 5l-4 4M10 5l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
71
|
+
</svg>
|
|
72
|
+
{:else}
|
|
73
|
+
<!-- File -->
|
|
74
|
+
<svg viewBox="0 0 20 20" fill="none">
|
|
75
|
+
<path d="M6 3h5l5 5v9a1 1 0 01-1 1H6a1 1 0 01-1-1V4a1 1 0 011-1z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
|
76
|
+
<path d="M11 3v5h5" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
|
77
|
+
</svg>
|
|
78
|
+
{/if}
|
|
79
|
+
</div>
|
|
80
|
+
<div class="fileupload-item-content">
|
|
81
|
+
<div class="fileupload-item-header">
|
|
82
|
+
<span class="fileupload-item-name">{name}</span>
|
|
83
|
+
{#if size && status !== 'error'}
|
|
84
|
+
<span class="fileupload-item-size">{formatSize(size)}</span>
|
|
85
|
+
{/if}
|
|
86
|
+
{#if onremove}
|
|
87
|
+
<button class="fileupload-item-remove" onclick={onremove} aria-label="Remove {name}">×</button>
|
|
88
|
+
{/if}
|
|
89
|
+
</div>
|
|
90
|
+
{#if status === 'uploading'}
|
|
91
|
+
<div class="fileupload-item-progress">
|
|
92
|
+
<Progress value={progress} />
|
|
93
|
+
<span class="fileupload-item-pct">{progress}%</span>
|
|
94
|
+
</div>
|
|
95
|
+
{/if}
|
|
96
|
+
{#if status === 'error' && error}
|
|
97
|
+
<span class="fileupload-item-error">{error}</span>
|
|
98
|
+
{/if}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<style>
|
|
103
|
+
.fileupload-item {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: flex-start;
|
|
106
|
+
gap: var(--fileupload-item-gap);
|
|
107
|
+
padding: var(--fileupload-item-padding);
|
|
108
|
+
background: var(--fileupload-item-bg);
|
|
109
|
+
border: var(--fileupload-item-border);
|
|
110
|
+
border-radius: var(--fileupload-item-radius);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.fileupload-item-icon {
|
|
114
|
+
flex-shrink: 0;
|
|
115
|
+
width: var(--fileupload-item-icon-size);
|
|
116
|
+
height: var(--fileupload-item-icon-size);
|
|
117
|
+
color: var(--color-text-muted);
|
|
118
|
+
display: flex;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.fileupload-item-icon svg {
|
|
122
|
+
width: 100%;
|
|
123
|
+
height: 100%;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.fileupload-item-uploading .fileupload-item-icon {
|
|
127
|
+
color: var(--color-accent);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.fileupload-item-complete .fileupload-item-icon {
|
|
131
|
+
color: var(--fileupload-item-complete-color);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.fileupload-item-error .fileupload-item-icon {
|
|
135
|
+
color: var(--fileupload-item-error-color);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.fileupload-item-content {
|
|
139
|
+
flex: 1;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
display: flex;
|
|
142
|
+
flex-direction: column;
|
|
143
|
+
gap: var(--space-2xs);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.fileupload-item-header {
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: var(--space-sm);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.fileupload-item-name {
|
|
153
|
+
font-family: var(--fileupload-item-name-font);
|
|
154
|
+
font-size: var(--fileupload-item-name-size);
|
|
155
|
+
color: var(--fileupload-item-name-color);
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
text-overflow: ellipsis;
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
flex: 1;
|
|
160
|
+
min-width: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.fileupload-item-complete .fileupload-item-name {
|
|
164
|
+
color: var(--fileupload-item-complete-color);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.fileupload-item-size {
|
|
168
|
+
font-family: var(--fileupload-item-size-font);
|
|
169
|
+
font-size: var(--fileupload-item-size-size);
|
|
170
|
+
color: var(--fileupload-item-size-color);
|
|
171
|
+
flex-shrink: 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.fileupload-item-remove {
|
|
175
|
+
all: unset;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
flex-shrink: 0;
|
|
178
|
+
width: 20px;
|
|
179
|
+
height: 20px;
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
justify-content: center;
|
|
183
|
+
color: var(--color-text-muted);
|
|
184
|
+
font-size: 16px;
|
|
185
|
+
line-height: 1;
|
|
186
|
+
border-radius: var(--radius-sm);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.fileupload-item-remove:hover {
|
|
190
|
+
color: var(--color-text);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.fileupload-item-remove:focus-visible {
|
|
194
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
195
|
+
outline-offset: var(--focus-ring-offset);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.fileupload-item-progress {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: var(--space-sm);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.fileupload-item-progress :global(.progress) {
|
|
205
|
+
flex: 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.fileupload-item-pct {
|
|
209
|
+
font-family: var(--fileupload-item-size-font);
|
|
210
|
+
font-size: var(--fileupload-item-size-size);
|
|
211
|
+
color: var(--fileupload-item-size-color);
|
|
212
|
+
flex-shrink: 0;
|
|
213
|
+
min-width: 3ch;
|
|
214
|
+
text-align: right;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.fileupload-item-error {
|
|
218
|
+
font-family: var(--fileupload-item-name-font);
|
|
219
|
+
font-size: var(--fileupload-item-size-size);
|
|
220
|
+
color: var(--fileupload-item-error-color);
|
|
221
|
+
}
|
|
222
|
+
</style>
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Input
|
|
3
|
+
|
|
4
|
+
Text input with label, help text, and error state.
|
|
5
|
+
Values displayed in Berkeley Mono (data font).
|
|
6
|
+
Consumes --input-* tokens from components.css.
|
|
7
|
+
|
|
8
|
+
@example Basic
|
|
9
|
+
<Input label="EMAIL" placeholder="you@example.com" />
|
|
10
|
+
|
|
11
|
+
@example With help text
|
|
12
|
+
<Input label="USERNAME" placeholder="Enter username" help="3-20 characters" />
|
|
13
|
+
|
|
14
|
+
@example Error
|
|
15
|
+
<Input label="EMAIL" value="bad" error="Please enter a valid email" />
|
|
16
|
+
|
|
17
|
+
@example With leading icon
|
|
18
|
+
<Input label="SEARCH" placeholder="Search...">
|
|
19
|
+
{#snippet leadingIcon()}
|
|
20
|
+
<PhMagnifyingGlass size={16} />
|
|
21
|
+
{/snippet}
|
|
22
|
+
</Input>
|
|
23
|
+
-->
|
|
24
|
+
<script module>
|
|
25
|
+
let _inputUid = 0;
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {'sm' | 'md' | 'lg'} Size
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
/** @type {string | undefined} */
|
|
35
|
+
label = undefined,
|
|
36
|
+
/** @type {string | undefined} */
|
|
37
|
+
placeholder = undefined,
|
|
38
|
+
/** @type {string} */
|
|
39
|
+
value = $bindable(''),
|
|
40
|
+
/** @type {string | undefined} */
|
|
41
|
+
help = undefined,
|
|
42
|
+
/** @type {string | undefined} */
|
|
43
|
+
error = undefined,
|
|
44
|
+
/** @type {Size} */
|
|
45
|
+
size = 'md',
|
|
46
|
+
/** @type {boolean} */
|
|
47
|
+
disabled = false,
|
|
48
|
+
/** @type {boolean} */
|
|
49
|
+
readonly = false,
|
|
50
|
+
/** @type {string} */
|
|
51
|
+
type = 'text',
|
|
52
|
+
/** @type {string | undefined} */
|
|
53
|
+
id = undefined,
|
|
54
|
+
/** @type {string} */
|
|
55
|
+
class: className = '',
|
|
56
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
57
|
+
leadingIcon = undefined,
|
|
58
|
+
...rest
|
|
59
|
+
} = $props();
|
|
60
|
+
|
|
61
|
+
const fallbackId = `input-${_inputUid++}`;
|
|
62
|
+
const inputId = $derived(id ?? fallbackId);
|
|
63
|
+
const hintId = $derived(`${inputId}-hint`);
|
|
64
|
+
const hasHint = $derived(!!error || !!help);
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<div class="input-group {className}">
|
|
68
|
+
{#if label}
|
|
69
|
+
<label class="input-label" for={inputId}>{label}</label>
|
|
70
|
+
{/if}
|
|
71
|
+
|
|
72
|
+
{#if leadingIcon}
|
|
73
|
+
<div class="input-icon-wrapper">
|
|
74
|
+
<span class="input-leading-icon" aria-hidden="true">
|
|
75
|
+
{@render leadingIcon()}
|
|
76
|
+
</span>
|
|
77
|
+
<input
|
|
78
|
+
id={inputId}
|
|
79
|
+
{type}
|
|
80
|
+
class="input input-{size} input-with-icon"
|
|
81
|
+
class:input-error={!!error}
|
|
82
|
+
class:input-readonly={readonly}
|
|
83
|
+
aria-invalid={error ? true : undefined}
|
|
84
|
+
aria-describedby={hasHint ? hintId : undefined}
|
|
85
|
+
{placeholder}
|
|
86
|
+
{disabled}
|
|
87
|
+
{readonly}
|
|
88
|
+
bind:value
|
|
89
|
+
{...rest}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
{:else}
|
|
93
|
+
<input
|
|
94
|
+
id={inputId}
|
|
95
|
+
{type}
|
|
96
|
+
class="input input-{size}"
|
|
97
|
+
class:input-error={!!error}
|
|
98
|
+
class:input-readonly={readonly}
|
|
99
|
+
aria-invalid={error ? true : undefined}
|
|
100
|
+
aria-describedby={hasHint ? hintId : undefined}
|
|
101
|
+
{placeholder}
|
|
102
|
+
{disabled}
|
|
103
|
+
{readonly}
|
|
104
|
+
bind:value
|
|
105
|
+
{...rest}
|
|
106
|
+
/>
|
|
107
|
+
{/if}
|
|
108
|
+
|
|
109
|
+
{#if error}
|
|
110
|
+
<span id={hintId} class="input-error-text" role="alert">{error}</span>
|
|
111
|
+
{:else if help}
|
|
112
|
+
<span id={hintId} class="input-help">{help}</span>
|
|
113
|
+
{/if}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<style>
|
|
117
|
+
.input-group {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
gap: var(--input-label-gap);
|
|
121
|
+
width: 100%;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.input-label {
|
|
125
|
+
font-family: var(--input-label-font);
|
|
126
|
+
font-size: var(--input-label-size);
|
|
127
|
+
letter-spacing: var(--input-label-tracking);
|
|
128
|
+
color: var(--input-label-color);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.input {
|
|
132
|
+
font-family: var(--input-font);
|
|
133
|
+
font-size: var(--input-font-size);
|
|
134
|
+
border: var(--input-border);
|
|
135
|
+
border-radius: var(--input-radius);
|
|
136
|
+
background: var(--input-bg);
|
|
137
|
+
color: var(--input-text);
|
|
138
|
+
transition: border var(--input-transition);
|
|
139
|
+
width: 100%;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.input-sm {
|
|
143
|
+
height: var(--input-sm-height);
|
|
144
|
+
padding: 0 var(--input-sm-padding-x);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.input-md {
|
|
148
|
+
height: var(--input-md-height);
|
|
149
|
+
padding: 0 var(--input-md-padding-x);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.input-lg {
|
|
153
|
+
height: var(--input-lg-height);
|
|
154
|
+
padding: 0 var(--input-lg-padding-x);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.input::placeholder {
|
|
158
|
+
color: var(--input-placeholder);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.input:focus {
|
|
162
|
+
outline: none;
|
|
163
|
+
border: var(--input-border-focus);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.input:disabled {
|
|
167
|
+
opacity: 0.5;
|
|
168
|
+
cursor: not-allowed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.input-readonly {
|
|
172
|
+
background: var(--color-surface-secondary);
|
|
173
|
+
cursor: default;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.input-error {
|
|
177
|
+
border-color: var(--input-error-border-color);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.input-error:focus {
|
|
181
|
+
border-color: var(--input-error-border-color);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.input-help {
|
|
185
|
+
font-family: var(--input-help-font);
|
|
186
|
+
font-size: var(--input-help-size);
|
|
187
|
+
color: var(--input-help-color);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.input-error-text {
|
|
191
|
+
font-family: var(--input-help-font);
|
|
192
|
+
font-size: var(--input-help-size);
|
|
193
|
+
color: var(--input-error-text);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* ─── Icon ─── */
|
|
197
|
+
.input-icon-wrapper {
|
|
198
|
+
position: relative;
|
|
199
|
+
width: 100%;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.input-leading-icon {
|
|
203
|
+
position: absolute;
|
|
204
|
+
left: var(--input-md-padding-x);
|
|
205
|
+
top: 50%;
|
|
206
|
+
transform: translateY(-50%);
|
|
207
|
+
display: flex;
|
|
208
|
+
width: 16px;
|
|
209
|
+
height: 16px;
|
|
210
|
+
color: var(--input-placeholder);
|
|
211
|
+
pointer-events: none;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.input-leading-icon :global(svg) {
|
|
215
|
+
width: 100%;
|
|
216
|
+
height: 100%;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.input-with-icon {
|
|
220
|
+
padding-left: calc(var(--input-md-padding-x) + 16px + var(--space-xs));
|
|
221
|
+
}
|
|
222
|
+
</style>
|