radtools 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 (133) hide show
  1. package/README.md +108 -0
  2. package/bin/radtools.js +5 -0
  3. package/dist/cli/index.js +427 -0
  4. package/package.json +55 -0
  5. package/templates/api-routes/assets/optimize/route.ts +94 -0
  6. package/templates/api-routes/assets/route.ts +159 -0
  7. package/templates/api-routes/components/create-folder/route.ts +55 -0
  8. package/templates/api-routes/components/route.ts +156 -0
  9. package/templates/api-routes/fonts/route.ts +96 -0
  10. package/templates/api-routes/fonts/upload/route.ts +79 -0
  11. package/templates/api-routes/read-css/route.ts +29 -0
  12. package/templates/api-routes/write-css/route.ts +423 -0
  13. package/templates/components/Rad_os/AppWindow.tsx +423 -0
  14. package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
  15. package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
  16. package/templates/components/icons/Icon.tsx +224 -0
  17. package/templates/components/icons/README.md +85 -0
  18. package/templates/components/icons/index.ts +20 -0
  19. package/templates/components/icons.tsx +164 -0
  20. package/templates/components/ui/Accordion.tsx +268 -0
  21. package/templates/components/ui/Alert.tsx +111 -0
  22. package/templates/components/ui/Badge.tsx +87 -0
  23. package/templates/components/ui/Breadcrumbs.tsx +88 -0
  24. package/templates/components/ui/Button.tsx +249 -0
  25. package/templates/components/ui/Card.tsx +137 -0
  26. package/templates/components/ui/Checkbox.tsx +137 -0
  27. package/templates/components/ui/ContextMenu.tsx +220 -0
  28. package/templates/components/ui/Dialog.tsx +264 -0
  29. package/templates/components/ui/Divider.tsx +70 -0
  30. package/templates/components/ui/DropdownMenu.tsx +301 -0
  31. package/templates/components/ui/HelpPanel.tsx +119 -0
  32. package/templates/components/ui/Input.tsx +176 -0
  33. package/templates/components/ui/Popover.tsx +211 -0
  34. package/templates/components/ui/Progress.tsx +158 -0
  35. package/templates/components/ui/Select.tsx +134 -0
  36. package/templates/components/ui/Sheet.tsx +316 -0
  37. package/templates/components/ui/Slider.tsx +223 -0
  38. package/templates/components/ui/Switch.tsx +155 -0
  39. package/templates/components/ui/Tabs.tsx +253 -0
  40. package/templates/components/ui/Toast.tsx +192 -0
  41. package/templates/components/ui/Tooltip.tsx +129 -0
  42. package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
  43. package/templates/components/ui/index.ts +84 -0
  44. package/templates/devtools/DevToolsPanel.tsx +261 -0
  45. package/templates/devtools/DevToolsProvider.tsx +43 -0
  46. package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
  47. package/templates/devtools/components/ColorPicker.tsx +33 -0
  48. package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
  49. package/templates/devtools/components/ContextualFooter.tsx +56 -0
  50. package/templates/devtools/components/DraggablePanel.tsx +43 -0
  51. package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
  52. package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
  53. package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
  54. package/templates/devtools/components/TokenDropdown.tsx +47 -0
  55. package/templates/devtools/components/TypographyFooter.tsx +145 -0
  56. package/templates/devtools/hooks/useMockState.ts +16 -0
  57. package/templates/devtools/index.ts +17 -0
  58. package/templates/devtools/lib/componentScanner.ts +78 -0
  59. package/templates/devtools/lib/cssParser.ts +465 -0
  60. package/templates/devtools/lib/searchIndexes.ts +45 -0
  61. package/templates/devtools/lib/selectorGenerator.ts +86 -0
  62. package/templates/devtools/store/index.ts +66 -0
  63. package/templates/devtools/store/slices/assetsSlice.ts +106 -0
  64. package/templates/devtools/store/slices/componentsSlice.ts +59 -0
  65. package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
  66. package/templates/devtools/store/slices/panelSlice.ts +17 -0
  67. package/templates/devtools/store/slices/typographySlice.ts +538 -0
  68. package/templates/devtools/store/slices/variablesSlice.ts +167 -0
  69. package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
  70. package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
  71. package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
  72. package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
  73. package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
  74. package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
  75. package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
  76. package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
  77. package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
  78. package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
  79. package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
  80. package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
  81. package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
  82. package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
  83. package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
  84. package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
  85. package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
  86. package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
  87. package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
  88. package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
  89. package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
  90. package/templates/devtools/types/index.ts +99 -0
  91. package/templates/globals.css +574 -0
  92. package/templates/hooks/index.ts +1 -0
  93. package/templates/hooks/useWindowManager.ts +212 -0
  94. package/templates/public/assets/icons/avatar.svg +18 -0
  95. package/templates/public/assets/icons/checkmark-filled.svg +14 -0
  96. package/templates/public/assets/icons/checkmark.svg +14 -0
  97. package/templates/public/assets/icons/chevron-down.svg +14 -0
  98. package/templates/public/assets/icons/close.svg +14 -0
  99. package/templates/public/assets/icons/copy.svg +14 -0
  100. package/templates/public/assets/icons/download.svg +14 -0
  101. package/templates/public/assets/icons/expand.svg +31 -0
  102. package/templates/public/assets/icons/file-blank.svg +17 -0
  103. package/templates/public/assets/icons/file-image.svg +19 -0
  104. package/templates/public/assets/icons/file-written.svg +17 -0
  105. package/templates/public/assets/icons/folder-closed.svg +17 -0
  106. package/templates/public/assets/icons/folder-open.svg +17 -0
  107. package/templates/public/assets/icons/hamburger.svg +18 -0
  108. package/templates/public/assets/icons/home-outline.svg +28 -0
  109. package/templates/public/assets/icons/home.svg +30 -0
  110. package/templates/public/assets/icons/hourglass.svg +25 -0
  111. package/templates/public/assets/icons/information-circle.svg +14 -0
  112. package/templates/public/assets/icons/information.svg +17 -0
  113. package/templates/public/assets/icons/lightning.svg +14 -0
  114. package/templates/public/assets/icons/locked.svg +17 -0
  115. package/templates/public/assets/icons/not-allowed.svg +14 -0
  116. package/templates/public/assets/icons/plus.svg +5 -0
  117. package/templates/public/assets/icons/power-thin.svg +17 -0
  118. package/templates/public/assets/icons/power.svg +17 -0
  119. package/templates/public/assets/icons/question-block.svg +14 -0
  120. package/templates/public/assets/icons/question.svg +17 -0
  121. package/templates/public/assets/icons/refresh-block.svg +14 -0
  122. package/templates/public/assets/icons/refresh.svg +17 -0
  123. package/templates/public/assets/icons/save.svg +14 -0
  124. package/templates/public/assets/icons/search.svg +25 -0
  125. package/templates/public/assets/icons/settings.svg +14 -0
  126. package/templates/public/assets/icons/trash-full.svg +21 -0
  127. package/templates/public/assets/icons/trash-open.svg +23 -0
  128. package/templates/public/assets/icons/trash.svg +18 -0
  129. package/templates/public/assets/icons/unlocked.svg +17 -0
  130. package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
  131. package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
  132. package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
  133. package/templates/public/assets/icons/wrench.svg +17 -0
@@ -0,0 +1,421 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useCallback, useEffect } from 'react';
4
+ import { useDevToolsStore } from '../../store';
5
+ import { Icon } from '@/components/icons';
6
+ import { useToast } from '@/components/ui/Toast';
7
+ import { detectFontPropertiesFromFilename } from '../../lib/cssParser';
8
+ import type { FontDefinition, FontFile } from '../../types';
9
+
10
+ interface UploadingFont {
11
+ file: File;
12
+ name: string;
13
+ family: string;
14
+ weight: number;
15
+ style: string;
16
+ format: FontFile['format'];
17
+ }
18
+
19
+ export function FontManager() {
20
+ const { fonts, addFont, updateFont, deleteFont, addFontFile, removeFontFile, loadTypographyFromCSS, loadFontsFromFilesystem } = useDevToolsStore();
21
+ const [uploadingFont, setUploadingFont] = useState<UploadingFont | null>(null);
22
+ const [isUploading, setIsUploading] = useState(false);
23
+ const [expandedFont, setExpandedFont] = useState<string | null>(null);
24
+ const [isReloading, setIsReloading] = useState(false);
25
+ const { addToast } = useToast();
26
+
27
+ // Reload fonts from filesystem on mount (more accurate than CSS)
28
+ useEffect(() => {
29
+ loadFontsFromFilesystem();
30
+ }, [loadFontsFromFilesystem]);
31
+
32
+ // Handle reload fonts - only use filesystem (more accurate than CSS)
33
+ const handleReload = useCallback(async () => {
34
+ setIsReloading(true);
35
+ try {
36
+ // Load from filesystem only - CSS may not have all fonts
37
+ await loadFontsFromFilesystem();
38
+ } catch (error) {
39
+ // Failed to reload fonts
40
+ } finally {
41
+ setIsReloading(false);
42
+ }
43
+ }, [loadFontsFromFilesystem]);
44
+
45
+ // Handle file drop/selection
46
+ const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
47
+ const file = e.target.files?.[0];
48
+ if (!file) return;
49
+
50
+ const ext = file.name.split('.').pop()?.toLowerCase();
51
+ if (!['woff2', 'woff', 'ttf', 'otf'].includes(ext || '')) {
52
+ addToast({
53
+ title: 'Unsupported format',
54
+ description: 'Please use woff2, woff, ttf, or otf',
55
+ variant: 'warning',
56
+ });
57
+ return;
58
+ }
59
+
60
+ // Auto-detect properties from filename
61
+ const { weight, style } = detectFontPropertiesFromFilename(file.name);
62
+
63
+ // Extract font name from filename (e.g., "Mondwest-Bold.woff2" -> "Mondwest")
64
+ const nameParts = file.name.replace(/\.[^.]+$/, '').split('-');
65
+ const name = nameParts[0] || file.name.replace(/\.[^.]+$/, '');
66
+
67
+ setUploadingFont({
68
+ file,
69
+ name,
70
+ family: name,
71
+ weight,
72
+ style,
73
+ format: ext as FontFile['format'],
74
+ });
75
+ }, [addToast]);
76
+
77
+ // Handle drop
78
+ const handleDrop = useCallback((e: React.DragEvent) => {
79
+ e.preventDefault();
80
+ const file = e.dataTransfer.files[0];
81
+ if (!file) return;
82
+
83
+ const ext = file.name.split('.').pop()?.toLowerCase();
84
+ if (!['woff2', 'woff', 'ttf', 'otf'].includes(ext || '')) {
85
+ addToast({
86
+ title: 'Unsupported format',
87
+ description: 'Please use woff2, woff, ttf, or otf',
88
+ variant: 'warning',
89
+ });
90
+ return;
91
+ }
92
+
93
+ const { weight, style } = detectFontPropertiesFromFilename(file.name);
94
+ const nameParts = file.name.replace(/\.[^.]+$/, '').split('-');
95
+ const name = nameParts[0] || file.name.replace(/\.[^.]+$/, '');
96
+
97
+ setUploadingFont({
98
+ file,
99
+ name,
100
+ family: name,
101
+ weight,
102
+ style,
103
+ format: ext as FontFile['format'],
104
+ });
105
+ }, [addToast]);
106
+
107
+ // Upload font to server
108
+ const handleUpload = async () => {
109
+ if (!uploadingFont) return;
110
+
111
+ setIsUploading(true);
112
+ try {
113
+ const formData = new FormData();
114
+ formData.append('file', uploadingFont.file);
115
+ formData.append('name', uploadingFont.name);
116
+ formData.append('family', uploadingFont.family);
117
+ formData.append('weight', uploadingFont.weight.toString());
118
+ formData.append('style', uploadingFont.style);
119
+ formData.append('format', uploadingFont.format);
120
+
121
+ const response = await fetch('/api/devtools/fonts/upload', {
122
+ method: 'POST',
123
+ body: formData,
124
+ });
125
+
126
+ if (!response.ok) {
127
+ throw new Error('Failed to upload font');
128
+ }
129
+
130
+ const result = await response.json();
131
+
132
+ // Check if we're adding to existing font family or creating new one
133
+ const existingFont = fonts.find(f => f.family === uploadingFont.family);
134
+
135
+ if (existingFont) {
136
+ // Add file to existing font
137
+ addFontFile(existingFont.id, {
138
+ weight: uploadingFont.weight,
139
+ style: uploadingFont.style,
140
+ format: uploadingFont.format,
141
+ path: result.path,
142
+ });
143
+ } else {
144
+ // Create new font
145
+ addFont({
146
+ name: uploadingFont.name,
147
+ family: uploadingFont.family,
148
+ files: [{
149
+ id: crypto.randomUUID(),
150
+ weight: uploadingFont.weight,
151
+ style: uploadingFont.style,
152
+ format: uploadingFont.format,
153
+ path: result.path,
154
+ }],
155
+ weights: [uploadingFont.weight],
156
+ styles: [uploadingFont.style],
157
+ });
158
+ }
159
+
160
+ setUploadingFont(null);
161
+
162
+ // Reload fonts from filesystem to reflect the new upload
163
+ await loadFontsFromFilesystem();
164
+ } catch (error) {
165
+ addToast({
166
+ title: 'Upload failed',
167
+ description: 'Failed to upload font. Please try again.',
168
+ variant: 'error',
169
+ });
170
+ } finally {
171
+ setIsUploading(false);
172
+ }
173
+ };
174
+
175
+ // Delete font
176
+ const handleDeleteFont = (fontId: string) => {
177
+ if (confirm('Are you sure you want to delete this font?')) {
178
+ deleteFont(fontId);
179
+ }
180
+ };
181
+
182
+ // Delete font file
183
+ const handleDeleteFontFile = (fontId: string, fileId: string) => {
184
+ removeFontFile(fontId, fileId);
185
+ };
186
+
187
+ // Get weight label
188
+ const getWeightLabel = (weight: number) => {
189
+ const labels: Record<number, string> = {
190
+ 100: 'Thin',
191
+ 200: 'Extra Light',
192
+ 300: 'Light',
193
+ 400: 'Regular',
194
+ 500: 'Medium',
195
+ 600: 'Semibold',
196
+ 700: 'Bold',
197
+ 800: 'Extra Bold',
198
+ 900: 'Black',
199
+ };
200
+ return labels[weight] || weight.toString();
201
+ };
202
+
203
+ return (
204
+ <div className="space-y-4">
205
+ <div className="flex items-center justify-between">
206
+ <h3 className="font-joystix text-sm text-black uppercase">Fonts</h3>
207
+ <button
208
+ onClick={handleReload}
209
+ disabled={isReloading}
210
+ className="flex items-center gap-1 px-2 py-1 text-xs font-mondwest text-black/60 hover:text-black border border-black/20 rounded-sm hover:bg-black/5 disabled:opacity-50 disabled:cursor-not-allowed"
211
+ title="Reload fonts from CSS"
212
+ >
213
+ <Icon name="refresh" size={12} />
214
+ {isReloading ? 'Reloading...' : 'Reload'}
215
+ </button>
216
+ </div>
217
+
218
+ {/* Font List */}
219
+ <div className="space-y-2">
220
+ {fonts.map((font) => (
221
+ <div
222
+ key={font.id}
223
+ className="border border-black rounded-sm bg-warm-cloud overflow-hidden"
224
+ >
225
+ {/* Font Header */}
226
+ <div
227
+ className="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-black/5"
228
+ onClick={() => setExpandedFont(expandedFont === font.id ? null : font.id)}
229
+ >
230
+ <div className="flex items-center gap-3">
231
+ <span
232
+ className="text-lg"
233
+ style={{ fontFamily: font.family }}
234
+ >
235
+ Aa
236
+ </span>
237
+ <div>
238
+ <div className="font-mondwest text-base text-black">
239
+ {font.name}
240
+ </div>
241
+ <div className="font-mondwest text-sm text-black/60">
242
+ {font.weights.length} weight{font.weights.length !== 1 ? 's' : ''} • {font.styles.join(', ')}
243
+ </div>
244
+ </div>
245
+ </div>
246
+ <div className="flex items-center gap-2">
247
+ <button
248
+ onClick={(e) => {
249
+ e.stopPropagation();
250
+ handleDeleteFont(font.id);
251
+ }}
252
+ className="text-black/50 hover:text-error-red flex items-center"
253
+ title="Delete font"
254
+ >
255
+ <Icon name="close" size={16} />
256
+ </button>
257
+ <span className="text-black/40">{expandedFont === font.id ? '▼' : '▶'}</span>
258
+ </div>
259
+ </div>
260
+
261
+ {/* Font Files (expanded) */}
262
+ {expandedFont === font.id && (
263
+ <div className="border-t border-black/20 px-3 py-2 space-y-1 bg-black/5">
264
+ {font.files.map((file) => (
265
+ <div key={file.id} className="flex items-center justify-between py-1">
266
+ <div className="font-mondwest text-sm text-black/70">
267
+ {getWeightLabel(file.weight)} {file.style !== 'normal' ? `(${file.style})` : ''} • {file.format}
268
+ </div>
269
+ <button
270
+ onClick={() => handleDeleteFontFile(font.id, file.id)}
271
+ className="text-black/40 hover:text-error-red flex items-center"
272
+ title="Remove file"
273
+ >
274
+ <Icon name="close" size={14} />
275
+ </button>
276
+ </div>
277
+ ))}
278
+ <div className="text-xs text-black/40 font-mono pt-1 border-t border-black/10">
279
+ {font.files[0]?.path}
280
+ </div>
281
+ </div>
282
+ )}
283
+ </div>
284
+ ))}
285
+
286
+ {fonts.length === 0 && (
287
+ <div className="text-center py-4 text-black/50 font-mondwest text-base">
288
+ No fonts installed
289
+ </div>
290
+ )}
291
+ </div>
292
+
293
+ {/* Upload Dropzone */}
294
+ <div
295
+ onDrop={handleDrop}
296
+ onDragOver={(e) => e.preventDefault()}
297
+ className="border-2 border-dashed border-black/30 rounded-md p-4 text-center hover:border-black/50 transition-colors"
298
+ >
299
+ <input
300
+ type="file"
301
+ id="font-upload"
302
+ accept=".woff2,.woff,.ttf,.otf"
303
+ onChange={handleFileSelect}
304
+ className="hidden"
305
+ />
306
+ <label
307
+ htmlFor="font-upload"
308
+ className="cursor-pointer block"
309
+ >
310
+ <div className="font-mondwest text-base text-black/60">
311
+ Drop font file here or click to upload
312
+ </div>
313
+ <div className="font-mondwest text-xs text-black/40 mt-1">
314
+ Supports woff2, woff, ttf, otf
315
+ </div>
316
+ </label>
317
+ </div>
318
+
319
+ {/* Upload Modal */}
320
+ {uploadingFont && (
321
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
322
+ <div className="bg-warm-cloud border-2 border-black rounded-md p-4 max-w-md w-full mx-4 shadow-[4px_4px_0_0_var(--color-black)]">
323
+ <h4 className="font-joystix text-sm text-black uppercase mb-4">
324
+ Configure Font
325
+ </h4>
326
+
327
+ <div className="space-y-3">
328
+ {/* Font Name */}
329
+ <div>
330
+ <label className="font-mondwest text-sm text-black/60 block mb-1">
331
+ Font Name
332
+ </label>
333
+ <input
334
+ type="text"
335
+ value={uploadingFont.name}
336
+ onChange={(e) => setUploadingFont({ ...uploadingFont, name: e.target.value })}
337
+ className="w-full px-3 py-2 border border-black rounded-sm font-mondwest text-base bg-white"
338
+ />
339
+ </div>
340
+
341
+ {/* Font Family */}
342
+ <div>
343
+ <label className="font-mondwest text-sm text-black/60 block mb-1">
344
+ CSS Font Family
345
+ </label>
346
+ <input
347
+ type="text"
348
+ value={uploadingFont.family}
349
+ onChange={(e) => setUploadingFont({ ...uploadingFont, family: e.target.value })}
350
+ className="w-full px-3 py-2 border border-black rounded-sm font-mondwest text-base bg-white"
351
+ />
352
+ </div>
353
+
354
+ {/* Weight */}
355
+ <div>
356
+ <label className="font-mondwest text-sm text-black/60 block mb-1">
357
+ Weight
358
+ </label>
359
+ <select
360
+ value={uploadingFont.weight}
361
+ onChange={(e) => setUploadingFont({ ...uploadingFont, weight: parseInt(e.target.value, 10) })}
362
+ className="w-full px-3 py-2 border border-black rounded-sm font-mondwest text-base bg-white"
363
+ >
364
+ <option value={100}>100 - Thin</option>
365
+ <option value={200}>200 - Extra Light</option>
366
+ <option value={300}>300 - Light</option>
367
+ <option value={400}>400 - Regular</option>
368
+ <option value={500}>500 - Medium</option>
369
+ <option value={600}>600 - Semibold</option>
370
+ <option value={700}>700 - Bold</option>
371
+ <option value={800}>800 - Extra Bold</option>
372
+ <option value={900}>900 - Black</option>
373
+ </select>
374
+ </div>
375
+
376
+ {/* Style */}
377
+ <div>
378
+ <label className="font-mondwest text-sm text-black/60 block mb-1">
379
+ Style
380
+ </label>
381
+ <select
382
+ value={uploadingFont.style}
383
+ onChange={(e) => setUploadingFont({ ...uploadingFont, style: e.target.value })}
384
+ className="w-full px-3 py-2 border border-black rounded-sm font-mondwest text-base bg-white"
385
+ >
386
+ <option value="normal">Normal</option>
387
+ <option value="italic">Italic</option>
388
+ </select>
389
+ </div>
390
+
391
+ {/* File Info */}
392
+ <div className="text-xs text-black/40 font-mono">
393
+ File: {uploadingFont.file.name}
394
+ </div>
395
+ </div>
396
+
397
+ {/* Actions */}
398
+ <div className="flex justify-end gap-2 mt-4">
399
+ <button
400
+ onClick={() => setUploadingFont(null)}
401
+ className="px-4 py-2 font-mondwest text-base text-black border border-black rounded-sm hover:bg-black/5"
402
+ >
403
+ Cancel
404
+ </button>
405
+ <button
406
+ onClick={handleUpload}
407
+ disabled={isUploading}
408
+ className="px-4 py-2 font-mondwest text-base text-black bg-sun-yellow border border-black rounded-sm hover:bg-sun-yellow/80 disabled:opacity-50"
409
+ >
410
+ {isUploading ? 'Uploading...' : 'Upload'}
411
+ </button>
412
+ </div>
413
+ </div>
414
+ </div>
415
+ )}
416
+ </div>
417
+ );
418
+ }
419
+
420
+ export default FontManager;
421
+