@miozu/jera 0.0.2 → 0.4.2

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 (82) hide show
  1. package/CLAUDE.md +734 -0
  2. package/README.md +219 -1
  3. package/llms.txt +97 -0
  4. package/package.json +54 -14
  5. package/src/actions/index.js +375 -0
  6. package/src/components/docs/CodeBlock.svelte +203 -0
  7. package/src/components/docs/DocSection.svelte +120 -0
  8. package/src/components/docs/PropsTable.svelte +136 -0
  9. package/src/components/docs/SplitPane.svelte +98 -0
  10. package/src/components/docs/index.js +14 -0
  11. package/src/components/feedback/Alert.svelte +234 -0
  12. package/src/components/feedback/EmptyState.svelte +179 -0
  13. package/src/components/feedback/ProgressBar.svelte +116 -0
  14. package/src/components/feedback/Skeleton.svelte +107 -0
  15. package/src/components/feedback/Spinner.svelte +77 -0
  16. package/src/components/feedback/Toast.svelte +261 -0
  17. package/src/components/forms/Checkbox.svelte +147 -0
  18. package/src/components/forms/Dropzone.svelte +248 -0
  19. package/src/components/forms/FileUpload.svelte +266 -0
  20. package/src/components/forms/IconInput.svelte +184 -0
  21. package/src/components/forms/Input.svelte +121 -0
  22. package/src/components/forms/NumberInput.svelte +225 -0
  23. package/src/components/forms/PinInput.svelte +169 -0
  24. package/src/components/forms/Radio.svelte +143 -0
  25. package/src/components/forms/RadioGroup.svelte +62 -0
  26. package/src/components/forms/RangeSlider.svelte +212 -0
  27. package/src/components/forms/SearchInput.svelte +175 -0
  28. package/src/components/forms/Select.svelte +324 -0
  29. package/src/components/forms/Switch.svelte +159 -0
  30. package/src/components/forms/Textarea.svelte +122 -0
  31. package/src/components/navigation/Accordion.svelte +65 -0
  32. package/src/components/navigation/AccordionItem.svelte +146 -0
  33. package/src/components/navigation/NavigationContainer.svelte +344 -0
  34. package/src/components/navigation/Sidebar.svelte +334 -0
  35. package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
  36. package/src/components/navigation/SidebarAccountItem.svelte +492 -0
  37. package/src/components/navigation/SidebarGroup.svelte +230 -0
  38. package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
  39. package/src/components/navigation/SidebarItem.svelte +210 -0
  40. package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
  41. package/src/components/navigation/SidebarPopover.svelte +145 -0
  42. package/src/components/navigation/SidebarSearch.svelte +236 -0
  43. package/src/components/navigation/SidebarSection.svelte +158 -0
  44. package/src/components/navigation/SidebarToggle.svelte +86 -0
  45. package/src/components/navigation/Tabs.svelte +239 -0
  46. package/src/components/navigation/WorkspaceMenu.svelte +416 -0
  47. package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
  48. package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
  49. package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
  50. package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
  51. package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
  52. package/src/components/navigation/index.js +22 -0
  53. package/src/components/overlays/ConfirmDialog.svelte +272 -0
  54. package/src/components/overlays/Dropdown.svelte +153 -0
  55. package/src/components/overlays/DropdownDivider.svelte +23 -0
  56. package/src/components/overlays/DropdownItem.svelte +97 -0
  57. package/src/components/overlays/Modal.svelte +232 -0
  58. package/src/components/overlays/Popover.svelte +206 -0
  59. package/src/components/primitives/Avatar.svelte +132 -0
  60. package/src/components/primitives/Badge.svelte +118 -0
  61. package/src/components/primitives/Button.svelte +214 -0
  62. package/src/components/primitives/Card.svelte +104 -0
  63. package/src/components/primitives/Divider.svelte +105 -0
  64. package/src/components/primitives/LazyImage.svelte +104 -0
  65. package/src/components/primitives/Link.svelte +122 -0
  66. package/src/components/primitives/Stat.svelte +197 -0
  67. package/src/components/primitives/StatusBadge.svelte +122 -0
  68. package/src/index.js +183 -0
  69. package/src/tokens/colors.css +157 -0
  70. package/src/tokens/effects.css +128 -0
  71. package/src/tokens/index.css +81 -0
  72. package/src/tokens/spacing.css +49 -0
  73. package/src/tokens/typography.css +79 -0
  74. package/src/utils/cn.svelte.js +175 -0
  75. package/src/utils/highlighter.js +124 -0
  76. package/src/utils/index.js +22 -0
  77. package/src/utils/navigation.svelte.js +423 -0
  78. package/src/utils/reactive.svelte.js +328 -0
  79. package/src/utils/sidebar.svelte.js +211 -0
  80. package/jera.js +0 -135
  81. package/www/components/jera/Input/Input.svelte +0 -63
  82. package/www/components/jera/Input/index.js +0 -1
@@ -0,0 +1,248 @@
1
+ <!--
2
+ @component Dropzone
3
+
4
+ A drag-and-drop file upload zone.
5
+
6
+ @example Basic
7
+ <Dropzone ondrop={handleFile} />
8
+
9
+ @example Custom accept types
10
+ <Dropzone
11
+ accept="image/*"
12
+ placeholder="Drop an image here"
13
+ ondrop={handleImage}
14
+ />
15
+
16
+ @example With file info display
17
+ <Dropzone
18
+ accept=".pdf"
19
+ placeholder="Drop a PDF document"
20
+ ondrop={(file) => console.log(file.name)}
21
+ />
22
+ -->
23
+ <script>
24
+ let {
25
+ accept = '*/*',
26
+ placeholder = 'Drag & drop a file here, or click to upload',
27
+ disabled = false,
28
+ class: className = '',
29
+ ondrop
30
+ } = $props();
31
+
32
+ let isDragging = $state(false);
33
+ let file = $state(null);
34
+
35
+ function formatBytes(bytes) {
36
+ if (bytes === 0) return '0 Bytes';
37
+ const k = 1024;
38
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
39
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
40
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
41
+ }
42
+
43
+ function handleDrop(event) {
44
+ event.preventDefault();
45
+ isDragging = false;
46
+
47
+ if (disabled) return;
48
+
49
+ const files = event.dataTransfer?.files;
50
+ if (!files?.length) return;
51
+
52
+ const droppedFile = files[0];
53
+
54
+ // Validate file type if accept is specified
55
+ if (accept !== '*/*') {
56
+ const acceptTypes = accept.split(',').map(t => t.trim());
57
+ const fileType = droppedFile.type;
58
+ const fileExt = '.' + droppedFile.name.split('.').pop().toLowerCase();
59
+
60
+ const isValid = acceptTypes.some(type => {
61
+ if (type.startsWith('.')) return fileExt === type.toLowerCase();
62
+ if (type.endsWith('/*')) return fileType.startsWith(type.slice(0, -1));
63
+ return fileType === type;
64
+ });
65
+
66
+ if (!isValid) {
67
+ return;
68
+ }
69
+ }
70
+
71
+ file = droppedFile;
72
+ ondrop?.(droppedFile);
73
+ }
74
+
75
+ function handleFileInput(event) {
76
+ const files = event.target?.files;
77
+ if (!files?.length) return;
78
+
79
+ file = files[0];
80
+ ondrop?.(file);
81
+ }
82
+
83
+ function handleDragOver(event) {
84
+ event.preventDefault();
85
+ if (!disabled) isDragging = true;
86
+ }
87
+
88
+ function handleDragLeave() {
89
+ isDragging = false;
90
+ }
91
+
92
+ function clearFile() {
93
+ file = null;
94
+ }
95
+ </script>
96
+
97
+ <!-- Note: To prevent browser's default file handling, add these to your root layout:
98
+ ondragover={(e) => e.preventDefault()}
99
+ ondrop={(e) => e.preventDefault()}
100
+ -->
101
+
102
+ <div
103
+ class="dropzone {isDragging ? 'dropzone-dragging' : ''} {disabled ? 'dropzone-disabled' : ''} {className}"
104
+ ondragenter={() => !disabled && (isDragging = true)}
105
+ ondragleave={handleDragLeave}
106
+ ondragover={handleDragOver}
107
+ ondrop={handleDrop}
108
+ role="region"
109
+ aria-label="File drop zone"
110
+ >
111
+ {#if isDragging}
112
+ <p class="dropzone-message">Release to drop the file</p>
113
+ {:else if file}
114
+ <div class="dropzone-file">
115
+ <div class="dropzone-file-info">
116
+ <p class="dropzone-file-name">{file.name}</p>
117
+ <p class="dropzone-file-meta">
118
+ {formatBytes(file.size)} &middot; {file.type || 'Unknown type'}
119
+ </p>
120
+ </div>
121
+ <button
122
+ type="button"
123
+ class="dropzone-clear"
124
+ onclick={clearFile}
125
+ aria-label="Remove file"
126
+ >
127
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
128
+ <line x1="18" y1="6" x2="6" y2="18"></line>
129
+ <line x1="6" y1="6" x2="18" y2="18"></line>
130
+ </svg>
131
+ </button>
132
+ </div>
133
+ {:else}
134
+ <p class="dropzone-message">{placeholder}</p>
135
+ {/if}
136
+
137
+ <input
138
+ type="file"
139
+ {accept}
140
+ {disabled}
141
+ class="dropzone-input"
142
+ onchange={handleFileInput}
143
+ />
144
+ </div>
145
+
146
+ <style>
147
+ .dropzone {
148
+ position: relative;
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ min-height: 8rem;
153
+ padding: var(--space-4);
154
+ border: 2px dashed var(--color-base02);
155
+ border-radius: var(--radius-lg);
156
+ background: var(--color-base01);
157
+ cursor: pointer;
158
+ transition: border-color 0.2s ease, background 0.2s ease;
159
+ }
160
+
161
+ .dropzone:hover:not(.dropzone-disabled) {
162
+ border-color: var(--color-base0D);
163
+ background: color-mix(in srgb, var(--color-base0D) 5%, transparent);
164
+ }
165
+
166
+ .dropzone-dragging {
167
+ border-color: var(--color-base0D);
168
+ background: color-mix(in srgb, var(--color-base0D) 10%, transparent);
169
+ }
170
+
171
+ .dropzone-disabled {
172
+ opacity: 0.5;
173
+ cursor: not-allowed;
174
+ }
175
+
176
+ .dropzone-message {
177
+ margin: 0;
178
+ font-size: var(--text-sm);
179
+ color: var(--color-base04);
180
+ text-align: center;
181
+ pointer-events: none;
182
+ }
183
+
184
+ .dropzone-input {
185
+ position: absolute;
186
+ inset: 0;
187
+ width: 100%;
188
+ height: 100%;
189
+ opacity: 0;
190
+ cursor: pointer;
191
+ }
192
+
193
+ .dropzone-disabled .dropzone-input {
194
+ cursor: not-allowed;
195
+ }
196
+
197
+ .dropzone-file {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: var(--space-3);
201
+ width: 100%;
202
+ padding: var(--space-3);
203
+ background: var(--color-base00);
204
+ border: 1px solid var(--color-base02);
205
+ border-radius: var(--radius-md);
206
+ }
207
+
208
+ .dropzone-file-info {
209
+ flex: 1;
210
+ min-width: 0;
211
+ }
212
+
213
+ .dropzone-file-name {
214
+ margin: 0;
215
+ font-size: var(--text-sm);
216
+ font-weight: 500;
217
+ color: var(--color-base07);
218
+ white-space: nowrap;
219
+ overflow: hidden;
220
+ text-overflow: ellipsis;
221
+ }
222
+
223
+ .dropzone-file-meta {
224
+ margin: var(--space-1) 0 0 0;
225
+ font-size: var(--text-xs);
226
+ color: var(--color-base04);
227
+ }
228
+
229
+ .dropzone-clear {
230
+ flex-shrink: 0;
231
+ display: flex;
232
+ align-items: center;
233
+ justify-content: center;
234
+ padding: var(--space-1);
235
+ background: transparent;
236
+ border: none;
237
+ border-radius: var(--radius-sm);
238
+ color: var(--color-base04);
239
+ cursor: pointer;
240
+ transition: background 0.15s ease, color 0.15s ease;
241
+ z-index: 1;
242
+ }
243
+
244
+ .dropzone-clear:hover {
245
+ background: var(--color-base02);
246
+ color: var(--color-base05);
247
+ }
248
+ </style>
@@ -0,0 +1,266 @@
1
+ <!--
2
+ @component FileUpload
3
+
4
+ A file upload component with drag-and-drop support.
5
+
6
+ @example
7
+ <FileUpload onchange={handleFiles} accept="image/*" />
8
+
9
+ @example
10
+ // Multiple files
11
+ <FileUpload onchange={handleFiles} multiple />
12
+ -->
13
+ <script>
14
+ import { cn } from '../../utils/cn.svelte.js';
15
+
16
+ let {
17
+ accept = '*',
18
+ multiple = false,
19
+ disabled = false,
20
+ maxSize = null,
21
+ label = 'Drop files here or click to upload',
22
+ hint = '',
23
+ class: className = '',
24
+ onchange,
25
+ onerror,
26
+ ...rest
27
+ } = $props();
28
+
29
+ let fileInput = $state(null);
30
+ let isDragging = $state(false);
31
+ let files = $state([]);
32
+
33
+ function validateFile(file) {
34
+ if (maxSize && file.size > maxSize) {
35
+ return `File "${file.name}" exceeds maximum size of ${formatSize(maxSize)}`;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function formatSize(bytes) {
41
+ if (bytes < 1024) return bytes + ' B';
42
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
43
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
44
+ }
45
+
46
+ function handleFiles(fileList) {
47
+ const newFiles = Array.from(fileList);
48
+ const errors = [];
49
+
50
+ for (const file of newFiles) {
51
+ const error = validateFile(file);
52
+ if (error) {
53
+ errors.push(error);
54
+ }
55
+ }
56
+
57
+ if (errors.length > 0) {
58
+ onerror?.(errors);
59
+ return;
60
+ }
61
+
62
+ files = multiple ? [...files, ...newFiles] : newFiles;
63
+ onchange?.(files);
64
+ }
65
+
66
+ function handleInputChange(e) {
67
+ if (e.target.files?.length) {
68
+ handleFiles(e.target.files);
69
+ }
70
+ }
71
+
72
+ function handleDrop(e) {
73
+ e.preventDefault();
74
+ isDragging = false;
75
+ if (!disabled && e.dataTransfer?.files?.length) {
76
+ handleFiles(e.dataTransfer.files);
77
+ }
78
+ }
79
+
80
+ function handleDragOver(e) {
81
+ e.preventDefault();
82
+ if (!disabled) {
83
+ isDragging = true;
84
+ }
85
+ }
86
+
87
+ function handleDragLeave(e) {
88
+ e.preventDefault();
89
+ isDragging = false;
90
+ }
91
+
92
+ function openFileDialog() {
93
+ if (!disabled) {
94
+ fileInput?.click();
95
+ }
96
+ }
97
+
98
+ function removeFile(index) {
99
+ files = files.filter((_, i) => i !== index);
100
+ onchange?.(files);
101
+ }
102
+ </script>
103
+
104
+ <div class={cn('file-upload', className)}>
105
+ <button
106
+ type="button"
107
+ class="upload-zone"
108
+ class:upload-zone-dragging={isDragging}
109
+ class:upload-zone-disabled={disabled}
110
+ onclick={openFileDialog}
111
+ ondrop={handleDrop}
112
+ ondragover={handleDragOver}
113
+ ondragleave={handleDragLeave}
114
+ {disabled}
115
+ >
116
+ <svg class="upload-icon" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
117
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
118
+ <polyline points="17 8 12 3 7 8"></polyline>
119
+ <line x1="12" y1="3" x2="12" y2="15"></line>
120
+ </svg>
121
+ <span class="upload-label">{label}</span>
122
+ {#if hint}
123
+ <span class="upload-hint">{hint}</span>
124
+ {/if}
125
+ </button>
126
+
127
+ <input
128
+ bind:this={fileInput}
129
+ type="file"
130
+ {accept}
131
+ {multiple}
132
+ {disabled}
133
+ onchange={handleInputChange}
134
+ class="upload-input"
135
+ {...rest}
136
+ />
137
+
138
+ {#if files.length > 0}
139
+ <ul class="file-list">
140
+ {#each files as file, index}
141
+ <li class="file-item">
142
+ <span class="file-name">{file.name}</span>
143
+ <span class="file-size">{formatSize(file.size)}</span>
144
+ <button
145
+ type="button"
146
+ class="file-remove"
147
+ onclick={() => removeFile(index)}
148
+ aria-label="Remove file"
149
+ >
150
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
151
+ <line x1="18" y1="6" x2="6" y2="18"></line>
152
+ <line x1="6" y1="6" x2="18" y2="18"></line>
153
+ </svg>
154
+ </button>
155
+ </li>
156
+ {/each}
157
+ </ul>
158
+ {/if}
159
+ </div>
160
+
161
+ <style>
162
+ .file-upload {
163
+ width: 100%;
164
+ }
165
+
166
+ .upload-zone {
167
+ display: flex;
168
+ flex-direction: column;
169
+ align-items: center;
170
+ justify-content: center;
171
+ gap: var(--space-2);
172
+ width: 100%;
173
+ padding: var(--space-6) var(--space-4);
174
+ background: var(--color-base00);
175
+ border: 2px dashed var(--color-base03);
176
+ border-radius: var(--radius-lg);
177
+ cursor: pointer;
178
+ transition: var(--transition-colors);
179
+ }
180
+
181
+ .upload-zone:hover:not(:disabled) {
182
+ border-color: var(--color-base0D);
183
+ background: color-mix(in srgb, var(--color-base0D) 5%, transparent);
184
+ }
185
+
186
+ .upload-zone-dragging {
187
+ border-color: var(--color-base0D);
188
+ background: color-mix(in srgb, var(--color-base0D) 10%, transparent);
189
+ }
190
+
191
+ .upload-zone-disabled {
192
+ opacity: 0.5;
193
+ cursor: not-allowed;
194
+ }
195
+
196
+ .upload-icon {
197
+ color: var(--color-base04);
198
+ }
199
+
200
+ .upload-label {
201
+ font-size: var(--text-sm);
202
+ font-weight: 500;
203
+ color: var(--color-base05);
204
+ }
205
+
206
+ .upload-hint {
207
+ font-size: var(--text-xs);
208
+ color: var(--color-base04);
209
+ }
210
+
211
+ .upload-input {
212
+ display: none;
213
+ }
214
+
215
+ .file-list {
216
+ list-style: none;
217
+ margin: var(--space-3) 0 0;
218
+ padding: 0;
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: var(--space-2);
222
+ }
223
+
224
+ .file-item {
225
+ display: flex;
226
+ align-items: center;
227
+ gap: var(--space-2);
228
+ padding: var(--space-2) var(--space-3);
229
+ background: var(--color-base01);
230
+ border: 1px solid var(--color-base02);
231
+ border-radius: var(--radius-md);
232
+ }
233
+
234
+ .file-name {
235
+ flex: 1;
236
+ font-size: var(--text-sm);
237
+ color: var(--color-base05);
238
+ overflow: hidden;
239
+ text-overflow: ellipsis;
240
+ white-space: nowrap;
241
+ }
242
+
243
+ .file-size {
244
+ font-size: var(--text-xs);
245
+ color: var(--color-base04);
246
+ white-space: nowrap;
247
+ }
248
+
249
+ .file-remove {
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: center;
253
+ padding: var(--space-1);
254
+ background: transparent;
255
+ border: none;
256
+ border-radius: var(--radius-sm);
257
+ color: var(--color-base04);
258
+ cursor: pointer;
259
+ transition: var(--transition-colors);
260
+ }
261
+
262
+ .file-remove:hover {
263
+ color: var(--color-base08);
264
+ background: color-mix(in srgb, var(--color-base08) 10%, transparent);
265
+ }
266
+ </style>
@@ -0,0 +1,184 @@
1
+ <!--
2
+ @component IconInput
3
+
4
+ An input wrapper that supports left/right icons, loading states, and clearable functionality.
5
+
6
+ @example Basic with icon
7
+ <IconInput bind:value={email} placeholder="Email">
8
+ {#snippet leftIcon()}
9
+ <MailIcon size={16} />
10
+ {/snippet}
11
+ </IconInput>
12
+
13
+ @example With loading and clearable
14
+ <IconInput
15
+ bind:value={search}
16
+ placeholder="Search..."
17
+ loading
18
+ clearable
19
+ onclear={() => search = ''}
20
+ >
21
+ {#snippet leftIcon()}
22
+ <SearchIcon size={16} />
23
+ {/snippet}
24
+ </IconInput>
25
+ -->
26
+ <script>
27
+ import Input from './Input.svelte';
28
+
29
+ let {
30
+ value = $bindable(''),
31
+ type = 'text',
32
+ placeholder = '',
33
+ disabled = false,
34
+ size = 'md',
35
+ loading = false,
36
+ clearable = false,
37
+ class: className = '',
38
+ leftIcon,
39
+ rightIcon,
40
+ oninput,
41
+ onchange,
42
+ onclear,
43
+ ...rest
44
+ } = $props();
45
+
46
+ function handleClear() {
47
+ value = '';
48
+ onclear?.();
49
+ }
50
+
51
+ const showClearButton = $derived(clearable && value && !loading);
52
+ const hasLeftIcon = $derived(!!leftIcon);
53
+ const hasRightIcon = $derived(!!rightIcon || loading || showClearButton);
54
+ </script>
55
+
56
+ <div class="icon-input icon-input-{size} {className}">
57
+ {#if leftIcon}
58
+ <span class="icon-left">
59
+ {#if loading}
60
+ <svg class="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
61
+ <circle cx="12" cy="12" r="10" opacity="0.2" />
62
+ <path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
63
+ </svg>
64
+ {:else}
65
+ {@render leftIcon()}
66
+ {/if}
67
+ </span>
68
+ {/if}
69
+
70
+ <Input
71
+ {type}
72
+ bind:value
73
+ {placeholder}
74
+ {disabled}
75
+ {size}
76
+ class="icon-input-field"
77
+ style="padding-left: {hasLeftIcon ? '2.5rem' : ''}; padding-right: {hasRightIcon ? '2.5rem' : ''};"
78
+ {oninput}
79
+ {onchange}
80
+ {...rest}
81
+ />
82
+
83
+ {#if showClearButton}
84
+ <button
85
+ type="button"
86
+ class="icon-clear"
87
+ onclick={handleClear}
88
+ aria-label="Clear input"
89
+ >
90
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
91
+ <line x1="18" y1="6" x2="6" y2="18"></line>
92
+ <line x1="6" y1="6" x2="18" y2="18"></line>
93
+ </svg>
94
+ </button>
95
+ {:else if rightIcon && !loading}
96
+ <span class="icon-right">
97
+ {@render rightIcon()}
98
+ </span>
99
+ {:else if loading && !leftIcon}
100
+ <span class="icon-right">
101
+ <svg class="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
102
+ <circle cx="12" cy="12" r="10" opacity="0.2" />
103
+ <path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
104
+ </svg>
105
+ </span>
106
+ {/if}
107
+ </div>
108
+
109
+ <style>
110
+ .icon-input {
111
+ position: relative;
112
+ display: inline-flex;
113
+ align-items: center;
114
+ width: 100%;
115
+ }
116
+
117
+ .icon-input :global(.icon-input-field) {
118
+ width: 100%;
119
+ }
120
+
121
+ .icon-left,
122
+ .icon-right {
123
+ position: absolute;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ color: var(--color-base04);
128
+ pointer-events: none;
129
+ }
130
+
131
+ .icon-left {
132
+ left: 0.75rem;
133
+ }
134
+
135
+ .icon-right {
136
+ right: 0.75rem;
137
+ }
138
+
139
+ .icon-input-sm .icon-left,
140
+ .icon-input-sm .icon-right {
141
+ left: 0.625rem;
142
+ }
143
+
144
+ .icon-input-sm .icon-right {
145
+ right: 0.625rem;
146
+ }
147
+
148
+ .icon-input-lg .icon-left {
149
+ left: 1rem;
150
+ }
151
+
152
+ .icon-input-lg .icon-right {
153
+ right: 1rem;
154
+ }
155
+
156
+ .spinner {
157
+ animation: spin 1s linear infinite;
158
+ }
159
+
160
+ @keyframes spin {
161
+ from { transform: rotate(0deg); }
162
+ to { transform: rotate(360deg); }
163
+ }
164
+
165
+ .icon-clear {
166
+ position: absolute;
167
+ right: 0.5rem;
168
+ display: flex;
169
+ align-items: center;
170
+ justify-content: center;
171
+ padding: 0.25rem;
172
+ background: transparent;
173
+ border: none;
174
+ border-radius: var(--radius-sm);
175
+ color: var(--color-base04);
176
+ cursor: pointer;
177
+ transition: var(--transition-colors);
178
+ }
179
+
180
+ .icon-clear:hover {
181
+ color: var(--color-base05);
182
+ background: var(--color-base02);
183
+ }
184
+ </style>