Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- docs/README.md +203 -0
- 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 -
|
| 42 |
.btn {
|
| 43 |
-
background:
|
| 44 |
-
padding: 10px 14px; border-radius:
|
| 45 |
-
cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: 0.15s;
|
| 46 |
-
min-height:
|
| 47 |
-webkit-tap-highlight-color: transparent;
|
| 48 |
}
|
| 49 |
-
.btn:hover { background: #
|
| 50 |
-
.btn:active { transform: scale(0.
|
| 51 |
-
.btn.active { background:
|
| 52 |
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 53 |
-
.btn-primary { background:
|
| 54 |
-
.btn-primary:hover { background: #
|
| 55 |
-
.btn-primary:active { background: #
|
| 56 |
-
.btn-sm { padding:
|
| 57 |
-
.btn-icon { width:
|
| 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:
|
| 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.
|
| 215 |
-
.card { background: #000; padding:
|
| 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 {
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 311 |
.active-multi .session-item { padding-left: 44px; }
|
| 312 |
-
.multi-delete-bar { display: none; justify-content: space-between; align-items: center; background: #
|
| 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:
|
| 626 |
-
<
|
| 627 |
-
|
| 628 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
</div>
|
| 630 |
|
| 631 |
-
<
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
<
|
|
|
|
| 635 |
</div>
|
| 636 |
-
<div style="display:flex;
|
| 637 |
-
<
|
| 638 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
</div>
|
| 640 |
</div>
|
| 641 |
|
| 642 |
-
<
|
| 643 |
-
|
| 644 |
-
<
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 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:
|
| 665 |
-
|
| 666 |
-
<
|
| 667 |
-
|
| 668 |
-
<
|
| 669 |
-
|
| 670 |
-
<button class="btn btn-sm" onclick="window.App.
|
| 671 |
-
<button class="btn btn-sm" onclick="window.App.
|
|
|
|
| 672 |
</div>
|
| 673 |
</div>
|
| 674 |
</div>
|
|
@@ -837,26 +864,25 @@
|
|
| 837 |
</div>
|
| 838 |
|
| 839 |
<header>
|
| 840 |
-
<div style="display:flex; align-items:center; gap:
|
| 841 |
-
<a href="/" class="btn btn-icon" title="
|
| 842 |
-
<svg width="
|
| 843 |
</a>
|
| 844 |
-
<
|
| 845 |
-
<
|
| 846 |
-
<
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
<
|
| 850 |
-
</
|
| 851 |
</div>
|
| 852 |
-
<div style="display:flex; gap:
|
| 853 |
-
<button class="btn btn-icon" title="Undo
|
| 854 |
-
<button class="btn btn-icon" title="Redo
|
| 855 |
-
<
|
| 856 |
-
<
|
| 857 |
-
<button class="btn" onclick="window.UI?.showDashboard()" style="background:transparent; border:1px solid #
|
| 858 |
-
<button class="btn
|
| 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 |
|