omnidesign 1.0.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/LICENSE +21 -0
- package/QUICKREF.md +150 -0
- package/README.md +576 -0
- package/bin/cli.js +390 -0
- package/bin/detect-ide.js +50 -0
- package/bin/install.js +8 -0
- package/logo.jpg +0 -0
- package/package.json +84 -0
- package/recipes/components/README.md +29 -0
- package/recipes/components/agent-card.md +314 -0
- package/recipes/components/ai-chat.md +252 -0
- package/recipes/components/bento-grid.md +186 -0
- package/recipes/components/code-block.md +503 -0
- package/recipes/components/file-upload.md +483 -0
- package/recipes/components/forms.md +238 -0
- package/recipes/components/hero-section.md +161 -0
- package/recipes/components/navbar.md +214 -0
- package/recipes/components/prompt-input.md +293 -0
- package/recipes/components/thinking-indicator.md +372 -0
- package/recipes/motion/README.md +3 -0
- package/recipes/motion/motion-system.md +437 -0
- package/recipes/patterns/README.md +3 -0
- package/skills/aider/omnidesign.md +67 -0
- package/skills/amp/SKILL.md +114 -0
- package/skills/antigravity/SKILL.md +114 -0
- package/skills/claude/omnidesign.md +111 -0
- package/skills/continue/omnidesign.yaml +29 -0
- package/skills/cursor/omnidesign.md +110 -0
- package/skills/kilo/SKILL.md +114 -0
- package/skills/opencode/omnidesign.md +110 -0
- package/skills/vscode/package.json +66 -0
- package/skills/zed/omnidesign.json +7 -0
- package/tokens/motion/README.md +3 -0
- package/tokens/primitives/README.md +3 -0
- package/tokens/primitives/color.json +219 -0
- package/tokens/primitives/motion.json +56 -0
- package/tokens/primitives/radii.json +37 -0
- package/tokens/primitives/shadows.json +34 -0
- package/tokens/primitives/spacing.json +67 -0
- package/tokens/primitives/typography.json +127 -0
- package/tokens/semantic/README.md +3 -0
- package/tokens/semantic/color.json +114 -0
- package/tokens/semantic/motion.json +44 -0
- package/tokens/semantic/radii.json +29 -0
- package/tokens/semantic/shadows.json +24 -0
- package/tokens/semantic/spacing.json +69 -0
- package/tokens/semantic/typography.json +118 -0
- package/tokens/shadows/README.md +3 -0
- package/tokens/themes/README.md +3 -0
- package/tokens/themes/berry.json +143 -0
- package/tokens/themes/brutalist.json +143 -0
- package/tokens/themes/coral.json +143 -0
- package/tokens/themes/corporate.json +143 -0
- package/tokens/themes/cream.json +143 -0
- package/tokens/themes/cyberpunk.json +143 -0
- package/tokens/themes/daylight.json +143 -0
- package/tokens/themes/deep-space.json +143 -0
- package/tokens/themes/forest.json +143 -0
- package/tokens/themes/graphite.json +143 -0
- package/tokens/themes/lavender.json +143 -0
- package/tokens/themes/midnight.json +143 -0
- package/tokens/themes/mint.json +143 -0
- package/tokens/themes/navy.json +143 -0
- package/tokens/themes/noir.json +143 -0
- package/tokens/themes/obsidian.json +143 -0
- package/tokens/themes/ocean.json +143 -0
- package/tokens/themes/paper.json +143 -0
- package/tokens/themes/ruby.json +143 -0
- package/tokens/themes/slate.json +143 -0
- package/tokens/themes/snow.json +143 -0
- package/tokens/themes/solar.json +143 -0
- package/tokens/themes/spring.json +143 -0
- package/tokens/themes/starry-night.json +143 -0
- package/tokens/themes/sunset.json +143 -0
- package/tokens/typography/FONT_GUIDE.md +381 -0
- package/tokens/typography/README.md +37 -0
- package/tokens/typography/font-collection.json +221 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# File Upload for AI
|
|
2
|
+
|
|
3
|
+
Drag-and-drop file upload with AI-specific features like context preview and file type indicators.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
- Uploading documents for AI analysis
|
|
7
|
+
- Attaching files to chat messages
|
|
8
|
+
- Adding context to AI prompts
|
|
9
|
+
- Knowledge base uploads
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
15
|
+
│ FileUpload │
|
|
16
|
+
│ ├─ DropZone (drag target with visual feedback) │
|
|
17
|
+
│ │ ├─ Upload Icon │
|
|
18
|
+
│ │ ├─ Instructions (drag here or click) │
|
|
19
|
+
│ │ └─ File Type Hints (PDF, images, etc.) │
|
|
20
|
+
│ ├─ FileList (uploaded files with previews) │
|
|
21
|
+
│ │ ├─ FileChip (thumbnail + name + remove) │
|
|
22
|
+
│ │ └─ FileProgress (upload progress bar) │
|
|
23
|
+
│ └─ UploadActions (clear all, upload button) │
|
|
24
|
+
└─────────────────────────────────────────────────────────────┘
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Token Usage
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
/* Drop Zone */
|
|
31
|
+
.drop-zone {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
padding: var(--spacing-xl);
|
|
37
|
+
background: var(--color-surface-raised);
|
|
38
|
+
border: 2px dashed var(--color-border-default);
|
|
39
|
+
border-radius: var(--radius-xl);
|
|
40
|
+
transition: all var(--duration-fast);
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.drop-zone:hover {
|
|
45
|
+
border-color: var(--color-interactive-primary);
|
|
46
|
+
background: rgba(37, 99, 235, 0.05);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.drop-zone.drag-over {
|
|
50
|
+
border-color: var(--color-interactive-primary);
|
|
51
|
+
background: rgba(37, 99, 235, 0.1);
|
|
52
|
+
transform: scale(1.02);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.drop-zone.error {
|
|
56
|
+
border-color: var(--color-status-error);
|
|
57
|
+
background: rgba(239, 68, 68, 0.05);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Upload Icon */
|
|
61
|
+
.upload-icon {
|
|
62
|
+
width: 48px;
|
|
63
|
+
height: 48px;
|
|
64
|
+
color: var(--color-text-muted);
|
|
65
|
+
margin-bottom: var(--spacing-md);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.drop-zone:hover .upload-icon {
|
|
69
|
+
color: var(--color-interactive-primary);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Instructions */
|
|
73
|
+
.upload-text {
|
|
74
|
+
color: var(--color-text-default);
|
|
75
|
+
font-weight: var(--font-weight-medium);
|
|
76
|
+
margin-bottom: var(--spacing-xs);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.upload-hint {
|
|
80
|
+
color: var(--color-text-muted);
|
|
81
|
+
font-size: var(--font-size-sm);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* File Type Badges */
|
|
85
|
+
.file-types {
|
|
86
|
+
display: flex;
|
|
87
|
+
gap: var(--spacing-xs);
|
|
88
|
+
margin-top: var(--spacing-md);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.file-type-badge {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: var(--spacing-2xs);
|
|
95
|
+
padding: var(--spacing-2xs) var(--spacing-xs);
|
|
96
|
+
background: var(--color-surface-sunken);
|
|
97
|
+
border-radius: var(--radius-sm);
|
|
98
|
+
font-size: var(--font-size-xs);
|
|
99
|
+
color: var(--color-text-muted);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* File Chip */
|
|
103
|
+
.file-chip {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: var(--spacing-sm);
|
|
107
|
+
padding: var(--spacing-sm);
|
|
108
|
+
background: var(--color-surface-raised);
|
|
109
|
+
border: 1px solid var(--color-border-default);
|
|
110
|
+
border-radius: var(--radius-lg);
|
|
111
|
+
max-width: 100%;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.file-thumbnail {
|
|
115
|
+
width: 40px;
|
|
116
|
+
height: 40px;
|
|
117
|
+
border-radius: var(--radius-md);
|
|
118
|
+
background: var(--color-surface-sunken);
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
flex-shrink: 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.file-thumbnail img {
|
|
126
|
+
width: 100%;
|
|
127
|
+
height: 100%;
|
|
128
|
+
object-fit: cover;
|
|
129
|
+
border-radius: var(--radius-md);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.file-thumbnail-icon {
|
|
133
|
+
width: 20px;
|
|
134
|
+
height: 20px;
|
|
135
|
+
color: var(--color-interactive-primary);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.file-info {
|
|
139
|
+
flex: 1;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.file-name {
|
|
144
|
+
font-weight: var(--font-weight-medium);
|
|
145
|
+
color: var(--color-text-default);
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
text-overflow: ellipsis;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.file-meta {
|
|
152
|
+
font-size: var(--font-size-xs);
|
|
153
|
+
color: var(--color-text-muted);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.file-remove {
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
width: 28px;
|
|
161
|
+
height: 28px;
|
|
162
|
+
border: none;
|
|
163
|
+
background: transparent;
|
|
164
|
+
border-radius: var(--radius-md);
|
|
165
|
+
color: var(--color-text-muted);
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
transition: all var(--duration-fast);
|
|
168
|
+
flex-shrink: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.file-remove:hover {
|
|
172
|
+
background: rgba(239, 68, 68, 0.1);
|
|
173
|
+
color: var(--color-status-error);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Progress Bar */
|
|
177
|
+
.file-progress {
|
|
178
|
+
margin-top: var(--spacing-xs);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.progress-bar {
|
|
182
|
+
height: 4px;
|
|
183
|
+
background: var(--color-surface-sunken);
|
|
184
|
+
border-radius: var(--radius-full);
|
|
185
|
+
overflow: hidden;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.progress-fill {
|
|
189
|
+
height: 100%;
|
|
190
|
+
background: var(--color-interactive-primary);
|
|
191
|
+
border-radius: var(--radius-full);
|
|
192
|
+
transition: width var(--duration-fast);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Upload Actions */
|
|
196
|
+
.upload-actions {
|
|
197
|
+
display: flex;
|
|
198
|
+
justify-content: flex-end;
|
|
199
|
+
gap: var(--spacing-sm);
|
|
200
|
+
margin-top: var(--spacing-md);
|
|
201
|
+
padding-top: var(--spacing-md);
|
|
202
|
+
border-top: 1px solid var(--color-border-default);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Context Preview (AI Feature) */
|
|
206
|
+
.context-preview {
|
|
207
|
+
margin-top: var(--spacing-md);
|
|
208
|
+
padding: var(--spacing-md);
|
|
209
|
+
background: var(--color-surface-sunken);
|
|
210
|
+
border-radius: var(--radius-lg);
|
|
211
|
+
border-left: 3px solid var(--color-interactive-primary);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.context-preview h4 {
|
|
215
|
+
font-size: var(--font-size-xs);
|
|
216
|
+
font-weight: var(--font-weight-semibold);
|
|
217
|
+
color: var(--color-text-muted);
|
|
218
|
+
text-transform: uppercase;
|
|
219
|
+
letter-spacing: 0.05em;
|
|
220
|
+
margin-bottom: var(--spacing-xs);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.context-text {
|
|
224
|
+
font-size: var(--font-size-sm);
|
|
225
|
+
color: var(--color-text-default);
|
|
226
|
+
line-height: var(--line-height-relaxed);
|
|
227
|
+
max-height: 100px;
|
|
228
|
+
overflow-y: auto;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## State Matrix
|
|
233
|
+
|
|
234
|
+
| Element | Default | Hover | Drag Over | Error | Uploading |
|
|
235
|
+
|---------|---------|-------|-----------|-------|-----------|
|
|
236
|
+
| Drop Zone | border-default | border-primary + tint | border-primary + scale | border-error | - |
|
|
237
|
+
| Upload Icon | muted | primary | primary | error | - |
|
|
238
|
+
| File Chip | raised bg | border-strong | - | - | - |
|
|
239
|
+
| Remove Button | ghost | error tint | - | - | - |
|
|
240
|
+
| Progress Bar | - | - | - | - | fill animates |
|
|
241
|
+
|
|
242
|
+
## Accessibility
|
|
243
|
+
|
|
244
|
+
```html
|
|
245
|
+
<div
|
|
246
|
+
class="drop-zone"
|
|
247
|
+
role="button"
|
|
248
|
+
tabindex="0"
|
|
249
|
+
aria-label="Drop files here or click to upload"
|
|
250
|
+
onDragOver={handleDragOver}
|
|
251
|
+
onDrop={handleDrop}
|
|
252
|
+
onKeyDown={handleKeyDown}
|
|
253
|
+
>
|
|
254
|
+
<UploadIcon aria-hidden="true" />
|
|
255
|
+
<p class="upload-text">Drop files here or click to upload</p>
|
|
256
|
+
<p class="upload-hint">Supports PDF, images, text files up to 10MB</p>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<!-- File list -->
|
|
260
|
+
<ul class="file-list" role="list" aria-label="Uploaded files">
|
|
261
|
+
<li class="file-chip" role="listitem">
|
|
262
|
+
<div class="file-thumbnail">
|
|
263
|
+
<img src="preview.jpg" alt="Preview of document.pdf" />
|
|
264
|
+
</div>
|
|
265
|
+
<div class="file-info">
|
|
266
|
+
<p class="file-name">document.pdf</p>
|
|
267
|
+
<p class="file-meta">2.4 MB • Ready to analyze</p>
|
|
268
|
+
</div>
|
|
269
|
+
<button
|
|
270
|
+
class="file-remove"
|
|
271
|
+
aria-label="Remove document.pdf"
|
|
272
|
+
onClick={handleRemove}
|
|
273
|
+
>
|
|
274
|
+
<CloseIcon aria-hidden="true" />
|
|
275
|
+
</button>
|
|
276
|
+
</li>
|
|
277
|
+
</ul>
|
|
278
|
+
|
|
279
|
+
<!-- Screen reader announcements -->
|
|
280
|
+
<div aria-live="polite" aria-atomic="true" class="sr-only">
|
|
281
|
+
File uploaded: document.pdf
|
|
282
|
+
</div>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Example: AI File Upload
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
function AIFileUpload({ onUpload, acceptedTypes = ['pdf', 'jpg', 'png', 'txt'] }) {
|
|
289
|
+
const [files, setFiles] = useState([]);
|
|
290
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
291
|
+
|
|
292
|
+
const handleDrop = (e) => {
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
setIsDragging(false);
|
|
295
|
+
const droppedFiles = Array.from(e.dataTransfer.files);
|
|
296
|
+
processFiles(droppedFiles);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const processFiles = async (newFiles) => {
|
|
300
|
+
for (const file of newFiles) {
|
|
301
|
+
const fileWithPreview = {
|
|
302
|
+
id: Math.random().toString(36),
|
|
303
|
+
file,
|
|
304
|
+
preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : null,
|
|
305
|
+
progress: 0,
|
|
306
|
+
status: 'uploading'
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
setFiles(prev => [...prev, fileWithPreview]);
|
|
310
|
+
|
|
311
|
+
// Simulate upload progress
|
|
312
|
+
await uploadFile(fileWithPreview);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const uploadFile = async (fileItem) => {
|
|
317
|
+
// Upload logic here
|
|
318
|
+
for (let progress = 0; progress <= 100; progress += 10) {
|
|
319
|
+
await new Promise(r => setTimeout(r, 200));
|
|
320
|
+
setFiles(prev => prev.map(f =>
|
|
321
|
+
f.id === fileItem.id ? { ...f, progress } : f
|
|
322
|
+
));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
setFiles(prev => prev.map(f =>
|
|
326
|
+
f.id === fileItem.id ? { ...f, status: 'ready', progress: 100 } : f
|
|
327
|
+
));
|
|
328
|
+
|
|
329
|
+
onUpload?.(fileItem.file);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const removeFile = (id) => {
|
|
333
|
+
setFiles(prev => {
|
|
334
|
+
const file = prev.find(f => f.id === id);
|
|
335
|
+
if (file?.preview) URL.revokeObjectURL(file.preview);
|
|
336
|
+
return prev.filter(f => f.id !== id);
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<div className="file-upload">
|
|
342
|
+
<div
|
|
343
|
+
className={`drop-zone ${isDragging ? 'drag-over' : ''}`}
|
|
344
|
+
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
|
345
|
+
onDragLeave={() => setIsDragging(false)}
|
|
346
|
+
onDrop={handleDrop}
|
|
347
|
+
onClick={() => inputRef.current?.click()}
|
|
348
|
+
>
|
|
349
|
+
<UploadIcon className="upload-icon" />
|
|
350
|
+
<p className="upload-text">Drop files here or click to upload</p>
|
|
351
|
+
<p className="upload-hint">
|
|
352
|
+
Supports {acceptedTypes.join(', ')} up to 10MB
|
|
353
|
+
</p>
|
|
354
|
+
|
|
355
|
+
<div className="file-types">
|
|
356
|
+
{acceptedTypes.map(type => (
|
|
357
|
+
<span key={type} className="file-type-badge">
|
|
358
|
+
<FileIcon /> {type.toUpperCase()}
|
|
359
|
+
</span>
|
|
360
|
+
))}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<input
|
|
365
|
+
ref={inputRef}
|
|
366
|
+
type="file"
|
|
367
|
+
multiple
|
|
368
|
+
accept={acceptedTypes.map(t => `.${t}`).join(',')}
|
|
369
|
+
onChange={(e) => processFiles(Array.from(e.target.files))}
|
|
370
|
+
hidden
|
|
371
|
+
/>
|
|
372
|
+
|
|
373
|
+
{files.length > 0 && (
|
|
374
|
+
<ul className="file-list">
|
|
375
|
+
{files.map(file => (
|
|
376
|
+
<li key={file.id} className="file-chip">
|
|
377
|
+
<div className="file-thumbnail">
|
|
378
|
+
{file.preview ? (
|
|
379
|
+
<img src={file.preview} alt="" />
|
|
380
|
+
) : (
|
|
381
|
+
<FileIcon className="file-thumbnail-icon" />
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
<div className="file-info">
|
|
386
|
+
<p className="file-name">{file.file.name}</p>
|
|
387
|
+
<p className="file-meta">
|
|
388
|
+
{formatSize(file.file.size)} •
|
|
389
|
+
{file.status === 'uploading' ? ` ${file.progress}%` : ' Ready'}
|
|
390
|
+
</p>
|
|
391
|
+
{file.status === 'uploading' && (
|
|
392
|
+
<div className="file-progress">
|
|
393
|
+
<div className="progress-bar">
|
|
394
|
+
<div
|
|
395
|
+
className="progress-fill"
|
|
396
|
+
style={{ width: `${file.progress}%` }}
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<button
|
|
404
|
+
className="file-remove"
|
|
405
|
+
onClick={() => removeFile(file.id)}
|
|
406
|
+
aria-label={`Remove ${file.file.name}`}
|
|
407
|
+
>
|
|
408
|
+
<CloseIcon />
|
|
409
|
+
</button>
|
|
410
|
+
</li>
|
|
411
|
+
))}
|
|
412
|
+
</ul>
|
|
413
|
+
)}
|
|
414
|
+
|
|
415
|
+
{files.length > 0 && (
|
|
416
|
+
<div className="upload-actions">
|
|
417
|
+
<button onClick={() => setFiles([])}>Clear all</button>
|
|
418
|
+
<button className="primary">Analyze files</button>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Example: Inline File Attachments (Chat)
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
function FileAttachments({ files, onRemove }) {
|
|
430
|
+
if (files.length === 0) return null;
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<div className="attachment-list">
|
|
434
|
+
{files.map(file => (
|
|
435
|
+
<div key={file.id} className="attachment-chip">
|
|
436
|
+
{file.preview ? (
|
|
437
|
+
<img src={file.preview} alt="" className="attachment-thumb" />
|
|
438
|
+
) : (
|
|
439
|
+
<FileIcon />
|
|
440
|
+
)}
|
|
441
|
+
<span className="attachment-name">{file.name}</span>
|
|
442
|
+
<button
|
|
443
|
+
className="attachment-remove"
|
|
444
|
+
onClick={() => onRemove(file.id)}
|
|
445
|
+
>
|
|
446
|
+
<CloseIcon />
|
|
447
|
+
</button>
|
|
448
|
+
</div>
|
|
449
|
+
))}
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Usage in chat input
|
|
455
|
+
function ChatInputWithFiles() {
|
|
456
|
+
const [attachments, setAttachments] = useState([]);
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<div className="chat-input-container">
|
|
460
|
+
<FileAttachments
|
|
461
|
+
files={attachments}
|
|
462
|
+
onRemove={(id) => setAttachments(prev => prev.filter(f => f.id !== id))}
|
|
463
|
+
/>
|
|
464
|
+
|
|
465
|
+
<div className="input-row">
|
|
466
|
+
<FileUploadButton
|
|
467
|
+
onFiles={(files) => setAttachments(prev => [...prev, ...files])}
|
|
468
|
+
/>
|
|
469
|
+
<textarea placeholder="Ask about these files..." />
|
|
470
|
+
<SendButton />
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Tokens Used
|
|
478
|
+
|
|
479
|
+
- **color**: surface-raised, surface-sunken, interactive-primary, text-default, text-muted, border-default, border-strong, status-error
|
|
480
|
+
- **spacing**: 2xs, xs, sm, md, xl
|
|
481
|
+
- **radii**: sm, md, lg, xl, full
|
|
482
|
+
- **typography**: size-xs, size-sm, weight-medium, weight-semibold
|
|
483
|
+
- **motion**: duration-fast
|