`. + - Change the hint text from `"drag files here or use upload"` to + `"drag files here or use --upload"` (with `--` prefix for terminal aesthetic). + - REMOVE the `` wrapper and the `` inside it. + - The component signature becomes `export function EmptyState()` with no arguments. + +5. Update the JSDoc to reflect the component no longer accepts props or contains an upload zone. + +In `apps/web/src/components/file-browser/FileBrowser.tsx`: + +1. Find the line (around line 358): ` ` +2. Change it to: ` ` + (Remove the folderId prop since EmptyState no longer accepts it.) + + + Run `npx tsc --noEmit` from `apps/web/` to confirm no TypeScript errors. + Grep EmptyState.tsx for "UploadZone" -- should return zero matches. + Grep EmptyState.tsx for "folderId" -- should return zero matches. + Grep EmptyState.tsx for "ls -la" -- should return one match. + Grep FileBrowser.tsx for "folderId={currentFolderId}" near EmptyState -- should return zero matches. + ++1. `npx tsc --noEmit` in apps/web passes (no type errors from prop removal) +2. No references to UploadZone remain in EmptyState.tsx +3. No references to folderId remain in EmptyState.tsx +4. The terminal art contains box-drawing characters and "ls -la" +5. CSS color tokens --color-text-muted and --color-text-dim exist in index.css +6. .empty-state-upload CSS class is removed from file-browser.css +7. Visual check (Playwright if available): empty state shows terminal box, green art, + muted label, dim hint text + + ++ +- EmptyState component renders a terminal-window with box-drawing chars (no old box ASCII) +- Terminal shows: $ ls -la, total 0, $ cursor-block +- No UploadZone inside EmptyState (toolbar handles uploads) +- EmptyState takes no props; FileBrowser renders ` + + diff --git a/.planning/quick/002-fix-empty-state-ascii-art/002-SUMMARY.md b/.planning/quick/002-fix-empty-state-ascii-art/002-SUMMARY.md new file mode 100644 index 0000000000..197fc49e49 --- /dev/null +++ b/.planning/quick/002-fix-empty-state-ascii-art/002-SUMMARY.md @@ -0,0 +1,100 @@ +--- +phase: quick-002 +plan: 01 +subsystem: ui +tags: [css, react, ascii-art, terminal-aesthetic, empty-state] + +# Dependency graph +requires: + - phase: 06.3-ui-structure-refactor + provides: EmptyState component and file-browser CSS +provides: + - Terminal-window ASCII art empty state with box-drawing characters + - Design-accurate color tokens (--color-text-muted, --color-text-dim) +affects: [] + +# Tech tracking +tech-stack: + added: [] + patterns: + - 'Box-drawing characters for terminal UI elements' + +key-files: + created: [] + modified: + - apps/web/src/components/file-browser/EmptyState.tsx + - apps/web/src/components/file-browser/FileBrowser.tsx + - apps/web/src/styles/file-browser.css + - apps/web/src/index.css + +key-decisions: + - 'Removed UploadZone from EmptyState entirely (toolbar provides upload)' + - 'Added --color-text-muted and --color-text-dim CSS tokens for design accuracy' + +patterns-established: + - 'Box-drawing chars (U+250C/2500/2510/2502/2514/2518) for terminal window UI' + +# Metrics +duration: 2min +completed: 2026-02-07 +--- + +# Quick Task 002: Fix Empty State ASCII Art Summary + +Terminal-window ASCII art with box-drawing characters replacing basic folder icon, UploadZone removed from EmptyState. + +## Performance + +- **Duration:** 2 min +- **Started:** 2026-02-07T00:02:44Z +- **Completed:** 2026-02-07T00:04:43Z +- **Tasks:** 2 +- **Files modified:** 4 + +## Accomplishments + +- Replaced basic folder ASCII art with polished terminal-window using box-drawing characters +- Terminal shows `$ ls -la`, `total 0`, and `$ cursor-block` for authentic empty directory feel +- Removed redundant UploadZone from EmptyState (toolbar already handles uploads) +- Added design-accurate color tokens: ASCII art green (#00D084), label muted (#8b9a8f), hint dim (#4a5a4e) + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Add CSS color tokens and update empty state styles** - `99bc84d` (style) +2. **Task 2: Replace ASCII art and remove UploadZone from EmptyState** - `ff97f12` (feat) + +## Files Created/Modified + +- `apps/web/src/index.css` - Added --color-text-muted and --color-text-dim design tokens +- `apps/web/src/styles/file-browser.css` - Updated empty state colors, removed .empty-state-upload class +- `apps/web/src/components/file-browser/EmptyState.tsx` - Rewritten with terminal-window box-drawing art, no props, no UploadZone +- `apps/web/src/components/file-browser/FileBrowser.tsx` - Removed folderId prop from EmptyState render + +## Decisions Made + +- Removed UploadZone from EmptyState entirely since the toolbar already provides upload functionality +- Added two new CSS custom properties (--color-text-muted, --color-text-dim) rather than using inline colors + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered + +None + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +- Empty state now matches the "EmptyState - Improved" design from the Pencil file +- Closes cosmetic UAT gap from Phase 6.3 + +--- + +_Quick Task: 002-fix-empty-state-ascii-art_ +_Completed: 2026-02-07_ diff --git a/.planning/todos/pending/2026-01-22-atomic-file-upload-flow.md b/.planning/todos/pending/2026-01-22-atomic-file-upload-flow.md index 6485f70591..dd1de52849 100644 --- a/.planning/todos/pending/2026-01-22-atomic-file-upload-flow.md +++ b/.planning/todos/pending/2026-01-22-atomic-file-upload-flow.md @@ -62,7 +62,7 @@ Optimize to a single atomic backend call that handles the entire upload flow: IPNS publish is external to the DB transaction — once published, cannot rollback: -``` +```text [DB Transaction] [External Call] ┌─────────────────────┐ ┌─────────────────┐ │ 1. Pin to IPFS │ │ │ @@ -83,26 +83,26 @@ IPNS publish is external to the DB transaction — once published, cannot rollba ### Options Evaluated -**Option A: Accept Partial State + Client Retry** +#### Option A: Accept Partial State + Client Retry - Pin → Record → Commit → Try IPNS - On IPNS failure: return `{ cid, ipnsError: true }`, client retries IPNS - Pros: Simple, file safely stored, client already has IPNS capability - Cons: Client retry logic, brief invisible window -**Option B: Optimistic IPNS (Publish First)** +#### Option B: Optimistic IPNS (Publish First) - Pin → Publish IPNS → Record → Commit - Pros: File always visible if exists - Cons: Worse failure mode (IPNS references non-existent file) -**Option C: Two-Phase with Pending State** +#### Option C: Two-Phase with Pending State - Record with `status: pending`, publish IPNS, then `status: confirmed` - Pros: Clear visibility, background retry - Cons: Complex state model, UI changes needed -**Option D: Idempotent Retry with Deduplication** +#### Option D: Idempotent Retry with Deduplication - Accept `{ expectedCid, idempotencyKey }`, skip completed steps on retry - Pros: True atomic from client view, safe retries @@ -140,6 +140,15 @@ IPNS publish is external to the DB transaction — once published, cannot rollba - Exact chunking algorithm match with Pinata for large files? - Should this be a new phase or quick task? +### CodeRabbit Review Notes (2026-02-07, PR #55) + +CodeRabbit independently identified the same per-file IPNS publish bottleneck: + +- **Call chain per file:** `addFile()` → `addFileToFolder()` → `updateFolderMetadata()` → `createAndPublishIpnsRecord()` — each triggering a separate IPNS publish (~2s each) +- **Suggested approach:** Create `addFilesToFolder` / `updateFolderMetadataBatch` function so `createAndPublishIpnsRecord` is invoked once after all files are added +- **Keep `addFile` for single-file use**, batch path calls `updateFolderMetadata` once at the end +- **Key files confirmed:** `EmptyState.tsx` (handleDrop loop), `useFileUpload.ts`, `upload.service.ts`, `folder.service.ts` (`addFileToFolder`, `updateFolderMetadata`) + --- -_Decision pending — return to this after Phase 7_ + diff --git a/.planning/todos/pending/2026-02-07-web-worker-large-file-encryption.md b/.planning/todos/pending/2026-02-07-web-worker-large-file-encryption.md new file mode 100644 index 0000000000..e14630e058 --- /dev/null +++ b/.planning/todos/pending/2026-02-07-web-worker-large-file-encryption.md @@ -0,0 +1,30 @@ +--- +created: 2026-02-07T12:00 +title: Offload large file encryption to Web Worker +area: ui +files: + - apps/web/src/services/file-crypto.service.ts + - apps/web/src/services/upload.service.ts +--- + +## Problem + +`encryptFile()` in `file-crypto.service.ts` calls `file.arrayBuffer()` and `encryptAesGcm()` on the main thread. For large files (approaching the 100MB limit), this blocks the UI thread and can cause the browser to appear frozen during encryption. + +Identified by CodeRabbit review on PR #55. + +## Solution + +Offload encryption to a Web Worker for files >= 10MB: + +1. Create a dedicated encryption worker (`encrypt.worker.ts`) +2. Worker receives `File`/`Blob`, generates file key + IV, runs `encryptAesGcm`, returns ciphertext + wrapped key +3. Main thread stays responsive, upload progress UI remains smooth +4. Files < 10MB continue using main thread (worker overhead not worth it for small files) + +**Considerations:** + +- Web Crypto API is available in Worker contexts +- Need to handle `Transferable` objects for zero-copy ArrayBuffer passing +- Worker bundling with Vite uses `new Worker(new URL(...), { type: 'module' })` pattern +- Error handling: worker crashes should fall back to main thread encryption diff --git a/apps/web/src/components/file-browser/EmptyState.tsx b/apps/web/src/components/file-browser/EmptyState.tsx index 96752549dd..a7146d464f 100644 --- a/apps/web/src/components/file-browser/EmptyState.tsx +++ b/apps/web/src/components/file-browser/EmptyState.tsx @@ -1,54 +1,120 @@ -import { UploadZone } from './UploadZone'; +import { useCallback, useState } from 'react'; +import { useDropzone, FileRejection } from 'react-dropzone'; +import { useFileUpload } from '../../hooks/useFileUpload'; +import { useFolder } from '../../hooks/useFolder'; -type EmptyStateProps = { - /** Folder ID for upload destination */ - folderId: string; -}; +const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB per FILE-01 /** - * Terminal-style ASCII art folder icon for empty state. + * Terminal-style ASCII art for empty state using box-drawing characters. + * Shows a mini terminal window with `ls -la` returning empty results. */ -const asciiArt = ` - ___________ - / /| - / / | -|__________| | -| | / -|__________|/ -`; +const terminalArt = `\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 +\u2502 $ ls -la \u2502 +\u2502 total 0 \u2502 +\u2502 $ \u2588 \u2502 +\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`; + +type EmptyStateProps = { + folderId: string; +}; -/** - * Empty state component shown when a folder has no contents. - * - * Displays terminal-style ASCII art with drop zone for uploads. - * Acts as both visual indicator and functional upload target. - * - * @example - * ```tsx - * function FileBrowser() { - * const { currentFolder, currentFolderId } = useFolderNavigation(); - * - * if (currentFolder?.children.length === 0) { - * return` with no args +- Colors match design: ASCII art #00D084, label #8b9a8f, hint #4a5a4e +- TypeScript compilation succeeds + ; - * } - * - * return ; - * } - * ``` - */ export function EmptyState({ folderId }: EmptyStateProps) { + const { upload, canUpload, isUploading } = useFileUpload(); + const { addFile } = useFolder(); + const [error, setError] = useState (null); + + const handleDrop = useCallback( + async (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + setError(null); + + if (rejectedFiles.length > 0) { + const oversized = rejectedFiles.filter((r) => + r.errors.some((e) => e.code === 'file-too-large') + ); + if (oversized.length > 0) { + setError(`Files exceed 100MB limit: ${oversized.map((r) => r.file.name).join(', ')}`); + return; + } + setError('Some files were rejected'); + return; + } + + if (acceptedFiles.length === 0) return; + + const totalSize = acceptedFiles.reduce((sum, f) => sum + f.size, 0); + if (!canUpload(totalSize)) { + setError('Not enough storage space for these files'); + return; + } + + try { + const uploadedFiles = await upload(acceptedFiles); + const failedRegistrations: string[] = []; + for (const uploaded of uploadedFiles) { + try { + await addFile(folderId, { + cid: uploaded.cid, + wrappedKey: uploaded.wrappedKey, + iv: uploaded.iv, + originalName: uploaded.originalName, + originalSize: uploaded.originalSize, + }); + } catch { + failedRegistrations.push(uploaded.originalName ?? 'Unnamed file'); + } + } + if (failedRegistrations.length > 0) { + setError(`Some files uploaded but could not be added: ${failedRegistrations.join(', ')}`); + } + } catch (err) { + if ((err as Error).message !== 'Upload cancelled by user') { + setError((err as Error).message); + } + } + }, + [upload, canUpload, addFile, folderId] + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop: handleDrop, + noClick: false, + multiple: true, + maxSize: MAX_FILE_SIZE, + }); + + const classes = ['empty-state']; + if (isDragActive) classes.push('empty-state-drag-active'); + if (isUploading) classes.push('empty-state-uploading'); + return ( - ++); } diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 853a97e28a..38e699b585 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -24,6 +24,8 @@ --color-border-dim: var(--color-green-darker); --color-text-primary: var(--color-green-primary); --color-text-secondary: var(--color-green-dim); + --color-text-muted: #8b9a8f; /* Muted gray-green for labels */ + --color-text-dim: #4a5a4e; /* Dim gray-green for hints */ /* Typography */ --font-family-mono: "JetBrains Mono", monospace; diff --git a/apps/web/src/styles/file-browser.css b/apps/web/src/styles/file-browser.css index fcdcfa8e3c..298216dadc 100644 --- a/apps/web/src/styles/file-browser.css +++ b/apps/web/src/styles/file-browser.css @@ -386,11 +386,17 @@ transition: border-color 0.2s ease, background-color 0.2s ease; } -.empty-state:hover { +.empty-state:hover, +.empty-state-drag-active { border-color: var(--color-green-primary); background-color: var(--color-green-darker); } +.empty-state-uploading { + opacity: 0.7; + pointer-events: none; +} + .empty-state-content { text-align: center; padding: var(--spacing-lg); @@ -401,7 +407,7 @@ font-family: var(--font-family-mono); font-size: var(--font-size-xs); line-height: 1.2; - color: var(--color-text-secondary); + color: var(--color-green-primary); margin: 0 0 var(--spacing-md) 0; white-space: pre; } @@ -418,19 +424,15 @@ font-family: var(--font-family-mono); font-size: var(--font-size-sm); margin: 0 0 var(--spacing-xs) 0; - color: var(--color-text-primary); + color: var(--color-text-muted); } .empty-state-hint { font-family: var(--font-family-mono); font-size: var(--font-size-xs); margin: 0; - color: var(--color-text-secondary); + color: var(--color-text-dim); text-transform: lowercase; } -.empty-state-upload { - margin-top: var(--spacing-md); -} - /* Note: Mobile responsive styles are in responsive.css */ diff --git a/designs/cipher-box-design.pen b/designs/cipher-box-design.pen index 2bab096021..f44ea4af90 100644 --- a/designs/cipher-box-design.pen +++ b/designs/cipher-box-design.pen @@ -1,5 +1,5 @@ { - "version": "2.6", + "version": "2.7", "children": [ { "type": "frame", @@ -6310,11 +6310,11 @@ { "type": "frame", "id": "xK9My", - "x": 700, + "x": 709, "y": 2207, "name": "Hover States", - "width": 500, - "height": 400, + "width": 491, + "height": 462, "fill": "#0a0a0a", "layout": "vertical", "gap": 24, @@ -6608,30 +6608,94 @@ }, "padding": [ 10, - 0 + 16 ], "justifyContent": "space_between", "alignItems": "center", "children": [ { - "type": "text", - "id": "jX6oE", + "type": "frame", + "id": "KHYLS", "name": "rowNormalName", - "fill": "#00D084", - "content": "[DIR] documents/", - "fontFamily": "JetBrains Mono", - "fontSize": 11, - "fontWeight": "normal" + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lRG7s", + "name": "rowNormalIcon", + "fill": "#00D084", + "content": "[DIR]", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "6HQvb", + "name": "rowNormalNameText", + "fill": "#00D084", + "content": "documents", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] }, { - "type": "text", - "id": "mKY3U", - "name": "rowNormalMeta", - "fill": "#006644", - "content": "-- 30 Jan 2026", - "fontFamily": "JetBrains Mono", - "fontSize": 10, - "fontWeight": "normal" + "type": "frame", + "id": "EwqEJ", + "name": "rowNormalSize", + "width": 120, + "children": [ + { + "type": "text", + "id": "QgZZ5", + "name": "rowNormalSizeText", + "fill": "#006644", + "content": "--", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "7hVtG", + "name": "rowNormalType", + "width": 120, + "children": [ + { + "type": "text", + "id": "2qmb9", + "name": "rowNormalTypeText", + "fill": "#006644", + "content": "directory", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ytkzi", + "name": "rowNormalModified", + "width": 180, + "children": [ + { + "type": "text", + "id": "L8H03", + "name": "rowNormalModifiedText", + "fill": "#006644", + "content": "30 Jan 2026", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] } ] }, @@ -6650,30 +6714,94 @@ }, "padding": [ 10, - 0 + 16 ], "justifyContent": "space_between", "alignItems": "center", "children": [ { - "type": "text", - "id": "CjQb2", + "type": "frame", + "id": "woh7K", "name": "rowHoverName", - "fill": "#00D084", - "content": "[DIR] documents/ (hover)", - "fontFamily": "JetBrains Mono", - "fontSize": 11, - "fontWeight": "normal" + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kaD0M", + "name": "rowHoverIcon", + "fill": "#00D084", + "content": "[DIR]", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "jhg9F", + "name": "rowHoverNameText", + "fill": "#00D084", + "content": "documents", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] }, { - "type": "text", - "id": "4Qxkv", - "name": "rowHoverMeta", - "fill": "#006644", - "content": "-- 30 Jan 2026", - "fontFamily": "JetBrains Mono", - "fontSize": 10, - "fontWeight": "normal" + "type": "frame", + "id": "IJVWm", + "name": "rowHoverSize", + "width": 120, + "children": [ + { + "type": "text", + "id": "FLSs9", + "name": "rowHoverSizeText", + "fill": "#006644", + "content": "--", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "69QUJ", + "name": "rowHoverType", + "width": 120, + "children": [ + { + "type": "text", + "id": "amV36", + "name": "rowHoverTypeText", + "fill": "#006644", + "content": "directory", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "InGuk", + "name": "rowHoverModified", + "width": 180, + "children": [ + { + "type": "text", + "id": "GFAYF", + "name": "rowHoverModifiedText", + "fill": "#006644", + "content": "30 Jan 2026", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] } ] }, @@ -6686,43 +6814,172 @@ "stroke": { "align": "inside", "thickness": { - "bottom": 1, - "left": 2 + "bottom": 1 }, - "fill": "#00D084" + "fill": "#003322" }, "padding": [ 10, - 0 + 16 ], "justifyContent": "space_between", "alignItems": "center", "children": [ { - "type": "text", - "id": "qhELR", + "type": "frame", + "id": "iVMjW", "name": "rowSelectedName", - "fill": "#00D084", - "content": "[DIR] documents/ (selected)", - "fontFamily": "JetBrains Mono", - "fontSize": 11, - "fontWeight": "600" + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "c3nD3", + "name": "rowSelectedIcon", + "fill": "#00D084", + "content": "[DIR]", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "bJWKg", + "name": "rowSelectedNameText", + "fill": "#00D084", + "content": "documents", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "600" + } + ] }, { - "type": "text", - "id": "WrRRp", - "name": "rowSelectedMeta", - "fill": "#00D084", - "content": "-- 30 Jan 2026", - "fontFamily": "JetBrains Mono", - "fontSize": 10, - "fontWeight": "normal" + "type": "frame", + "id": "bXb3Y", + "name": "rowSelectedSize", + "width": 120, + "children": [ + { + "type": "text", + "id": "TsuVh", + "name": "rowSelectedSizeText", + "fill": "#00D084", + "content": "--", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FU1cL", + "name": "rowSelectedType", + "width": 120, + "children": [ + { + "type": "text", + "id": "T44b5", + "name": "rowSelectedTypeText", + "fill": "#00D084", + "content": "directory", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "8Xksy", + "name": "rowSelectedModified", + "width": 180, + "children": [ + { + "type": "text", + "id": "zwdrl", + "name": "rowSelectedModifiedText", + "fill": "#00D084", + "content": "30 Jan 2026", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] } ] } ] } ] + }, + { + "type": "frame", + "id": "XyFqv", + "x": 0, + "y": 2769, + "name": "EmptyState - Improved", + "width": 600, + "height": 400, + "fill": "#000000", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#00D084" + }, + "layout": "vertical", + "padding": 40, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CXeTR", + "name": "content", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Fswdw", + "name": "asciiArt", + "fill": "#00D084", + "content": " ┌──────────────────────┐\n │ $ ls -la │\n │ │\n │ total 0 │\n │ │\n │ $ █ │\n └──────────────────────┘", + "lineHeight": 1.3, + "textAlign": "center", + "fontFamily": "Fira Code", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vuc2z", + "name": "emptyText", + "fill": "#8b9a8f", + "content": "// EMPTY DIRECTORY", + "textAlign": "center", + "fontFamily": "Fira Code", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "AGB2H", + "name": "hintText", + "fill": "#4a5a4e", + "content": "drag files here or use --upload", + "textAlign": "center", + "fontFamily": "Fira Code", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] } ] } \ No newline at end of file-+ {error && ( +// EMPTY DIRECTORY
-drag files here or use upload
--+- + {isUploading ? '// UPLOADING...' : isDragActive ? '// DROP FILES' : '// EMPTY DIRECTORY'} +
+drag files here or use --upload
+ {error} + ++ )}