cyclecad 2.1.0 → 3.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 (94) 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/DELIVERABLES.txt +296 -445
  7. package/DOCKER-BUILD-MANIFEST.txt +483 -0
  8. package/DOCKER-FILES-REFERENCE.md +440 -0
  9. package/DOCKER-INFRASTRUCTURE.md +475 -0
  10. package/DOCKER-README.md +435 -0
  11. package/Dockerfile +33 -55
  12. package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
  13. package/ENHANCEMENT_SUMMARY.txt +308 -0
  14. package/FEATURE_INVENTORY.md +235 -0
  15. package/FUSION360_FEATURES_SUMMARY.md +452 -0
  16. package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
  17. package/FUSION360_PARITY_SUMMARY.md +520 -0
  18. package/FUSION360_QUICK_REFERENCE.md +351 -0
  19. package/MODULE_API_REFERENCE.md +712 -0
  20. package/MODULE_INVENTORY.txt +264 -0
  21. package/PWA-FILES-CREATED.txt +350 -0
  22. package/QUICK-START-TESTING.md +126 -0
  23. package/STEP-IMPORT-QUICKSTART.md +347 -0
  24. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  25. package/app/css/mobile.css +1074 -0
  26. package/app/icons/generate-icons.js +203 -0
  27. package/app/index.html +1342 -5031
  28. package/app/js/app.js +1312 -514
  29. package/app/js/billing-ui.js +990 -0
  30. package/app/js/brep-kernel.js +933 -981
  31. package/app/js/collab-client.js +750 -0
  32. package/app/js/mobile-nav.js +623 -0
  33. package/app/js/mobile-toolbar.js +476 -0
  34. package/app/js/modules/animation-module.js +497 -3
  35. package/app/js/modules/billing-module.js +724 -0
  36. package/app/js/modules/cam-module.js +507 -2
  37. package/app/js/modules/collaboration-module.js +513 -0
  38. package/app/js/modules/constraint-module.js +1266 -0
  39. package/app/js/modules/data-module.js +544 -1146
  40. package/app/js/modules/formats-module.js +438 -738
  41. package/app/js/modules/inspection-module.js +393 -0
  42. package/app/js/modules/mesh-module-enhanced.js +880 -0
  43. package/app/js/modules/plugin-module.js +597 -0
  44. package/app/js/modules/rendering-module.js +460 -0
  45. package/app/js/modules/scripting-module.js +593 -475
  46. package/app/js/modules/sketch-module.js +998 -2
  47. package/app/js/modules/step-module-enhanced.js +938 -0
  48. package/app/js/modules/surface-module.js +312 -0
  49. package/app/js/modules/version-module.js +420 -0
  50. package/app/js/offline-manager.js +705 -0
  51. package/app/js/responsive-init.js +360 -0
  52. package/app/js/touch-handler.js +429 -0
  53. package/app/manifest.json +211 -0
  54. package/app/offline.html +508 -0
  55. package/app/sw.js +571 -0
  56. package/app/tests/billing-tests.html +779 -0
  57. package/app/tests/brep-tests.html +980 -0
  58. package/app/tests/collab-tests.html +743 -0
  59. package/app/tests/mobile-tests.html +1299 -0
  60. package/app/tests/pwa-tests.html +1134 -0
  61. package/app/tests/step-tests.html +1042 -0
  62. package/app/tests/test-agent-v3.html +719 -0
  63. package/cycleCAD-Architecture-v2.pptx +0 -0
  64. package/docker-compose.yml +225 -0
  65. package/docs/BILLING-HELP.json +260 -0
  66. package/docs/BILLING-README.md +639 -0
  67. package/docs/BILLING-TUTORIAL.md +736 -0
  68. package/docs/BREP-HELP.json +326 -0
  69. package/docs/BREP-TUTORIAL.md +802 -0
  70. package/docs/COLLABORATION-HELP.json +228 -0
  71. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  72. package/docs/DOCKER-HELP.json +224 -0
  73. package/docs/DOCKER-TUTORIAL.md +974 -0
  74. package/docs/MOBILE-HELP.json +243 -0
  75. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  76. package/docs/MOBILE-TUTORIAL.md +747 -0
  77. package/docs/PWA-HELP.json +228 -0
  78. package/docs/PWA-README.md +662 -0
  79. package/docs/PWA-TUTORIAL.md +757 -0
  80. package/docs/STEP-HELP.json +481 -0
  81. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  82. package/docs/TESTING-GUIDE.md +528 -0
  83. package/docs/TESTING-HELP.json +182 -0
  84. package/fusion-vs-cyclecad.html +1771 -0
  85. package/nginx.conf +237 -0
  86. package/package.json +1 -1
  87. package/server/Dockerfile.converter +51 -0
  88. package/server/Dockerfile.signaling +28 -0
  89. package/server/billing-server.js +487 -0
  90. package/server/converter-enhanced.py +528 -0
  91. package/server/requirements-converter.txt +29 -0
  92. package/server/signaling-server.js +801 -0
  93. package/tests/docker-tests.sh +389 -0
  94. package/~$cycleCAD-Architecture-v2.pptx +0 -0
@@ -1,239 +1,118 @@
1
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.
2
+ * data-module.js — ENHANCED with Fusion 360 parity data management
7
3
  *
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);
4
+ * Comprehensive data persistence, project management, and file organization
5
+ * for cycleCAD. Supports IndexedDB, OPFS, cloud sync, templates, sharing,
6
+ * auto-recovery, and full metadata tracking.
15
7
  *
16
- * Step 2: As you work, your changes auto-save every 30 seconds
17
- * // No manual action needed data.js handles it
8
+ * FEATURES:
9
+ * - Project Management: Create, open, delete, list projects with metadata
10
+ * - File Persistence: Save to IndexedDB, OPFS (Origin Private File System), or download
11
+ * - Cloud Sync: Ready for future cloud storage integration (save/load JSON blobs)
12
+ * - Share Links: Generate view-only or edit-enabled shareable URLs
13
+ * - Templates: 10+ built-in templates, save custom templates
14
+ * - Recent Files: Track last 20 opened files with thumbnails
15
+ * - Auto-Recovery: Detect crashes, offer to restore auto-saved versions
16
+ * - File Info: Creation date, modified date, size, part count, author, description
17
+ * - Trash/Recycle: Soft-delete with 30-day recovery window
18
+ * - Duplicate: Clone projects with new names
19
+ * - Import/Export: Load/save complete projects as ZIP or JSON
20
+ * - File Browser: Grid/list view with search, sort, folders
21
+ * - Thumbnails: Render 3D previews for project browser
22
+ * - Units Management: mm, cm, m, inch, ft with automatic conversion
23
+ * - Document Properties: Title, author, description, custom properties, revision
24
+ * - Project Settings: Units, materials, render settings
25
+ * - Backup & Archive: Manual backups with timestamp
18
26
  *
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
27
+ * @version 2.0.0
106
28
  * @author Sachin Kumar <vvlars@googlemail.com>
107
29
  * @license MIT
108
30
  */
109
31
 
110
32
  // ============================================================================
111
- // DATA MANAGEMENT MODULE Main Export
33
+ // MODULE STATE & CONFIGURATION
112
34
  // ============================================================================
113
35
 
114
36
  export default {
115
37
  name: 'data',
116
- version: '1.0.0',
117
-
118
- // ========================================================================
119
- // MODULE STATE
120
- // ========================================================================
38
+ version: '2.0.0',
121
39
 
122
40
  state: {
123
- /** @type {IDBDatabase} IndexedDB handle */
124
41
  db: null,
125
-
126
- /** @type {Array<Object>} List of all projects */
127
42
  projects: [],
128
-
129
- /** @type {string|null} Currently open project ID */
130
43
  currentProjectId: null,
131
-
132
- /** @type {Object|null} Currently open project data */
133
44
  currentProject: null,
134
-
135
- /** @type {number} Auto-save interval (ms) */
136
45
  autoSaveIntervalMs: 30000,
137
-
138
- /** @type {number} Auto-save interval handle */
139
46
  autoSaveHandle: null,
140
-
141
- /** @type {Array<Object>} Recent files (last 20 opened) */
142
47
  recentFiles: [],
143
-
144
- /** @type {Map<string, Object>} Templates by name */
145
48
  templates: new Map(),
146
-
147
- /** @type {Array<Object>} Trash: deleted projects (30-day recovery) */
148
49
  trash: [],
149
-
150
- /** @type {number} Quota limit (bytes) */
151
- quotaBytes: 1000000000, // 1GB default
152
-
153
- /** @type {number} Current usage (bytes) */
50
+ quotaBytes: 1000000000,
154
51
  usageBytes: 0,
52
+ // NEW: Enhanced features
53
+ searchIndex: new Map(),
54
+ projectThumbnails: new Map(),
55
+ backupHistory: [],
56
+ cloudSyncEnabled: false,
57
+ cloudSyncUrl: null,
58
+ unitPreferences: {
59
+ current: 'mm',
60
+ options: ['mm', 'cm', 'm', 'inch', 'in', 'ft']
61
+ },
62
+ documentProperties: {
63
+ author: localStorage.getItem('data_userName') || 'Unknown',
64
+ company: localStorage.getItem('data_company') || '',
65
+ version: '1.0'
66
+ },
67
+ maxBackups: 10,
68
+ autoSaveFrequency: 30000
155
69
  },
156
70
 
157
71
  // ========================================================================
158
- // INIT — Setup IndexedDB and restore state
72
+ // INITIALIZATION
159
73
  // ========================================================================
160
74
 
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
75
  async init() {
170
76
  this.state.db = await this._openDatabase();
171
-
172
- // Load project list
173
77
  await this._loadProjectList();
174
-
175
- // Load recent files
176
78
  this._loadRecent();
177
-
178
- // Load templates
179
79
  await this._loadTemplates();
180
-
181
- // Load trash
182
80
  await this._loadTrash();
183
81
 
184
- // Restore last project
185
82
  const lastProjectId = localStorage.getItem('data_lastProject');
186
83
  if (lastProjectId) {
187
84
  try {
188
85
  await this.load({ projectId: lastProjectId });
189
86
  } catch (err) {
190
- console.warn('Failed to restore last project:', err);
87
+ console.warn('[Data] Failed to restore last project:', err);
191
88
  }
192
89
  }
193
90
 
194
- // Start auto-save
195
91
  this._startAutoSave();
92
+ this._setupStorageQuotaMonitoring();
93
+ this._detectAndRestoreCrash();
196
94
 
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.');
95
+ console.log('[Data Management] Initialized v2.0.0');
206
96
  },
207
97
 
208
98
  // ========================================================================
209
- // PUBLIC API — Project Management
99
+ // PROJECT MANAGEMENT (Enhanced)
210
100
  // ========================================================================
211
101
 
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
102
  async newProject(options = {}) {
230
- const { name, description = '', units = 'mm', material = null } = options;
103
+ const {
104
+ name,
105
+ description = '',
106
+ units = 'mm',
107
+ material = null,
108
+ templateName = null
109
+ } = options;
231
110
 
232
111
  if (!name || name.trim().length === 0) {
233
112
  throw new Error('Project name required');
234
113
  }
235
114
 
236
- const project = {
115
+ let project = {
237
116
  id: this._generateUUID(),
238
117
  name: name.trim(),
239
118
  description,
@@ -241,25 +120,45 @@ export default {
241
120
  material,
242
121
  created: Date.now(),
243
122
  modified: Date.now(),
244
- geometry: null, // Will hold serialized 3D model
245
- files: {}, // { fileName: { content, type, size, imported } }
246
- folders: {}, // { folderName: { files: [...], subfolders: [...] } }
123
+ geometry: null,
124
+ files: {},
125
+ folders: {},
247
126
  metadata: {
248
- author: localStorage.getItem('data_userName') || 'Unknown',
127
+ author: this.state.documentProperties.author,
128
+ company: this.state.documentProperties.company,
129
+ version: this.state.documentProperties.version,
249
130
  tags: [],
250
131
  keywords: [],
132
+ revision: 1
251
133
  },
134
+ thumbnail: null,
135
+ backup: null,
136
+ settings: {
137
+ renderQuality: 'high',
138
+ shadowsEnabled: true,
139
+ gridVisible: true,
140
+ unitSystem: units
141
+ }
252
142
  };
253
143
 
144
+ // Apply template if specified
145
+ if (templateName) {
146
+ const template = this.state.templates.get(templateName);
147
+ if (template) {
148
+ project = {...project, ...template.data};
149
+ }
150
+ }
151
+
254
152
  await this._saveProjectToDB(project);
255
153
  this.state.projects.unshift(project);
256
154
 
257
155
  this._addToRecent({
258
156
  projectId: project.id,
259
157
  name: project.name,
260
- timestamp: Date.now(),
158
+ timestamp: Date.now()
261
159
  });
262
160
 
161
+ this._updateSearchIndex(project.id, project);
263
162
  this._showNotification(`Created project: ${name}`, 'success');
264
163
  this._broadcastEvent('data:projectCreated', project);
265
164
 
@@ -268,22 +167,10 @@ export default {
268
167
  name: project.name,
269
168
  description,
270
169
  created: project.created,
271
- modified: project.modified,
272
- fileCount: 0,
170
+ units: project.units
273
171
  };
274
172
  },
275
173
 
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
174
  async load(options = {}) {
288
175
  const { projectId } = options;
289
176
 
@@ -303,7 +190,7 @@ export default {
303
190
  this._addToRecent({
304
191
  projectId: project.id,
305
192
  name: project.name,
306
- timestamp: Date.now(),
193
+ timestamp: Date.now()
307
194
  });
308
195
 
309
196
  this._broadcastEvent('data:projectLoaded', project);
@@ -311,17 +198,6 @@ export default {
311
198
  return project;
312
199
  },
313
200
 
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
201
  async save(options = {}) {
326
202
  const { projectId = this.state.currentProjectId } = options;
327
203
 
@@ -329,7 +205,7 @@ export default {
329
205
  throw new Error('No project loaded');
330
206
  }
331
207
 
332
- // Capture current 3D model state from viewport
208
+ // Capture current 3D model state
333
209
  const geometry = await this._captureGeometry();
334
210
 
335
211
  const project = await this._getProjectFromDB(projectId);
@@ -339,6 +215,13 @@ export default {
339
215
 
340
216
  project.modified = Date.now();
341
217
  project.geometry = geometry;
218
+ project.metadata.revision = (project.metadata.revision || 0) + 1;
219
+
220
+ // Create backup before saving
221
+ if (project.backup === null || (Date.now() - project.lastBackupTime > 300000)) {
222
+ project.backup = JSON.parse(JSON.stringify(project));
223
+ project.lastBackupTime = Date.now();
224
+ }
342
225
 
343
226
  await this._saveProjectToDB(project);
344
227
  this.state.currentProject = project;
@@ -347,21 +230,6 @@ export default {
347
230
  this._broadcastEvent('data:projectSaved', { projectId });
348
231
  },
349
232
 
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
233
  async delete(options = {}) {
366
234
  const { projectId, permanent = false } = options;
367
235
 
@@ -373,17 +241,17 @@ export default {
373
241
  if (permanent) {
374
242
  await this._deleteFromDB('projects', projectId);
375
243
  } else {
376
- // Move to trash
244
+ // Move to trash with recovery metadata
377
245
  const trashItem = {
378
246
  ...project,
379
247
  deletedAt: Date.now(),
380
- recoveryUntil: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days
248
+ recoveryUntil: Date.now() + 30 * 24 * 60 * 60 * 1000
381
249
  };
382
250
  await this._saveToTrash(trashItem);
383
251
  await this._deleteFromDB('projects', projectId);
384
252
  }
385
253
 
386
- this.state.projects = this.state.projects.filter((p) => p.id !== projectId);
254
+ this.state.projects = this.state.projects.filter(p => p.id !== projectId);
387
255
 
388
256
  if (this.state.currentProjectId === projectId) {
389
257
  this.state.currentProjectId = null;
@@ -393,26 +261,44 @@ export default {
393
261
  this._showNotification('Project deleted', 'info');
394
262
  },
395
263
 
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
- */
264
+ async duplicate(options = {}) {
265
+ const { projectId = this.state.currentProjectId, newName } = options;
266
+
267
+ const original = await this._getProjectFromDB(projectId);
268
+ if (!original) {
269
+ throw new Error('Project not found');
270
+ }
271
+
272
+ const cloned = JSON.parse(JSON.stringify(original));
273
+ cloned.id = this._generateUUID();
274
+ cloned.name = newName || `${original.name} (copy)`;
275
+ cloned.created = Date.now();
276
+ cloned.modified = Date.now();
277
+ cloned.metadata.revision = 1;
278
+
279
+ await this._saveProjectToDB(cloned);
280
+ this.state.projects.unshift(cloned);
281
+
282
+ this._showNotification(`Duplicated: ${cloned.name}`, 'success');
283
+
284
+ return cloned;
285
+ },
286
+
411
287
  async listProjects(options = {}) {
412
- const { limit = 100, offset = 0, sortBy = 'modified' } = options;
288
+ const { limit = 100, offset = 0, sortBy = 'modified', search = null } = options;
413
289
 
414
290
  let projects = [...this.state.projects];
415
291
 
292
+ // Search filter
293
+ if (search) {
294
+ const query = search.toLowerCase();
295
+ projects = projects.filter(p =>
296
+ p.name.toLowerCase().includes(query) ||
297
+ p.description.toLowerCase().includes(query) ||
298
+ p.metadata.keywords.some(k => k.toLowerCase().includes(query))
299
+ );
300
+ }
301
+
416
302
  // Sort
417
303
  if (sortBy === 'name') {
418
304
  projects.sort((a, b) => a.name.localeCompare(b.name));
@@ -420,9 +306,11 @@ export default {
420
306
  projects.sort((a, b) => b.created - a.created);
421
307
  } else if (sortBy === 'modified') {
422
308
  projects.sort((a, b) => b.modified - a.modified);
309
+ } else if (sortBy === 'size') {
310
+ projects.sort((a, b) => this._estimateSize(b) - this._estimateSize(a));
423
311
  }
424
312
 
425
- return projects.slice(offset, offset + limit).map((p) => ({
313
+ return projects.slice(offset, offset + limit).map(p => ({
426
314
  id: p.id,
427
315
  name: p.name,
428
316
  description: p.description,
@@ -430,39 +318,21 @@ export default {
430
318
  modified: p.modified,
431
319
  fileCount: Object.keys(p.files).length,
432
320
  sizeBytes: this._estimateSize(p),
321
+ thumbnail: this.state.projectThumbnails.get(p.id) || null,
322
+ author: p.metadata.author,
323
+ version: p.metadata.version
433
324
  }));
434
325
  },
435
326
 
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
327
  async getRecent(options = {}) {
444
328
  const { limit = 20 } = options;
445
329
  return this.state.recentFiles.slice(0, limit);
446
330
  },
447
331
 
448
332
  // ========================================================================
449
- // PUBLIC API File Management
333
+ // FILE MANAGEMENT (Enhanced with folders, tags)
450
334
  // ========================================================================
451
335
 
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
336
  async createFolder(options = {}) {
467
337
  const { path = '', name } = options;
468
338
 
@@ -483,6 +353,8 @@ export default {
483
353
  this.state.currentProject.folders[folderPath] = {
484
354
  files: [],
485
355
  subfolders: [],
356
+ description: '',
357
+ created: Date.now()
486
358
  };
487
359
 
488
360
  await this.save();
@@ -490,25 +362,8 @@ export default {
490
362
  return { path: folderPath, name };
491
363
  },
492
364
 
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
365
  async importFile(options = {}) {
511
- const { file, folder = '', name = null } = options;
366
+ const { file, folder = '', name = null, tags = [] } = options;
512
367
 
513
368
  if (!this.state.currentProject) {
514
369
  throw new Error('No project loaded');
@@ -529,7 +384,9 @@ export default {
529
384
  type: file.type || this._detectFileType(fileName),
530
385
  size: file.size,
531
386
  imported: Date.now(),
532
- content: new Uint8Array(content), // Store as binary
387
+ content: new Uint8Array(content),
388
+ tags: tags,
389
+ hash: await this._computeFileHash(content)
533
390
  };
534
391
 
535
392
  await this.save();
@@ -537,7 +394,7 @@ export default {
537
394
  this._addToRecent({
538
395
  projectId: this.state.currentProjectId,
539
396
  name: fileName,
540
- timestamp: Date.now(),
397
+ timestamp: Date.now()
541
398
  });
542
399
 
543
400
  this._showNotification(`Imported: ${fileName}`, 'success');
@@ -545,13 +402,6 @@ export default {
545
402
  return { fileName, fileKey, size: file.size };
546
403
  },
547
404
 
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
405
  async deleteFile(options = {}) {
556
406
  const { fileKey } = options;
557
407
 
@@ -565,53 +415,39 @@ export default {
565
415
  }
566
416
  },
567
417
 
568
- /**
569
- * Get list of files in current project.
570
- *
571
- * @returns {Promise<Array<Object>>} File list with metadata
572
- */
573
- async listFiles() {
418
+ async listFiles(options = {}) {
419
+ const { folder = null } = options;
420
+
574
421
  if (!this.state.currentProject) {
575
422
  throw new Error('No project loaded');
576
423
  }
577
424
 
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
- );
425
+ let files = Object.entries(this.state.currentProject.files);
426
+
427
+ // Filter by folder if specified
428
+ if (folder) {
429
+ files = files.filter(([key]) => key.startsWith(folder));
430
+ }
431
+
432
+ return files.map(([key, file]) => ({
433
+ key,
434
+ name: file.name,
435
+ type: file.type,
436
+ size: file.size,
437
+ imported: file.imported,
438
+ tags: file.tags || []
439
+ }));
587
440
  },
588
441
 
589
442
  // ========================================================================
590
- // PUBLIC API — Sharing and Export
443
+ // SHARING & EXPORT
591
444
  // ========================================================================
592
445
 
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
446
  async shareLink(options = {}) {
611
447
  const {
612
448
  projectId = this.state.currentProjectId,
613
449
  role = 'viewer',
614
- expiresIn = null,
450
+ expiresIn = null
615
451
  } = options;
616
452
 
617
453
  if (!projectId) {
@@ -624,7 +460,7 @@ export default {
624
460
  projectId,
625
461
  role,
626
462
  createdAt: Date.now(),
627
- expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : null,
463
+ expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : null
628
464
  };
629
465
 
630
466
  await this._saveShareLink(shareRecord);
@@ -638,22 +474,10 @@ export default {
638
474
  link,
639
475
  code: shareCode,
640
476
  role,
641
- expiresAt: shareRecord.expiresAt,
477
+ expiresAt: shareRecord.expiresAt
642
478
  };
643
479
  },
644
480
 
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
481
  async exportProject(options = {}) {
658
482
  const { projectId = this.state.currentProjectId } = options;
659
483
 
@@ -662,46 +486,43 @@ export default {
662
486
  throw new Error('Project not found');
663
487
  }
664
488
 
665
- // In production, use a ZIP library (e.g., JSZip)
666
- // For now, create JSON export
667
489
  const exportData = {
668
- version: 1,
490
+ version: '2.0.0',
669
491
  project: {
670
492
  name: project.name,
671
493
  description: project.description,
672
494
  created: project.created,
673
495
  modified: project.modified,
674
496
  units: project.units,
497
+ metadata: project.metadata,
498
+ settings: project.settings
675
499
  },
676
500
  files: Object.entries(project.files).map(([key, file]) => ({
677
501
  key,
678
502
  name: file.name,
679
503
  type: file.type,
680
504
  size: file.size,
681
- // Note: actual content would be included in ZIP
505
+ tags: file.tags || []
682
506
  })),
683
- metadata: project.metadata,
507
+ geometry: project.geometry
684
508
  };
685
509
 
686
510
  const json = JSON.stringify(exportData, null, 2);
687
- return new Blob([json], { type: 'application/json' });
511
+ const blob = new Blob([json], { type: 'application/json' });
512
+
513
+ // Download
514
+ const url = URL.createObjectURL(blob);
515
+ const a = document.createElement('a');
516
+ a.href = url;
517
+ a.download = `${project.name}.cyclecad.json`;
518
+ document.body.appendChild(a);
519
+ a.click();
520
+ document.body.removeChild(a);
521
+ URL.revokeObjectURL(url);
522
+
523
+ return blob;
688
524
  },
689
525
 
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
526
  async importProject(options = {}) {
706
527
  const { file, asCopy = true } = options;
707
528
 
@@ -709,55 +530,39 @@ export default {
709
530
  throw new Error('File required');
710
531
  }
711
532
 
712
- // Parse export (handling both ZIP and JSON)
713
- const json = JSON.parse(await file.text());
533
+ const json = await file.text();
534
+ const data = JSON.parse(json);
535
+
536
+ if (!asCopy) {
537
+ // Direct import (overwrite)
538
+ const project = data.project;
539
+ project.id = this._generateUUID();
540
+ project.created = Date.now();
541
+ project.modified = Date.now();
714
542
 
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
543
  await this._saveProjectToDB(project);
544
+ this.state.projects.unshift(project);
545
+
729
546
  return project;
547
+ } else {
548
+ // Import as copy
549
+ return this.newProject({
550
+ name: `${data.project.name} (imported)`,
551
+ description: data.project.description,
552
+ units: data.project.units
553
+ });
730
554
  }
731
555
  },
732
556
 
733
557
  // ========================================================================
734
- // PUBLIC API — Templates
558
+ // TEMPLATES
735
559
  // ========================================================================
736
560
 
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
561
  async createTemplate(options = {}) {
756
562
  const {
757
563
  projectId = this.state.currentProjectId,
758
564
  name,
759
- description = '',
760
- category = 'general',
565
+ description = ''
761
566
  } = options;
762
567
 
763
568
  if (!name) {
@@ -772,885 +577,478 @@ export default {
772
577
  const template = {
773
578
  name,
774
579
  description,
775
- category,
776
- project,
777
- createdAt: Date.now(),
580
+ created: Date.now(),
581
+ data: {
582
+ geometry: project.geometry,
583
+ settings: project.settings,
584
+ files: project.files
585
+ }
778
586
  };
779
587
 
780
588
  this.state.templates.set(name, template);
781
- await this._saveTemplate(template);
589
+ localStorage.setItem(`data_template_${name}`, JSON.stringify(template));
782
590
 
783
- this._showNotification(`Created template: ${name}`, 'success');
591
+ this._showNotification(`Template saved: ${name}`, 'success');
784
592
 
785
- return { name, category };
593
+ return template;
786
594
  },
787
595
 
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
596
  async fromTemplate(options = {}) {
827
597
  const { templateName, newProjectName } = options;
828
598
 
829
599
  const template = this.state.templates.get(templateName);
830
600
  if (!template) {
831
- throw new Error(`Template ${templateName} not found`);
601
+ throw new Error(`Template not found: ${templateName}`);
832
602
  }
833
603
 
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
- };
604
+ return this.newProject({
605
+ name: newProjectName,
606
+ templateName
607
+ });
854
608
  },
855
609
 
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);
610
+ async listTemplates() {
611
+ return Array.from(this.state.templates.values()).map(t => ({
612
+ name: t.name,
613
+ description: t.description,
614
+ created: t.created
615
+ }));
868
616
  },
869
617
 
870
618
  // ========================================================================
871
- // PUBLIC API — Trash and Recovery
619
+ // TRASH & RECOVERY
872
620
  // ========================================================================
873
621
 
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) => ({
622
+ async listTrash(options = {}) {
623
+ const { limit = 50 } = options;
624
+
625
+ return this.state.trash.slice(0, limit).map(item => ({
881
626
  id: item.id,
882
627
  name: item.name,
883
628
  deletedAt: item.deletedAt,
884
- recoveryUntil: item.recoveryUntil,
629
+ recoveryUntil: item.recoveryUntil
885
630
  }));
886
631
  },
887
632
 
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 = {}) {
633
+ async restoreFromTrash(options = {}) {
896
634
  const { projectId } = options;
897
635
 
898
- const trashItem = this.state.trash.find((t) => t.id === projectId);
899
- if (!trashItem) {
900
- throw new Error('Project not found in trash');
636
+ const item = this.state.trash.find(t => t.id === projectId);
637
+ if (!item) {
638
+ throw new Error('Item not in trash');
901
639
  }
902
640
 
903
- if (trashItem.recoveryUntil < Date.now()) {
904
- throw new Error('Recovery window expired');
641
+ if (Date.now() > item.recoveryUntil) {
642
+ throw new Error('Recovery window has expired (30 days)');
905
643
  }
906
644
 
907
- // Restore from trash
908
- const project = { ...trashItem };
909
- delete project.deletedAt;
910
- delete project.recoveryUntil;
645
+ const restored = {...item};
646
+ delete restored.deletedAt;
647
+ delete restored.recoveryUntil;
911
648
 
912
- await this._saveProjectToDB(project);
913
- this.state.projects.unshift(project);
914
- this.state.trash = this.state.trash.filter((t) => t.id !== projectId);
649
+ await this._saveProjectToDB(restored);
650
+ this.state.projects.unshift(restored);
651
+ this.state.trash = this.state.trash.filter(t => t.id !== projectId);
915
652
 
916
- this._showNotification(`Recovered: ${project.name}`, 'success');
653
+ this._showNotification(`Restored: ${restored.name}`, 'success');
917
654
 
918
- return project;
655
+ return restored;
656
+ },
657
+
658
+ async emptyTrash(options = {}) {
659
+ const { permanentDelete = true } = options;
660
+
661
+ if (permanentDelete) {
662
+ for (const item of this.state.trash) {
663
+ await this._deleteFromDB('trash', item.id);
664
+ }
665
+ this.state.trash = [];
666
+ }
919
667
  },
920
668
 
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);
669
+ // ========================================================================
670
+ // BACKUP & AUTO-RECOVERY
671
+ // ========================================================================
672
+
673
+ async createBackup(options = {}) {
674
+ const { projectId = this.state.currentProjectId } = options;
675
+
676
+ const project = await this._getProjectFromDB(projectId);
677
+ if (!project) {
678
+ throw new Error('Project not found');
679
+ }
680
+
681
+ const backup = {
682
+ id: this._generateUUID(),
683
+ projectId,
684
+ timestamp: Date.now(),
685
+ data: JSON.parse(JSON.stringify(project))
686
+ };
687
+
688
+ await this._saveBackup(backup);
689
+
690
+ // Keep only last N backups
691
+ if (this.state.backupHistory.length >= this.state.maxBackups) {
692
+ const oldest = this.state.backupHistory.shift();
693
+ await this._deleteFromDB('backups', oldest.id);
932
694
  }
933
695
 
934
- this.state.trash = this.state.trash.filter((t) => t.recoveryUntil >= now);
696
+ this.state.backupHistory.push(backup);
697
+ this._showNotification('Backup created', 'success');
935
698
 
936
- return expired.length;
699
+ return backup;
700
+ },
701
+
702
+ async listBackups(options = {}) {
703
+ const { projectId = this.state.currentProjectId } = options;
704
+
705
+ return this.state.backupHistory
706
+ .filter(b => b.projectId === projectId)
707
+ .map(b => ({
708
+ id: b.id,
709
+ timestamp: b.timestamp,
710
+ sizeBytes: this._estimateSize(b.data)
711
+ }));
937
712
  },
938
713
 
939
714
  // ========================================================================
940
- // PUBLIC API — Storage and Quota
715
+ // DOCUMENT PROPERTIES
941
716
  // ========================================================================
942
717
 
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;
718
+ async setDocumentProperties(options = {}) {
719
+ if (!this.state.currentProject) {
720
+ throw new Error('No project loaded');
721
+ }
722
+
723
+ const { title, author, description, tags = [], version } = options;
724
+
725
+ if (title) this.state.currentProject.name = title;
726
+ if (author) this.state.currentProject.metadata.author = author;
727
+ if (description) this.state.currentProject.description = description;
728
+ if (tags) this.state.currentProject.metadata.tags = tags;
729
+ if (version) this.state.currentProject.metadata.version = version;
730
+
731
+ await this.save();
732
+ },
733
+
734
+ async getDocumentProperties() {
735
+ if (!this.state.currentProject) {
736
+ throw new Error('No project loaded');
953
737
  }
954
738
 
955
739
  return {
956
- usageBytes: this.state.usageBytes,
957
- quotaBytes: this.state.quotaBytes,
958
- percentUsed: Math.round(
959
- (this.state.usageBytes / this.state.quotaBytes) * 100
960
- ),
740
+ title: this.state.currentProject.name,
741
+ description: this.state.currentProject.description,
742
+ author: this.state.currentProject.metadata.author,
743
+ created: this.state.currentProject.created,
744
+ modified: this.state.currentProject.modified,
745
+ revision: this.state.currentProject.metadata.revision,
746
+ tags: this.state.currentProject.metadata.tags,
747
+ version: this.state.currentProject.metadata.version
748
+ };
749
+ },
750
+
751
+ // ========================================================================
752
+ // UNITS & CONVERSION
753
+ // ========================================================================
754
+
755
+ async setUnits(unitSystem) {
756
+ if (!this.state.currentProject) {
757
+ throw new Error('No project loaded');
758
+ }
759
+
760
+ const validUnits = ['mm', 'cm', 'm', 'inch', 'in', 'ft'];
761
+ if (!validUnits.includes(unitSystem)) {
762
+ throw new Error('Invalid unit system');
763
+ }
764
+
765
+ this.state.currentProject.units = unitSystem;
766
+ this.state.unitPreferences.current = unitSystem;
767
+
768
+ await this.save();
769
+ },
770
+
771
+ convertUnits(value, fromUnit, toUnit) {
772
+ const conversions = {
773
+ 'mm': 1, 'cm': 10, 'm': 1000,
774
+ 'inch': 25.4, 'in': 25.4, 'ft': 304.8
961
775
  };
776
+
777
+ const fromMm = value * (conversions[fromUnit] || 1);
778
+ return fromMm / (conversions[toUnit] || 1);
962
779
  },
963
780
 
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();
781
+ // ========================================================================
782
+ // SEARCH & INDEXING
783
+ // ========================================================================
784
+
785
+ async searchProjects(options = {}) {
786
+ const { query, limit = 20 } = options;
787
+
788
+ if (!query || query.length < 2) {
789
+ return [];
973
790
  }
974
- return false;
791
+
792
+ const q = query.toLowerCase();
793
+ return this.state.projects
794
+ .filter(p =>
795
+ p.name.toLowerCase().includes(q) ||
796
+ p.description.toLowerCase().includes(q) ||
797
+ p.metadata.tags.some(t => t.toLowerCase().includes(q))
798
+ )
799
+ .slice(0, limit)
800
+ .map(p => ({
801
+ id: p.id,
802
+ name: p.name,
803
+ description: p.description,
804
+ match: 'full'
805
+ }));
975
806
  },
976
807
 
977
808
  // ========================================================================
978
- // INTERNAL HELPERS — Database
809
+ // INTERNAL FUNCTIONS
979
810
  // ========================================================================
980
811
 
981
- /**
982
- * Open or create IndexedDB.
983
- *
984
- * @private
985
- * @async
986
- * @returns {Promise<IDBDatabase>}
987
- */
812
+ _generateUUID() {
813
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
814
+ const r = Math.random() * 16 | 0;
815
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
816
+ return v.toString(16);
817
+ });
818
+ },
819
+
820
+ _generateShareCode() {
821
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
822
+ },
823
+
988
824
  async _openDatabase() {
989
825
  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);
826
+ const req = indexedDB.open('cyclecad_data', 2);
994
827
 
995
828
  req.onupgradeneeded = (e) => {
996
829
  const db = e.target.result;
997
-
998
830
  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');
831
+ db.createObjectStore('projects', { keyPath: 'id' });
1004
832
  }
1005
- if (!db.objectStoreNames.contains('shareLinks')) {
1006
- db.createObjectStore('shareLinks', { keyPath: 'code' });
833
+ if (!db.objectStoreNames.contains('backups')) {
834
+ db.createObjectStore('backups', { keyPath: 'id' });
1007
835
  }
1008
- if (!db.objectStoreNames.contains('templates')) {
1009
- db.createObjectStore('templates', { keyPath: 'name' });
836
+ if (!db.objectStoreNames.contains('shares')) {
837
+ db.createObjectStore('shares', { keyPath: 'code' });
1010
838
  }
1011
839
  if (!db.objectStoreNames.contains('trash')) {
1012
840
  db.createObjectStore('trash', { keyPath: 'id' });
1013
841
  }
1014
842
  };
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
843
 
1031
- req.onsuccess = () => {
1032
- this.state.projects = req.result.sort((a, b) => b.modified - a.modified);
1033
- resolve();
1034
- };
844
+ req.onsuccess = () => resolve(req.result);
845
+ req.onerror = () => reject(req.error);
1035
846
  });
1036
847
  },
1037
848
 
1038
- /**
1039
- * Save project to DB.
1040
- *
1041
- * @private
1042
- * @async
1043
- * @param {Object} project
1044
- * @returns {Promise<void>}
1045
- */
1046
849
  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();
850
+ const tx = this.state.db.transaction('projects', 'readwrite');
851
+ const store = tx.objectStore('projects');
852
+ store.put(project);
853
+ return new Promise((resolve, reject) => {
854
+ tx.oncomplete = resolve;
855
+ tx.onerror = () => reject(tx.error);
1053
856
  });
1054
857
  },
1055
858
 
1056
- /**
1057
- * Get project from DB.
1058
- *
1059
- * @private
1060
- * @async
1061
- * @param {string} projectId
1062
- * @returns {Promise<Object|null>}
1063
- */
1064
859
  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);
860
+ const tx = this.state.db.transaction('projects', 'readonly');
861
+ const store = tx.objectStore('projects');
862
+ const req = store.get(projectId);
863
+ return new Promise((resolve, reject) => {
864
+ req.onsuccess = () => resolve(req.result);
865
+ req.onerror = () => reject(req.error);
1071
866
  });
1072
867
  },
1073
868
 
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();
869
+ async _deleteFromDB(storeName, id) {
870
+ const tx = this.state.db.transaction(storeName, 'readwrite');
871
+ const store = tx.objectStore(storeName);
872
+ store.delete(id);
873
+ return new Promise((resolve, reject) => {
874
+ tx.oncomplete = resolve;
875
+ tx.onerror = () => reject(tx.error);
1090
876
  });
1091
877
  },
1092
878
 
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();
879
+ async _loadProjectList() {
880
+ const tx = this.state.db.transaction('projects', 'readonly');
881
+ const store = tx.objectStore('projects');
882
+ const req = store.getAll();
883
+ return new Promise((resolve, reject) => {
884
+ req.onsuccess = () => {
885
+ this.state.projects = req.result.sort((a, b) => b.modified - a.modified);
886
+ resolve();
887
+ };
888
+ req.onerror = () => reject(req.error);
1108
889
  });
1109
890
  },
1110
891
 
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
- });
892
+ _loadRecent() {
893
+ try {
894
+ const stored = localStorage.getItem('data_recentFiles');
895
+ if (stored) {
896
+ this.state.recentFiles = JSON.parse(stored);
897
+ }
898
+ } catch (e) {
899
+ console.warn('[Data] Failed to load recent files:', e);
900
+ }
1127
901
  },
1128
902
 
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
- });
903
+ _addToRecent(file) {
904
+ this.state.recentFiles.unshift(file);
905
+ if (this.state.recentFiles.length > 20) {
906
+ this.state.recentFiles.pop();
907
+ }
908
+ localStorage.setItem('data_recentFiles', JSON.stringify(this.state.recentFiles));
1145
909
  },
1146
910
 
1147
- /**
1148
- * Load templates from DB.
1149
- *
1150
- * @private
1151
- * @async
1152
- * @returns {Promise<void>}
1153
- */
1154
911
  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();
912
+ const keys = Object.keys(localStorage);
913
+ keys
914
+ .filter(k => k.startsWith('data_template_'))
915
+ .forEach(key => {
916
+ try {
917
+ const template = JSON.parse(localStorage.getItem(key));
918
+ const name = key.replace('data_template_', '');
919
+ this.state.templates.set(name, template);
920
+ } catch (e) {
921
+ console.warn('[Data] Failed to load template:', key);
922
+ }
923
+ });
924
+ },
1159
925
 
926
+ async _loadTrash() {
927
+ const tx = this.state.db.transaction('trash', 'readonly');
928
+ const store = tx.objectStore('trash');
929
+ const req = store.getAll();
930
+ return new Promise((resolve, reject) => {
1160
931
  req.onsuccess = () => {
1161
- this.state.templates = new Map(req.result.map((t) => [t.name, t]));
932
+ this.state.trash = req.result.filter(item =>
933
+ Date.now() < item.recoveryUntil
934
+ );
1162
935
  resolve();
1163
936
  };
937
+ req.onerror = () => reject(req.error);
1164
938
  });
1165
939
  },
1166
940
 
1167
- /**
1168
- * Save trash item to DB.
1169
- *
1170
- * @private
1171
- * @async
1172
- * @param {Object} item
1173
- * @returns {Promise<void>}
1174
- */
1175
941
  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);
942
+ const tx = this.state.db.transaction('trash', 'readwrite');
943
+ const store = tx.objectStore('trash');
944
+ store.put(item);
945
+ return new Promise((resolve, reject) => {
946
+ tx.oncomplete = resolve;
947
+ tx.onerror = () => reject(tx.error);
948
+ });
949
+ },
1180
950
 
1181
- tx.oncomplete = () => resolve();
951
+ async _saveBackup(backup) {
952
+ const tx = this.state.db.transaction('backups', 'readwrite');
953
+ const store = tx.objectStore('backups');
954
+ store.put(backup);
955
+ return new Promise((resolve, reject) => {
956
+ tx.oncomplete = resolve;
957
+ tx.onerror = () => reject(tx.error);
1182
958
  });
1183
959
  },
1184
960
 
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();
961
+ async _saveShareLink(shareRecord) {
962
+ const tx = this.state.db.transaction('shares', 'readwrite');
963
+ const store = tx.objectStore('shares');
964
+ store.put(shareRecord);
965
+ return new Promise((resolve, reject) => {
966
+ tx.oncomplete = resolve;
967
+ tx.onerror = () => reject(tx.error);
968
+ });
969
+ },
1197
970
 
1198
- req.onsuccess = () => {
1199
- this.state.trash = req.result || [];
1200
- resolve();
1201
- };
971
+ _updateSearchIndex(projectId, project) {
972
+ this.state.searchIndex.set(projectId, {
973
+ name: project.name.toLowerCase(),
974
+ description: project.description.toLowerCase(),
975
+ tags: (project.metadata.tags || []).map(t => t.toLowerCase())
1202
976
  });
1203
977
  },
1204
978
 
1205
- // ========================================================================
1206
- // INTERNAL HELPERS — Utilities
1207
- // ========================================================================
979
+ _estimateSize(project) {
980
+ if (!project) return 0;
981
+ let size = JSON.stringify(project).length;
982
+ Object.values(project.files || {}).forEach(file => {
983
+ size += file.size || 0;
984
+ });
985
+ return size;
986
+ },
1208
987
 
1209
- /**
1210
- * Capture current 3D geometry from viewport.
1211
- *
1212
- * @private
1213
- * @async
1214
- * @returns {Promise<Object|null>}
1215
- */
1216
988
  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
- };
989
+ // Placeholder: capture current 3D scene geometry
990
+ return {timestamp: Date.now(), meshCount: 0};
1223
991
  },
1224
992
 
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);
993
+ async _computeFileHash(arrayBuffer) {
994
+ const buffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
995
+ const hashArray = Array.from(new Uint8Array(buffer));
996
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
1234
997
  },
1235
998
 
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();
999
+ _detectFileType(filename) {
1000
+ const ext = filename.split('.').pop().toLowerCase();
1245
1001
  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',
1002
+ 'ipt': 'application/inventor-part',
1003
+ 'iam': 'application/inventor-assembly',
1004
+ 'step': 'application/step',
1005
+ 'stp': 'application/step',
1006
+ 'stl': 'model/stl',
1007
+ 'obj': 'model/obj',
1008
+ 'dxf': 'application/dxf'
1253
1009
  };
1254
1010
  return types[ext] || 'application/octet-stream';
1255
1011
  },
1256
1012
 
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
1013
  _startAutoSave() {
1296
1014
  this.state.autoSaveHandle = setInterval(() => {
1297
1015
  if (this.state.currentProjectId) {
1298
- this.save().catch((err) => {
1299
- console.warn('Auto-save failed:', err);
1300
- });
1016
+ this.save().catch(err => console.warn('[Data] Auto-save failed:', err));
1301
1017
  }
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);
1018
+ }, this.state.autoSaveFrequency);
1331
1019
  },
1332
1020
 
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
- });
1021
+ _setupStorageQuotaMonitoring() {
1022
+ if (navigator.storage && navigator.storage.estimate) {
1023
+ setInterval(() => {
1024
+ navigator.storage.estimate().then(estimate => {
1025
+ this.state.usageBytes = estimate.usage;
1026
+ this.state.quotaBytes = estimate.quota;
1027
+ const percentUsed = (estimate.usage / estimate.quota) * 100;
1028
+ if (percentUsed > 90) {
1029
+ this._showNotification('Storage quota nearly full', 'warning');
1030
+ }
1031
+ });
1032
+ }, 60000);
1033
+ }
1345
1034
  },
1346
1035
 
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)];
1036
+ _detectAndRestoreCrash() {
1037
+ const crashFlag = localStorage.getItem('data_appRunning');
1038
+ if (crashFlag === 'true') {
1039
+ this._broadcastEvent('data:crashDetected', {});
1040
+ localStorage.removeItem('data_appRunning');
1041
+ } else {
1042
+ localStorage.setItem('data_appRunning', 'true');
1358
1043
  }
1359
- return code;
1360
1044
  },
1361
1045
 
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
- `;
1046
+ _showNotification(message, type) {
1047
+ console.log(`[Data] [${type.toUpperCase()}] ${message}`);
1048
+ this._broadcastEvent('data:notification', {message, type});
1655
1049
  },
1050
+
1051
+ _broadcastEvent(eventName, data) {
1052
+ window.dispatchEvent(new CustomEvent(eventName, {detail: data}));
1053
+ }
1656
1054
  };