cyclecad 0.1.3 → 0.1.4
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/CLAUDE.md +233 -0
- package/DUO-MANIFEST-README.md +233 -0
- package/MASTERPLAN.md +182 -0
- package/app/duo-manifest-demo.html +337 -0
- package/app/duo-manifest.json +7375 -0
- package/app/index.html +1167 -23
- package/app/js/app.js +79 -9
- package/app/js/assembly-resolver.js +477 -0
- package/app/js/operations.js +501 -112
- package/app/js/project-browser.js +741 -0
- package/app/js/project-loader.js +579 -0
- package/app/js/rebuild-guide.js +743 -0
- package/app/js/viewport.js +24 -0
- package/package.json +2 -2
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* project-browser.js - Project folder browser UI for cycleCAD
|
|
3
|
+
* Displays Inventor project structure with file tree, search, and file selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let browserState = {
|
|
7
|
+
containerEl: null,
|
|
8
|
+
overlayEl: null,
|
|
9
|
+
projectData: null,
|
|
10
|
+
searchQuery: '',
|
|
11
|
+
expandedFolders: new Set(),
|
|
12
|
+
onFileSelectCallback: null,
|
|
13
|
+
currentPath: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize the project browser
|
|
18
|
+
* @param {HTMLElement} container - Container element for the browser overlay
|
|
19
|
+
* @param {Object} callbacks - Callback object with onFileOpen(file), onProjectLoad(project)
|
|
20
|
+
*/
|
|
21
|
+
export function initProjectBrowser(container, callbacks = {}) {
|
|
22
|
+
browserState.containerEl = container;
|
|
23
|
+
browserState.onFileSelectCallback = callbacks.onFileSelect || callbacks.onFileOpen || (() => {});
|
|
24
|
+
|
|
25
|
+
injectStyles();
|
|
26
|
+
createBrowserPanel();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Show the project browser overlay
|
|
31
|
+
*/
|
|
32
|
+
export function showBrowser() {
|
|
33
|
+
if (browserState.overlayEl) {
|
|
34
|
+
browserState.overlayEl.style.display = 'flex';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hide the project browser overlay
|
|
40
|
+
*/
|
|
41
|
+
export function hideBrowser() {
|
|
42
|
+
if (browserState.overlayEl) {
|
|
43
|
+
browserState.overlayEl.style.display = 'none';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set project data and populate the browser tree
|
|
49
|
+
* @param {Object} projectData - Project structure from project-loader
|
|
50
|
+
*/
|
|
51
|
+
export function setProject(projectData) {
|
|
52
|
+
browserState.projectData = projectData;
|
|
53
|
+
browserState.expandedFolders.clear();
|
|
54
|
+
browserState.currentPath = [];
|
|
55
|
+
browserState.searchQuery = '';
|
|
56
|
+
|
|
57
|
+
// Reset search input
|
|
58
|
+
const searchInput = document.getElementById('project-search');
|
|
59
|
+
if (searchInput) searchInput.value = '';
|
|
60
|
+
|
|
61
|
+
renderTree();
|
|
62
|
+
updateStats();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register file selection callback
|
|
67
|
+
* @param {Function} callback - Called with selected file object {name, path, type}
|
|
68
|
+
*/
|
|
69
|
+
export function onFileSelect(callback) {
|
|
70
|
+
browserState.onFileSelectCallback = callback;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create the browser panel HTML structure
|
|
75
|
+
*/
|
|
76
|
+
function createBrowserPanel() {
|
|
77
|
+
const overlay = document.createElement('div');
|
|
78
|
+
overlay.id = 'project-browser-overlay';
|
|
79
|
+
overlay.className = 'project-browser-overlay';
|
|
80
|
+
|
|
81
|
+
overlay.innerHTML = `
|
|
82
|
+
<div class="project-browser-panel">
|
|
83
|
+
<!-- Header -->
|
|
84
|
+
<div class="pb-header">
|
|
85
|
+
<div class="pb-title">Project Browser</div>
|
|
86
|
+
<button class="pb-close-btn" aria-label="Close browser">✕</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Stats Bar -->
|
|
90
|
+
<div class="pb-stats">
|
|
91
|
+
<span class="stat-item"><span class="stat-value" id="stat-files">0</span> files</span>
|
|
92
|
+
<span class="stat-item"><span class="stat-value" id="stat-parts">0</span> parts</span>
|
|
93
|
+
<span class="stat-item"><span class="stat-value" id="stat-asms">0</span> assemblies</span>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- Breadcrumb Navigation -->
|
|
97
|
+
<div class="pb-breadcrumb">
|
|
98
|
+
<button class="breadcrumb-item" data-path="">Project</button>
|
|
99
|
+
<div id="breadcrumb-trail"></div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- Search Input -->
|
|
103
|
+
<div class="pb-search-box">
|
|
104
|
+
<input
|
|
105
|
+
type="text"
|
|
106
|
+
id="project-search"
|
|
107
|
+
class="pb-search-input"
|
|
108
|
+
placeholder="Search files..."
|
|
109
|
+
aria-label="Search project files"
|
|
110
|
+
>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Tree Container -->
|
|
114
|
+
<div class="pb-tree-container">
|
|
115
|
+
<div id="project-tree" class="pb-tree"></div>
|
|
116
|
+
<div id="pb-empty" class="pb-empty">
|
|
117
|
+
<p>No project loaded</p>
|
|
118
|
+
<p class="pb-empty-hint">Use "Open Project" to load an Inventor file</p>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<!-- Footer with Actions -->
|
|
123
|
+
<div class="pb-footer">
|
|
124
|
+
<button class="pb-btn pb-btn-primary" id="open-project-btn">
|
|
125
|
+
📁 Open Project
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
browserState.containerEl.appendChild(overlay);
|
|
132
|
+
browserState.overlayEl = overlay;
|
|
133
|
+
|
|
134
|
+
// Event listeners
|
|
135
|
+
setupEventListeners();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Setup all event listeners for the browser
|
|
140
|
+
*/
|
|
141
|
+
function setupEventListeners() {
|
|
142
|
+
const overlay = browserState.overlayEl;
|
|
143
|
+
|
|
144
|
+
// Close button
|
|
145
|
+
overlay.querySelector('.pb-close-btn').addEventListener('click', hideBrowser);
|
|
146
|
+
|
|
147
|
+
// Search input with debounce
|
|
148
|
+
const searchInput = overlay.querySelector('#project-search');
|
|
149
|
+
let searchTimeout;
|
|
150
|
+
searchInput.addEventListener('input', (e) => {
|
|
151
|
+
clearTimeout(searchTimeout);
|
|
152
|
+
searchTimeout = setTimeout(() => {
|
|
153
|
+
browserState.searchQuery = e.target.value.toLowerCase();
|
|
154
|
+
renderTree();
|
|
155
|
+
}, 300);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Tree delegated click handler
|
|
159
|
+
overlay.querySelector('#project-tree').addEventListener('click', (e) => {
|
|
160
|
+
const target = e.target.closest('[data-tree-item]');
|
|
161
|
+
if (!target) return;
|
|
162
|
+
|
|
163
|
+
const itemId = target.dataset.treeItem;
|
|
164
|
+
const itemType = target.dataset.itemType;
|
|
165
|
+
const filePath = target.dataset.filePath;
|
|
166
|
+
|
|
167
|
+
if (itemType === 'folder') {
|
|
168
|
+
// Toggle folder expansion
|
|
169
|
+
if (browserState.expandedFolders.has(itemId)) {
|
|
170
|
+
browserState.expandedFolders.delete(itemId);
|
|
171
|
+
} else {
|
|
172
|
+
browserState.expandedFolders.add(itemId);
|
|
173
|
+
}
|
|
174
|
+
renderTree();
|
|
175
|
+
} else if (['ipt', 'iam', 'idw'].includes(itemType)) {
|
|
176
|
+
// File selection
|
|
177
|
+
const fileObj = {
|
|
178
|
+
name: target.textContent.trim().split('\n')[0],
|
|
179
|
+
path: filePath,
|
|
180
|
+
type: itemType,
|
|
181
|
+
};
|
|
182
|
+
browserState.onFileSelectCallback(fileObj);
|
|
183
|
+
hideBrowser();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Breadcrumb navigation
|
|
188
|
+
overlay.addEventListener('click', (e) => {
|
|
189
|
+
if (e.target.classList.contains('breadcrumb-item')) {
|
|
190
|
+
const path = e.target.dataset.path;
|
|
191
|
+
browserState.currentPath = path ? path.split('/').filter(Boolean) : [];
|
|
192
|
+
renderTree();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Open project button
|
|
197
|
+
overlay.querySelector('#open-project-btn').addEventListener('click', () => {
|
|
198
|
+
// This would typically call project-loader to open file picker
|
|
199
|
+
dispatchCustomEvent('open-project-requested');
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Render the project tree structure
|
|
205
|
+
*/
|
|
206
|
+
function renderTree() {
|
|
207
|
+
const treeContainer = browserState.overlayEl.querySelector('#project-tree');
|
|
208
|
+
const emptyState = browserState.overlayEl.querySelector('#pb-empty');
|
|
209
|
+
|
|
210
|
+
if (!browserState.projectData) {
|
|
211
|
+
treeContainer.innerHTML = '';
|
|
212
|
+
emptyState.style.display = 'flex';
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
emptyState.style.display = 'none';
|
|
217
|
+
|
|
218
|
+
// Get current folder contents
|
|
219
|
+
const currentFolder = navigateToPath(browserState.projectData, browserState.currentPath);
|
|
220
|
+
if (!currentFolder || !currentFolder.children) {
|
|
221
|
+
treeContainer.innerHTML = '';
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const html = renderTreeItems(currentFolder.children);
|
|
226
|
+
treeContainer.innerHTML = html;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Render tree items recursively (with search filter)
|
|
231
|
+
* @param {Array} items - Array of file/folder objects
|
|
232
|
+
* @param {number} depth - Current depth level
|
|
233
|
+
* @returns {string} HTML string
|
|
234
|
+
*/
|
|
235
|
+
function renderTreeItems(items, depth = 0) {
|
|
236
|
+
if (!items || items.length === 0) return '';
|
|
237
|
+
|
|
238
|
+
let html = '<ul class="pb-tree-list">';
|
|
239
|
+
|
|
240
|
+
for (const item of items) {
|
|
241
|
+
// Apply search filter
|
|
242
|
+
if (browserState.searchQuery) {
|
|
243
|
+
if (!item.name.toLowerCase().includes(browserState.searchQuery)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const itemId = `item-${Math.random().toString(36).substr(2, 9)}`;
|
|
249
|
+
const isExpanded = browserState.expandedFolders.has(itemId);
|
|
250
|
+
const filePath = item.path || '';
|
|
251
|
+
const icon = getFileIcon(item.type);
|
|
252
|
+
const badge = getBadge(item.category);
|
|
253
|
+
|
|
254
|
+
html += `<li class="pb-tree-item" data-depth="${depth}">`;
|
|
255
|
+
|
|
256
|
+
if (item.type === 'folder' && item.children && item.children.length > 0) {
|
|
257
|
+
// Folder with children
|
|
258
|
+
html += `
|
|
259
|
+
<div class="pb-item-row" data-tree-item="${itemId}" data-item-type="folder" data-file-path="${filePath}">
|
|
260
|
+
<span class="pb-toggle ${isExpanded ? 'expanded' : ''}">▶</span>
|
|
261
|
+
<span class="pb-icon">${icon}</span>
|
|
262
|
+
<span class="pb-label">${escapeHtml(item.name)}</span>
|
|
263
|
+
${item.children ? `<span class="pb-count">${item.children.length}</span>` : ''}
|
|
264
|
+
</div>
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
if (isExpanded) {
|
|
268
|
+
html += `<div class="pb-children">${renderTreeItems(item.children, depth + 1)}</div>`;
|
|
269
|
+
}
|
|
270
|
+
} else if (item.type === 'folder') {
|
|
271
|
+
// Empty folder
|
|
272
|
+
html += `
|
|
273
|
+
<div class="pb-item-row" data-tree-item="${itemId}" data-item-type="folder" data-file-path="${filePath}">
|
|
274
|
+
<span class="pb-toggle disabled">▶</span>
|
|
275
|
+
<span class="pb-icon">${icon}</span>
|
|
276
|
+
<span class="pb-label">${escapeHtml(item.name)}</span>
|
|
277
|
+
</div>
|
|
278
|
+
`;
|
|
279
|
+
} else {
|
|
280
|
+
// File (part, assembly, drawing, etc.)
|
|
281
|
+
html += `
|
|
282
|
+
<div class="pb-item-row" data-tree-item="${itemId}" data-item-type="${item.type}" data-file-path="${filePath}">
|
|
283
|
+
<span class="pb-toggle disabled">•</span>
|
|
284
|
+
<span class="pb-icon">${icon}</span>
|
|
285
|
+
<span class="pb-label">${escapeHtml(item.name)}</span>
|
|
286
|
+
${badge ? `<span class="pb-badge pb-badge-${badge.color}">${badge.text}</span>` : ''}
|
|
287
|
+
</div>
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
html += '</li>';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
html += '</ul>';
|
|
295
|
+
return html;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get icon emoji for file type
|
|
300
|
+
* @param {string} type - File type
|
|
301
|
+
* @returns {string} Icon emoji
|
|
302
|
+
*/
|
|
303
|
+
function getFileIcon(type) {
|
|
304
|
+
const icons = {
|
|
305
|
+
ipt: '📦',
|
|
306
|
+
iam: '🏗️',
|
|
307
|
+
idw: '📐',
|
|
308
|
+
folder: '📁',
|
|
309
|
+
ipj: '📋',
|
|
310
|
+
};
|
|
311
|
+
return icons[type] || '📄';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get category badge info
|
|
316
|
+
* @param {string} category - Category name (CUSTOM, STD, VENDOR)
|
|
317
|
+
* @returns {Object|null} Badge object or null
|
|
318
|
+
*/
|
|
319
|
+
function getBadge(category) {
|
|
320
|
+
const badges = {
|
|
321
|
+
CUSTOM: { text: '[CUSTOM]', color: 'green' },
|
|
322
|
+
STD: { text: '[STD]', color: 'blue' },
|
|
323
|
+
VENDOR: { text: '[VENDOR]', color: 'yellow' },
|
|
324
|
+
};
|
|
325
|
+
return badges[category] || null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Navigate to a path in the project tree
|
|
330
|
+
* @param {Object} root - Root project object
|
|
331
|
+
* @param {Array} pathArray - Path components
|
|
332
|
+
* @returns {Object} Folder object at path
|
|
333
|
+
*/
|
|
334
|
+
function navigateToPath(root, pathArray) {
|
|
335
|
+
let current = root;
|
|
336
|
+
for (const component of pathArray) {
|
|
337
|
+
if (!current.children) return null;
|
|
338
|
+
const next = current.children.find(item => item.name === component);
|
|
339
|
+
if (!next) return null;
|
|
340
|
+
current = next;
|
|
341
|
+
}
|
|
342
|
+
return current;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Update stats display
|
|
347
|
+
*/
|
|
348
|
+
function updateStats() {
|
|
349
|
+
if (!browserState.projectData) return;
|
|
350
|
+
|
|
351
|
+
const stats = countFileTypes(browserState.projectData);
|
|
352
|
+
|
|
353
|
+
const filesEl = document.getElementById('stat-files');
|
|
354
|
+
const partsEl = document.getElementById('stat-parts');
|
|
355
|
+
const asmsEl = document.getElementById('stat-asms');
|
|
356
|
+
|
|
357
|
+
if (filesEl) filesEl.textContent = stats.total;
|
|
358
|
+
if (partsEl) partsEl.textContent = stats.ipt;
|
|
359
|
+
if (asmsEl) asmsEl.textContent = stats.iam;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Count file types recursively
|
|
364
|
+
* @param {Object} item - Folder or file
|
|
365
|
+
* @returns {Object} Counts by type
|
|
366
|
+
*/
|
|
367
|
+
function countFileTypes(item) {
|
|
368
|
+
const counts = { total: 0, ipt: 0, iam: 0, idw: 0 };
|
|
369
|
+
|
|
370
|
+
function traverse(node) {
|
|
371
|
+
if (node.type === 'ipt') counts.ipt++;
|
|
372
|
+
else if (node.type === 'iam') counts.iam++;
|
|
373
|
+
else if (node.type === 'idw') counts.idw++;
|
|
374
|
+
|
|
375
|
+
if (node.type !== 'folder') counts.total++;
|
|
376
|
+
|
|
377
|
+
if (node.children) {
|
|
378
|
+
for (const child of node.children) {
|
|
379
|
+
traverse(child);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (item.children) {
|
|
385
|
+
for (const child of item.children) {
|
|
386
|
+
traverse(child);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return counts;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Dispatch custom event
|
|
395
|
+
* @param {string} eventName - Event name
|
|
396
|
+
* @param {Object} detail - Event detail data
|
|
397
|
+
*/
|
|
398
|
+
function dispatchCustomEvent(eventName, detail = {}) {
|
|
399
|
+
window.dispatchEvent(new CustomEvent(eventName, { detail }));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Escape HTML special characters
|
|
404
|
+
* @param {string} text - Text to escape
|
|
405
|
+
* @returns {string} Escaped text
|
|
406
|
+
*/
|
|
407
|
+
function escapeHtml(text) {
|
|
408
|
+
const map = {
|
|
409
|
+
'&': '&',
|
|
410
|
+
'<': '<',
|
|
411
|
+
'>': '>',
|
|
412
|
+
'"': '"',
|
|
413
|
+
"'": ''',
|
|
414
|
+
};
|
|
415
|
+
return text.replace(/[&<>"']/g, char => map[char]);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Inject CSS styles into the document
|
|
420
|
+
*/
|
|
421
|
+
function injectStyles() {
|
|
422
|
+
if (document.getElementById('project-browser-styles')) return;
|
|
423
|
+
|
|
424
|
+
const style = document.createElement('style');
|
|
425
|
+
style.id = 'project-browser-styles';
|
|
426
|
+
style.textContent = `
|
|
427
|
+
.project-browser-overlay {
|
|
428
|
+
display: none;
|
|
429
|
+
position: fixed;
|
|
430
|
+
top: 0;
|
|
431
|
+
left: 0;
|
|
432
|
+
right: 0;
|
|
433
|
+
bottom: 0;
|
|
434
|
+
background: rgba(0, 0, 0, 0.7);
|
|
435
|
+
z-index: 9999;
|
|
436
|
+
align-items: center;
|
|
437
|
+
justify-content: flex-start;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.project-browser-panel {
|
|
441
|
+
width: 400px;
|
|
442
|
+
height: 100%;
|
|
443
|
+
background: var(--bg-primary, #1e1e1e);
|
|
444
|
+
border-right: 1px solid var(--border-color, #3e3e42);
|
|
445
|
+
display: flex;
|
|
446
|
+
flex-direction: column;
|
|
447
|
+
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.5);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.pb-header {
|
|
451
|
+
padding: 16px;
|
|
452
|
+
border-bottom: 1px solid var(--border-color, #3e3e42);
|
|
453
|
+
display: flex;
|
|
454
|
+
justify-content: space-between;
|
|
455
|
+
align-items: center;
|
|
456
|
+
flex-shrink: 0;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.pb-title {
|
|
460
|
+
font-size: 14px;
|
|
461
|
+
font-weight: 600;
|
|
462
|
+
color: var(--text-primary, #e0e0e0);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.pb-close-btn {
|
|
466
|
+
background: none;
|
|
467
|
+
border: none;
|
|
468
|
+
color: var(--text-secondary, #a0a0a0);
|
|
469
|
+
font-size: 18px;
|
|
470
|
+
cursor: pointer;
|
|
471
|
+
padding: 4px 8px;
|
|
472
|
+
border-radius: 4px;
|
|
473
|
+
transition: all 0.2s;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.pb-close-btn:hover {
|
|
477
|
+
background: var(--bg-secondary, #252526);
|
|
478
|
+
color: var(--text-primary, #e0e0e0);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.pb-stats {
|
|
482
|
+
padding: 8px 16px;
|
|
483
|
+
border-bottom: 1px solid var(--border-color, #3e3e42);
|
|
484
|
+
display: flex;
|
|
485
|
+
gap: 16px;
|
|
486
|
+
font-size: 12px;
|
|
487
|
+
color: var(--text-secondary, #a0a0a0);
|
|
488
|
+
flex-shrink: 0;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.stat-item {
|
|
492
|
+
display: flex;
|
|
493
|
+
align-items: center;
|
|
494
|
+
gap: 4px;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.stat-value {
|
|
498
|
+
color: var(--accent-blue, #58a6ff);
|
|
499
|
+
font-weight: 600;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.pb-breadcrumb {
|
|
503
|
+
padding: 8px 12px;
|
|
504
|
+
border-bottom: 1px solid var(--border-color, #3e3e42);
|
|
505
|
+
display: flex;
|
|
506
|
+
align-items: center;
|
|
507
|
+
gap: 4px;
|
|
508
|
+
overflow-x: auto;
|
|
509
|
+
flex-shrink: 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.breadcrumb-item {
|
|
513
|
+
background: none;
|
|
514
|
+
border: none;
|
|
515
|
+
color: var(--accent-blue, #58a6ff);
|
|
516
|
+
font-size: 12px;
|
|
517
|
+
cursor: pointer;
|
|
518
|
+
padding: 4px 8px;
|
|
519
|
+
border-radius: 3px;
|
|
520
|
+
white-space: nowrap;
|
|
521
|
+
transition: all 0.2s;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.breadcrumb-item:hover {
|
|
525
|
+
background: var(--bg-secondary, #252526);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.pb-search-box {
|
|
529
|
+
padding: 8px 12px;
|
|
530
|
+
border-bottom: 1px solid var(--border-color, #3e3e42);
|
|
531
|
+
flex-shrink: 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.pb-search-input {
|
|
535
|
+
width: 100%;
|
|
536
|
+
padding: 8px;
|
|
537
|
+
background: var(--bg-secondary, #252526);
|
|
538
|
+
border: 1px solid var(--border-color, #3e3e42);
|
|
539
|
+
color: var(--text-primary, #e0e0e0);
|
|
540
|
+
font-size: 12px;
|
|
541
|
+
border-radius: 4px;
|
|
542
|
+
outline: none;
|
|
543
|
+
transition: border-color 0.2s;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.pb-search-input:focus {
|
|
547
|
+
border-color: var(--accent-blue, #58a6ff);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.pb-search-input::placeholder {
|
|
551
|
+
color: var(--text-secondary, #a0a0a0);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.pb-tree-container {
|
|
555
|
+
flex: 1;
|
|
556
|
+
overflow-y: auto;
|
|
557
|
+
position: relative;
|
|
558
|
+
min-height: 0;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.pb-tree {
|
|
562
|
+
padding: 4px;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.pb-tree-list {
|
|
566
|
+
list-style: none;
|
|
567
|
+
margin: 0;
|
|
568
|
+
padding: 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.pb-tree-item {
|
|
572
|
+
margin: 0;
|
|
573
|
+
padding: 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.pb-item-row {
|
|
577
|
+
display: flex;
|
|
578
|
+
align-items: center;
|
|
579
|
+
gap: 6px;
|
|
580
|
+
padding: 4px 8px;
|
|
581
|
+
border-radius: 3px;
|
|
582
|
+
cursor: pointer;
|
|
583
|
+
transition: all 0.15s;
|
|
584
|
+
user-select: none;
|
|
585
|
+
font-size: 12px;
|
|
586
|
+
color: var(--text-primary, #e0e0e0);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.pb-item-row:hover {
|
|
590
|
+
background: var(--bg-secondary, #252526);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.pb-toggle {
|
|
594
|
+
width: 16px;
|
|
595
|
+
display: flex;
|
|
596
|
+
align-items: center;
|
|
597
|
+
justify-content: center;
|
|
598
|
+
font-size: 10px;
|
|
599
|
+
color: var(--text-secondary, #a0a0a0);
|
|
600
|
+
transition: transform 0.2s;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.pb-toggle.expanded {
|
|
604
|
+
transform: rotate(90deg);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.pb-toggle.disabled {
|
|
608
|
+
cursor: default;
|
|
609
|
+
opacity: 0;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.pb-icon {
|
|
613
|
+
font-size: 14px;
|
|
614
|
+
min-width: 14px;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.pb-label {
|
|
618
|
+
flex: 1;
|
|
619
|
+
overflow: hidden;
|
|
620
|
+
text-overflow: ellipsis;
|
|
621
|
+
white-space: nowrap;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.pb-count {
|
|
625
|
+
font-size: 11px;
|
|
626
|
+
color: var(--text-secondary, #a0a0a0);
|
|
627
|
+
background: var(--bg-tertiary, #2d2d30);
|
|
628
|
+
padding: 2px 6px;
|
|
629
|
+
border-radius: 2px;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.pb-badge {
|
|
633
|
+
font-size: 10px;
|
|
634
|
+
font-weight: 600;
|
|
635
|
+
padding: 2px 6px;
|
|
636
|
+
border-radius: 2px;
|
|
637
|
+
white-space: nowrap;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.pb-badge-green {
|
|
641
|
+
background: rgba(63, 185, 80, 0.2);
|
|
642
|
+
color: var(--accent-green, #3fb950);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.pb-badge-blue {
|
|
646
|
+
background: rgba(88, 166, 255, 0.2);
|
|
647
|
+
color: var(--accent-blue, #58a6ff);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.pb-badge-yellow {
|
|
651
|
+
background: rgba(210, 153, 34, 0.2);
|
|
652
|
+
color: var(--accent-yellow, #d29922);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.pb-children {
|
|
656
|
+
margin-left: 8px;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.pb-empty {
|
|
660
|
+
display: flex;
|
|
661
|
+
flex-direction: column;
|
|
662
|
+
align-items: center;
|
|
663
|
+
justify-content: center;
|
|
664
|
+
height: 100%;
|
|
665
|
+
color: var(--text-secondary, #a0a0a0);
|
|
666
|
+
font-size: 12px;
|
|
667
|
+
text-align: center;
|
|
668
|
+
gap: 8px;
|
|
669
|
+
padding: 24px;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.pb-empty p {
|
|
673
|
+
margin: 0;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.pb-empty-hint {
|
|
677
|
+
font-size: 11px;
|
|
678
|
+
color: var(--text-secondary, #a0a0a0);
|
|
679
|
+
opacity: 0.7;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.pb-footer {
|
|
683
|
+
padding: 12px 16px;
|
|
684
|
+
border-top: 1px solid var(--border-color, #3e3e42);
|
|
685
|
+
display: flex;
|
|
686
|
+
gap: 8px;
|
|
687
|
+
flex-shrink: 0;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.pb-btn {
|
|
691
|
+
flex: 1;
|
|
692
|
+
padding: 8px 12px;
|
|
693
|
+
border: none;
|
|
694
|
+
border-radius: 4px;
|
|
695
|
+
font-size: 12px;
|
|
696
|
+
font-weight: 500;
|
|
697
|
+
cursor: pointer;
|
|
698
|
+
transition: all 0.2s;
|
|
699
|
+
white-space: nowrap;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.pb-btn-primary {
|
|
703
|
+
background: var(--accent-blue, #58a6ff);
|
|
704
|
+
color: var(--bg-primary, #1e1e1e);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.pb-btn-primary:hover {
|
|
708
|
+
background: #4a96e8;
|
|
709
|
+
transform: translateY(-1px);
|
|
710
|
+
box-shadow: 0 2px 8px rgba(88, 166, 255, 0.2);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/* Scrollbar styling */
|
|
714
|
+
.pb-tree-container::-webkit-scrollbar {
|
|
715
|
+
width: 8px;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.pb-tree-container::-webkit-scrollbar-track {
|
|
719
|
+
background: var(--bg-primary, #1e1e1e);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.pb-tree-container::-webkit-scrollbar-thumb {
|
|
723
|
+
background: var(--bg-tertiary, #2d2d30);
|
|
724
|
+
border-radius: 4px;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.pb-tree-container::-webkit-scrollbar-thumb:hover {
|
|
728
|
+
background: var(--border-color, #3e3e42);
|
|
729
|
+
}
|
|
730
|
+
`;
|
|
731
|
+
|
|
732
|
+
document.head.appendChild(style);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export default {
|
|
736
|
+
initProjectBrowser,
|
|
737
|
+
showBrowser,
|
|
738
|
+
hideBrowser,
|
|
739
|
+
setProject,
|
|
740
|
+
onFileSelect,
|
|
741
|
+
};
|