@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.
Files changed (52) hide show
  1. package/components/Alert.svelte +100 -0
  2. package/components/Badge.svelte +108 -0
  3. package/components/BottomNav.svelte +37 -0
  4. package/components/BottomNavItem.svelte +121 -0
  5. package/components/Button.svelte +269 -0
  6. package/components/Card.svelte +108 -0
  7. package/components/Checkbox.svelte +138 -0
  8. package/components/CodeBlock.svelte +187 -0
  9. package/components/CodeEditor.svelte +221 -0
  10. package/components/CollapsibleSection.svelte +160 -0
  11. package/components/Combobox.svelte +396 -0
  12. package/components/EmptyState.svelte +148 -0
  13. package/components/FileUpload.svelte +280 -0
  14. package/components/FileUploadItem.svelte +222 -0
  15. package/components/Input.svelte +222 -0
  16. package/components/KeyValue.svelte +79 -0
  17. package/components/Label.svelte +49 -0
  18. package/components/List.svelte +70 -0
  19. package/components/ListItem.svelte +125 -0
  20. package/components/Menu.svelte +161 -0
  21. package/components/MenuItem.svelte +120 -0
  22. package/components/MenuSeparator.svelte +34 -0
  23. package/components/Modal.svelte +260 -0
  24. package/components/OptionGrid.svelte +195 -0
  25. package/components/Panel.svelte +256 -0
  26. package/components/Popover.svelte +194 -0
  27. package/components/Progress.svelte +78 -0
  28. package/components/Select.svelte +182 -0
  29. package/components/Separator.svelte +47 -0
  30. package/components/Sidebar.svelte +106 -0
  31. package/components/SidebarItem.svelte +154 -0
  32. package/components/SidebarSection.svelte +43 -0
  33. package/components/Skeleton.svelte +79 -0
  34. package/components/Status.svelte +104 -0
  35. package/components/Stepper.svelte +142 -0
  36. package/components/Tab.svelte +94 -0
  37. package/components/TabList.svelte +36 -0
  38. package/components/TabPanel.svelte +45 -0
  39. package/components/Tabs.svelte +46 -0
  40. package/components/Tag.svelte +96 -0
  41. package/components/Textarea.svelte +143 -0
  42. package/components/Toast.svelte +114 -0
  43. package/components/Toggle.svelte +132 -0
  44. package/components/index.js +70 -0
  45. package/package.json +45 -0
  46. package/tokens/base.css +175 -0
  47. package/tokens/components.css +530 -0
  48. package/tokens/semantic.css +211 -0
  49. package/tokens/themes/aiaiai.css +53 -0
  50. package/tokens/themes/bespoke-example.css +148 -0
  51. package/tokens/themes/branded-example.css +55 -0
  52. 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} &middot; {/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}">&times;</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>