@gallop.software/studio 2.0.2 → 2.0.4

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 (45) hide show
  1. package/app/layout.tsx +2 -2
  2. package/app/page.tsx +1 -1
  3. package/dist/components/StudioUI.d.mts +15 -0
  4. package/dist/components/StudioUI.d.ts +15 -0
  5. package/dist/components/StudioUI.js +6572 -0
  6. package/dist/components/StudioUI.js.map +1 -0
  7. package/dist/components/StudioUI.mjs +6572 -0
  8. package/dist/components/StudioUI.mjs.map +1 -0
  9. package/package.json +6 -2
  10. package/src/components/AddNewModal.tsx +0 -402
  11. package/src/components/ErrorModal.tsx +0 -89
  12. package/src/components/R2SetupModal.tsx +0 -400
  13. package/src/components/StudioBreadcrumb.tsx +0 -115
  14. package/src/components/StudioButton.tsx +0 -200
  15. package/src/components/StudioContext.tsx +0 -219
  16. package/src/components/StudioDetailView.tsx +0 -714
  17. package/src/components/StudioFileGrid.tsx +0 -704
  18. package/src/components/StudioFileList.tsx +0 -743
  19. package/src/components/StudioFolderPicker.tsx +0 -342
  20. package/src/components/StudioModal.tsx +0 -473
  21. package/src/components/StudioPreview.tsx +0 -399
  22. package/src/components/StudioSettings.tsx +0 -536
  23. package/src/components/StudioToolbar.tsx +0 -1448
  24. package/src/components/StudioUI.tsx +0 -731
  25. package/src/components/styles/common.ts +0 -236
  26. package/src/components/tokens.ts +0 -78
  27. package/src/components/useStudioActions.tsx +0 -497
  28. package/src/config/index.ts +0 -7
  29. package/src/config/workspace.ts +0 -52
  30. package/src/handlers/favicon.ts +0 -152
  31. package/src/handlers/files.ts +0 -784
  32. package/src/handlers/images.ts +0 -949
  33. package/src/handlers/import.ts +0 -190
  34. package/src/handlers/index.ts +0 -168
  35. package/src/handlers/list.ts +0 -627
  36. package/src/handlers/scan.ts +0 -311
  37. package/src/handlers/utils/cdn.ts +0 -234
  38. package/src/handlers/utils/files.ts +0 -64
  39. package/src/handlers/utils/index.ts +0 -4
  40. package/src/handlers/utils/meta.ts +0 -102
  41. package/src/handlers/utils/thumbnails.ts +0 -98
  42. package/src/hooks/useFileList.ts +0 -143
  43. package/src/index.tsx +0 -36
  44. package/src/lib/api.ts +0 -176
  45. package/src/types.ts +0 -119
@@ -1,704 +0,0 @@
1
- /** @jsxImportSource @emotion/react */
2
- 'use client'
3
-
4
- import { useState } from 'react'
5
- import { css, keyframes } from '@emotion/react'
6
- import { useFileList } from '../hooks/useFileList'
7
- import { colors, fontSize } from './tokens'
8
- import { FileItem } from '../types'
9
-
10
- const spin = keyframes`
11
- to { transform: rotate(360deg); }
12
- `
13
-
14
- const styles = {
15
- loading: css`
16
- display: flex;
17
- align-items: center;
18
- justify-content: center;
19
- flex: 1;
20
- min-height: 300px;
21
- `,
22
- spinner: css`
23
- width: 32px;
24
- height: 32px;
25
- border-radius: 50%;
26
- border: 3px solid ${colors.border};
27
- border-top-color: ${colors.primary};
28
- animation: ${spin} 0.8s linear infinite;
29
- `,
30
- empty: css`
31
- display: flex;
32
- flex-direction: column;
33
- align-items: center;
34
- justify-content: center;
35
- flex: 1;
36
- min-height: 300px;
37
- color: ${colors.textSecondary};
38
- `,
39
- emptyIcon: css`
40
- width: 48px;
41
- height: 48px;
42
- margin-bottom: 16px;
43
- opacity: 0.5;
44
- `,
45
- emptyText: css`
46
- font-size: ${fontSize.base};
47
- margin: 0 0 4px 0;
48
-
49
- &:last-child {
50
- color: ${colors.textMuted};
51
- font-size: ${fontSize.sm};
52
- }
53
- `,
54
- scanButton: css`
55
- margin-top: 16px;
56
- padding: 10px 24px;
57
- font-size: ${fontSize.base};
58
- font-weight: 500;
59
- background: ${colors.primary};
60
- color: white;
61
- border: none;
62
- border-radius: 8px;
63
- cursor: pointer;
64
- transition: background 0.15s ease;
65
-
66
- &:hover:not(:disabled) {
67
- background: ${colors.primaryHover};
68
- }
69
-
70
- &:disabled {
71
- opacity: 0.6;
72
- cursor: not-allowed;
73
- }
74
- `,
75
- grid: css`
76
- display: grid;
77
- grid-template-columns: 1fr;
78
- gap: 12px;
79
-
80
- @media (min-width: 480px) { grid-template-columns: repeat(2, 1fr); }
81
- @media (min-width: 768px) { grid-template-columns: repeat(3, 1fr); }
82
- @media (min-width: 1024px) { grid-template-columns: repeat(4, 1fr); }
83
- @media (min-width: 1280px) { grid-template-columns: repeat(5, 1fr); }
84
- `,
85
- item: css`
86
- position: relative;
87
- border-radius: 8px;
88
- border: 1px solid ${colors.border};
89
- overflow: hidden;
90
- cursor: pointer;
91
- transition: all 0.15s ease;
92
- background-color: ${colors.surface};
93
- user-select: none;
94
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
95
-
96
- &:hover {
97
- border-color: #d0d5dd;
98
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
99
- }
100
- `,
101
- itemSelected: css`
102
- border-color: ${colors.primary};
103
- box-shadow: 0 0 0 1px ${colors.primary};
104
-
105
- &:hover {
106
- border-color: ${colors.primary};
107
- box-shadow: 0 0 0 1px ${colors.primary};
108
- }
109
- `,
110
- parentItem: css`
111
- cursor: pointer;
112
-
113
- &:hover {
114
- border-color: ${colors.primary};
115
- }
116
- `,
117
- checkboxWrapper: css`
118
- position: absolute;
119
- top: 0;
120
- left: 0;
121
- z-index: 10;
122
- padding: 8px;
123
- cursor: pointer;
124
- `,
125
- checkbox: css`
126
- width: 18px;
127
- height: 18px;
128
- accent-color: ${colors.primary};
129
- cursor: pointer;
130
- `,
131
- content: css`
132
- position: relative;
133
- aspect-ratio: 1;
134
- display: flex;
135
- align-items: center;
136
- justify-content: center;
137
- padding: 16px;
138
- background: ${colors.background};
139
- `,
140
- folderIcon: css`
141
- width: 56px;
142
- height: 56px;
143
- color: #f9935e;
144
- `,
145
- imagesFolderIcon: css`
146
- width: 56px;
147
- height: 56px;
148
- color: ${colors.imagesFolder};
149
- `,
150
- imagesFolderWrapper: css`
151
- position: relative;
152
- `,
153
- lockIcon: css`
154
- position: absolute;
155
- bottom: 4px;
156
- right: 4px;
157
- width: 16px;
158
- height: 16px;
159
- color: ${colors.imagesFolder};
160
- background: white;
161
- border-radius: 50%;
162
- padding: 2px;
163
- `,
164
- parentIcon: css`
165
- width: 56px;
166
- height: 56px;
167
- color: ${colors.textMuted};
168
- `,
169
- fileIcon: css`
170
- width: 40px;
171
- height: 40px;
172
- color: ${colors.textMuted};
173
- `,
174
- image: css`
175
- max-width: 100%;
176
- max-height: 100%;
177
- object-fit: contain;
178
- border-radius: 4px;
179
- `,
180
- noThumbnail: css`
181
- display: flex;
182
- flex-direction: column;
183
- align-items: center;
184
- justify-content: center;
185
- gap: 8px;
186
- padding: 16px;
187
- background: ${colors.background};
188
- border: 2px dashed ${colors.border};
189
- border-radius: 8px;
190
- cursor: pointer;
191
- transition: all 0.15s ease;
192
- width: 80%;
193
- height: 60%;
194
-
195
- &:hover {
196
- border-color: ${colors.primary};
197
- background: ${colors.surfaceHover};
198
- }
199
- `,
200
- noThumbnailIcon: css`
201
- width: 32px;
202
- height: 32px;
203
- color: ${colors.textMuted};
204
- `,
205
- noThumbnailText: css`
206
- font-size: ${fontSize.xs};
207
- color: ${colors.textMuted};
208
- text-align: center;
209
- `,
210
- label: css`
211
- padding: 10px 12px;
212
- background-color: ${colors.surface};
213
- border-top: 1px solid ${colors.borderLight};
214
- `,
215
- labelRow: css`
216
- display: flex;
217
- flex-direction: column;
218
- gap: 2px;
219
- `,
220
- labelText: css`
221
- flex: 1;
222
- min-width: 0;
223
- `,
224
- copyBtn: css`
225
- position: absolute;
226
- top: 4px;
227
- right: 4px;
228
- z-index: 10;
229
- height: 28px;
230
- width: 28px;
231
- color: ${colors.textMuted};
232
- background: transparent;
233
- border: none;
234
- padding: 0;
235
- cursor: pointer;
236
- border-radius: 4px;
237
- transition: all 0.15s ease;
238
- display: flex;
239
- align-items: center;
240
- justify-content: center;
241
-
242
- &:hover {
243
- color: ${colors.text};
244
- }
245
- `,
246
- copyIcon: css`
247
- width: 18px;
248
- height: 18px;
249
- `,
250
- statusBtn: css`
251
- position: absolute;
252
- top: 4px;
253
- right: 28px;
254
- z-index: 10;
255
- height: 28px;
256
- width: 28px;
257
- background: transparent;
258
- border: none;
259
- padding: 0;
260
- display: flex;
261
- align-items: center;
262
- justify-content: center;
263
- `,
264
- cloudIcon: css`
265
- width: 20px;
266
- height: 20px;
267
- color: #f59e0b;
268
- transform: translateY(1px);
269
- `,
270
- folderStats: css`
271
- display: flex;
272
- align-items: center;
273
- gap: 8px;
274
- margin: 2px 0 0 0;
275
- `,
276
- folderStat: css`
277
- display: flex;
278
- align-items: center;
279
- gap: 3px;
280
- font-size: ${fontSize.xs};
281
- `,
282
- folderStatLocal: css`
283
- color: ${colors.textMuted};
284
- `,
285
- folderStatCloud: css`
286
- color: #f59e0b;
287
- `,
288
- folderStatRemote: css`
289
- color: #ef4444;
290
- `,
291
- folderStatIcon: css`
292
- width: 14px;
293
- height: 14px;
294
- `,
295
- folderStatIconCloud: css`
296
- width: 14px;
297
- height: 14px;
298
- color: #f59e0b;
299
- `,
300
- folderStatIconLocal: css`
301
- width: 14px;
302
- height: 14px;
303
- color: ${colors.textSecondary};
304
- `,
305
- folderStatIconRemote: css`
306
- width: 14px;
307
- height: 14px;
308
- color: #ef4444;
309
- `,
310
- storedLabel: css`
311
- display: flex;
312
- align-items: center;
313
- margin: 2px 0 0 0;
314
- `,
315
- storedIconCloud: css`
316
- width: 16px;
317
- height: 16px;
318
- color: #f59e0b;
319
- `,
320
- storedIconRemote: css`
321
- width: 16px;
322
- height: 16px;
323
- color: #ef4444;
324
- `,
325
- globeIcon: css`
326
- width: 18px;
327
- height: 18px;
328
- color: #ef4444;
329
- `,
330
- tooltip: css`
331
- position: absolute;
332
- top: 50%;
333
- right: 100%;
334
- transform: translateY(-50%);
335
- background: #1a1f36;
336
- color: white;
337
- padding: 4px 8px;
338
- border-radius: 4px;
339
- font-size: 12px;
340
- white-space: nowrap;
341
- margin-right: 6px;
342
- pointer-events: none;
343
- z-index: 100;
344
-
345
- &::before {
346
- content: '';
347
- position: absolute;
348
- left: 100%;
349
- top: 50%;
350
- transform: translateY(-50%);
351
- border: 4px solid transparent;
352
- border-left-color: #1a1f36;
353
- }
354
- `,
355
- openBtn: css`
356
- position: absolute;
357
- bottom: 8px;
358
- right: 8px;
359
- z-index: 10;
360
- height: 28px;
361
- font-size: ${fontSize.sm};
362
- font-weight: 500;
363
- color: ${colors.primary};
364
- background: ${colors.surface};
365
- border: 1px solid ${colors.border};
366
- padding: 0 8px;
367
- cursor: pointer;
368
- border-radius: 4px;
369
- transition: all 0.15s ease;
370
- display: flex;
371
- align-items: center;
372
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
373
-
374
- &:hover {
375
- background-color: ${colors.primaryLight};
376
- border-color: ${colors.primary};
377
- }
378
- `,
379
- name: css`
380
- font-size: ${fontSize.sm};
381
- font-weight: 500;
382
- color: ${colors.text};
383
- white-space: nowrap;
384
- overflow: hidden;
385
- text-overflow: ellipsis;
386
- margin: 0;
387
- letter-spacing: -0.01em;
388
- direction: rtl;
389
- text-align: left;
390
- `,
391
- size: css`
392
- font-size: ${fontSize.xs};
393
- color: ${colors.textMuted};
394
- margin: 2px 0 0 0;
395
- `,
396
- selectAllRow: css`
397
- display: flex;
398
- align-items: center;
399
- margin-bottom: 16px;
400
- padding: 12px 16px;
401
- background: ${colors.surface};
402
- border-radius: 8px;
403
- border: 1px solid ${colors.border};
404
- `,
405
- selectAllLabel: css`
406
- display: flex;
407
- align-items: center;
408
- gap: 10px;
409
- font-size: ${fontSize.base};
410
- font-weight: 500;
411
- color: ${colors.textSecondary};
412
- cursor: pointer;
413
-
414
- &:hover {
415
- color: ${colors.text};
416
- }
417
- `,
418
- selectAllCheckbox: css`
419
- width: 16px;
420
- height: 16px;
421
- accent-color: ${colors.primary};
422
- `,
423
- }
424
-
425
- export function StudioFileGrid() {
426
- const {
427
- loading,
428
- sortedItems,
429
- metaEmpty,
430
- isAtRoot,
431
- isSearching,
432
- allItemsSelected,
433
- someItemsSelected,
434
- selectedItems,
435
- navigateUp,
436
- handleItemClick,
437
- handleOpen,
438
- handleGenerateThumbnail,
439
- handleSelectAll,
440
- triggerScan,
441
- } = useFileList()
442
-
443
- if (loading) {
444
- return (
445
- <div css={styles.loading}>
446
- <div css={styles.spinner} />
447
- </div>
448
- )
449
- }
450
-
451
- // Show scan prompt when meta is empty
452
- if (metaEmpty && isAtRoot) {
453
- return (
454
- <div css={styles.empty}>
455
- <svg css={styles.emptyIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
456
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
457
- </svg>
458
- <p css={styles.emptyText}>No files tracked yet</p>
459
- <p css={styles.emptyText}>Click Scan to discover files in your public folder</p>
460
- <button
461
- css={styles.scanButton}
462
- onClick={triggerScan}
463
- >
464
- Scan for Files
465
- </button>
466
- </div>
467
- )
468
- }
469
-
470
- if (sortedItems.length === 0 && isAtRoot) {
471
- return (
472
- <div css={styles.empty}>
473
- <svg css={styles.emptyIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
474
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
475
- </svg>
476
- <p css={styles.emptyText}>No files in this folder</p>
477
- <p css={styles.emptyText}>Upload images or click Scan in the toolbar</p>
478
- </div>
479
- )
480
- }
481
-
482
- return (
483
- <div>
484
- {sortedItems.length > 0 && (
485
- <div css={styles.selectAllRow}>
486
- <label css={styles.selectAllLabel}>
487
- <input
488
- type="checkbox"
489
- css={styles.selectAllCheckbox}
490
- checked={allItemsSelected}
491
- ref={(el) => {
492
- if (el) el.indeterminate = someItemsSelected && !allItemsSelected
493
- }}
494
- onChange={handleSelectAll}
495
- />
496
- Select all ({sortedItems.length})
497
- </label>
498
- </div>
499
- )}
500
- <div css={styles.grid}>
501
- {/* Parent folder navigation - hide when searching */}
502
- {!isAtRoot && !isSearching && (
503
- <div
504
- css={[styles.item, styles.parentItem]}
505
- onClick={navigateUp}
506
- >
507
- <div css={styles.content}>
508
- <svg css={styles.parentIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
509
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
510
- </svg>
511
- </div>
512
- <div css={styles.label}>
513
- <p css={styles.name}>..</p>
514
- <p css={styles.size}>Parent folder</p>
515
- </div>
516
- </div>
517
- )}
518
-
519
- {sortedItems.map((item) => (
520
- <GridItem
521
- key={item.path}
522
- item={item}
523
- isSelected={selectedItems.has(item.path)}
524
- onClick={(e) => handleItemClick(item, e)}
525
- onOpen={() => handleOpen(item)}
526
- onGenerateThumbnail={() => handleGenerateThumbnail(item)}
527
- />
528
- ))}
529
- </div>
530
- </div>
531
- )
532
- }
533
-
534
- interface GridItemProps {
535
- item: FileItem
536
- isSelected: boolean
537
- onClick: (e: React.MouseEvent) => void
538
- onOpen: () => void
539
- onGenerateThumbnail: () => void
540
- }
541
-
542
- function GridItem({ item, isSelected, onClick, onOpen, onGenerateThumbnail }: GridItemProps) {
543
- const [showCopied, setShowCopied] = useState(false)
544
- const isFolder = item.type === 'folder'
545
- const isImage = !isFolder && item.thumbnail !== undefined
546
- const isProtected = item.isProtected || (isFolder && item.name === 'images' && item.path === 'public/images')
547
-
548
- const handleCopyPath = (e: React.MouseEvent) => {
549
- e.stopPropagation()
550
- const pathToCopy = '/' + item.path.replace(/^public\//, '')
551
- navigator.clipboard.writeText(pathToCopy)
552
- setShowCopied(true)
553
- setTimeout(() => setShowCopied(false), 1500)
554
- }
555
-
556
- const handleClick = (e: React.MouseEvent) => {
557
- // Protected items cannot be selected, only opened
558
- if (isProtected) {
559
- e.stopPropagation()
560
- onOpen()
561
- return
562
- }
563
- onClick(e)
564
- }
565
-
566
- return (
567
- <div
568
- css={[styles.item, isSelected && !isProtected && styles.itemSelected]}
569
- onClick={handleClick}
570
- >
571
- {!isProtected && (
572
- <div
573
- css={styles.checkboxWrapper}
574
- onClick={(e) => e.stopPropagation()}
575
- >
576
- <input
577
- type="checkbox"
578
- css={styles.checkbox}
579
- checked={isSelected}
580
- onChange={() => onClick({} as React.MouseEvent)}
581
- />
582
- </div>
583
- )}
584
-
585
-
586
- <div css={styles.content}>
587
- {/* Copy button - top right of image box */}
588
- <button
589
- css={styles.copyBtn}
590
- onClick={handleCopyPath}
591
- title="Copy file path"
592
- >
593
- {showCopied && <span css={styles.tooltip}>Copied!</span>}
594
- <svg css={styles.copyIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
595
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
596
- </svg>
597
- </button>
598
-
599
- {/* Open button - bottom right of image box */}
600
- <button
601
- css={styles.openBtn}
602
- onClick={(e) => {
603
- e.stopPropagation()
604
- onOpen()
605
- }}
606
- >
607
- Open
608
- </button>
609
-
610
- {isFolder ? (
611
- isProtected ? (
612
- <div css={styles.imagesFolderWrapper}>
613
- <svg css={styles.imagesFolderIcon} fill="currentColor" viewBox="0 0 24 24">
614
- <path d="M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" />
615
- </svg>
616
- <svg css={styles.lockIcon} fill="currentColor" viewBox="0 0 20 20">
617
- <path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
618
- </svg>
619
- </div>
620
- ) : (
621
- <svg css={styles.folderIcon} fill="currentColor" viewBox="0 0 24 24">
622
- <path d="M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" />
623
- </svg>
624
- )
625
- ) : isImage && item.thumbnail ? (
626
- <img
627
- css={styles.image}
628
- src={item.thumbnail}
629
- alt={item.name}
630
- loading="lazy"
631
- />
632
- ) : (
633
- <svg css={styles.fileIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
634
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
635
- </svg>
636
- )}
637
- </div>
638
-
639
- <div css={styles.label}>
640
- <div css={styles.labelRow}>
641
- <div css={styles.labelText}>
642
- <p css={styles.name} title={item.name}>{item.name}</p>
643
- {isFolder ? (
644
- <div css={styles.folderStats}>
645
- {item.localCount !== undefined && item.localCount > 0 && (
646
- <span css={[styles.folderStat, styles.folderStatLocal]} title={`${item.localCount} local`}>
647
- <svg css={styles.folderStatIconLocal} fill="none" stroke="currentColor" viewBox="0 0 24 24">
648
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
649
- </svg>
650
- {item.localCount}
651
- </span>
652
- )}
653
- {item.cloudCount !== undefined && item.cloudCount > 0 && (
654
- <span css={[styles.folderStat, styles.folderStatCloud]} title={`${item.cloudCount} in cloud`}>
655
- <svg css={styles.folderStatIconCloud} fill="none" stroke="currentColor" viewBox="0 0 24 24">
656
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
657
- </svg>
658
- {item.cloudCount}
659
- </span>
660
- )}
661
- {item.remoteCount !== undefined && item.remoteCount > 0 && (
662
- <span css={[styles.folderStat, styles.folderStatRemote]} title={`${item.remoteCount} remote`}>
663
- <svg css={styles.folderStatIconRemote} fill="none" stroke="currentColor" viewBox="0 0 24 24">
664
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
665
- </svg>
666
- {item.remoteCount}
667
- </span>
668
- )}
669
- {!item.localCount && !item.cloudCount && !item.remoteCount && item.fileCount !== undefined && (
670
- <span css={styles.size}>{item.fileCount} files</span>
671
- )}
672
- </div>
673
- ) : item.cdnPushed ? (
674
- <p css={styles.storedLabel}>
675
- <svg css={item.isRemote ? styles.storedIconRemote : styles.storedIconCloud} fill="none" stroke="currentColor" viewBox="0 0 24 24">
676
- {item.isRemote ? (
677
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
678
- ) : (
679
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
680
- )}
681
- </svg>
682
- </p>
683
- ) : (
684
- item.size !== undefined && <p css={styles.size}>{formatFileSize(item.size)}</p>
685
- )}
686
- </div>
687
- </div>
688
- </div>
689
- </div>
690
- )
691
- }
692
-
693
- function formatFileSize(bytes: number): string {
694
- if (bytes < 1024) return `${bytes} B`
695
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
696
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
697
- }
698
-
699
- function getParentPath(path: string): string {
700
- const parts = path.split('/')
701
- parts.pop() // Remove current folder
702
- return parts.join('/') + '/'
703
- }
704
-