cyclecad 2.0.1 → 2.1.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 (33) hide show
  1. package/IMPLEMENTATION_GUIDE.md +502 -0
  2. package/INTEGRATION-GUIDE.md +377 -0
  3. package/MODULES_PHASES_6_7.md +780 -0
  4. package/app/index.html +106 -2
  5. package/app/js/brep-kernel.js +1353 -455
  6. package/app/js/help-module.js +1437 -0
  7. package/app/js/kernel.js +364 -40
  8. package/app/js/modules/animation-module.js +967 -0
  9. package/app/js/modules/assembly-module.js +47 -3
  10. package/app/js/modules/cam-module.js +1067 -0
  11. package/app/js/modules/collaboration-module.js +1102 -0
  12. package/app/js/modules/data-module.js +1656 -0
  13. package/app/js/modules/drawing-module.js +54 -8
  14. package/app/js/modules/formats-module.js +1173 -0
  15. package/app/js/modules/inspection-module.js +937 -0
  16. package/app/js/modules/mesh-module.js +968 -0
  17. package/app/js/modules/operations-module.js +40 -7
  18. package/app/js/modules/plugin-module.js +957 -0
  19. package/app/js/modules/rendering-module.js +1306 -0
  20. package/app/js/modules/scripting-module.js +955 -0
  21. package/app/js/modules/simulation-module.js +60 -3
  22. package/app/js/modules/sketch-module.js +1032 -90
  23. package/app/js/modules/step-module.js +47 -6
  24. package/app/js/modules/surface-module.js +728 -0
  25. package/app/js/modules/version-module.js +1410 -0
  26. package/app/js/modules/viewport-module.js +95 -8
  27. package/app/test-agent-v2.html +881 -1316
  28. package/docs/ARCHITECTURE.html +838 -1408
  29. package/docs/DEVELOPER-GUIDE.md +1504 -0
  30. package/docs/TUTORIAL.md +740 -0
  31. package/package.json +1 -1
  32. package/.github/scripts/cad-diff.js +0 -590
  33. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,1656 @@
1
+ /**
2
+ * @file data-module.js
3
+ * @description Data persistence and project management for cycleCAD.
4
+ * Save projects locally to IndexedDB, share via links, import/export,
5
+ * manage file organization, and sync with cloud storage.
6
+ * Supports Inventor files, STEP, STL, DXF, and cycleCAD native format.
7
+ *
8
+ * @tutorial Creating and Saving Your First Project
9
+ * Step 1: Click Data → New Project
10
+ * const project = await kernel.exec('data.newProject', {
11
+ * name: 'Bike Pump Design',
12
+ * description: 'Complete bike pump assembly'
13
+ * });
14
+ * console.log('Project ID:', project.id);
15
+ *
16
+ * Step 2: As you work, your changes auto-save every 30 seconds
17
+ * // No manual action needed — data.js handles it
18
+ *
19
+ * Step 3: Manually save a snapshot
20
+ * await kernel.exec('data.save', {
21
+ * projectId: project.id
22
+ * });
23
+ *
24
+ * @tutorial Organizing Files in Your Project
25
+ * Projects contain a folder structure for organizing designs.
26
+ *
27
+ * Step 1: Create folders
28
+ * await kernel.exec('data.createFolder', {
29
+ * projectId: 'proj-123',
30
+ * path: 'parts/',
31
+ * name: 'Main Body Parts'
32
+ * });
33
+ *
34
+ * Step 2: Import files into folders
35
+ * const file = new File([...], 'pump-body.ipt');
36
+ * await kernel.exec('data.importFile', {
37
+ * projectId: 'proj-123',
38
+ * folder: 'parts/',
39
+ * file: file
40
+ * });
41
+ *
42
+ * Step 3: Browse the project tree in the Data panel
43
+ * // Shows: Bike Pump Design
44
+ * // ├─ parts/
45
+ * // │ └─ pump-body.ipt
46
+ * // └─ assemblies/
47
+ *
48
+ * @tutorial Sharing a Project with a Link
49
+ * Generate shareable links for viewing (read-only) or editing.
50
+ *
51
+ * Step 1: Create a share link
52
+ * const link = await kernel.exec('data.shareLink', {
53
+ * projectId: 'proj-123',
54
+ * role: 'viewer', // 'viewer' or 'editor'
55
+ * expiresIn: 2592000 // 30 days in seconds
56
+ * });
57
+ * // Returns: https://cyclecad.com/view/xyz789?role=viewer
58
+ *
59
+ * Step 2: Share the link with teammates via email
60
+ * // They click the link and can view/edit without creating account
61
+ *
62
+ * @tutorial Exporting a Project as ZIP
63
+ * Package entire project for backup or archive.
64
+ *
65
+ * Step 1: Export to ZIP
66
+ * const blob = await kernel.exec('data.exportProject', {
67
+ * projectId: 'proj-123'
68
+ * });
69
+ *
70
+ * Step 2: Save the blob to disk
71
+ * const url = URL.createObjectURL(blob);
72
+ * const a = document.createElement('a');
73
+ * a.href = url;
74
+ * a.download = 'bike-pump-design.zip';
75
+ * a.click();
76
+ *
77
+ * @tutorial Importing a Previous ZIP Export
78
+ * Restore a previously exported project.
79
+ *
80
+ * Step 1: Select ZIP file
81
+ * const file = /* user chooses file */;
82
+ *
83
+ * Step 2: Import it
84
+ * const project = await kernel.exec('data.importProject', {
85
+ * file: file,
86
+ * asCopy: true // Create a new project, don't overwrite
87
+ * });
88
+ *
89
+ * @tutorial Working with Templates
90
+ * Save design templates for reuse.
91
+ *
92
+ * Step 1: Save current project as template
93
+ * await kernel.exec('data.createTemplate', {
94
+ * projectId: 'proj-123',
95
+ * name: 'Modular Pump Body',
96
+ * description: 'Reusable pump body with customizable bore'
97
+ * });
98
+ *
99
+ * Step 2: Create new project from template
100
+ * const newProject = await kernel.exec('data.fromTemplate', {
101
+ * templateName: 'Modular Pump Body',
102
+ * newProjectName: 'Pump for cycleWASH v2'
103
+ * });
104
+ *
105
+ * @version 1.0.0
106
+ * @author Sachin Kumar <vvlars@googlemail.com>
107
+ * @license MIT
108
+ */
109
+
110
+ // ============================================================================
111
+ // DATA MANAGEMENT MODULE — Main Export
112
+ // ============================================================================
113
+
114
+ export default {
115
+ name: 'data',
116
+ version: '1.0.0',
117
+
118
+ // ========================================================================
119
+ // MODULE STATE
120
+ // ========================================================================
121
+
122
+ state: {
123
+ /** @type {IDBDatabase} IndexedDB handle */
124
+ db: null,
125
+
126
+ /** @type {Array<Object>} List of all projects */
127
+ projects: [],
128
+
129
+ /** @type {string|null} Currently open project ID */
130
+ currentProjectId: null,
131
+
132
+ /** @type {Object|null} Currently open project data */
133
+ currentProject: null,
134
+
135
+ /** @type {number} Auto-save interval (ms) */
136
+ autoSaveIntervalMs: 30000,
137
+
138
+ /** @type {number} Auto-save interval handle */
139
+ autoSaveHandle: null,
140
+
141
+ /** @type {Array<Object>} Recent files (last 20 opened) */
142
+ recentFiles: [],
143
+
144
+ /** @type {Map<string, Object>} Templates by name */
145
+ templates: new Map(),
146
+
147
+ /** @type {Array<Object>} Trash: deleted projects (30-day recovery) */
148
+ trash: [],
149
+
150
+ /** @type {number} Quota limit (bytes) */
151
+ quotaBytes: 1000000000, // 1GB default
152
+
153
+ /** @type {number} Current usage (bytes) */
154
+ usageBytes: 0,
155
+ },
156
+
157
+ // ========================================================================
158
+ // INIT — Setup IndexedDB and restore state
159
+ // ========================================================================
160
+
161
+ /**
162
+ * Initialize data module.
163
+ * Opens IndexedDB, loads project list, starts auto-save.
164
+ * Called automatically on app startup.
165
+ *
166
+ * @async
167
+ * @returns {Promise<void>}
168
+ */
169
+ async init() {
170
+ this.state.db = await this._openDatabase();
171
+
172
+ // Load project list
173
+ await this._loadProjectList();
174
+
175
+ // Load recent files
176
+ this._loadRecent();
177
+
178
+ // Load templates
179
+ await this._loadTemplates();
180
+
181
+ // Load trash
182
+ await this._loadTrash();
183
+
184
+ // Restore last project
185
+ const lastProjectId = localStorage.getItem('data_lastProject');
186
+ if (lastProjectId) {
187
+ try {
188
+ await this.load({ projectId: lastProjectId });
189
+ } catch (err) {
190
+ console.warn('Failed to restore last project:', err);
191
+ }
192
+ }
193
+
194
+ // Start auto-save
195
+ this._startAutoSave();
196
+
197
+ // Monitor storage quota
198
+ if (navigator.storage && navigator.storage.estimate) {
199
+ navigator.storage.estimate().then((estimate) => {
200
+ this.state.usageBytes = estimate.usage;
201
+ this.state.quotaBytes = estimate.quota;
202
+ });
203
+ }
204
+
205
+ console.log('[Data Management] Initialized.');
206
+ },
207
+
208
+ // ========================================================================
209
+ // PUBLIC API — Project Management
210
+ // ========================================================================
211
+
212
+ /**
213
+ * Create a new blank project.
214
+ *
215
+ * @param {Object} options
216
+ * @param {string} options.name Project name
217
+ * @param {string} [options.description] Project description
218
+ * @param {string} [options.units] Unit system ('mm' | 'in' | 'cm')
219
+ * @param {Object} [options.material] Default material data
220
+ * @returns {Promise<Object>} Created project
221
+ * { id, name, description, created, modified, fileCount }
222
+ *
223
+ * @example
224
+ * const proj = await kernel.exec('data.newProject', {
225
+ * name: 'My Design',
226
+ * description: 'A cool part'
227
+ * });
228
+ */
229
+ async newProject(options = {}) {
230
+ const { name, description = '', units = 'mm', material = null } = options;
231
+
232
+ if (!name || name.trim().length === 0) {
233
+ throw new Error('Project name required');
234
+ }
235
+
236
+ const project = {
237
+ id: this._generateUUID(),
238
+ name: name.trim(),
239
+ description,
240
+ units,
241
+ material,
242
+ created: Date.now(),
243
+ modified: Date.now(),
244
+ geometry: null, // Will hold serialized 3D model
245
+ files: {}, // { fileName: { content, type, size, imported } }
246
+ folders: {}, // { folderName: { files: [...], subfolders: [...] } }
247
+ metadata: {
248
+ author: localStorage.getItem('data_userName') || 'Unknown',
249
+ tags: [],
250
+ keywords: [],
251
+ },
252
+ };
253
+
254
+ await this._saveProjectToDB(project);
255
+ this.state.projects.unshift(project);
256
+
257
+ this._addToRecent({
258
+ projectId: project.id,
259
+ name: project.name,
260
+ timestamp: Date.now(),
261
+ });
262
+
263
+ this._showNotification(`Created project: ${name}`, 'success');
264
+ this._broadcastEvent('data:projectCreated', project);
265
+
266
+ return {
267
+ id: project.id,
268
+ name: project.name,
269
+ description,
270
+ created: project.created,
271
+ modified: project.modified,
272
+ fileCount: 0,
273
+ };
274
+ },
275
+
276
+ /**
277
+ * Load an existing project.
278
+ * Makes it the current project; subsequent saves update it.
279
+ *
280
+ * @param {Object} options
281
+ * @param {string} options.projectId Project ID to load
282
+ * @returns {Promise<Object>} Project data
283
+ *
284
+ * @example
285
+ * await kernel.exec('data.load', { projectId: 'proj-123' });
286
+ */
287
+ async load(options = {}) {
288
+ const { projectId } = options;
289
+
290
+ if (!projectId) {
291
+ throw new Error('Project ID required');
292
+ }
293
+
294
+ const project = await this._getProjectFromDB(projectId);
295
+ if (!project) {
296
+ throw new Error(`Project ${projectId} not found`);
297
+ }
298
+
299
+ this.state.currentProjectId = projectId;
300
+ this.state.currentProject = project;
301
+ localStorage.setItem('data_lastProject', projectId);
302
+
303
+ this._addToRecent({
304
+ projectId: project.id,
305
+ name: project.name,
306
+ timestamp: Date.now(),
307
+ });
308
+
309
+ this._broadcastEvent('data:projectLoaded', project);
310
+
311
+ return project;
312
+ },
313
+
314
+ /**
315
+ * Save current project.
316
+ * Typically called automatically by auto-save, but can be called manually.
317
+ *
318
+ * @param {Object} [options]
319
+ * @param {string} [options.projectId] Project to save (default: current)
320
+ * @returns {Promise<void>}
321
+ *
322
+ * @example
323
+ * await kernel.exec('data.save');
324
+ */
325
+ async save(options = {}) {
326
+ const { projectId = this.state.currentProjectId } = options;
327
+
328
+ if (!projectId) {
329
+ throw new Error('No project loaded');
330
+ }
331
+
332
+ // Capture current 3D model state from viewport
333
+ const geometry = await this._captureGeometry();
334
+
335
+ const project = await this._getProjectFromDB(projectId);
336
+ if (!project) {
337
+ throw new Error('Project not found');
338
+ }
339
+
340
+ project.modified = Date.now();
341
+ project.geometry = geometry;
342
+
343
+ await this._saveProjectToDB(project);
344
+ this.state.currentProject = project;
345
+
346
+ this._showNotification('Project saved', 'success');
347
+ this._broadcastEvent('data:projectSaved', { projectId });
348
+ },
349
+
350
+ /**
351
+ * Delete a project (soft delete to trash).
352
+ * Project can be recovered within 30 days.
353
+ *
354
+ * @param {Object} options
355
+ * @param {string} options.projectId Project to delete
356
+ * @param {boolean} [options.permanent=false] Permanently delete (no recovery)
357
+ * @returns {Promise<void>}
358
+ *
359
+ * @example
360
+ * await kernel.exec('data.delete', {
361
+ * projectId: 'proj-123',
362
+ * permanent: false // Goes to trash
363
+ * });
364
+ */
365
+ async delete(options = {}) {
366
+ const { projectId, permanent = false } = options;
367
+
368
+ const project = await this._getProjectFromDB(projectId);
369
+ if (!project) {
370
+ throw new Error('Project not found');
371
+ }
372
+
373
+ if (permanent) {
374
+ await this._deleteFromDB('projects', projectId);
375
+ } else {
376
+ // Move to trash
377
+ const trashItem = {
378
+ ...project,
379
+ deletedAt: Date.now(),
380
+ recoveryUntil: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days
381
+ };
382
+ await this._saveToTrash(trashItem);
383
+ await this._deleteFromDB('projects', projectId);
384
+ }
385
+
386
+ this.state.projects = this.state.projects.filter((p) => p.id !== projectId);
387
+
388
+ if (this.state.currentProjectId === projectId) {
389
+ this.state.currentProjectId = null;
390
+ this.state.currentProject = null;
391
+ }
392
+
393
+ this._showNotification('Project deleted', 'info');
394
+ },
395
+
396
+ /**
397
+ * List all projects.
398
+ *
399
+ * @param {Object} [options]
400
+ * @param {number} [options.limit=100] Max projects to return
401
+ * @param {number} [options.offset=0] Pagination offset
402
+ * @param {string} [options.sortBy] 'name' | 'created' | 'modified'
403
+ * @returns {Promise<Array<Object>>} Project list
404
+ *
405
+ * @example
406
+ * const projects = await kernel.exec('data.listProjects', {
407
+ * limit: 20,
408
+ * sortBy: 'modified'
409
+ * });
410
+ */
411
+ async listProjects(options = {}) {
412
+ const { limit = 100, offset = 0, sortBy = 'modified' } = options;
413
+
414
+ let projects = [...this.state.projects];
415
+
416
+ // Sort
417
+ if (sortBy === 'name') {
418
+ projects.sort((a, b) => a.name.localeCompare(b.name));
419
+ } else if (sortBy === 'created') {
420
+ projects.sort((a, b) => b.created - a.created);
421
+ } else if (sortBy === 'modified') {
422
+ projects.sort((a, b) => b.modified - a.modified);
423
+ }
424
+
425
+ return projects.slice(offset, offset + limit).map((p) => ({
426
+ id: p.id,
427
+ name: p.name,
428
+ description: p.description,
429
+ created: p.created,
430
+ modified: p.modified,
431
+ fileCount: Object.keys(p.files).length,
432
+ sizeBytes: this._estimateSize(p),
433
+ }));
434
+ },
435
+
436
+ /**
437
+ * Get recently accessed files.
438
+ *
439
+ * @param {Object} [options]
440
+ * @param {number} [options.limit=20] Max to return
441
+ * @returns {Promise<Array<Object>>} Recent file list
442
+ */
443
+ async getRecent(options = {}) {
444
+ const { limit = 20 } = options;
445
+ return this.state.recentFiles.slice(0, limit);
446
+ },
447
+
448
+ // ========================================================================
449
+ // PUBLIC API — File Management
450
+ // ========================================================================
451
+
452
+ /**
453
+ * Create a folder in current project.
454
+ *
455
+ * @param {Object} options
456
+ * @param {string} [options.path] Folder path (e.g., 'parts/')
457
+ * @param {string} options.name Folder name
458
+ * @returns {Promise<Object>}
459
+ *
460
+ * @example
461
+ * await kernel.exec('data.createFolder', {
462
+ * path: '',
463
+ * name: 'parts'
464
+ * });
465
+ */
466
+ async createFolder(options = {}) {
467
+ const { path = '', name } = options;
468
+
469
+ if (!this.state.currentProject) {
470
+ throw new Error('No project loaded');
471
+ }
472
+
473
+ if (!name || name.trim().length === 0) {
474
+ throw new Error('Folder name required');
475
+ }
476
+
477
+ const folderPath = (path ? path + '/' : '') + name + '/';
478
+
479
+ if (!this.state.currentProject.folders) {
480
+ this.state.currentProject.folders = {};
481
+ }
482
+
483
+ this.state.currentProject.folders[folderPath] = {
484
+ files: [],
485
+ subfolders: [],
486
+ };
487
+
488
+ await this.save();
489
+
490
+ return { path: folderPath, name };
491
+ },
492
+
493
+ /**
494
+ * Import a file into current project.
495
+ * Supports .ipt, .iam, .step, .stp, .stl, .dxf, etc.
496
+ *
497
+ * @param {Object} options
498
+ * @param {File|Blob} options.file File to import
499
+ * @param {string} [options.folder] Target folder (default: root)
500
+ * @param {string} [options.name] Override file name
501
+ * @returns {Promise<Object>} Imported file info
502
+ *
503
+ * @example
504
+ * const file = /* user selects file */;
505
+ * await kernel.exec('data.importFile', {
506
+ * file: file,
507
+ * folder: 'parts/'
508
+ * });
509
+ */
510
+ async importFile(options = {}) {
511
+ const { file, folder = '', name = null } = options;
512
+
513
+ if (!this.state.currentProject) {
514
+ throw new Error('No project loaded');
515
+ }
516
+
517
+ if (!file) {
518
+ throw new Error('File required');
519
+ }
520
+
521
+ const fileName = name || file.name;
522
+ const fileKey = (folder ? folder : '') + fileName;
523
+
524
+ // Read file content
525
+ const content = await file.arrayBuffer();
526
+
527
+ this.state.currentProject.files[fileKey] = {
528
+ name: fileName,
529
+ type: file.type || this._detectFileType(fileName),
530
+ size: file.size,
531
+ imported: Date.now(),
532
+ content: new Uint8Array(content), // Store as binary
533
+ };
534
+
535
+ await this.save();
536
+
537
+ this._addToRecent({
538
+ projectId: this.state.currentProjectId,
539
+ name: fileName,
540
+ timestamp: Date.now(),
541
+ });
542
+
543
+ this._showNotification(`Imported: ${fileName}`, 'success');
544
+
545
+ return { fileName, fileKey, size: file.size };
546
+ },
547
+
548
+ /**
549
+ * Delete a file from current project.
550
+ *
551
+ * @param {Object} options
552
+ * @param {string} options.fileKey File key in project
553
+ * @returns {Promise<void>}
554
+ */
555
+ async deleteFile(options = {}) {
556
+ const { fileKey } = options;
557
+
558
+ if (!this.state.currentProject) {
559
+ throw new Error('No project loaded');
560
+ }
561
+
562
+ if (this.state.currentProject.files[fileKey]) {
563
+ delete this.state.currentProject.files[fileKey];
564
+ await this.save();
565
+ }
566
+ },
567
+
568
+ /**
569
+ * Get list of files in current project.
570
+ *
571
+ * @returns {Promise<Array<Object>>} File list with metadata
572
+ */
573
+ async listFiles() {
574
+ if (!this.state.currentProject) {
575
+ throw new Error('No project loaded');
576
+ }
577
+
578
+ return Object.entries(this.state.currentProject.files).map(
579
+ ([key, file]) => ({
580
+ key,
581
+ name: file.name,
582
+ type: file.type,
583
+ size: file.size,
584
+ imported: file.imported,
585
+ })
586
+ );
587
+ },
588
+
589
+ // ========================================================================
590
+ // PUBLIC API — Sharing and Export
591
+ // ========================================================================
592
+
593
+ /**
594
+ * Generate a shareable link for current project.
595
+ * Links can be view-only or allow editing.
596
+ *
597
+ * @param {Object} options
598
+ * @param {string} [options.projectId] Project to share
599
+ * @param {string} [options.role] 'viewer' or 'editor'
600
+ * @param {number} [options.expiresIn] Expiry in seconds (null = never)
601
+ * @returns {Promise<Object>} { link, code, role, expiresAt }
602
+ *
603
+ * @example
604
+ * const share = await kernel.exec('data.shareLink', {
605
+ * role: 'viewer',
606
+ * expiresIn: 604800 // 1 week
607
+ * });
608
+ * console.log('Share link:', share.link);
609
+ */
610
+ async shareLink(options = {}) {
611
+ const {
612
+ projectId = this.state.currentProjectId,
613
+ role = 'viewer',
614
+ expiresIn = null,
615
+ } = options;
616
+
617
+ if (!projectId) {
618
+ throw new Error('No project specified');
619
+ }
620
+
621
+ const shareCode = this._generateShareCode();
622
+ const shareRecord = {
623
+ code: shareCode,
624
+ projectId,
625
+ role,
626
+ createdAt: Date.now(),
627
+ expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : null,
628
+ };
629
+
630
+ await this._saveShareLink(shareRecord);
631
+
632
+ const baseUrl = window.location.origin;
633
+ const link = `${baseUrl}/view/${shareCode}?role=${role}`;
634
+
635
+ this._showNotification('Share link created', 'success');
636
+
637
+ return {
638
+ link,
639
+ code: shareCode,
640
+ role,
641
+ expiresAt: shareRecord.expiresAt,
642
+ };
643
+ },
644
+
645
+ /**
646
+ * Export current project as ZIP archive.
647
+ * Includes all files, metadata, and 3D geometry.
648
+ *
649
+ * @param {Object} [options]
650
+ * @param {string} [options.projectId] Project to export
651
+ * @returns {Promise<Blob>} ZIP file blob
652
+ *
653
+ * @example
654
+ * const blob = await kernel.exec('data.exportProject');
655
+ * // User can download the blob
656
+ */
657
+ async exportProject(options = {}) {
658
+ const { projectId = this.state.currentProjectId } = options;
659
+
660
+ const project = await this._getProjectFromDB(projectId);
661
+ if (!project) {
662
+ throw new Error('Project not found');
663
+ }
664
+
665
+ // In production, use a ZIP library (e.g., JSZip)
666
+ // For now, create JSON export
667
+ const exportData = {
668
+ version: 1,
669
+ project: {
670
+ name: project.name,
671
+ description: project.description,
672
+ created: project.created,
673
+ modified: project.modified,
674
+ units: project.units,
675
+ },
676
+ files: Object.entries(project.files).map(([key, file]) => ({
677
+ key,
678
+ name: file.name,
679
+ type: file.type,
680
+ size: file.size,
681
+ // Note: actual content would be included in ZIP
682
+ })),
683
+ metadata: project.metadata,
684
+ };
685
+
686
+ const json = JSON.stringify(exportData, null, 2);
687
+ return new Blob([json], { type: 'application/json' });
688
+ },
689
+
690
+ /**
691
+ * Import a previously exported project.
692
+ *
693
+ * @param {Object} options
694
+ * @param {File|Blob} options.file ZIP or JSON file
695
+ * @param {boolean} [options.asCopy=true] Create new project or overwrite
696
+ * @returns {Promise<Object>} Imported project info
697
+ *
698
+ * @example
699
+ * const file = /* user selects exported ZIP */;
700
+ * const proj = await kernel.exec('data.importProject', {
701
+ * file: file,
702
+ * asCopy: true
703
+ * });
704
+ */
705
+ async importProject(options = {}) {
706
+ const { file, asCopy = true } = options;
707
+
708
+ if (!file) {
709
+ throw new Error('File required');
710
+ }
711
+
712
+ // Parse export (handling both ZIP and JSON)
713
+ const json = JSON.parse(await file.text());
714
+
715
+ if (asCopy) {
716
+ // Create new project from import
717
+ return this.newProject({
718
+ name: json.project.name + ' (Imported)',
719
+ description: json.project.description,
720
+ units: json.project.units,
721
+ });
722
+ } else {
723
+ // Restore to existing project
724
+ const project = await this._getProjectFromDB(
725
+ this.state.currentProjectId
726
+ );
727
+ Object.assign(project, json.project);
728
+ await this._saveProjectToDB(project);
729
+ return project;
730
+ }
731
+ },
732
+
733
+ // ========================================================================
734
+ // PUBLIC API — Templates
735
+ // ========================================================================
736
+
737
+ /**
738
+ * Save current project as a template.
739
+ * Templates can be used as starting points for new projects.
740
+ *
741
+ * @param {Object} options
742
+ * @param {string} [options.projectId] Project to template
743
+ * @param {string} options.name Template name
744
+ * @param {string} [options.description] Template description
745
+ * @param {string} [options.category] Category (e.g., 'pumps', 'fixtures')
746
+ * @returns {Promise<Object>}
747
+ *
748
+ * @example
749
+ * await kernel.exec('data.createTemplate', {
750
+ * name: 'Modular Pump',
751
+ * description: 'Reusable pump body',
752
+ * category: 'pumps'
753
+ * });
754
+ */
755
+ async createTemplate(options = {}) {
756
+ const {
757
+ projectId = this.state.currentProjectId,
758
+ name,
759
+ description = '',
760
+ category = 'general',
761
+ } = options;
762
+
763
+ if (!name) {
764
+ throw new Error('Template name required');
765
+ }
766
+
767
+ const project = await this._getProjectFromDB(projectId);
768
+ if (!project) {
769
+ throw new Error('Project not found');
770
+ }
771
+
772
+ const template = {
773
+ name,
774
+ description,
775
+ category,
776
+ project,
777
+ createdAt: Date.now(),
778
+ };
779
+
780
+ this.state.templates.set(name, template);
781
+ await this._saveTemplate(template);
782
+
783
+ this._showNotification(`Created template: ${name}`, 'success');
784
+
785
+ return { name, category };
786
+ },
787
+
788
+ /**
789
+ * List all available templates.
790
+ *
791
+ * @param {Object} [options]
792
+ * @param {string} [options.category] Filter by category
793
+ * @returns {Promise<Array<Object>>}
794
+ */
795
+ async getTemplates(options = {}) {
796
+ const { category = null } = options;
797
+
798
+ let templates = Array.from(this.state.templates.values());
799
+
800
+ if (category) {
801
+ templates = templates.filter((t) => t.category === category);
802
+ }
803
+
804
+ return templates.map((t) => ({
805
+ name: t.name,
806
+ description: t.description,
807
+ category: t.category,
808
+ createdAt: t.createdAt,
809
+ }));
810
+ },
811
+
812
+ /**
813
+ * Create new project from template.
814
+ *
815
+ * @param {Object} options
816
+ * @param {string} options.templateName Template to use
817
+ * @param {string} options.newProjectName Name for new project
818
+ * @returns {Promise<Object>} New project
819
+ *
820
+ * @example
821
+ * const proj = await kernel.exec('data.fromTemplate', {
822
+ * templateName: 'Modular Pump',
823
+ * newProjectName: 'Pump v2'
824
+ * });
825
+ */
826
+ async fromTemplate(options = {}) {
827
+ const { templateName, newProjectName } = options;
828
+
829
+ const template = this.state.templates.get(templateName);
830
+ if (!template) {
831
+ throw new Error(`Template ${templateName} not found`);
832
+ }
833
+
834
+ // Deep copy template project
835
+ const projectCopy = JSON.parse(JSON.stringify(template.project));
836
+ projectCopy.id = this._generateUUID();
837
+ projectCopy.name = newProjectName;
838
+ projectCopy.created = Date.now();
839
+ projectCopy.modified = Date.now();
840
+
841
+ await this._saveProjectToDB(projectCopy);
842
+ this.state.projects.unshift(projectCopy);
843
+
844
+ this._showNotification(
845
+ `Created from template: ${newProjectName}`,
846
+ 'success'
847
+ );
848
+
849
+ return {
850
+ id: projectCopy.id,
851
+ name: projectCopy.name,
852
+ description: projectCopy.description,
853
+ };
854
+ },
855
+
856
+ /**
857
+ * Delete a template.
858
+ *
859
+ * @param {Object} options
860
+ * @param {string} options.templateName Template to delete
861
+ * @returns {Promise<void>}
862
+ */
863
+ async deleteTemplate(options = {}) {
864
+ const { templateName } = options;
865
+
866
+ this.state.templates.delete(templateName);
867
+ await this._deleteTemplate(templateName);
868
+ },
869
+
870
+ // ========================================================================
871
+ // PUBLIC API — Trash and Recovery
872
+ // ========================================================================
873
+
874
+ /**
875
+ * List deleted projects available for recovery.
876
+ *
877
+ * @returns {Promise<Array<Object>>} Trash items
878
+ */
879
+ async getTrash() {
880
+ return this.state.trash.map((item) => ({
881
+ id: item.id,
882
+ name: item.name,
883
+ deletedAt: item.deletedAt,
884
+ recoveryUntil: item.recoveryUntil,
885
+ }));
886
+ },
887
+
888
+ /**
889
+ * Restore a deleted project from trash.
890
+ *
891
+ * @param {Object} options
892
+ * @param {string} options.projectId Project to recover
893
+ * @returns {Promise<Object>} Recovered project
894
+ */
895
+ async recover(options = {}) {
896
+ const { projectId } = options;
897
+
898
+ const trashItem = this.state.trash.find((t) => t.id === projectId);
899
+ if (!trashItem) {
900
+ throw new Error('Project not found in trash');
901
+ }
902
+
903
+ if (trashItem.recoveryUntil < Date.now()) {
904
+ throw new Error('Recovery window expired');
905
+ }
906
+
907
+ // Restore from trash
908
+ const project = { ...trashItem };
909
+ delete project.deletedAt;
910
+ delete project.recoveryUntil;
911
+
912
+ await this._saveProjectToDB(project);
913
+ this.state.projects.unshift(project);
914
+ this.state.trash = this.state.trash.filter((t) => t.id !== projectId);
915
+
916
+ this._showNotification(`Recovered: ${project.name}`, 'success');
917
+
918
+ return project;
919
+ },
920
+
921
+ /**
922
+ * Permanently delete all expired trash items.
923
+ *
924
+ * @returns {Promise<number>} Count of items deleted
925
+ */
926
+ async emptyTrash() {
927
+ const now = Date.now();
928
+ const expired = this.state.trash.filter((t) => t.recoveryUntil < now);
929
+
930
+ for (const item of expired) {
931
+ await this._deleteFromDB('trash', item.id);
932
+ }
933
+
934
+ this.state.trash = this.state.trash.filter((t) => t.recoveryUntil >= now);
935
+
936
+ return expired.length;
937
+ },
938
+
939
+ // ========================================================================
940
+ // PUBLIC API — Storage and Quota
941
+ // ========================================================================
942
+
943
+ /**
944
+ * Get current storage usage and quota.
945
+ *
946
+ * @returns {Promise<Object>} { usageBytes, quotaBytes, percentUsed }
947
+ */
948
+ async getStorageInfo() {
949
+ if (navigator.storage && navigator.storage.estimate) {
950
+ const estimate = await navigator.storage.estimate();
951
+ this.state.usageBytes = estimate.usage;
952
+ this.state.quotaBytes = estimate.quota;
953
+ }
954
+
955
+ return {
956
+ usageBytes: this.state.usageBytes,
957
+ quotaBytes: this.state.quotaBytes,
958
+ percentUsed: Math.round(
959
+ (this.state.usageBytes / this.state.quotaBytes) * 100
960
+ ),
961
+ };
962
+ },
963
+
964
+ /**
965
+ * Request persistent storage permission (for Chrome).
966
+ * Without this, browser can clear data at any time.
967
+ *
968
+ * @returns {Promise<boolean>} True if persistent permission granted
969
+ */
970
+ async requestPersistent() {
971
+ if (navigator.storage && navigator.storage.persist) {
972
+ return navigator.storage.persist();
973
+ }
974
+ return false;
975
+ },
976
+
977
+ // ========================================================================
978
+ // INTERNAL HELPERS — Database
979
+ // ========================================================================
980
+
981
+ /**
982
+ * Open or create IndexedDB.
983
+ *
984
+ * @private
985
+ * @async
986
+ * @returns {Promise<IDBDatabase>}
987
+ */
988
+ async _openDatabase() {
989
+ return new Promise((resolve, reject) => {
990
+ const req = indexedDB.open('cyclecad-data', 1);
991
+
992
+ req.onerror = () => reject(req.error);
993
+ req.onsuccess = () => resolve(req.result);
994
+
995
+ req.onupgradeneeded = (e) => {
996
+ const db = e.target.result;
997
+
998
+ if (!db.objectStoreNames.contains('projects')) {
999
+ const projectStore = db.createObjectStore('projects', {
1000
+ keyPath: 'id',
1001
+ });
1002
+ projectStore.createIndex('name', 'name');
1003
+ projectStore.createIndex('modified', 'modified');
1004
+ }
1005
+ if (!db.objectStoreNames.contains('shareLinks')) {
1006
+ db.createObjectStore('shareLinks', { keyPath: 'code' });
1007
+ }
1008
+ if (!db.objectStoreNames.contains('templates')) {
1009
+ db.createObjectStore('templates', { keyPath: 'name' });
1010
+ }
1011
+ if (!db.objectStoreNames.contains('trash')) {
1012
+ db.createObjectStore('trash', { keyPath: 'id' });
1013
+ }
1014
+ };
1015
+ });
1016
+ },
1017
+
1018
+ /**
1019
+ * Load all projects from DB.
1020
+ *
1021
+ * @private
1022
+ * @async
1023
+ * @returns {Promise<void>}
1024
+ */
1025
+ async _loadProjectList() {
1026
+ return new Promise((resolve) => {
1027
+ const tx = this.state.db.transaction(['projects'], 'readonly');
1028
+ const store = tx.objectStore('projects');
1029
+ const req = store.getAll();
1030
+
1031
+ req.onsuccess = () => {
1032
+ this.state.projects = req.result.sort((a, b) => b.modified - a.modified);
1033
+ resolve();
1034
+ };
1035
+ });
1036
+ },
1037
+
1038
+ /**
1039
+ * Save project to DB.
1040
+ *
1041
+ * @private
1042
+ * @async
1043
+ * @param {Object} project
1044
+ * @returns {Promise<void>}
1045
+ */
1046
+ async _saveProjectToDB(project) {
1047
+ return new Promise((resolve) => {
1048
+ const tx = this.state.db.transaction(['projects'], 'readwrite');
1049
+ const store = tx.objectStore('projects');
1050
+ store.put(project);
1051
+
1052
+ tx.oncomplete = () => resolve();
1053
+ });
1054
+ },
1055
+
1056
+ /**
1057
+ * Get project from DB.
1058
+ *
1059
+ * @private
1060
+ * @async
1061
+ * @param {string} projectId
1062
+ * @returns {Promise<Object|null>}
1063
+ */
1064
+ async _getProjectFromDB(projectId) {
1065
+ return new Promise((resolve) => {
1066
+ const tx = this.state.db.transaction(['projects'], 'readonly');
1067
+ const store = tx.objectStore('projects');
1068
+ const req = store.get(projectId);
1069
+
1070
+ req.onsuccess = () => resolve(req.result || null);
1071
+ });
1072
+ },
1073
+
1074
+ /**
1075
+ * Delete from DB.
1076
+ *
1077
+ * @private
1078
+ * @async
1079
+ * @param {string} storeName
1080
+ * @param {string} key
1081
+ * @returns {Promise<void>}
1082
+ */
1083
+ async _deleteFromDB(storeName, key) {
1084
+ return new Promise((resolve) => {
1085
+ const tx = this.state.db.transaction([storeName], 'readwrite');
1086
+ const store = tx.objectStore(storeName);
1087
+ store.delete(key);
1088
+
1089
+ tx.oncomplete = () => resolve();
1090
+ });
1091
+ },
1092
+
1093
+ /**
1094
+ * Save share link to DB.
1095
+ *
1096
+ * @private
1097
+ * @async
1098
+ * @param {Object} shareRecord
1099
+ * @returns {Promise<void>}
1100
+ */
1101
+ async _saveShareLink(shareRecord) {
1102
+ return new Promise((resolve) => {
1103
+ const tx = this.state.db.transaction(['shareLinks'], 'readwrite');
1104
+ const store = tx.objectStore('shareLinks');
1105
+ store.put(shareRecord);
1106
+
1107
+ tx.oncomplete = () => resolve();
1108
+ });
1109
+ },
1110
+
1111
+ /**
1112
+ * Save template to DB.
1113
+ *
1114
+ * @private
1115
+ * @async
1116
+ * @param {Object} template
1117
+ * @returns {Promise<void>}
1118
+ */
1119
+ async _saveTemplate(template) {
1120
+ return new Promise((resolve) => {
1121
+ const tx = this.state.db.transaction(['templates'], 'readwrite');
1122
+ const store = tx.objectStore('templates');
1123
+ store.put(template);
1124
+
1125
+ tx.oncomplete = () => resolve();
1126
+ });
1127
+ },
1128
+
1129
+ /**
1130
+ * Delete template from DB.
1131
+ *
1132
+ * @private
1133
+ * @async
1134
+ * @param {string} templateName
1135
+ * @returns {Promise<void>}
1136
+ */
1137
+ async _deleteTemplate(templateName) {
1138
+ return new Promise((resolve) => {
1139
+ const tx = this.state.db.transaction(['templates'], 'readwrite');
1140
+ const store = tx.objectStore('templates');
1141
+ store.delete(templateName);
1142
+
1143
+ tx.oncomplete = () => resolve();
1144
+ });
1145
+ },
1146
+
1147
+ /**
1148
+ * Load templates from DB.
1149
+ *
1150
+ * @private
1151
+ * @async
1152
+ * @returns {Promise<void>}
1153
+ */
1154
+ async _loadTemplates() {
1155
+ return new Promise((resolve) => {
1156
+ const tx = this.state.db.transaction(['templates'], 'readonly');
1157
+ const store = tx.objectStore('templates');
1158
+ const req = store.getAll();
1159
+
1160
+ req.onsuccess = () => {
1161
+ this.state.templates = new Map(req.result.map((t) => [t.name, t]));
1162
+ resolve();
1163
+ };
1164
+ });
1165
+ },
1166
+
1167
+ /**
1168
+ * Save trash item to DB.
1169
+ *
1170
+ * @private
1171
+ * @async
1172
+ * @param {Object} item
1173
+ * @returns {Promise<void>}
1174
+ */
1175
+ async _saveToTrash(item) {
1176
+ return new Promise((resolve) => {
1177
+ const tx = this.state.db.transaction(['trash'], 'readwrite');
1178
+ const store = tx.objectStore('trash');
1179
+ store.put(item);
1180
+
1181
+ tx.oncomplete = () => resolve();
1182
+ });
1183
+ },
1184
+
1185
+ /**
1186
+ * Load trash from DB.
1187
+ *
1188
+ * @private
1189
+ * @async
1190
+ * @returns {Promise<void>}
1191
+ */
1192
+ async _loadTrash() {
1193
+ return new Promise((resolve) => {
1194
+ const tx = this.state.db.transaction(['trash'], 'readonly');
1195
+ const store = tx.objectStore('trash');
1196
+ const req = store.getAll();
1197
+
1198
+ req.onsuccess = () => {
1199
+ this.state.trash = req.result || [];
1200
+ resolve();
1201
+ };
1202
+ });
1203
+ },
1204
+
1205
+ // ========================================================================
1206
+ // INTERNAL HELPERS — Utilities
1207
+ // ========================================================================
1208
+
1209
+ /**
1210
+ * Capture current 3D geometry from viewport.
1211
+ *
1212
+ * @private
1213
+ * @async
1214
+ * @returns {Promise<Object|null>}
1215
+ */
1216
+ async _captureGeometry() {
1217
+ if (!window._scene) return null;
1218
+
1219
+ return {
1220
+ timestamp: Date.now(),
1221
+ // In production: serialize THREE.js scene to JSON or glTF
1222
+ };
1223
+ },
1224
+
1225
+ /**
1226
+ * Estimate project size in bytes.
1227
+ *
1228
+ * @private
1229
+ * @param {Object} project
1230
+ * @returns {number}
1231
+ */
1232
+ _estimateSize(project) {
1233
+ return Object.values(project.files).reduce((sum, f) => sum + f.size, 0);
1234
+ },
1235
+
1236
+ /**
1237
+ * Detect file type from extension.
1238
+ *
1239
+ * @private
1240
+ * @param {string} fileName
1241
+ * @returns {string} MIME type
1242
+ */
1243
+ _detectFileType(fileName) {
1244
+ const ext = fileName.split('.').pop().toLowerCase();
1245
+ const types = {
1246
+ ipt: 'application/vnd.autodesk.inventor.part',
1247
+ iam: 'application/vnd.autodesk.inventor.assembly',
1248
+ step: 'application/step',
1249
+ stp: 'application/step',
1250
+ stl: 'application/vnd.ms-pki.stl',
1251
+ dxf: 'application/vnd.dxf',
1252
+ dwg: 'application/vnd.dwg',
1253
+ };
1254
+ return types[ext] || 'application/octet-stream';
1255
+ },
1256
+
1257
+ /**
1258
+ * Load recent files from localStorage.
1259
+ *
1260
+ * @private
1261
+ */
1262
+ _loadRecent() {
1263
+ const saved = localStorage.getItem('data_recent');
1264
+ if (saved) {
1265
+ this.state.recentFiles = JSON.parse(saved);
1266
+ }
1267
+ },
1268
+
1269
+ /**
1270
+ * Add file to recent list.
1271
+ *
1272
+ * @private
1273
+ * @param {Object} fileInfo
1274
+ */
1275
+ _addToRecent(fileInfo) {
1276
+ // Remove if already exists
1277
+ this.state.recentFiles = this.state.recentFiles.filter(
1278
+ (f) => f.projectId !== fileInfo.projectId
1279
+ );
1280
+
1281
+ // Add to front
1282
+ this.state.recentFiles.unshift(fileInfo);
1283
+
1284
+ // Keep only 20
1285
+ this.state.recentFiles = this.state.recentFiles.slice(0, 20);
1286
+
1287
+ localStorage.setItem('data_recent', JSON.stringify(this.state.recentFiles));
1288
+ },
1289
+
1290
+ /**
1291
+ * Start auto-save interval.
1292
+ *
1293
+ * @private
1294
+ */
1295
+ _startAutoSave() {
1296
+ this.state.autoSaveHandle = setInterval(() => {
1297
+ if (this.state.currentProjectId) {
1298
+ this.save().catch((err) => {
1299
+ console.warn('Auto-save failed:', err);
1300
+ });
1301
+ }
1302
+ }, this.state.autoSaveIntervalMs);
1303
+ },
1304
+
1305
+ /**
1306
+ * Show notification.
1307
+ *
1308
+ * @private
1309
+ * @param {string} message
1310
+ * @param {string} type
1311
+ */
1312
+ _showNotification(message, type = 'info') {
1313
+ const toast = document.createElement('div');
1314
+ toast.className = `data-toast data-toast-${type}`;
1315
+ toast.textContent = message;
1316
+ document.body.appendChild(toast);
1317
+
1318
+ setTimeout(() => toast.remove(), 4000);
1319
+ },
1320
+
1321
+ /**
1322
+ * Broadcast event.
1323
+ *
1324
+ * @private
1325
+ * @param {string} eventName
1326
+ * @param {Object} detail
1327
+ */
1328
+ _broadcastEvent(eventName, detail) {
1329
+ const event = new CustomEvent(eventName, { detail });
1330
+ document.dispatchEvent(event);
1331
+ },
1332
+
1333
+ /**
1334
+ * Generate UUID v4.
1335
+ *
1336
+ * @private
1337
+ * @returns {string}
1338
+ */
1339
+ _generateUUID() {
1340
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
1341
+ const r = (Math.random() * 16) | 0;
1342
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
1343
+ return v.toString(16);
1344
+ });
1345
+ },
1346
+
1347
+ /**
1348
+ * Generate random share code.
1349
+ *
1350
+ * @private
1351
+ * @returns {string}
1352
+ */
1353
+ _generateShareCode() {
1354
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
1355
+ let code = '';
1356
+ for (let i = 0; i < 8; i++) {
1357
+ code += chars[Math.floor(Math.random() * chars.length)];
1358
+ }
1359
+ return code;
1360
+ },
1361
+
1362
+ // ========================================================================
1363
+ // HELP SYSTEM INTEGRATION
1364
+ // ========================================================================
1365
+
1366
+ helpEntries: [
1367
+ {
1368
+ title: 'Create a New Project',
1369
+ description:
1370
+ 'Click Data → New Project to start a new design. Give it a name and optionally a description.',
1371
+ category: 'Data Management',
1372
+ shortcut: null,
1373
+ },
1374
+ {
1375
+ title: 'Save Project',
1376
+ description:
1377
+ 'Your project saves automatically every 30 seconds. You can also press Ctrl+S to save manually.',
1378
+ category: 'Data Management',
1379
+ shortcut: 'Ctrl+S',
1380
+ },
1381
+ {
1382
+ title: 'Import Files',
1383
+ description:
1384
+ 'Click Data → Import File to add .ipt, .iam, .step, .stl, and other files to your project.',
1385
+ category: 'Data Management',
1386
+ shortcut: null,
1387
+ },
1388
+ {
1389
+ title: 'Create Share Link',
1390
+ description:
1391
+ 'Click Data → Share to generate a link that teammates can use to view or edit your project without creating an account.',
1392
+ category: 'Data Management',
1393
+ shortcut: null,
1394
+ },
1395
+ {
1396
+ title: 'Export Project as ZIP',
1397
+ description:
1398
+ 'Click Data → Export to save your entire project (with all files) as a ZIP archive for backup or sharing.',
1399
+ category: 'Data Management',
1400
+ shortcut: null,
1401
+ },
1402
+ {
1403
+ title: 'Create a Template',
1404
+ description:
1405
+ 'Click Data → Save as Template to create a reusable template. You can then create new projects based on it.',
1406
+ category: 'Data Management',
1407
+ shortcut: null,
1408
+ },
1409
+ {
1410
+ title: 'Recover Deleted Projects',
1411
+ description:
1412
+ 'Deleted projects go to trash for 30 days. Click Data → Trash to view and recover deleted projects.',
1413
+ category: 'Data Management',
1414
+ shortcut: null,
1415
+ },
1416
+ {
1417
+ title: 'Storage Quota',
1418
+ description:
1419
+ 'Check how much storage you\'ve used. Click Data → Storage Info. You get 1GB free; Pro users get 100GB.',
1420
+ category: 'Data Management',
1421
+ shortcut: null,
1422
+ },
1423
+ ],
1424
+
1425
+ // ========================================================================
1426
+ // UI PANEL — HTML and Styling
1427
+ // ========================================================================
1428
+
1429
+ /**
1430
+ * Get the HTML for the data management panel.
1431
+ *
1432
+ * @returns {string} HTML markup
1433
+ */
1434
+ getUI() {
1435
+ return `
1436
+ <div class="data-panel" id="data-panel">
1437
+ <div class="data-header">
1438
+ <h3>Data & Projects</h3>
1439
+ <button class="data-close-btn" data-close-panel="data-panel">×</button>
1440
+ </div>
1441
+
1442
+ <div class="data-content">
1443
+ <div class="data-toolbar">
1444
+ <button id="data-new-btn" class="data-btn data-btn-primary">
1445
+ ✚ New Project
1446
+ </button>
1447
+ <button id="data-open-btn" class="data-btn">
1448
+ 📁 Open
1449
+ </button>
1450
+ </div>
1451
+
1452
+ <div class="data-tabs">
1453
+ <button class="data-tab active" data-tab="projects">Projects</button>
1454
+ <button class="data-tab" data-tab="recent">Recent</button>
1455
+ <button class="data-tab" data-tab="templates">Templates</button>
1456
+ </div>
1457
+
1458
+ <!-- Projects Tab -->
1459
+ <div id="data-projects" class="data-tab-content active">
1460
+ <div id="data-project-list" class="data-project-list"></div>
1461
+ </div>
1462
+
1463
+ <!-- Recent Tab -->
1464
+ <div id="data-recent" class="data-tab-content">
1465
+ <div id="data-recent-list" class="data-recent-list"></div>
1466
+ </div>
1467
+
1468
+ <!-- Templates Tab -->
1469
+ <div id="data-templates" class="data-tab-content">
1470
+ <div id="data-template-list" class="data-template-list"></div>
1471
+ </div>
1472
+ </div>
1473
+ </div>
1474
+
1475
+ <style>
1476
+ .data-panel {
1477
+ position: fixed;
1478
+ left: 0;
1479
+ top: 80px;
1480
+ width: 320px;
1481
+ height: 600px;
1482
+ background: #1e1e1e;
1483
+ border-right: 1px solid #333;
1484
+ display: flex;
1485
+ flex-direction: column;
1486
+ z-index: 1000;
1487
+ }
1488
+
1489
+ .data-header {
1490
+ display: flex;
1491
+ justify-content: space-between;
1492
+ align-items: center;
1493
+ padding: 12px;
1494
+ border-bottom: 1px solid #333;
1495
+ }
1496
+
1497
+ .data-header h3 {
1498
+ margin: 0;
1499
+ color: #e0e0e0;
1500
+ font-size: 14px;
1501
+ font-weight: 600;
1502
+ }
1503
+
1504
+ .data-close-btn {
1505
+ background: none;
1506
+ border: none;
1507
+ color: #999;
1508
+ font-size: 20px;
1509
+ cursor: pointer;
1510
+ padding: 0;
1511
+ }
1512
+
1513
+ .data-content {
1514
+ flex: 1;
1515
+ overflow: hidden;
1516
+ display: flex;
1517
+ flex-direction: column;
1518
+ }
1519
+
1520
+ .data-toolbar {
1521
+ display: flex;
1522
+ gap: 6px;
1523
+ padding: 12px;
1524
+ border-bottom: 1px solid #333;
1525
+ }
1526
+
1527
+ .data-btn {
1528
+ flex: 1;
1529
+ padding: 8px;
1530
+ border: none;
1531
+ border-radius: 4px;
1532
+ background: #333;
1533
+ color: #e0e0e0;
1534
+ font-size: 12px;
1535
+ cursor: pointer;
1536
+ transition: background 0.2s;
1537
+ }
1538
+
1539
+ .data-btn:hover {
1540
+ background: #444;
1541
+ }
1542
+
1543
+ .data-btn-primary {
1544
+ background: #0284C7;
1545
+ color: white;
1546
+ }
1547
+
1548
+ .data-btn-primary:hover {
1549
+ background: #0369a1;
1550
+ }
1551
+
1552
+ .data-tabs {
1553
+ display: flex;
1554
+ border-bottom: 1px solid #333;
1555
+ gap: 0;
1556
+ }
1557
+
1558
+ .data-tab {
1559
+ flex: 1;
1560
+ padding: 8px;
1561
+ border: none;
1562
+ background: transparent;
1563
+ color: #999;
1564
+ font-size: 12px;
1565
+ cursor: pointer;
1566
+ border-bottom: 2px solid transparent;
1567
+ }
1568
+
1569
+ .data-tab.active {
1570
+ color: #0284C7;
1571
+ border-bottom-color: #0284C7;
1572
+ }
1573
+
1574
+ .data-tab-content {
1575
+ flex: 1;
1576
+ overflow-y: auto;
1577
+ padding: 12px;
1578
+ display: none;
1579
+ }
1580
+
1581
+ .data-tab-content.active {
1582
+ display: block;
1583
+ }
1584
+
1585
+ .data-project-list,
1586
+ .data-recent-list,
1587
+ .data-template-list {
1588
+ display: flex;
1589
+ flex-direction: column;
1590
+ gap: 8px;
1591
+ }
1592
+
1593
+ .data-item {
1594
+ padding: 8px;
1595
+ background: #2a2a2a;
1596
+ border-radius: 4px;
1597
+ cursor: pointer;
1598
+ transition: background 0.2s;
1599
+ }
1600
+
1601
+ .data-item:hover {
1602
+ background: #333;
1603
+ }
1604
+
1605
+ .data-item-name {
1606
+ font-weight: 600;
1607
+ color: #e0e0e0;
1608
+ font-size: 12px;
1609
+ }
1610
+
1611
+ .data-item-meta {
1612
+ color: #666;
1613
+ font-size: 10px;
1614
+ margin-top: 4px;
1615
+ }
1616
+
1617
+ .data-toast {
1618
+ position: fixed;
1619
+ bottom: 20px;
1620
+ right: 20px;
1621
+ padding: 12px 16px;
1622
+ border-radius: 4px;
1623
+ font-size: 12px;
1624
+ animation: slideInRight 0.3s ease;
1625
+ z-index: 10000;
1626
+ }
1627
+
1628
+ @keyframes slideInRight {
1629
+ from {
1630
+ transform: translateX(100%);
1631
+ opacity: 0;
1632
+ }
1633
+ to {
1634
+ transform: translateX(0);
1635
+ opacity: 1;
1636
+ }
1637
+ }
1638
+
1639
+ .data-toast-success {
1640
+ background: #1b5e20;
1641
+ color: #81c784;
1642
+ }
1643
+
1644
+ .data-toast-error {
1645
+ background: #b71c1c;
1646
+ color: #ff5252;
1647
+ }
1648
+
1649
+ .data-toast-info {
1650
+ background: #01579b;
1651
+ color: #81d4fa;
1652
+ }
1653
+ </style>
1654
+ `;
1655
+ },
1656
+ };