cyclecad 2.0.0 → 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.
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/app/index.html +106 -2
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +967 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1067 -0
- package/app/js/modules/collaboration-module.js +1102 -0
- package/app/js/modules/data-module.js +1656 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +1173 -0
- package/app/js/modules/inspection-module.js +937 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +957 -0
- package/app/js/modules/rendering-module.js +1306 -0
- package/app/js/modules/scripting-module.js +955 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +1032 -90
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +728 -0
- package/app/js/modules/version-module.js +1410 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/.github/scripts/cad-diff.js +0 -590
- 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
|
+
};
|