cyclecad 0.1.4 → 0.1.5

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.
@@ -29,6 +29,9 @@ let isAnimating = false;
29
29
  let gridHelper = null;
30
30
  let axisLines = null;
31
31
  let referencePlanes = {};
32
+ let groundPlane = null;
33
+ let selectionOutline = null;
34
+ let hemiLight = null;
32
35
 
33
36
  // Camera animation state
34
37
  let cameraAnimationState = {
@@ -181,6 +184,10 @@ function setupLighting() {
181
184
  const fillLight = new THREE.DirectionalLight(COLORS.fill, 0.3);
182
185
  fillLight.position.set(-100, 50, -100);
183
186
  scene.add(fillLight);
187
+
188
+ // Hemisphere light for natural environment feel (sky blue → ground grey)
189
+ hemiLight = new THREE.HemisphereLight(0x4488cc, 0x222222, 0.25);
190
+ scene.add(hemiLight);
184
191
  }
185
192
 
186
193
  // ============================================================================
@@ -238,6 +245,16 @@ function setupGridAndOrigin() {
238
245
  axisLines = new THREE.LineSegments(axisGeometry, axisMaterial);
239
246
  scene.add(axisLines);
240
247
 
248
+ // Ground shadow plane (invisible but receives shadows)
249
+ const groundGeom = new THREE.PlaneGeometry(GRID_SIZE * 2, GRID_SIZE * 2);
250
+ const groundMat = new THREE.ShadowMaterial({ opacity: 0.15 });
251
+ groundPlane = new THREE.Mesh(groundGeom, groundMat);
252
+ groundPlane.rotation.x = -Math.PI / 2;
253
+ groundPlane.position.y = -0.01; // Just below grid to avoid z-fighting
254
+ groundPlane.receiveShadow = true;
255
+ groundPlane.userData.isGround = true;
256
+ scene.add(groundPlane);
257
+
241
258
  // Create reference planes (semi-transparent quads)
242
259
  createReferencePlanes();
243
260
  }
@@ -426,6 +443,13 @@ function calculateOptimalDistance() {
426
443
  */
427
444
  export function addToScene(object) {
428
445
  if (scene) {
446
+ // Auto-enable shadows on meshes
447
+ object.traverse((child) => {
448
+ if (child.isMesh) {
449
+ child.castShadow = true;
450
+ child.receiveShadow = true;
451
+ }
452
+ });
429
453
  scene.add(object);
430
454
  }
431
455
  }
@@ -516,6 +540,62 @@ export function toggleWireframe(enabled) {
516
540
  }
517
541
  }
518
542
 
543
+ // ============================================================================
544
+ // Selection Highlighting
545
+ // ============================================================================
546
+
547
+ /**
548
+ * Highlight a mesh with a colored outline/glow effect
549
+ * @param {THREE.Mesh} mesh - Mesh to highlight
550
+ * @param {number} color - Highlight color (default: blue)
551
+ */
552
+ export function highlightMesh(mesh, color = 0x58a6ff) {
553
+ // Remove existing highlight
554
+ clearHighlight();
555
+
556
+ if (!mesh || !mesh.geometry) return;
557
+
558
+ // Create outline using scaled clone with BackSide rendering
559
+ const outlineMat = new THREE.MeshBasicMaterial({
560
+ color: color,
561
+ side: THREE.BackSide,
562
+ transparent: true,
563
+ opacity: 0.4,
564
+ });
565
+
566
+ selectionOutline = new THREE.Mesh(mesh.geometry.clone(), outlineMat);
567
+ selectionOutline.scale.copy(mesh.scale).multiplyScalar(1.03);
568
+ selectionOutline.position.copy(mesh.position);
569
+ selectionOutline.rotation.copy(mesh.rotation);
570
+ selectionOutline.userData.isHighlight = true;
571
+ scene.add(selectionOutline);
572
+ }
573
+
574
+ /**
575
+ * Clear the current selection highlight
576
+ */
577
+ export function clearHighlight() {
578
+ if (selectionOutline) {
579
+ scene.remove(selectionOutline);
580
+ if (selectionOutline.geometry) selectionOutline.geometry.dispose();
581
+ if (selectionOutline.material) selectionOutline.material.dispose();
582
+ selectionOutline = null;
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Enable castShadow on a mesh (call after adding to scene)
588
+ * @param {THREE.Object3D} object - Object to enable shadows on
589
+ */
590
+ export function enableShadows(object) {
591
+ object.traverse((child) => {
592
+ if (child.isMesh) {
593
+ child.castShadow = true;
594
+ child.receiveShadow = true;
595
+ }
596
+ });
597
+ }
598
+
519
599
  // ============================================================================
520
600
  // Accessors
521
601
  // ============================================================================
@@ -664,4 +744,7 @@ export function dispose() {
664
744
  gridHelper = null;
665
745
  axisLines = null;
666
746
  referencePlanes = {};
747
+ groundPlane = null;
748
+ selectionOutline = null;
749
+ hemiLight = null;
667
750
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyclecad",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Browser-based parametric 3D CAD modeler with AI-powered tools, native Inventor file parsing, and smart assembly management. No install required.",
5
5
  "main": "index.html",
6
6
  "scripts": {
@@ -1,233 +0,0 @@
1
- # DUO Inventor Project Manifest
2
-
3
- ## Overview
4
-
5
- A complete JSON manifest of the cycleWASH DUO Inventor project has been generated and is ready for browser-based loading without requiring File System Access API.
6
-
7
- **Files Generated:**
8
- - `/app/duo-manifest.json` — Complete project structure (313 KB)
9
- - `/app/duo-manifest-demo.html` — Interactive viewer (9.3 KB)
10
-
11
- ## Project Statistics
12
-
13
- | Metric | Value |
14
- |--------|-------|
15
- | **Total Files** | 474 |
16
- | **Parts (.ipt)** | 393 |
17
- | **Assemblies (.iam)** | 80 |
18
- | **Custom Parts** | 214 |
19
- | **Standard Parts (DIN/ISO)** | 51 |
20
- | **Vendor/Buyout Parts** | 128 |
21
- | **Main Assembly Size** | 9.07 MB |
22
-
23
- ## Manifest Structure
24
-
25
- ```json
26
- {
27
- "name": "DUO Durchgehend Inventor",
28
- "description": "cycleWASH DUO — Automatic Bicycle Washing Machine",
29
- "ipj": "D-ZBG-DUO-Anlage.ipj",
30
- "workspace": "Workspaces/Arbeitsbereich",
31
- "contentCenter": "Libraries/Content Center Files",
32
- "stats": {
33
- "total": 474,
34
- "parts": 393,
35
- "assemblies": 80,
36
- "custom": 214,
37
- "standard": 51,
38
- "vendor": 128
39
- },
40
- "tree": {
41
- "name": "DUO Durchgehend Inventor",
42
- "type": "folder",
43
- "children": [ /* hierarchical folder structure */ ]
44
- },
45
- "assemblies": [
46
- {
47
- "name": "D-ZBG-DUO-Anlage.iam",
48
- "path": "Workspaces/Arbeitsbereich/Zusatzoptionen/DUOdurch/D-ZBG-DUO-Anlage.iam",
49
- "size": 9500000,
50
- "isMain": true
51
- },
52
- /* ... 79 more assemblies ... */
53
- ],
54
- "parts": [
55
- {
56
- "name": "TrägerHöhe1.ipt",
57
- "path": "Workspaces/Arbeitsbereich/DUO Anlage/Gestell/TrägerHöhe1.ipt",
58
- "category": "custom",
59
- "size": 237568
60
- },
61
- /* ... 392 more parts ... */
62
- ]
63
- }
64
- ```
65
-
66
- ## Usage in Browser
67
-
68
- ### Load the Manifest
69
- ```javascript
70
- // Fetch the manifest in your app
71
- fetch('./duo-manifest.json')
72
- .then(r => r.json())
73
- .then(manifest => {
74
- console.log('Loaded', manifest.stats.parts, 'parts');
75
- console.log('Assemblies:', manifest.assemblies.length);
76
- });
77
- ```
78
-
79
- ### Query the Data
80
- ```javascript
81
- // Find all custom parts
82
- const customParts = manifest.parts.filter(p => p.category === 'custom');
83
-
84
- // Find the main assembly
85
- const mainAssembly = manifest.assemblies.find(a => a.isMain);
86
-
87
- // Get parts in a specific folder
88
- const frameparts = manifest.parts.filter(p =>
89
- p.path.includes('Gestell')
90
- );
91
-
92
- // Sort by size
93
- const largestParts = [...manifest.parts]
94
- .sort((a, b) => b.size - a.size)
95
- .slice(0, 10);
96
- ```
97
-
98
- ## File Categories
99
-
100
- ### Custom Parts (214)
101
- Parts designed for cycleWASH DUO, typically with "D-" prefix:
102
- - `D-TrägerHöhe1.ipt`
103
- - `D-Wanne v2.ipt`
104
- - `D-Deckblech.ipt`
105
-
106
- Location: `Workspaces/Arbeitsbereich/`
107
-
108
- ### Standard Parts (51)
109
- DIN/ISO standard hardware from Inventor's Content Center:
110
- - `ISO 4017 - M6 x 12 - A2.ipt` (hex cap screws)
111
- - `DIN 6912 - M6 x 10 - A2.ipt` (socket cap screws)
112
- - `DIN 912 - M3 x 8 - A2.ipt`
113
-
114
- Location: `Libraries/Content Center Files/de-DE/`
115
-
116
- ### Vendor Parts (128)
117
- Bought-in/standard components from suppliers:
118
- - igus linear guides
119
- - Interroll rollers
120
- - WEG electric motors
121
- - Rittal electrical enclosures
122
- - UNISTAR_2000B components
123
-
124
- Location: `Workspaces/Arbeitsbereich/Zukaufteile/`
125
-
126
- ## Largest Components
127
-
128
- | Part | Size | Category |
129
- |------|------|----------|
130
- | KD-161083.ipt | 21.5 MB | Vendor |
131
- | Rampe NX.ipt | 10.9 MB | Custom |
132
- | 502PU AND MOTOR.ipt | 10.9 MB | Vendor |
133
- | KD-158948.ipt | 9.3 MB | Vendor |
134
- | Große Ecke neuer Beschnitt alleine2.ipt | 5.0 MB | Custom |
135
-
136
- ## Main Assembly
137
-
138
- **Name:** D-ZBG-DUO-Anlage.iam
139
- **Path:** `Workspaces/Arbeitsbereich/Zusatzoptionen/DUOdurch/D-ZBG-DUO-Anlage.iam`
140
- **Size:** 9.07 MB
141
- **Components:** 47 (per parsing)
142
-
143
- ## Folder Structure
144
-
145
- ```
146
- DUO Durchgehend Inventor/
147
- ├── D-ZBG-DUO-Anlage.ipj (project file)
148
- ├── Libraries/
149
- │ └── Content Center Files/
150
- │ └── de-DE/
151
- │ ├── DIN 1587/
152
- │ ├── DIN 580/
153
- │ ├── DIN 6912/
154
- │ ├── DIN 7349 ees/
155
- │ ├── ISO 4017/
156
- │ ├── ISO 4762/
157
- │ └── ... (other DIN/ISO standards)
158
- └── Workspaces/
159
- └── Arbeitsbereich/
160
- ├── DUO Anlage/
161
- │ ├── Gestell/ (frame components)
162
- │ ├── Lenkerhalterung/ (handlebar holder)
163
- │ └── ... (other subsystems)
164
- ├── MiniDuo NX_11/ (smaller variant)
165
- ├── Übernommen/ (legacy parts)
166
- ├── Zukaufteile/ (vendor parts)
167
- │ ├── igus/
168
- │ ├── Interroll/
169
- │ ├── Rittal/
170
- │ ├── WEG/
171
- │ └── UNISTAR_2000B/
172
- └── Zusatzoptionen/ (optional modules)
173
- └── DUOdurch/
174
- ├── Gestell/
175
- ├── Raddreheinheit/
176
- ├── Schiebebürsten/
177
- └── D-ZBG-DUO-Anlage.iam (main assembly)
178
- ```
179
-
180
- ## Integration Points
181
-
182
- ### cyclecad Project Browser
183
- The manifest is designed to integrate with `project-browser.js`:
184
- - Load manifest at startup
185
- - Render folder tree with file type icons
186
- - Filter by category (custom/standard/vendor)
187
- - Search parts by name
188
- - Display statistics dashboard
189
-
190
- ### Assembly Resolver
191
- The manifest enables `assembly-resolver.js` to:
192
- - Quickly reference assembly paths
193
- - Build dependency graphs
194
- - Generate BOMs without parsing binary files
195
- - Map component relationships
196
-
197
- ### Browser UI
198
- No File System Access API needed:
199
- - All 393 parts queryable client-side
200
- - Manifest loads as JSON (standard XHR/Fetch)
201
- - Works on static hosting (GitHub Pages, CDN)
202
- - Progressive loading for large assemblies
203
-
204
- ## Demo Viewer
205
-
206
- Open `duo-manifest-demo.html` in a browser to:
207
- - View project statistics
208
- - Browse all assemblies
209
- - Search parts (first 50 shown)
210
- - Explore folder hierarchy
211
- - Copy file paths for integration
212
-
213
- ## Manifest Generation
214
-
215
- **Source:** `/sessions/sharp-modest-allen/mnt/cyclecad/example/DUO Durchgehend Inventor/`
216
-
217
- **Generated with:**
218
- ```bash
219
- node build-manifest.js > duo-manifest.json
220
- ```
221
-
222
- **Validation:**
223
- - ✅ 393 .ipt files parsed and categorized
224
- - ✅ 80 .iam assemblies indexed
225
- - ✅ Folder hierarchy preserved
226
- - ✅ File sizes included (bytes)
227
- - ✅ Relative paths normalized
228
-
229
- ---
230
-
231
- **Last Updated:** 2026-03-24
232
- **Manifest Size:** 313 KB
233
- **Format:** JSON (UTF-8, no compression)
@@ -1,337 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>DUO Manifest Viewer</title>
7
- <style>
8
- body {
9
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
- margin: 0;
11
- padding: 20px;
12
- background: #f5f5f5;
13
- color: #333;
14
- }
15
- .container {
16
- max-width: 1200px;
17
- margin: 0 auto;
18
- background: white;
19
- padding: 20px;
20
- border-radius: 8px;
21
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
22
- }
23
- h1 {
24
- margin-top: 0;
25
- color: #0066cc;
26
- }
27
- .stats {
28
- display: grid;
29
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
30
- gap: 20px;
31
- margin: 20px 0;
32
- }
33
- .stat-box {
34
- background: #f9f9f9;
35
- border-left: 4px solid #0066cc;
36
- padding: 15px;
37
- border-radius: 4px;
38
- }
39
- .stat-box h3 {
40
- margin: 0 0 10px 0;
41
- color: #666;
42
- font-size: 12px;
43
- text-transform: uppercase;
44
- letter-spacing: 0.5px;
45
- }
46
- .stat-box .value {
47
- font-size: 32px;
48
- font-weight: bold;
49
- color: #0066cc;
50
- }
51
- .stat-box .sublabel {
52
- font-size: 12px;
53
- color: #999;
54
- margin-top: 5px;
55
- }
56
- .tabs {
57
- display: flex;
58
- gap: 0;
59
- border-bottom: 2px solid #eee;
60
- margin: 20px 0;
61
- }
62
- .tab-btn {
63
- padding: 10px 20px;
64
- background: none;
65
- border: none;
66
- cursor: pointer;
67
- font-size: 14px;
68
- color: #666;
69
- border-bottom: 2px solid transparent;
70
- margin-bottom: -2px;
71
- transition: all 0.2s;
72
- }
73
- .tab-btn:hover {
74
- color: #0066cc;
75
- }
76
- .tab-btn.active {
77
- color: #0066cc;
78
- border-bottom-color: #0066cc;
79
- }
80
- .tab-content {
81
- display: none;
82
- }
83
- .tab-content.active {
84
- display: block;
85
- }
86
- table {
87
- width: 100%;
88
- border-collapse: collapse;
89
- font-size: 13px;
90
- }
91
- th {
92
- background: #f5f5f5;
93
- padding: 10px;
94
- text-align: left;
95
- font-weight: 600;
96
- border-bottom: 1px solid #ddd;
97
- }
98
- td {
99
- padding: 10px;
100
- border-bottom: 1px solid #eee;
101
- }
102
- tr:hover {
103
- background: #fafafa;
104
- }
105
- .category-badge {
106
- display: inline-block;
107
- padding: 2px 8px;
108
- border-radius: 3px;
109
- font-size: 11px;
110
- font-weight: 600;
111
- text-transform: uppercase;
112
- letter-spacing: 0.3px;
113
- }
114
- .category-badge.custom {
115
- background: #e3f2fd;
116
- color: #1976d2;
117
- }
118
- .category-badge.standard {
119
- background: #f3e5f5;
120
- color: #7b1fa2;
121
- }
122
- .category-badge.vendor {
123
- background: #fff3e0;
124
- color: #e65100;
125
- }
126
- .tree-view {
127
- margin: 20px 0;
128
- }
129
- .tree-item {
130
- margin-left: 20px;
131
- padding: 5px 0;
132
- font-size: 13px;
133
- }
134
- .tree-folder {
135
- font-weight: 600;
136
- color: #0066cc;
137
- margin-top: 10px;
138
- }
139
- .file-size {
140
- color: #999;
141
- font-size: 12px;
142
- }
143
- .loading {
144
- text-align: center;
145
- color: #999;
146
- padding: 40px;
147
- }
148
- .error {
149
- background: #ffebee;
150
- color: #c62828;
151
- padding: 15px;
152
- border-radius: 4px;
153
- margin: 20px 0;
154
- }
155
- </style>
156
- </head>
157
- <body>
158
- <div class="container">
159
- <h1>DUO Durchgehend Inventor — Manifest Viewer</h1>
160
- <div class="loading">Loading manifest...</div>
161
- </div>
162
-
163
- <script>
164
- async function loadManifest() {
165
- try {
166
- const response = await fetch('./duo-manifest.json');
167
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
168
-
169
- const manifest = await response.json();
170
- renderManifest(manifest);
171
- } catch (error) {
172
- document.querySelector('.container').innerHTML = `
173
- <h1>DUO Durchgehend Inventor — Manifest Viewer</h1>
174
- <div class="error">Error loading manifest: ${error.message}</div>
175
- `;
176
- }
177
- }
178
-
179
- function formatSize(bytes) {
180
- if (bytes === 0) return '0 B';
181
- const k = 1024;
182
- const sizes = ['B', 'KB', 'MB', 'GB'];
183
- const i = Math.floor(Math.log(bytes) / Math.log(k));
184
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
185
- }
186
-
187
- function renderManifest(manifest) {
188
- const s = manifest.stats;
189
- const html = `
190
- <h1>${manifest.name}</h1>
191
- <p><strong>${manifest.description}</strong></p>
192
-
193
- <div class="stats">
194
- <div class="stat-box">
195
- <h3>Total Files</h3>
196
- <div class="value">${s.total}</div>
197
- </div>
198
- <div class="stat-box">
199
- <h3>Parts (.ipt)</h3>
200
- <div class="value">${s.parts}</div>
201
- <div class="sublabel">Custom: ${s.custom} • Standard: ${s.standard} • Vendor: ${s.vendor}</div>
202
- </div>
203
- <div class="stat-box">
204
- <h3>Assemblies (.iam)</h3>
205
- <div class="value">${s.assemblies}</div>
206
- </div>
207
- </div>
208
-
209
- <div class="tabs">
210
- <button class="tab-btn active" data-tab="project">Project Info</button>
211
- <button class="tab-btn" data-tab="assemblies">Assemblies</button>
212
- <button class="tab-btn" data-tab="parts">Parts</button>
213
- <button class="tab-btn" data-tab="tree">Folder Tree</button>
214
- </div>
215
-
216
- <div id="project" class="tab-content active">
217
- <table>
218
- <tr>
219
- <th>Property</th>
220
- <th>Value</th>
221
- </tr>
222
- <tr>
223
- <td>Project File</td>
224
- <td><code>${manifest.ipj}</code></td>
225
- </tr>
226
- <tr>
227
- <td>Workspace</td>
228
- <td><code>${manifest.workspace}</code></td>
229
- </tr>
230
- <tr>
231
- <td>Content Center</td>
232
- <td><code>${manifest.contentCenter}</code></td>
233
- </tr>
234
- <tr>
235
- <td>Total Components</td>
236
- <td>${s.total}</td>
237
- </tr>
238
- <tr>
239
- <td>Main Assembly</td>
240
- <td><code>${manifest.assemblies.find(a => a.isMain)?.path || 'N/A'}</code></td>
241
- </tr>
242
- </table>
243
- </div>
244
-
245
- <div id="assemblies" class="tab-content">
246
- <table>
247
- <tr>
248
- <th>Assembly Name</th>
249
- <th>Path</th>
250
- <th>Size</th>
251
- <th></th>
252
- </tr>
253
- ${manifest.assemblies.map(a => `
254
- <tr>
255
- <td><strong>${a.name}</strong></td>
256
- <td><code style="font-size: 11px">${a.path}</code></td>
257
- <td>${formatSize(a.size)}</td>
258
- <td>${a.isMain ? '<span style="color: #d32f2f; font-weight: 600;">● MAIN</span>' : ''}</td>
259
- </tr>
260
- `).join('')}
261
- </table>
262
- </div>
263
-
264
- <div id="parts" class="tab-content">
265
- <table>
266
- <tr>
267
- <th>Part Name</th>
268
- <th>Category</th>
269
- <th>Path</th>
270
- <th>Size</th>
271
- </tr>
272
- ${manifest.parts.slice(0, 50).map(p => `
273
- <tr>
274
- <td>${p.name}</td>
275
- <td><span class="category-badge ${p.category}">${p.category}</span></td>
276
- <td><code style="font-size: 11px">${p.path}</code></td>
277
- <td>${formatSize(p.size)}</td>
278
- </tr>
279
- `).join('')}
280
- <tr>
281
- <td colspan="4" style="text-align: center; color: #999; font-size: 12px;">
282
- Showing 50 of ${manifest.parts.length} parts
283
- </td>
284
- </tr>
285
- </table>
286
- </div>
287
-
288
- <div id="tree" class="tab-content">
289
- <div class="tree-view">
290
- ${renderTree(manifest.tree, 0)}
291
- </div>
292
- </div>
293
- `;
294
-
295
- document.querySelector('.container').innerHTML = html;
296
-
297
- // Tab switching
298
- document.querySelectorAll('.tab-btn').forEach(btn => {
299
- btn.addEventListener('click', (e) => {
300
- document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
301
- document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
302
- e.target.classList.add('active');
303
- document.getElementById(e.target.dataset.tab).classList.add('active');
304
- });
305
- });
306
- }
307
-
308
- function renderTree(node, depth, limit = 100, count = { n: 0 }) {
309
- if (count.n >= limit) return '';
310
- count.n++;
311
-
312
- if (node.type === 'folder') {
313
- const margin = depth * 20;
314
- const items = (node.children || []).slice(0, 50);
315
- return `
316
- <div class="tree-item" style="margin-left: ${margin}px; font-weight: 600; color: #0066cc;">
317
- 📁 ${node.name}
318
- </div>
319
- ${items.map(child => renderTree(child, depth + 1, limit, count)).join('')}
320
- ${(node.children || []).length > 50 ? `<div class="tree-item" style="margin-left: ${(depth+1)*20}px; color: #999;">... and ${node.children.length - 50} more</div>` : ''}
321
- `;
322
- } else {
323
- const margin = depth * 20;
324
- const badge = node.category ? `<span class="category-badge ${node.category}" style="margin-left: 10px">${node.category}</span>` : '';
325
- return `
326
- <div class="tree-item" style="margin-left: ${margin}px;">
327
- 📄 ${node.name} <span class="file-size">${formatSize(node.size)}</span>${badge}
328
- </div>
329
- `;
330
- }
331
- }
332
-
333
- // Load on page load
334
- loadManifest();
335
- </script>
336
- </body>
337
- </html>