cyclecad 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
  2. package/BILLING-INDEX.md +293 -0
  3. package/BILLING-INTEGRATION-GUIDE.md +414 -0
  4. package/COLLABORATION-INDEX.md +440 -0
  5. package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
  6. package/DOCKER-BUILD-MANIFEST.txt +483 -0
  7. package/DOCKER-FILES-REFERENCE.md +440 -0
  8. package/DOCKER-INFRASTRUCTURE.md +475 -0
  9. package/DOCKER-README.md +435 -0
  10. package/Dockerfile +33 -55
  11. package/PWA-FILES-CREATED.txt +350 -0
  12. package/QUICK-START-TESTING.md +126 -0
  13. package/STEP-IMPORT-QUICKSTART.md +347 -0
  14. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  15. package/app/css/mobile.css +1074 -0
  16. package/app/icons/generate-icons.js +203 -0
  17. package/app/index.html +93 -0
  18. package/app/js/billing-ui.js +990 -0
  19. package/app/js/brep-kernel.js +933 -981
  20. package/app/js/collab-client.js +750 -0
  21. package/app/js/mobile-nav.js +623 -0
  22. package/app/js/mobile-toolbar.js +476 -0
  23. package/app/js/modules/billing-module.js +724 -0
  24. package/app/js/modules/step-module-enhanced.js +938 -0
  25. package/app/js/offline-manager.js +705 -0
  26. package/app/js/responsive-init.js +360 -0
  27. package/app/js/touch-handler.js +429 -0
  28. package/app/manifest.json +211 -0
  29. package/app/offline.html +508 -0
  30. package/app/sw.js +571 -0
  31. package/app/tests/billing-tests.html +779 -0
  32. package/app/tests/brep-tests.html +980 -0
  33. package/app/tests/collab-tests.html +743 -0
  34. package/app/tests/mobile-tests.html +1299 -0
  35. package/app/tests/pwa-tests.html +1134 -0
  36. package/app/tests/step-tests.html +1042 -0
  37. package/app/tests/test-agent-v3.html +719 -0
  38. package/docker-compose.yml +225 -0
  39. package/docs/BILLING-HELP.json +260 -0
  40. package/docs/BILLING-README.md +639 -0
  41. package/docs/BILLING-TUTORIAL.md +736 -0
  42. package/docs/BREP-HELP.json +326 -0
  43. package/docs/BREP-TUTORIAL.md +802 -0
  44. package/docs/COLLABORATION-HELP.json +228 -0
  45. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  46. package/docs/DOCKER-HELP.json +224 -0
  47. package/docs/DOCKER-TUTORIAL.md +974 -0
  48. package/docs/MOBILE-HELP.json +243 -0
  49. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  50. package/docs/MOBILE-TUTORIAL.md +747 -0
  51. package/docs/PWA-HELP.json +228 -0
  52. package/docs/PWA-README.md +662 -0
  53. package/docs/PWA-TUTORIAL.md +757 -0
  54. package/docs/STEP-HELP.json +481 -0
  55. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  56. package/docs/TESTING-GUIDE.md +528 -0
  57. package/docs/TESTING-HELP.json +182 -0
  58. package/fusion-vs-cyclecad.html +1771 -0
  59. package/nginx.conf +237 -0
  60. package/package.json +1 -1
  61. package/server/Dockerfile.converter +51 -0
  62. package/server/Dockerfile.signaling +28 -0
  63. package/server/billing-server.js +487 -0
  64. package/server/converter-enhanced.py +528 -0
  65. package/server/requirements-converter.txt +29 -0
  66. package/server/signaling-server.js +801 -0
  67. package/tests/docker-tests.sh +389 -0
@@ -0,0 +1,705 @@
1
+ /**
2
+ * Offline Manager for cycleCAD
3
+ * Handles online/offline detection, operation queuing, sync, and PWA features
4
+ * ~400 lines
5
+ */
6
+
7
+ class OfflineManager {
8
+ constructor() {
9
+ this.isOnline = navigator.onLine;
10
+ this.isSWSupported = 'serviceWorker' in navigator;
11
+ this.isDBSupported = 'indexedDB' in window;
12
+ this.operationQueue = [];
13
+ this.syncInProgress = false;
14
+ this.updateAvailable = false;
15
+ this.cacheSize = 0;
16
+
17
+ this.init();
18
+ }
19
+
20
+ /**
21
+ * Initialize offline manager
22
+ */
23
+ async init() {
24
+ console.log('[OfflineManager] Initializing...');
25
+
26
+ // Register service worker
27
+ if (this.isSWSupported) {
28
+ await this.registerServiceWorker();
29
+ } else {
30
+ console.warn('[OfflineManager] Service Workers not supported');
31
+ }
32
+
33
+ // Setup online/offline listeners
34
+ window.addEventListener('online', () => this.handleOnline());
35
+ window.addEventListener('offline', () => this.handleOffline());
36
+
37
+ // Setup IndexedDB
38
+ if (this.isDBSupported) {
39
+ await this.initializeDatabase();
40
+ }
41
+
42
+ // Request permissions
43
+ this.requestPermissions();
44
+
45
+ // Sync offline operations if online
46
+ if (this.isOnline) {
47
+ await this.syncOperations();
48
+ }
49
+
50
+ // Check for updates
51
+ this.checkForUpdates();
52
+
53
+ // Setup UI
54
+ this.setupUI();
55
+
56
+ console.log('[OfflineManager] Ready. Online:', this.isOnline);
57
+ }
58
+
59
+ /**
60
+ * Register service worker
61
+ */
62
+ async registerServiceWorker() {
63
+ try {
64
+ const registration = await navigator.serviceWorker.register('/app/sw.js', {
65
+ scope: '/app/'
66
+ });
67
+
68
+ console.log('[SW] Registered successfully');
69
+
70
+ // Listen for updates
71
+ registration.addEventListener('updatefound', () => {
72
+ const newWorker = registration.installing;
73
+ newWorker.addEventListener('statechange', () => {
74
+ if (newWorker.state === 'activated') {
75
+ this.showUpdatePrompt();
76
+ }
77
+ });
78
+ });
79
+
80
+ // Handle messages from SW
81
+ navigator.serviceWorker.addEventListener('message', (event) => {
82
+ this.handleSWMessage(event.data);
83
+ });
84
+
85
+ } catch (err) {
86
+ console.error('[SW] Registration failed:', err);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Initialize IndexedDB
92
+ */
93
+ async initializeDatabase() {
94
+ return new Promise((resolve, reject) => {
95
+ const request = indexedDB.open('cyclecad', 1);
96
+
97
+ request.onerror = () => reject(request.error);
98
+ request.onsuccess = () => {
99
+ this.db = request.result;
100
+ console.log('[DB] Opened successfully');
101
+ resolve();
102
+ };
103
+
104
+ request.onupgradeneeded = (event) => {
105
+ const db = event.target.result;
106
+
107
+ // Create object stores if they don't exist
108
+ if (!db.objectStoreNames.contains('operationQueue')) {
109
+ db.createObjectStore('operationQueue', { keyPath: 'id', autoIncrement: true });
110
+ }
111
+ if (!db.objectStoreNames.contains('projects')) {
112
+ db.createObjectStore('projects', { keyPath: 'id' });
113
+ }
114
+ if (!db.objectStoreNames.contains('drafts')) {
115
+ db.createObjectStore('drafts', { keyPath: 'id', autoIncrement: true });
116
+ }
117
+
118
+ console.log('[DB] Upgraded successfully');
119
+ };
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Queue an operation for offline sync
125
+ */
126
+ async queueOperation(operation) {
127
+ if (!this.isOnline && this.isDBSupported) {
128
+ try {
129
+ const tx = this.db.transaction('operationQueue', 'readwrite');
130
+ const store = tx.objectStore('operationQueue');
131
+
132
+ await new Promise((resolve, reject) => {
133
+ const req = store.add({
134
+ timestamp: Date.now(),
135
+ data: operation
136
+ });
137
+ req.onerror = () => reject(req.error);
138
+ req.onsuccess = () => resolve(req.result);
139
+ });
140
+
141
+ console.log('[Offline] Operation queued:', operation);
142
+ this.showNotification('Operation queued. Will sync when online.');
143
+
144
+ return true;
145
+ } catch (err) {
146
+ console.error('[DB] Queue failed:', err);
147
+ return false;
148
+ }
149
+ }
150
+
151
+ return false;
152
+ }
153
+
154
+ /**
155
+ * Sync offline operations when back online
156
+ */
157
+ async syncOperations() {
158
+ if (this.syncInProgress) return;
159
+ if (!this.isDBSupported) return;
160
+
161
+ this.syncInProgress = true;
162
+
163
+ try {
164
+ const tx = this.db.transaction('operationQueue', 'readonly');
165
+ const store = tx.objectStore('operationQueue');
166
+
167
+ const operations = await new Promise((resolve, reject) => {
168
+ const req = store.getAll();
169
+ req.onerror = () => reject(req.error);
170
+ req.onsuccess = () => resolve(req.result);
171
+ });
172
+
173
+ console.log('[Offline] Syncing', operations.length, 'queued operations...');
174
+
175
+ if (operations.length === 0) {
176
+ this.syncInProgress = false;
177
+ return;
178
+ }
179
+
180
+ // Notify SW to sync
181
+ if (navigator.serviceWorker.controller) {
182
+ navigator.serviceWorker.controller.postMessage({
183
+ type: 'SYNC_OPERATIONS'
184
+ });
185
+ }
186
+
187
+ // Show sync progress
188
+ this.showSyncProgress(0, operations.length);
189
+
190
+ // Sync each operation
191
+ let syncedCount = 0;
192
+ for (const op of operations) {
193
+ try {
194
+ const response = await fetch('/api/operations', {
195
+ method: 'POST',
196
+ headers: { 'Content-Type': 'application/json' },
197
+ body: JSON.stringify(op.data)
198
+ });
199
+
200
+ if (response.ok) {
201
+ // Remove from queue
202
+ const txW = this.db.transaction('operationQueue', 'readwrite');
203
+ await new Promise((resolve, reject) => {
204
+ const req = txW.objectStore('operationQueue').delete(op.id);
205
+ req.onerror = () => reject(req.error);
206
+ req.onsuccess = () => resolve();
207
+ });
208
+
209
+ syncedCount++;
210
+ this.showSyncProgress(syncedCount, operations.length);
211
+ console.log('[Offline] Synced operation:', op.id);
212
+ }
213
+ } catch (err) {
214
+ console.error('[Offline] Sync failed for operation:', op.id, err);
215
+ }
216
+ }
217
+
218
+ console.log('[Offline] Sync complete:', syncedCount, '/', operations.length);
219
+ this.showNotification(`Synced ${syncedCount} operation(s).`);
220
+
221
+ } catch (err) {
222
+ console.error('[Offline] Sync failed:', err);
223
+ this.showNotification('Sync failed. Will retry.');
224
+ } finally {
225
+ this.syncInProgress = false;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Handle online event
231
+ */
232
+ handleOnline() {
233
+ console.log('[Offline] Online detected');
234
+ this.isOnline = true;
235
+
236
+ this.showNotification('Back online! Syncing changes...');
237
+ this.updateOfflineBanner();
238
+ this.syncOperations();
239
+ }
240
+
241
+ /**
242
+ * Handle offline event
243
+ */
244
+ handleOffline() {
245
+ console.log('[Offline] Offline detected');
246
+ this.isOnline = false;
247
+
248
+ this.showNotification('You are offline. Changes will sync when online.');
249
+ this.updateOfflineBanner();
250
+ }
251
+
252
+ /**
253
+ * Handle messages from service worker
254
+ */
255
+ handleSWMessage(data) {
256
+ console.log('[Offline] SW message:', data.type);
257
+
258
+ if (data.type === 'UPDATE_AVAILABLE') {
259
+ this.showUpdatePrompt();
260
+ }
261
+
262
+ if (data.type === 'SYNC_COMPLETE') {
263
+ this.showNotification('All changes synced!');
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Show update prompt
269
+ */
270
+ showUpdatePrompt() {
271
+ this.updateAvailable = true;
272
+
273
+ if (!document.getElementById('update-prompt')) {
274
+ const prompt = document.createElement('div');
275
+ prompt.id = 'update-prompt';
276
+ prompt.innerHTML = `
277
+ <div style="
278
+ position: fixed;
279
+ bottom: 20px;
280
+ right: 20px;
281
+ background: #0284C7;
282
+ color: white;
283
+ padding: 16px 20px;
284
+ border-radius: 8px;
285
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
286
+ z-index: 999999;
287
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
288
+ max-width: 320px;
289
+ ">
290
+ <strong>Update available!</strong>
291
+ <p style="margin: 8px 0 0 0; font-size: 14px;">
292
+ A new version of cycleCAD is ready.
293
+ </p>
294
+ <div style="margin-top: 12px; display: flex; gap: 8px;">
295
+ <button onclick="window.location.reload()" style="
296
+ background: white;
297
+ color: #0284C7;
298
+ border: none;
299
+ padding: 8px 16px;
300
+ border-radius: 4px;
301
+ cursor: pointer;
302
+ font-weight: 600;
303
+ flex: 1;
304
+ ">Update Now</button>
305
+ <button onclick="this.closest('#update-prompt').remove()" style="
306
+ background: rgba(255,255,255,0.2);
307
+ color: white;
308
+ border: none;
309
+ padding: 8px 16px;
310
+ border-radius: 4px;
311
+ cursor: pointer;
312
+ flex: 1;
313
+ ">Later</button>
314
+ </div>
315
+ </div>
316
+ `;
317
+ document.body.appendChild(prompt);
318
+
319
+ // Auto-dismiss after 10 seconds
320
+ setTimeout(() => {
321
+ const el = document.getElementById('update-prompt');
322
+ if (el) el.remove();
323
+ }, 10000);
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Show offline banner
329
+ */
330
+ updateOfflineBanner() {
331
+ let banner = document.getElementById('offline-banner');
332
+
333
+ if (!this.isOnline) {
334
+ if (!banner) {
335
+ banner = document.createElement('div');
336
+ banner.id = 'offline-banner';
337
+ banner.style.cssText = `
338
+ position: fixed;
339
+ top: 0;
340
+ left: 0;
341
+ right: 0;
342
+ background: #EF4444;
343
+ color: white;
344
+ padding: 12px 20px;
345
+ text-align: center;
346
+ font-weight: 500;
347
+ z-index: 999998;
348
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
349
+ `;
350
+ banner.textContent = 'You are offline. Your changes will sync when you reconnect.';
351
+ document.body.insertBefore(banner, document.body.firstChild);
352
+ }
353
+ } else {
354
+ if (banner) banner.remove();
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Show notification toast
360
+ */
361
+ showNotification(message, duration = 4000) {
362
+ const toast = document.createElement('div');
363
+ toast.style.cssText = `
364
+ position: fixed;
365
+ bottom: 20px;
366
+ left: 20px;
367
+ background: #1F2937;
368
+ color: white;
369
+ padding: 16px 20px;
370
+ border-radius: 8px;
371
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
372
+ z-index: 999998;
373
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
374
+ font-size: 14px;
375
+ max-width: 400px;
376
+ animation: slideIn 0.3s ease-out;
377
+ `;
378
+ toast.textContent = message;
379
+
380
+ document.body.appendChild(toast);
381
+
382
+ setTimeout(() => {
383
+ toast.style.animation = 'slideOut 0.3s ease-in';
384
+ setTimeout(() => toast.remove(), 300);
385
+ }, duration);
386
+ }
387
+
388
+ /**
389
+ * Show sync progress
390
+ */
391
+ showSyncProgress(current, total) {
392
+ let progress = document.getElementById('sync-progress');
393
+
394
+ if (!progress) {
395
+ progress = document.createElement('div');
396
+ progress.id = 'sync-progress';
397
+ progress.style.cssText = `
398
+ position: fixed;
399
+ bottom: 20px;
400
+ left: 20px;
401
+ background: #1F2937;
402
+ color: white;
403
+ padding: 12px 16px;
404
+ border-radius: 8px;
405
+ z-index: 999998;
406
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
407
+ font-size: 13px;
408
+ min-width: 300px;
409
+ `;
410
+ document.body.appendChild(progress);
411
+ }
412
+
413
+ const pct = Math.round((current / total) * 100);
414
+ progress.innerHTML = `
415
+ <div>Syncing changes... ${current}/${total}</div>
416
+ <div style="
417
+ background: rgba(255,255,255,0.1);
418
+ height: 4px;
419
+ border-radius: 2px;
420
+ margin-top: 8px;
421
+ overflow: hidden;
422
+ ">
423
+ <div style="
424
+ background: #0284C7;
425
+ height: 100%;
426
+ width: ${pct}%;
427
+ transition: width 0.2s ease;
428
+ "></div>
429
+ </div>
430
+ `;
431
+
432
+ if (current >= total) {
433
+ setTimeout(() => progress.remove(), 1000);
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Get cache size
439
+ */
440
+ async getCacheSize() {
441
+ return new Promise((resolve) => {
442
+ if (!this.isSWSupported) {
443
+ resolve(0);
444
+ return;
445
+ }
446
+
447
+ navigator.serviceWorker.controller?.postMessage(
448
+ { type: 'GET_CACHE_SIZE' },
449
+ [new MessageChannel().port2]
450
+ );
451
+
452
+ navigator.serviceWorker.addEventListener('message', (event) => {
453
+ if (event.data.size !== undefined) {
454
+ this.cacheSize = event.data.size;
455
+ resolve(event.data.size);
456
+ }
457
+ });
458
+ });
459
+ }
460
+
461
+ /**
462
+ * Clear cache
463
+ */
464
+ async clearCache() {
465
+ return new Promise((resolve) => {
466
+ if (!this.isSWSupported) {
467
+ resolve(false);
468
+ return;
469
+ }
470
+
471
+ const channel = new MessageChannel();
472
+ navigator.serviceWorker.controller?.postMessage(
473
+ { type: 'CLEAR_CACHE' },
474
+ [channel.port2]
475
+ );
476
+
477
+ channel.port1.onmessage = (event) => {
478
+ if (event.data.success) {
479
+ this.cacheSize = 0;
480
+ this.showNotification('Cache cleared successfully.');
481
+ resolve(true);
482
+ }
483
+ };
484
+ });
485
+ }
486
+
487
+ /**
488
+ * Check for updates
489
+ */
490
+ checkForUpdates() {
491
+ if (!this.isSWSupported) return;
492
+
493
+ // Check every hour
494
+ setInterval(async () => {
495
+ try {
496
+ const registration = await navigator.serviceWorker.getRegistration('/app/');
497
+ if (registration) {
498
+ await registration.update();
499
+ }
500
+ } catch (err) {
501
+ console.error('[Offline] Update check failed:', err);
502
+ }
503
+ }, 60 * 60 * 1000);
504
+ }
505
+
506
+ /**
507
+ * Request permissions for notifications and install
508
+ */
509
+ requestPermissions() {
510
+ // Install prompt
511
+ window.addEventListener('beforeinstallprompt', (event) => {
512
+ event.preventDefault();
513
+ this.installPrompt = event;
514
+ this.showInstallPrompt();
515
+ });
516
+
517
+ window.addEventListener('appinstalled', () => {
518
+ console.log('[PWA] App installed successfully');
519
+ this.installPrompt = null;
520
+ });
521
+ }
522
+
523
+ /**
524
+ * Show install prompt
525
+ */
526
+ showInstallPrompt() {
527
+ if (!this.installPrompt || window.matchMedia('(display-mode: standalone)').matches) {
528
+ return;
529
+ }
530
+
531
+ if (!document.getElementById('install-prompt')) {
532
+ const prompt = document.createElement('div');
533
+ prompt.id = 'install-prompt';
534
+ prompt.innerHTML = `
535
+ <div style="
536
+ position: fixed;
537
+ bottom: 20px;
538
+ right: 20px;
539
+ background: white;
540
+ border: 2px solid #0284C7;
541
+ border-radius: 12px;
542
+ padding: 16px 20px;
543
+ box-shadow: 0 8px 24px rgba(0,0,0,0.12);
544
+ z-index: 999999;
545
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
546
+ max-width: 320px;
547
+ ">
548
+ <strong style="color: #1F2937;">Add to Home Screen</strong>
549
+ <p style="margin: 8px 0 0 0; font-size: 14px; color: #4B5563;">
550
+ Access cycleCAD directly from your home screen or app drawer.
551
+ </p>
552
+ <div style="margin-top: 12px; display: flex; gap: 8px;">
553
+ <button id="install-btn" style="
554
+ background: #0284C7;
555
+ color: white;
556
+ border: none;
557
+ padding: 8px 16px;
558
+ border-radius: 6px;
559
+ cursor: pointer;
560
+ font-weight: 600;
561
+ flex: 1;
562
+ ">Install</button>
563
+ <button onclick="this.closest('#install-prompt').remove()" style="
564
+ background: #F3F4F6;
565
+ color: #1F2937;
566
+ border: none;
567
+ padding: 8px 16px;
568
+ border-radius: 6px;
569
+ cursor: pointer;
570
+ flex: 1;
571
+ ">Not now</button>
572
+ </div>
573
+ </div>
574
+ `;
575
+ document.body.appendChild(prompt);
576
+
577
+ document.getElementById('install-btn').addEventListener('click', () => {
578
+ this.installPrompt.prompt();
579
+ this.installPrompt.userChoice.then((choice) => {
580
+ if (choice.outcome === 'accepted') {
581
+ console.log('[PWA] Install accepted');
582
+ }
583
+ document.getElementById('install-prompt')?.remove();
584
+ this.installPrompt = null;
585
+ });
586
+ });
587
+
588
+ // Auto-dismiss after 15 seconds
589
+ setTimeout(() => {
590
+ const el = document.getElementById('install-prompt');
591
+ if (el) el.remove();
592
+ }, 15000);
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Setup UI controls
598
+ */
599
+ setupUI() {
600
+ // Add offline manager UI to settings panel if it exists
601
+ const settingsPanel = document.getElementById('settings-panel');
602
+ if (settingsPanel) {
603
+ const offlineSection = document.createElement('div');
604
+ offlineSection.id = 'offline-section';
605
+ offlineSection.innerHTML = `
606
+ <div style="padding: 12px 0; border-top: 1px solid #E5E7EB;">
607
+ <h3 style="margin: 0 0 12px 0; font-size: 14px; font-weight: 600; color: #1F2937;">
608
+ Offline & Cache
609
+ </h3>
610
+ <button id="cache-status-btn" style="
611
+ width: 100%;
612
+ padding: 10px;
613
+ margin-bottom: 8px;
614
+ background: #F3F4F6;
615
+ border: 1px solid #D1D5DB;
616
+ border-radius: 6px;
617
+ cursor: pointer;
618
+ font-size: 13px;
619
+ text-align: left;
620
+ ">
621
+ <div>Cache: Calculating...</div>
622
+ </button>
623
+ <button id="clear-cache-btn" style="
624
+ width: 100%;
625
+ padding: 10px;
626
+ background: #FEE2E2;
627
+ border: 1px solid #FCA5A5;
628
+ border-radius: 6px;
629
+ cursor: pointer;
630
+ font-weight: 500;
631
+ color: #DC2626;
632
+ font-size: 13px;
633
+ ">
634
+ Clear Cache
635
+ </button>
636
+ </div>
637
+ `;
638
+ settingsPanel.appendChild(offlineSection);
639
+
640
+ // Cache size display
641
+ this.getCacheSize().then((size) => {
642
+ const btn = document.getElementById('cache-status-btn');
643
+ if (btn) {
644
+ const sizeStr = this.formatBytes(size);
645
+ btn.textContent = `Cache: ${sizeStr}`;
646
+ }
647
+ });
648
+
649
+ // Clear cache handler
650
+ document.getElementById('clear-cache-btn')?.addEventListener('click', async () => {
651
+ if (confirm('Clear all cached files? You can still work offline with cached projects.')) {
652
+ await this.clearCache();
653
+ const btn = document.getElementById('cache-status-btn');
654
+ if (btn) btn.textContent = 'Cache: 0 B';
655
+ }
656
+ });
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Format bytes to human-readable
662
+ */
663
+ formatBytes(bytes) {
664
+ if (bytes === 0) return '0 B';
665
+ const k = 1024;
666
+ const sizes = ['B', 'KB', 'MB', 'GB'];
667
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
668
+ return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i];
669
+ }
670
+ }
671
+
672
+ // Initialize on page load
673
+ window.offlineManager = new OfflineManager();
674
+
675
+ // Add styles for animations
676
+ const style = document.createElement('style');
677
+ style.textContent = `
678
+ @keyframes slideIn {
679
+ from {
680
+ opacity: 0;
681
+ transform: translateX(-20px);
682
+ }
683
+ to {
684
+ opacity: 1;
685
+ transform: translateX(0);
686
+ }
687
+ }
688
+
689
+ @keyframes slideOut {
690
+ from {
691
+ opacity: 1;
692
+ transform: translateX(0);
693
+ }
694
+ to {
695
+ opacity: 0;
696
+ transform: translateX(-20px);
697
+ }
698
+ }
699
+ `;
700
+ document.head.appendChild(style);
701
+
702
+ // Export for testing
703
+ if (typeof module !== 'undefined' && module.exports) {
704
+ module.exports = OfflineManager;
705
+ }