Jaimodiji commited on
Commit
9575119
·
verified ·
1 Parent(s): bf92db0

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. docs/README.md +203 -0
  2. public/color_rm.html +108 -82
docs/README.md ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ColorRM Pro
2
+
3
+ A real-time collaborative PDF annotation and drawing application built with modern web technologies.
4
+
5
+ ## Architecture Overview
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────┐
9
+ │ Frontend │
10
+ │ ┌──────────────────────────────────────────────────────────┐ │
11
+ │ │ color_rm.html │ │
12
+ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
13
+ │ │ │ ColorRm │ │ ColorRm │ │ ColorRm │ │ │
14
+ │ │ │ Session.js │──│ Renderer.js │──│ LiveSync.js │ │ │
15
+ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
16
+ │ └──────────────────────────────────────────────────────────┘ │
17
+ └─────────────────────────────────────────────────────────────────┘
18
+
19
+
20
+ ┌─────────────────────────────────────────────────────────────────┐
21
+ │ Backend │
22
+ │ ┌──────────────────────┐ ┌─────────────────────────────┐ │
23
+ │ │ Cloudflare Workers │ │ HuggingFace Spaces │ │
24
+ │ │ ┌────────────────┐ │ │ ┌───────────────────────┐ │ │
25
+ │ │ │ worker.ts │ │ │ │ Vite Dev Server │ │ │
26
+ │ │ │ - API Routes │ │ │ │ - Static files │ │ │
27
+ │ │ │ - R2 Storage │ │ │ │ - PDF Convert (7861) │ │ │
28
+ │ │ └────────────────┘ │ │ └───────────────────────┘ │ │
29
+ │ │ ┌────────────────┐ │ └─────────────────────────────┘ │
30
+ │ │ │ Durable Objects│ │ │
31
+ │ │ │ - ColorRm DO │ │ ┌─────────────────────────────┐ │
32
+ │ │ └────────────────┘ │ │ Liveblocks │ │
33
+ │ └──────────────────────┘ │ - Real-time sync │ │
34
+ │ │ - Presence │ │
35
+ │ │ - Room management │ │
36
+ │ └─────────────────────────────┘ │
37
+ └─────────────────────────────────────────────────────────────────┘
38
+ ```
39
+
40
+ ## Key Modules
41
+
42
+ ### Frontend (`public/scripts/modules/`)
43
+
44
+ | Module | Description |
45
+ |--------|-------------|
46
+ | `ColorRmSession.js` | Main application controller. Handles state, tools, page management, import/export |
47
+ | `ColorRmRenderer.js` | Canvas rendering engine. Draws strokes, shapes, images, handles transformations |
48
+ | `ColorRmStorage.js` | IndexedDB and R2 storage. Manages local persistence and cloud sync |
49
+ | `ColorRmLiveSync.js` | Liveblocks integration. Real-time collaboration, cursors, history sync |
50
+ | `ColorRmSvgImporter.js` | SVG parsing and import. Converts SVG elements to ColorRM history items |
51
+ | `ColorRmSvgExporter.js` | SVG export. Renders history to vector SVG format |
52
+
53
+ ### Backend (`worker/`)
54
+
55
+ | File | Description |
56
+ |------|-------------|
57
+ | `worker.ts` | Main Cloudflare Worker entry point. Routes API requests |
58
+ | `colorRmAssets.ts` | R2 storage handlers for pages, history, modifications |
59
+ | `pdfToSvg.ts` | PDF conversion job management (placeholder for CF Workers) |
60
+ | `ColorRmDurableObject.ts` | Durable Object for room state management |
61
+
62
+ ### Commands (`cmd/`)
63
+
64
+ | Script | Description |
65
+ |--------|-------------|
66
+ | `pdf_convert_server.mjs` | Local PDF to SVG server using pdf2svg binary |
67
+ | `hf_init.mjs` | HuggingFace authentication initialization |
68
+ | `hf_backup.mjs` | Periodic backup to HuggingFace Hub |
69
+ | `hf_restore.mjs` | Restore from HuggingFace Hub on startup |
70
+
71
+ ## Data Flow
72
+
73
+ ### SVG Import Flow
74
+
75
+ ```
76
+ 1. User selects SVG file
77
+
78
+ 2. ColorRmSvgImporter.importSvg() parses SVG
79
+
80
+ 3. Elements are classified:
81
+ ├── Images/Text → Rasterized to background blob
82
+ └── Strokes/Shapes → Kept as vector history items
83
+
84
+ 4. Page object created with blob + history
85
+
86
+ 5. Sync to cloud:
87
+ ├── Blob → R2 via _uploadPageBlob()
88
+ ├── History → R2 via /api/color_rm/history/:sessionId/:pageId
89
+ └── Structure → Liveblocks via _syncPageStructureToLive()
90
+
91
+ 6. Other users receive notification via Liveblocks
92
+
93
+ 7. Other users fetch page blob from R2
94
+ ```
95
+
96
+ ### History Sync (Delta Architecture)
97
+
98
+ For pages with base history (SVG imports):
99
+
100
+ ```
101
+ ┌─────────────────────────────────────────────────────────────┐
102
+ │ R2 Storage (Base) │
103
+ │ /api/color_rm/history/:sessionId/:pageId │
104
+ │ - Original SVG items (large, stored once) │
105
+ └─────────────────────────────────────────────────────────────┘
106
+ +
107
+ ┌─────────────────────────────────────────────────────────────┐
108
+ │ Liveblocks (Deltas Only) │
109
+ │ - New strokes added by users │
110
+ │ - Modifications to base items (position, color, etc.) │
111
+ └─────────────────────────────────────────────────────────────┘
112
+ =
113
+ ┌─────────────────────────────────────────────────────────────┐
114
+ │ Final Rendered Page │
115
+ │ BaseHistory + Deltas + Modifications │
116
+ └─────────────────────────────────────────────────────────────┘
117
+ ```
118
+
119
+ ### Thresholds
120
+
121
+ | Condition | Threshold | Action |
122
+ |-----------|-----------|--------|
123
+ | SVG page history | > 400 items | Use R2 for base, Liveblocks for deltas |
124
+ | Regular page history | > 2000 items | Convert to hybrid R2/Liveblocks |
125
+ | Modifications | > 100 items | Store in R2, sync metadata via Liveblocks |
126
+
127
+ ## API Endpoints
128
+
129
+ ### Page Management
130
+
131
+ | Method | Endpoint | Description |
132
+ |--------|----------|-------------|
133
+ | POST | `/api/color_rm/page/:sessionId/:pageId` | Upload page blob |
134
+ | GET | `/api/color_rm/page/:sessionId/:pageId` | Download page blob |
135
+ | DELETE | `/api/color_rm/page/:sessionId/:pageId` | Delete page |
136
+
137
+ ### History
138
+
139
+ | Method | Endpoint | Description |
140
+ |--------|----------|-------------|
141
+ | POST | `/api/color_rm/history/:sessionId/:pageId` | Upload base history |
142
+ | GET | `/api/color_rm/history/:sessionId/:pageId` | Get base history |
143
+
144
+ ### Page Structure
145
+
146
+ | Method | Endpoint | Description |
147
+ |--------|----------|-------------|
148
+ | GET | `/api/color_rm/page_structure/:sessionId` | Get page order |
149
+ | POST | `/api/color_rm/page_structure/:sessionId` | Update page order |
150
+
151
+ ### PDF Conversion (Local Server)
152
+
153
+ | Method | Endpoint | Description |
154
+ |--------|----------|-------------|
155
+ | POST | `/convert/pdf` | Upload PDF for conversion |
156
+ | GET | `/convert/status/:jobId` | Check conversion status |
157
+ | GET | `/convert/page/:jobId/:pageNum` | Download converted SVG page |
158
+
159
+ ## Development
160
+
161
+ ### Local Development
162
+
163
+ ```bash
164
+ npm install
165
+ npm run dev
166
+ ```
167
+
168
+ ### HuggingFace Spaces Deployment
169
+
170
+ The Dockerfile runs:
171
+ 1. `hf_init.mjs` - Authenticate with HF
172
+ 2. `hf_restore.mjs` - Restore previous state
173
+ 3. `npm run dev` - Vite dev server (port 7860)
174
+ 4. `hf_backup.mjs` - Periodic backup worker
175
+ 5. `pdf_convert_server.mjs` - PDF conversion (port 7861)
176
+
177
+ ### Environment Variables
178
+
179
+ | Variable | Description |
180
+ |----------|-------------|
181
+ | `HF_TOKEN` | HuggingFace API token |
182
+ | `LIVEBLOCKS_SECRET_KEY` | Liveblocks API key |
183
+ | `PORT` | Dev server port (default: 7860) |
184
+
185
+ ## Tools
186
+
187
+ | Tool | Shortcut | Description |
188
+ |------|----------|-------------|
189
+ | Move | V | Select and move objects |
190
+ | Hand | H | Pan the canvas |
191
+ | Lasso | L | Free-form selection |
192
+ | Pen | P | Freehand drawing |
193
+ | Shape | S | Draw shapes (rect, ellipse, arrow, etc.) |
194
+ | Text | T | Add text annotations |
195
+ | Eraser | E | Erase strokes |
196
+ | Box/Capture | B | Capture region to clipboard |
197
+
198
+ ## Collaboration Features
199
+
200
+ - **Real-time cursors**: See other users' cursors
201
+ - **Live history sync**: Strokes appear instantly for all users
202
+ - **Page structure sync**: Page order synchronized across users
203
+ - **Presence indicators**: See who's online in the room
public/color_rm.html CHANGED
@@ -38,24 +38,24 @@
38
  -webkit-font-smoothing: antialiased;
39
  }
40
 
41
- /* UI Components - Touch Optimized */
42
  .btn {
43
- background: var(--bg-surface); border: 1px solid var(--border); color: var(--text-main);
44
- padding: 10px 14px; border-radius: 6px; font-size: 0.875rem; font-weight: 500;
45
- cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: 0.15s;
46
- min-height: 44px; /* Touch target minimum */
47
  -webkit-tap-highlight-color: transparent;
48
  }
49
- .btn:hover { background: #1a1a1a; border-color: #555; }
50
- .btn:active { transform: scale(0.97); background: #222; }
51
- .btn.active { background: var(--primary); border-color: var(--primary); color: #000; }
52
  .btn:disabled { opacity: 0.4; cursor: not-allowed; }
53
- .btn-primary { background: var(--primary); border-color: var(--primary); color: #000; }
54
- .btn-primary:hover { background: #e0e0e0; }
55
- .btn-primary:active { background: #ccc; transform: scale(0.97); }
56
- .btn-sm { padding: 8px 12px; font-size: 0.8rem; min-height: 36px; }
57
- .btn-icon { width: 44px; height: 44px; justify-content: center; padding: 0; }
58
-
59
  /* Sidebar Tabs - Touch Optimized */
60
  .sb-tabs { display: flex; border-bottom: 1px solid var(--border); background: var(--bg-panel); flex-shrink: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; -ms-overflow-style: none; }
61
  .sb-tabs::-webkit-scrollbar { display: none; }
@@ -140,7 +140,7 @@
140
  .shape-btn:active { transform: scale(0.92); background: #111; }
141
  .shape-btn.active { background: #fff; color: #000; border-color: #fff; }
142
 
143
- header { height: 56px; background: var(--bg-panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; z-index: 50; flex-shrink: 0; }
144
 
145
  .workspace { display: flex; flex: 1; overflow: hidden; position: relative; }
146
 
@@ -210,10 +210,10 @@
210
  .picker-header { padding: 12px; background: #111; border-bottom: 1px solid #333; cursor: move; display: flex; justify-content: space-between; align-items: center; font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
211
  .picker-body { padding: 20px; display: flex; flex-direction: column; align-items: center; gap: 16px; }
212
 
213
- /* Modals */
214
- .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); z-index: 200; display: none; place-items: center; }
215
- .card { background: #000; padding: 32px; border-radius: 8px; border: 1px solid #333; max-width: 440px; width: 90%; display: flex; flex-direction: column; max-height: 90vh; box-shadow: 0 8px 30px rgba(0,0,0,0.5); }
216
-
217
  /* Loader Style */
218
  .spinner { width: 32px; height: 32px; border: 2px solid #333; border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto; }
219
  @keyframes spin { 100% { transform: rotate(360deg); } }
@@ -302,14 +302,39 @@
302
  .opt-input { background: #000; border: 1px solid #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 0.8rem; outline: none; }
303
  .opt-input:focus { border-color: #fff; }
304
 
305
- /* Session List */
306
- .session-item { position: relative; padding: 16px; border-bottom: 1px solid #222; cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: background 0.2s; }
307
- .session-item:hover { background: #0a0a0a; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  .session-checkbox { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; cursor: pointer; display: none; accent-color: var(--accent); border: 1px solid #444; border-radius: 4px; }
309
  .active-multi .session-checkbox { display: block; }
310
- .session-item.selected { background: rgba(0, 112, 243, 0.1); border-left: 2px solid var(--accent); }
311
  .active-multi .session-item { padding-left: 44px; }
312
- .multi-delete-bar { display: none; justify-content: space-between; align-items: center; background: #111; border: 1px solid #333; padding: 12px; border-radius: 4px; margin-top: 16px; }
313
  .multi-delete-bar.show { display: flex; }
314
 
315
  /* Remote Cursors */
@@ -620,55 +645,57 @@
620
  </div>
621
  </div>
622
 
623
- <!-- Dashboard -->
624
  <div id="dashboardModal" class="overlay">
625
- <div class="card" style="max-width: 450px;">
626
- <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:15px;">
627
- <h2 style="margin:0">🎨 ColorRM Pro</h2>
628
- <button onclick="UI.hideDashboard()" style="background:none; border:none; color:#888; cursor:pointer;"><i class="bi bi-x-lg"></i></button>
 
 
 
 
 
 
 
 
629
  </div>
630
 
631
- <div style="background:rgba(255,255,255,0.03); padding:10px; border-radius:8px; margin-bottom:15px; font-size:0.8rem; border:1px solid var(--border);">
632
- <div style="display:flex; justify-content:space-between; margin-bottom:4px;">
633
- <span style="color:#888;">My User ID:</span>
634
- <span id="dashUserId" style="color:var(--accent); font-family:monospace;"></span>
 
635
  </div>
636
- <div style="display:flex; justify-content:space-between;">
637
- <span style="color:#888;">Active Project:</span>
638
- <span id="dashProjId" style="color:var(--primary); font-family:monospace;"></span>
 
 
 
 
639
  </div>
640
  </div>
641
 
642
- <div style="display:flex; gap:10px; margin-bottom:15px;">
643
- <input type="text" id="newProjectName" placeholder="New Project Name" style="flex:1; background:var(--bg-body); border:1px solid var(--border); padding:10px; color:white; border-radius:6px;">
644
- <button class="btn btn-primary" onclick="window.App.createNewProject()">Create</button>
645
- <button class="btn" onclick="window.App.createFolder()" title="New Folder" style="width:40px; justify-content:center;"><i class="bi bi-folder-plus"></i></button>
646
- </div>
647
- <div style="display:flex; gap:10px; margin-bottom:15px;">
648
- <button id="importBtn" class="btn" style="flex:1; justify-content:center; border-color:#444;">
649
- <i class="bi bi-file-earmark-plus"></i> Import Files
650
- </button>
651
- <button class="btn" onclick="window.PDFLibrary?.show((pdf) => { const file = new File([pdf.blob], pdf.name + '.pdf', { type: 'application/pdf' }); window.App?.handleImport({ target: { files: [file] } }, false); })" style="flex:1; justify-content:center; border-color:#444;">
652
- <i class="bi bi-folder-plus"></i> Open Library
653
- </button>
654
- </div>
655
-
656
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
657
- <h4 style="color:var(--text-muted); font-size:0.7rem; text-transform:uppercase; margin:0;">Recent Projects</h4>
658
- <div style="display:flex; gap:8px;">
659
- <button class="btn btn-sm" onclick="window.App.reuploadBaseFile()" title="Repair Server File"><i class="bi bi-cloud-upload"></i> Restore</button>
660
- <button id="dashEditBtn" class="btn btn-sm" onclick="window.App.toggleMultiSelect()"><i class="bi bi-pencil-square"></i> Edit</button>
661
  </div>
662
  </div>
663
-
664
- <div id="sessionList" class="session-list" style="flex:1; overflow:auto; min-height:200px; border-top:1px solid var(--border);"></div>
665
-
666
- <div id="multiDeleteBar" class="multi-delete-bar">
667
- <span id="multiDeleteCount" style="font-size:0.8rem; color:#ef4444; font-weight:600;">0 selected</span>
668
- <div style="display:flex; gap:8px;">
669
- <button class="btn btn-sm" onclick="window.App.selectAllSessions()" style="background:none; border-color:#444;">All</button>
670
- <button class="btn btn-sm" onclick="window.App.showMoveModal()" style="background:none; border-color:#444;">Move</button>
671
- <button class="btn btn-sm" onclick="window.App.deleteSelectedSessions()" style="background:#ef4444; border-color:#ef4444; color:white;">Delete</button>
 
672
  </div>
673
  </div>
674
  </div>
@@ -837,26 +864,25 @@
837
  </div>
838
 
839
  <header>
840
- <div style="display:flex; align-items:center; gap:16px;">
841
- <a href="/" class="btn btn-icon" title="Back to Hub" style="text-decoration:none; color:white; background:transparent; border:none; padding:0; width:auto; height:auto;">
842
- <svg width="22" height="22" viewBox="0 0 76 65" fill="#fff"><path d="M37.5274 0L75.0548 65H0L37.5274 0Z"></path></svg>
843
  </a>
844
- <button class="btn btn-icon" title="Share URL" onclick="window.App?.shareSession()" style="background:transparent; border:none; color:#888;"><i class="bi bi-share"></i></button>
845
- <div style="width:1px; height:24px; background:#333;"></div>
846
- <span style="font-weight:700; font-size:0.95rem; letter-spacing:-0.02em;" id="headerTitle">ColorRM Pro</span>
847
- <div id="syncStatus" style="font-size:0.7rem; color:#888; margin-left:8px; display:flex; align-items:center; gap:6px; font-weight:600; text-transform:uppercase;"></div>
848
- <button id="syncToggle" class="btn btn-icon" title="Toggle Sync (currently ON)" onclick="window.App?.toggleSync()" style="background:transparent; border:none; color:#4ade80; font-size:0.85rem; padding:4px 8px;">
849
- <i class="bi bi-cloud-check"></i>
850
- </button>
851
  </div>
852
- <div style="display:flex; gap:12px;">
853
- <button class="btn btn-icon" title="Undo (Ctrl+Z)" onclick="window.App?.undo()" style="background:transparent; border:none;"><i class="bi bi-arrow-counterclockwise"></i></button>
854
- <button class="btn btn-icon" title="Redo (Ctrl+Shift+Z)" onclick="window.App?.redo()" style="background:transparent; border:none;"><i class="bi bi-arrow-clockwise"></i></button>
855
- <button class="btn btn-icon" title="Keyboard Shortcuts (?)" onclick="window.App?.showShortcutsHelp()" style="background:transparent; border:none;"><i class="bi bi-keyboard"></i></button>
856
- <div style="width:1px; height:24px; background:#333; margin:0 4px;"></div>
857
- <button class="btn" onclick="window.UI?.showDashboard()" style="background:transparent; border:1px solid #333; font-weight:600;">Projects</button>
858
- <button class="btn" id="splitViewToggle" onclick="window.App?.toggleSplitView()" style="background:transparent; border:1px solid #333; font-weight:600;" title="Toggle Split View Mode"><i class="bi bi-diagram-2"></i> Split</button>
859
- <button class="btn btn-primary" onclick="window.UI?.showExportModal()" style="font-weight:700;">Export</button>
860
  </div>
861
  </header>
862
 
 
38
  -webkit-font-smoothing: antialiased;
39
  }
40
 
41
+ /* UI Components - Vercel Style */
42
  .btn {
43
+ background: #0a0a0a; border: 1px solid #262626; color: #fafafa;
44
+ padding: 10px 14px; border-radius: 8px; font-size: 0.875rem; font-weight: 500;
45
+ cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: all 0.15s;
46
+ min-height: 40px;
47
  -webkit-tap-highlight-color: transparent;
48
  }
49
+ .btn:hover { background: #171717; border-color: #333; }
50
+ .btn:active { transform: scale(0.98); background: #1a1a1a; }
51
+ .btn.active { background: #fafafa; border-color: #fafafa; color: #000; }
52
  .btn:disabled { opacity: 0.4; cursor: not-allowed; }
53
+ .btn-primary { background: #fafafa; border-color: #fafafa; color: #000; font-weight: 600; }
54
+ .btn-primary:hover { background: #e5e5e5; border-color: #e5e5e5; }
55
+ .btn-primary:active { background: #d4d4d4; transform: scale(0.98); }
56
+ .btn-sm { padding: 6px 10px; font-size: 0.8rem; min-height: 32px; border-radius: 6px; }
57
+ .btn-icon { width: 36px; height: 36px; justify-content: center; padding: 0; min-height: 36px; }
58
+
59
  /* Sidebar Tabs - Touch Optimized */
60
  .sb-tabs { display: flex; border-bottom: 1px solid var(--border); background: var(--bg-panel); flex-shrink: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; -ms-overflow-style: none; }
61
  .sb-tabs::-webkit-scrollbar { display: none; }
 
140
  .shape-btn:active { transform: scale(0.92); background: #111; }
141
  .shape-btn.active { background: #fff; color: #000; border-color: #fff; }
142
 
143
+ header { height: 52px; background: #000; border-bottom: 1px solid #1a1a1a; display: flex; align-items: center; justify-content: space-between; padding: 0 16px; z-index: 50; flex-shrink: 0; }
144
 
145
  .workspace { display: flex; flex: 1; overflow: hidden; position: relative; }
146
 
 
210
  .picker-header { padding: 12px; background: #111; border-bottom: 1px solid #333; cursor: move; display: flex; justify-content: space-between; align-items: center; font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
211
  .picker-body { padding: 20px; display: flex; flex-direction: column; align-items: center; gap: 16px; }
212
 
213
+ /* Modals - Vercel Style */
214
+ .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(8px); z-index: 200; display: none; place-items: center; }
215
+ .card { background: #000; padding: 24px; border-radius: 12px; border: 1px solid #1a1a1a; max-width: 440px; width: 90%; display: flex; flex-direction: column; max-height: 90vh; box-shadow: 0 16px 70px rgba(0,0,0,0.5); }
216
+
217
  /* Loader Style */
218
  .spinner { width: 32px; height: 32px; border: 2px solid #333; border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto; }
219
  @keyframes spin { 100% { transform: rotate(360deg); } }
 
302
  .opt-input { background: #000; border: 1px solid #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 0.8rem; outline: none; }
303
  .opt-input:focus { border-color: #fff; }
304
 
305
+ /* Session List - Vercel Style */
306
+ .session-item {
307
+ position: relative;
308
+ padding: 14px 16px;
309
+ margin-bottom: 8px;
310
+ border-radius: 8px;
311
+ background: #0a0a0a;
312
+ border: 1px solid #1a1a1a;
313
+ cursor: pointer;
314
+ display: flex;
315
+ justify-content: space-between;
316
+ align-items: center;
317
+ transition: all 0.15s;
318
+ }
319
+ .session-item:hover {
320
+ background: #111;
321
+ border-color: #262626;
322
+ }
323
+ .session-item .session-name {
324
+ font-weight: 500;
325
+ font-size: 0.9rem;
326
+ color: #fafafa;
327
+ }
328
+ .session-item .session-meta {
329
+ font-size: 0.75rem;
330
+ color: #666;
331
+ margin-top: 4px;
332
+ }
333
  .session-checkbox { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; cursor: pointer; display: none; accent-color: var(--accent); border: 1px solid #444; border-radius: 4px; }
334
  .active-multi .session-checkbox { display: block; }
335
+ .session-item.selected { background: rgba(0, 112, 243, 0.08); border-color: #0070f3; }
336
  .active-multi .session-item { padding-left: 44px; }
337
+ .multi-delete-bar { display: none; justify-content: space-between; align-items: center; background: #0a0a0a; border: 1px solid #262626; padding: 12px 16px; border-radius: 8px; }
338
  .multi-delete-bar.show { display: flex; }
339
 
340
  /* Remote Cursors */
 
645
  </div>
646
  </div>
647
 
648
+ <!-- Dashboard - Vercel-like Clean Design -->
649
  <div id="dashboardModal" class="overlay">
650
+ <div class="card" style="max-width: 520px; padding: 0; overflow: hidden;">
651
+ <!-- Header -->
652
+ <div style="padding: 24px 24px 20px; border-bottom: 1px solid #1a1a1a;">
653
+ <div style="display: flex; justify-content: space-between; align-items: center;">
654
+ <div style="display: flex; align-items: center; gap: 12px;">
655
+ <svg width="24" height="24" viewBox="0 0 76 65" fill="#fff"><path d="M37.5274 0L75.0548 65H0L37.5274 0Z"></path></svg>
656
+ <span style="font-size: 1.1rem; font-weight: 600; letter-spacing: -0.02em;">Projects</span>
657
+ </div>
658
+ <button onclick="UI.hideDashboard()" style="background: none; border: none; color: #666; cursor: pointer; padding: 8px; border-radius: 6px; transition: 0.15s;" onmouseover="this.style.background='#1a1a1a'" onmouseout="this.style.background='none'">
659
+ <i class="bi bi-x-lg"></i>
660
+ </button>
661
+ </div>
662
  </div>
663
 
664
+ <!-- Create New -->
665
+ <div style="padding: 20px 24px; border-bottom: 1px solid #1a1a1a;">
666
+ <div style="display: flex; gap: 10px;">
667
+ <input type="text" id="newProjectName" placeholder="Project name..." style="flex: 1; background: #0a0a0a; border: 1px solid #262626; padding: 12px 14px; color: white; border-radius: 8px; font-size: 0.9rem; transition: border-color 0.15s;" onfocus="this.style.borderColor='#404040'" onblur="this.style.borderColor='#262626'">
668
+ <button class="btn btn-primary" onclick="window.App.createNewProject()" style="padding: 12px 20px; border-radius: 8px; font-weight: 600;">Create</button>
669
  </div>
670
+ <div style="display: flex; gap: 8px; margin-top: 12px;">
671
+ <button id="importBtn" class="btn" style="flex: 1; justify-content: center; background: #0a0a0a; border-color: #262626; font-size: 0.85rem; padding: 10px;">
672
+ <i class="bi bi-plus-lg"></i> Import
673
+ </button>
674
+ <button class="btn" onclick="window.App.createFolder()" style="background: #0a0a0a; border-color: #262626; padding: 10px 14px;" title="New Folder">
675
+ <i class="bi bi-folder-plus"></i>
676
+ </button>
677
  </div>
678
  </div>
679
 
680
+ <!-- Project List -->
681
+ <div style="padding: 16px 24px 8px;">
682
+ <div style="display: flex; justify-content: space-between; align-items: center;">
683
+ <span style="font-size: 0.75rem; color: #666; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em;">Recent</span>
684
+ <button id="dashEditBtn" class="btn btn-sm" onclick="window.App.toggleMultiSelect()" style="background: transparent; border: none; color: #666; padding: 4px 8px; font-size: 0.75rem;">
685
+ <i class="bi bi-pencil"></i> Edit
686
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
687
  </div>
688
  </div>
689
+
690
+ <div id="sessionList" class="session-list" style="flex: 1; overflow: auto; min-height: 240px; max-height: 360px; padding: 0 24px 24px;"></div>
691
+
692
+ <!-- Multi-select Bar -->
693
+ <div id="multiDeleteBar" class="multi-delete-bar" style="margin: 0 24px 24px; border-radius: 8px; background: #0a0a0a;">
694
+ <span id="multiDeleteCount" style="font-size: 0.8rem; color: #888; font-weight: 500;">0 selected</span>
695
+ <div style="display: flex; gap: 8px;">
696
+ <button class="btn btn-sm" onclick="window.App.selectAllSessions()" style="background: transparent; border-color: #333; color: #888;">All</button>
697
+ <button class="btn btn-sm" onclick="window.App.showMoveModal()" style="background: transparent; border-color: #333; color: #888;">Move</button>
698
+ <button class="btn btn-sm" onclick="window.App.deleteSelectedSessions()" style="background: #dc2626; border-color: #dc2626; color: white;">Delete</button>
699
  </div>
700
  </div>
701
  </div>
 
864
  </div>
865
 
866
  <header>
867
+ <div style="display:flex; align-items:center; gap:12px;">
868
+ <a href="/" class="btn btn-icon" title="Home" style="text-decoration:none; color:white; background:transparent; border:none; padding:0;">
869
+ <svg width="20" height="20" viewBox="0 0 76 65" fill="#fff"><path d="M37.5274 0L75.0548 65H0L37.5274 0Z"></path></svg>
870
  </a>
871
+ <div style="width:1px; height:20px; background:#262626;"></div>
872
+ <span style="font-weight:600; font-size:0.9rem; letter-spacing:-0.02em; color:#fafafa;" id="headerTitle">Untitled</span>
873
+ <div id="syncStatus" style="display:flex; align-items:center; gap:6px;">
874
+ <button id="syncToggle" title="Sync status" onclick="window.App?.toggleSync()" style="background:transparent; border:none; color:#4ade80; cursor:pointer; padding:4px; display:flex; align-items:center;">
875
+ <i class="bi bi-cloud-check"></i>
876
+ </button>
877
+ </div>
878
  </div>
879
+ <div style="display:flex; gap:8px; align-items:center;">
880
+ <button class="btn btn-icon" title="Undo" onclick="window.App?.undo()" style="background:transparent; border:none; color:#888;"><i class="bi bi-arrow-counterclockwise"></i></button>
881
+ <button class="btn btn-icon" title="Redo" onclick="window.App?.redo()" style="background:transparent; border:none; color:#888;"><i class="bi bi-arrow-clockwise"></i></button>
882
+ <div style="width:1px; height:20px; background:#262626;"></div>
883
+ <button class="btn btn-icon" title="Share" onclick="window.App?.shareSession()" style="background:transparent; border:none; color:#888;"><i class="bi bi-share"></i></button>
884
+ <button class="btn" onclick="window.UI?.showDashboard()" style="background:transparent; border:1px solid #262626; color:#fafafa; font-weight:500; padding:8px 14px;">Projects</button>
885
+ <button class="btn btn-primary" onclick="window.UI?.showExportModal()" style="font-weight:600; padding:8px 16px;">Export</button>
 
886
  </div>
887
  </header>
888