virtual-tree-canvas 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +181 -27
  3. package/docs/icon-catalog.html +67 -0
  4. package/docs/icon-catalog.json +392 -0
  5. package/docs/icon-catalog.md +171 -0
  6. package/examples/generate-tree.js +86 -0
  7. package/examples/reorder-table.html +33 -0
  8. package/examples/units.html +43 -0
  9. package/package.json +7 -3
  10. package/resources/icons/bicycle.svg +1 -0
  11. package/resources/icons/bird.svg +1 -0
  12. package/resources/icons/box.svg +1 -0
  13. package/resources/icons/building.svg +1 -0
  14. package/resources/icons/calendar.svg +1 -0
  15. package/resources/icons/car.svg +1 -0
  16. package/resources/icons/cat.svg +1 -0
  17. package/resources/icons/child.svg +1 -0
  18. package/resources/icons/clock.svg +1 -0
  19. package/resources/icons/cloud.svg +1 -0
  20. package/resources/icons/data-bus.svg +1 -0
  21. package/resources/icons/database.svg +1 -0
  22. package/resources/icons/dog.svg +1 -0
  23. package/resources/icons/drone.svg +1 -0
  24. package/resources/icons/fish.svg +1 -0
  25. package/resources/icons/flower.svg +1 -0
  26. package/resources/icons/fog.svg +1 -0
  27. package/resources/icons/globe.svg +1 -0
  28. package/resources/icons/grip.svg +1 -0
  29. package/resources/icons/heart.svg +1 -0
  30. package/resources/icons/helicopter.svg +1 -0
  31. package/resources/icons/insect.svg +1 -0
  32. package/resources/icons/leaf.svg +1 -0
  33. package/resources/icons/link.svg +1 -0
  34. package/resources/icons/lock.svg +1 -0
  35. package/resources/icons/moon.svg +1 -0
  36. package/resources/icons/network.svg +1 -0
  37. package/resources/icons/people.svg +1 -0
  38. package/resources/icons/person.svg +1 -0
  39. package/resources/icons/pin.svg +1 -0
  40. package/resources/icons/radar.svg +1 -0
  41. package/resources/icons/rain.svg +1 -0
  42. package/resources/icons/seedling.svg +1 -0
  43. package/resources/icons/sensor.svg +1 -0
  44. package/resources/icons/server.svg +1 -0
  45. package/resources/icons/ship.svg +1 -0
  46. package/resources/icons/snow.svg +1 -0
  47. package/resources/icons/star-filled.svg +1 -0
  48. package/resources/icons/star.svg +1 -0
  49. package/resources/icons/storm.svg +1 -0
  50. package/resources/icons/sun.svg +1 -0
  51. package/resources/icons/thermometer.svg +1 -0
  52. package/resources/icons/trash.svg +1 -0
  53. package/resources/icons/tree.svg +1 -0
  54. package/resources/icons/wind.svg +1 -0
  55. package/src/assets/icons.js +75 -25
  56. package/src/core/icon-registry.js +6 -8
  57. package/src/core/theme-manager.js +17 -4
  58. package/src/core/tree-cell-layout.js +9 -0
  59. package/src/core/tree-column-model.js +27 -3
  60. package/src/core/tree-model.js +20 -0
  61. package/src/core/types.js +1 -0
  62. package/src/core/view-options.js +37 -0
  63. package/src/core/visible-row-model.js +1 -1
  64. package/src/index.d.ts +209 -5
  65. package/src/index.js +3 -0
  66. package/src/input/index.js +1 -0
  67. package/src/input/row-actions.js +115 -0
  68. package/src/input/row-reorder-input.js +168 -0
  69. package/src/input/tree-filter-bar.js +55 -0
  70. package/src/input/tree-tooltip.js +55 -0
  71. package/src/input/tree-view-input-controller.js +10 -3
  72. package/src/inspector/cell-editor-manager.js +42 -13
  73. package/src/inspector/pane-layout.js +12 -0
  74. package/src/renderers/checkbox.js +14 -0
  75. package/src/renderers/tree-row-renderer.js +103 -62
  76. package/src/tree-view-controller.js +309 -57
  77. package/src/tree-view.js +183 -0
  78. package/src/view/styles.js +14 -0
  79. package/src/assets/icons/air.svg +0 -1
  80. package/src/assets/icons/radar.svg +0 -1
  81. /package/{src/assets → resources}/icons/aircraft.svg +0 -0
  82. /package/{src/assets/icons/bus.svg → resources/icons/bus-vehicle.svg} +0 -0
  83. /package/{src/assets → resources}/icons/control.svg +0 -0
  84. /package/{src/assets → resources}/icons/damage.svg +0 -0
  85. /package/{src/assets → resources}/icons/error.svg +0 -0
  86. /package/{src/assets → resources}/icons/folder.svg +0 -0
  87. /package/{src/assets → resources}/icons/ground.svg +0 -0
  88. /package/{src/assets → resources}/icons/inspector-array.svg +0 -0
  89. /package/{src/assets → resources}/icons/inspector-object.svg +0 -0
  90. /package/{src/assets → resources}/icons/inspector-value.svg +0 -0
  91. /package/{src/assets → resources}/icons/munition.svg +0 -0
  92. /package/{src/assets → resources}/icons/placeholder.svg +0 -0
  93. /package/{src/assets → resources}/icons/point.svg +0 -0
  94. /package/{src/assets → resources}/icons/situation.svg +0 -0
  95. /package/{src/assets → resources}/icons/space.svg +0 -0
  96. /package/{src/assets → resources}/icons/subsurface.svg +0 -0
  97. /package/{src/assets → resources}/icons/surface.svg +0 -0
  98. /package/{src/assets → resources}/icons/task.svg +0 -0
  99. /package/{src/assets → resources}/icons/track.svg +0 -0
  100. /package/{src/assets → resources}/icons/warning.svg +0 -0
@@ -0,0 +1,43 @@
1
+ <!doctype html>
2
+ <html lang="en"><meta charset="utf-8"><title>Numeric units — virtual-tree-canvas</title>
3
+ <style>
4
+ body{margin:24px;background:#0a0f1c;color:#e8edf4;font:14px system-ui}h1{font-size:21px}p{color:#8ea0b5}.host{position:relative;height:230px;border:1px solid #2b3b52;border-radius:8px;overflow:hidden;margin:16px 0;max-width:1050px}canvas{display:block;width:100%;height:100%}#result{font-family:monospace}
5
+ </style>
6
+ <h1>Values with units</h1><p>Units remain visible while editing. Display precision never changes the stored numeric value.</p>
7
+ <div id="pane" class="host"></div><div id="table" class="host"></div><div id="result"></div>
8
+ <script type="module">
9
+ import { TreeView } from '../src/index.js';
10
+ const controllers=[], views=[];
11
+ for (const presentation of ['pane','table']) {
12
+ const host=document.getElementById(presentation);
13
+ const view=new TreeView(host,{mode:'inspector',presentation,filter:false,initialExpandDepth:3});
14
+ const c=view.controller;
15
+ view.setModel({speed:125.45678,altitude:1250.25,acceleration:-9.80665,temperature:21.125,tiny:0.00001234,legacy:12.34567},{
16
+ speed:{unit:'m/s',precision:2,min:0,max:300},altitude:{unit:'m',precision:1},acceleration:{unit:'m/s²',precision:3,readonly:true},temperature:{unit:'°C',precision:2},tiny:{unit:'rad',precision:8},legacy:{}
17
+ });controllers.push(c);views.push(view);
18
+ }
19
+ window.unitControllers=controllers;
20
+ window.addEventListener('pagehide',()=>views.forEach(view=>view.destroy()));
21
+ // Small browser integration check, activated explicitly for automated validation.
22
+ if (new URLSearchParams(location.search).has('test')) {
23
+ const assert=(v,m)=>{if(!v)throw Error(m)};
24
+ try {
25
+ for(const c of controllers){
26
+ const col=c.columnModel.columns.find(x=>x.kind==='inspectorPane'||x.kind==='inspectorValue');
27
+ const box=c.canvas.getBoundingClientRect();
28
+ const hit=c.hitTest(col.x+col.width-60,42);
29
+ assert(hit.part==='number','range numeric hit');
30
+ assert(c.cellEditor.handlePointerDown({},hit),'open numeric editor');
31
+ const input=c.cellEditor.overlay;
32
+ assert(input.value==='125.45678','editor retains full precision');
33
+ assert(input.getAttribute('aria-label').includes('m/s'),'accessible unit');
34
+ assert(input.getBoundingClientRect().right < box.left+col.x+col.width-30,'unit remains outside editor');
35
+ input.value='127.123456';input.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter'}));
36
+ assert(c.model.index.getNode('model:speed').data.value===127.123456,'numeric edit');
37
+ assert(c.model.index.getNode('model:speed').data.valueText==='127.12','display precision after edit');
38
+ c.render();
39
+ }
40
+ document.getElementById('result').textContent='PASS: pane/table editing, precision, units and geometry';
41
+ } catch(e){document.getElementById('result').textContent='FAIL: '+e.stack}
42
+ }
43
+ </script></html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtual-tree-canvas",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "High-performance Canvas2D virtual tree/table widget for very large hierarchical datasets.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -20,9 +20,12 @@
20
20
  },
21
21
  "files": [
22
22
  "src",
23
+ "resources",
23
24
  "docs",
24
25
  "README.md",
25
- "LICENSE"
26
+ "LICENSE",
27
+ "examples",
28
+ "CHANGELOG.md"
26
29
  ],
27
30
  "private": false,
28
31
  "publishConfig": {
@@ -30,7 +33,8 @@
30
33
  },
31
34
  "scripts": {
32
35
  "demo": "python3 -m http.server 4173 --bind localhost",
33
- "test": "node --test test/tree-view-controller.test.js test/theme-icons.test.js test/tree-columns.test.js test/benchmark.test.js test/tree-worker-operations.test.js test/model-inspector.test.js test/dynamic-state.test.js"
36
+ "test": "node --test test/*.test.js",
37
+ "test:browser": "node test/run-browser.mjs"
34
38
  },
35
39
  "keywords": [
36
40
  "tree",
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="16" r="4"/><circle cx="19" cy="16" r="4"/><path d="m5 16 5-9 5 9H5m10 0 3-12h-4M8 7h5m5-3h3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13q6 0 8-7 3-4 6 0l4 2-4 2q0 9-11 9l-3-6Zm5 2 5-4M10 19v3m4-4v4"/><circle cx="15" cy="6" r=".6" fill="currentColor"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-5 9 5v10l-9 5-9-5Zm0 0 9 5 9-5m-9 5v10M8 4l9 5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22V3h16v19M2 22h20M9 22v-5h6v5M8 7h1m6 0h1M8 12h1m6 0h1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M3 10h18M8 2v5m8-5v5M7 14h2m3 0h2m3 0h1M7 18h2m3 0h2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m4 10 2-6h12l2 6M3 10h18v8H3Zm2 8v3m14-3v3M6 14h2m8 0h2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m5 9-1-6 6 4h4l6-4-1 6a7 7 0 1 1-14 0ZM2 13h5m10 0h5M9 11h.01M15 11h.01m-4 4 1 1 1-1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="3"/><path d="M12 8v8m-6-5 6 2 6-2m-6 5-4 5m4-5 4 5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18a5 5 0 1 1 1-10 6 6 0 0 1 11 1 4.5 4.5 0 0 1 0 9Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h18M6 8v4m6 0v4m6-8v4"/><rect x="3" y="3" width="6" height="5" rx="1"/><rect x="15" y="3" width="6" height="5" rx="1"/><rect x="9" y="16" width="6" height="5" rx="1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 4 18 4 18 0V5M3 12c0 4 18 4 18 0"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6q4-3 8 0l2 10q-6 8-12 0ZM8 6 4 5 2 13l4 3M16 6l4-1 2 8-4 3M9 11h.01M15 11h.01m-4 4 1 1 1-1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="6" height="6" rx="2"/><path d="m9 9-4-4m10 4 4-4M9 15l-4 4m10-4 4 4"/><circle cx="5" cy="5" r="3"/><circle cx="19" cy="5" r="3"/><circle cx="5" cy="19" r="3"/><circle cx="19" cy="19" r="3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12q7-12 15-2l4-5v14l-4-5Q10 24 3 12Zm10-7v14"/><circle cx="8" cy="11" r=".7" fill="currentColor"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 14v8m0-4q-7 1-7-4 5-1 7 4m0 2q7 0 7-4-5-1-7 4M9 6C5 0 1 7 7 9c-5 5 3 9 5 3 3 6 10 1 5-3 6-2 1-9-3-3 1-7-7-7-5 0Z"/><circle cx="12" cy="8" r="2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 11a3.5 3.5 0 0 1-.6-6.95A5 5 0 0 1 15 5h2a3 3 0 0 1 0 6H6Z"/><path d="M4 15h16M2 19h7m4 0h9"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><ellipse cx="12" cy="12" rx="4" ry="10"/><path d="M2 12h20"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21 3 12C-2 5 7-1 12 6c5-7 14-1 9 6Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 17V9h8q6 0 6 7l-5 2H6L2 8m4 5H2m9-4V4M3 4h17M7 18v3m9-3v3M4 21h16M14 9v5h5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="14" rx="5" ry="7"/><path d="M9 7V5l-2-2m8 4V5l2-2M7 11 3 8m4 7H2m5 3-4 3m14-10 4-3m-4 7h5m-5 3 4 3M12 7v14"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 3Q3 1 3 13q0 8 8 8 12-1 9-18ZM3 21 16 8"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m9 15 6-6m-4-4 2-2a5 5 0 0 1 7 7l-3 3a5 5 0 0 1-6 1m2 5-2 2a5 5 0 0 1-7-7l3-3a5 5 0 0 1 6-1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="12" rx="2"/><path d="M7 10V7a5 5 0 0 1 10 0v3M12 15v3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 15A9 9 0 0 1 9 3a9 9 0 1 0 11 12Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="5" rx="1"/><rect x="2" y="17" width="6" height="5" rx="1"/><rect x="16" y="17" width="6" height="5" rx="1"/><path d="M12 7v5M5 17v-5h14v5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="7" r="3"/><path d="M2 21v-3a7 7 0 0 1 14 0v3M17 4a3 3 0 0 1 0 6m2 4a6 6 0 0 1 3 5v2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="7" r="4"/><path d="M4 21v-2a8 8 0 0 1 16 0v2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m8 3 8 0-1 6 4 4v2H5v-2l4-4-1-6ZM12 15v7"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4.5"/><path d="m12 12 6.4-6.4"/><g fill="currentColor" stroke="none"><circle cx="12" cy="12" r="1.1"/><circle cx="7" cy="15" r="1.1"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 15a4 4 0 1 1 1-8 5 5 0 0 1 10 1 3.5 3.5 0 0 1 2 7M7 18l-1 3m6-3-1 3m6-3-1 3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22V11M12 15C3 15 2 10 3 5c6-1 10 2 9 10Zm0-4c0-7 4-9 9-8 1 5-2 9-9 8Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M7 7a7 7 0 0 0 0 10m10-10a7 7 0 0 1 0 10M4 4a11 11 0 0 0 0 16M20 4a11 11 0 0 1 0 16"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="7" rx="2"/><rect x="3" y="14" width="18" height="7" rx="2"/><path d="M7 6.5h.01M7 17.5h.01M12 6.5h5m-5 11h5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m3 13 9-4 9 4-3 6M6 19l-3-6m4-2V5h10v6M12 5V2M2 21q3-4 6 0 4-4 8 0 3-4 6 0"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M3 7l18 10M3 17 21 7M9 4l3 3 3-3m-6 16 3-3 3 3M3 10l4-1-1-4m12 14-1-4 4-1M3 14l4 1-1 4M18 5l-1 4 4 1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m12 2 3 6 7 1-5 5 1 7-6-3-6 3 1-7-5-5 7-1Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m12 2 3 6 7 1-5 5 1 7-6-3-6 3 1-7-5-5 7-1Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 15a4 4 0 1 1 0-8 5 5 0 0 1 10 1 3.5 3.5 0 0 1 2 7m-5-4-5 7h5l-2 5 6-8h-5Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14V5a3 3 0 0 1 6 0v9a5 5 0 1 1-6 0ZM12 8v9"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M9 6V3h6v3M6 6l1 15h10l1-15M10 10v7m4-7v7"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m12 2-6 8h3l-6 7h7v5h4v-5h7l-6-7h3Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8h12a3 3 0 1 0-3-3M2 12h17a3 3 0 1 1-3 3M4 16h5a3 3 0 1 1-3 3"/></svg>
@@ -1,25 +1,75 @@
1
- /** Built-in SVG sources. Use registerIcon(name, urlOrSvg) to add application icons. */
2
- export const builtinIconUrls = Object.freeze({
3
- placeholder: new URL('./icons/placeholder.svg', import.meta.url).href,
4
- folder: new URL('./icons/folder.svg', import.meta.url).href,
5
- aircraft: new URL('./icons/aircraft.svg', import.meta.url).href,
6
- radar: new URL('./icons/radar.svg', import.meta.url).href,
7
- warning: new URL('./icons/warning.svg', import.meta.url).href,
8
- error: new URL('./icons/error.svg', import.meta.url).href,
9
- task: new URL('./icons/task.svg', import.meta.url).href,
10
- bus: new URL('./icons/bus.svg', import.meta.url).href,
11
- track: new URL('./icons/track.svg', import.meta.url).href,
12
- point: new URL('./icons/point.svg', import.meta.url).href,
13
- munition: new URL('./icons/munition.svg', import.meta.url).href,
14
- air: new URL('./icons/air.svg', import.meta.url).href,
15
- ground: new URL('./icons/ground.svg', import.meta.url).href,
16
- surface: new URL('./icons/surface.svg', import.meta.url).href,
17
- subsurface: new URL('./icons/subsurface.svg', import.meta.url).href,
18
- space: new URL('./icons/space.svg', import.meta.url).href,
19
- control: new URL('./icons/control.svg', import.meta.url).href,
20
- situation: new URL('./icons/situation.svg', import.meta.url).href,
21
- damage: new URL('./icons/damage.svg', import.meta.url).href,
22
- 'inspector-object': new URL('./icons/inspector-object.svg', import.meta.url).href,
23
- 'inspector-array': new URL('./icons/inspector-array.svg', import.meta.url).href,
24
- 'inspector-value': new URL('./icons/inspector-value.svg', import.meta.url).href,
25
- });
1
+ /** Resolve built-in SVG resources independently of the bundled JavaScript location.
2
+ * @param {string | URL} [iconsBaseUrl] Directory URL, with or without a trailing slash.
3
+ */
4
+ export function createBuiltinIconUrls(iconsBaseUrl) {
5
+ const base = String(iconsBaseUrl ?? new URL('../../resources/icons/', import.meta.url)).replace(/\/+$/, '');
6
+ return Object.freeze(Object.fromEntries(builtinIconNames.map(name => [name, `${base}/${name}.svg`])));
7
+ }
8
+
9
+ export const builtinIconNames = Object.freeze([
10
+ 'aircraft',
11
+ 'bicycle',
12
+ 'bird',
13
+ 'box',
14
+ 'building',
15
+ 'bus-vehicle',
16
+ 'calendar',
17
+ 'car',
18
+ 'cat',
19
+ 'child',
20
+ 'clock',
21
+ 'cloud',
22
+ 'control',
23
+ 'damage',
24
+ 'data-bus',
25
+ 'database',
26
+ 'dog',
27
+ 'drone',
28
+ 'error',
29
+ 'fish',
30
+ 'flower',
31
+ 'fog',
32
+ 'folder',
33
+ 'globe',
34
+ 'grip',
35
+ 'ground',
36
+ 'heart',
37
+ 'helicopter',
38
+ 'insect',
39
+ 'inspector-array',
40
+ 'inspector-object',
41
+ 'inspector-value',
42
+ 'leaf',
43
+ 'link',
44
+ 'lock',
45
+ 'moon',
46
+ 'munition',
47
+ 'network',
48
+ 'people',
49
+ 'person',
50
+ 'pin',
51
+ 'placeholder',
52
+ 'point',
53
+ 'radar',
54
+ 'rain',
55
+ 'seedling',
56
+ 'sensor',
57
+ 'server',
58
+ 'ship',
59
+ 'situation',
60
+ 'snow',
61
+ 'space',
62
+ 'star',
63
+ 'star-filled',
64
+ 'trash',
65
+ 'storm',
66
+ 'subsurface',
67
+ 'sun',
68
+ 'surface',
69
+ 'task',
70
+ 'thermometer',
71
+ 'track',
72
+ 'tree',
73
+ 'warning',
74
+ 'wind',
75
+ ]);
@@ -1,4 +1,4 @@
1
- import { builtinIconUrls } from '../assets/icons.js';
1
+ import { createBuiltinIconUrls } from '../assets/icons.js';
2
2
 
3
3
  /**
4
4
  * Icon source registry for the Canvas2D tree renderer.
@@ -8,11 +8,11 @@ import { builtinIconUrls } from '../assets/icons.js';
8
8
  * colour combination. The render hot path is therefore a single drawImage.
9
9
  */
10
10
  export class IconRegistry {
11
- constructor({ pixelRatio } = {}) {
11
+ constructor({ pixelRatio, iconsBaseUrl } = {}) {
12
12
  this.icons = new Map();
13
13
  this.listeners = new Set();
14
14
  this.pixelRatio = pixelRatio ?? devicePixelRatio();
15
- this.#registerBuiltIns();
15
+ this.#registerBuiltIns(iconsBaseUrl);
16
16
  }
17
17
 
18
18
  /** Register an SVG string/URL, an image URL, ImageBitmap, or legacy Canvas draw function. */
@@ -91,9 +91,7 @@ export class IconRegistry {
91
91
  error: null,
92
92
  };
93
93
  this.icons.set(name, entry);
94
- // Fetching source eagerly moves network and SVG parsing out of scrolling.
95
- // Avoid file-URL fetch attempts while the package is exercised in Node.
96
- if (typeof window !== 'undefined') this.#loadSvg(entry);
94
+ // Load on prepare()/draw(): unused catalog icons do not make network requests.
97
95
  return entry;
98
96
  }
99
97
 
@@ -162,8 +160,8 @@ export class IconRegistry {
162
160
  for (const listener of this.listeners) listener();
163
161
  }
164
162
 
165
- #registerBuiltIns() {
166
- for (const [name, url] of Object.entries(builtinIconUrls)) this.register(name, url);
163
+ #registerBuiltIns(iconsBaseUrl) {
164
+ for (const [name, url] of Object.entries(createBuiltinIconUrls(iconsBaseUrl))) this.register(name, url);
167
165
  }
168
166
  }
169
167
 
@@ -32,11 +32,12 @@ export const darkTheme = {
32
32
  borderStrong: '#2b3b52',
33
33
  shadow: 'rgba(0,0,0,.24)',
34
34
  },
35
+ valueColors: { number: '#8bd5ee', bigint: '#8bd5ee', string: '#dde5ef', boolean: '#86d6b5', enum: '#c4b5fd', null: '#9aa9bc', undefined: '#9aa9bc' },
35
36
  types: {
36
37
  root: { icon: 'folder', color: '#38bdf8' },
37
38
  system: { icon: 'folder', color: '#818cf8' },
38
39
  platform: { icon: 'aircraft', color: '#60a5fa' },
39
- air: { icon: 'air', color: '#60a5fa' },
40
+ air: { icon: 'aircraft', color: '#60a5fa' },
40
41
  ground: { icon: 'ground', color: '#a3e635' },
41
42
  surface: { icon: 'surface', color: '#22d3ee' },
42
43
  subsurface: { icon: 'subsurface', color: '#38bdf8' },
@@ -51,7 +52,7 @@ export const darkTheme = {
51
52
  warning: { icon: 'warning', color: '#facc15' },
52
53
  error: { icon: 'error', color: '#ef4444' },
53
54
  task: { icon: 'task', color: '#f97316' },
54
- bus: { icon: 'bus', color: '#22d3ee' },
55
+ bus: { icon: 'data-bus', color: '#22d3ee' },
55
56
  object: { icon: 'inspector-object', color: '#7dd3fc' },
56
57
  array: { icon: 'inspector-array', color: '#a78bfa' },
57
58
  string: { icon: 'inspector-value', color: '#94a3b8' },
@@ -68,6 +69,7 @@ export const darkTheme = {
68
69
 
69
70
  export const lightTheme = {
70
71
  ...darkTheme,
72
+ valueColors: { number: '#075985', bigint: '#075985', string: '#182438', boolean: '#176249', enum: '#6b3fa0', null: '#576579', undefined: '#576579' },
71
73
  colors: {
72
74
  background: '#f8fafc',
73
75
  row: '#ffffff',
@@ -129,7 +131,7 @@ export const tacticalTheme = {
129
131
  ...darkTheme.types,
130
132
  root: { icon: 'folder', color: '#7ddc92' },
131
133
  platform: { icon: 'aircraft', color: '#93c572' },
132
- air: { icon: 'air', color: '#93c572' },
134
+ air: { icon: 'aircraft', color: '#93c572' },
133
135
  ground: { icon: 'ground', color: '#c7f36f' },
134
136
  surface: { icon: 'surface', color: '#67e8f9' },
135
137
  subsurface: { icon: 'subsurface', color: '#7dd3fc' },
@@ -143,7 +145,7 @@ export const tacticalTheme = {
143
145
  damage: { icon: 'damage', color: '#ff8fab' },
144
146
  warning: { icon: 'warning', color: '#ffd166' },
145
147
  error: { icon: 'error', color: '#ff5c5c' },
146
- bus: { icon: 'bus', color: '#67e8f9' },
148
+ bus: { icon: 'data-bus', color: '#67e8f9' },
147
149
  },
148
150
  };
149
151
 
@@ -188,7 +190,17 @@ export class ThemeManager extends EventTarget {
188
190
  }
189
191
  }
190
192
 
193
+ export function resolveTheme(theme = {}) {
194
+ if (typeof theme === 'string') {
195
+ if (!Object.hasOwn(themes, theme)) throw new TypeError(`Unknown virtual tree theme: ${theme}`);
196
+ return themes[theme];
197
+ }
198
+ if (!theme || typeof theme !== 'object' || Array.isArray(theme)) throw new TypeError('theme must be a theme name or object');
199
+ return theme;
200
+ }
201
+
191
202
  function normalizeTheme(theme = {}) {
203
+ theme = resolveTheme(theme);
192
204
  const base = mergeTheme(darkTheme, theme);
193
205
  return {
194
206
  ...base,
@@ -218,6 +230,7 @@ function mergeTheme(base, override) {
218
230
  ...base,
219
231
  ...override,
220
232
  colors: { ...base.colors, ...(override.colors ?? {}) },
233
+ valueColors: { ...base.valueColors, ...(override.valueColors ?? {}) },
221
234
  types: { ...base.types, ...(override.types ?? {}) },
222
235
  statuses: { ...base.statuses, ...(override.statuses ?? {}) },
223
236
  };
@@ -0,0 +1,9 @@
1
+ /** Share tree-cell positions between painting, hit testing and tooltips. */
2
+ export function treeCellLayout(depth, indentWidth, hasHierarchy) {
3
+ const indent = depth * indentWidth;
4
+ return {
5
+ chevronX: hasHierarchy ? indent + 10 : null,
6
+ iconX: indent + (hasHierarchy ? 27 : 8),
7
+ labelX: indent + (hasHierarchy ? 50 : 31),
8
+ };
9
+ }
@@ -69,6 +69,26 @@ export class TreeColumnModel {
69
69
  if (!this.columns.some((column) => column.kind === 'tree' || column.kind === 'inspectorPane')) {
70
70
  this.columns.unshift(normalizeColumn(builtInColumns.tree, 0));
71
71
  }
72
+ this.setRowActions(this.rowActionCount ?? 0);
73
+ this.setRowReorder(this.rowReorder);
74
+ this.#layout();
75
+ }
76
+
77
+ setRowReorder(enabled) {
78
+ this.rowReorder = Boolean(enabled);
79
+ this.columns = this.columns.filter(column => column.id !== '__vtc_row_order');
80
+ if (this.rowReorder) this.columns.unshift(normalizeColumn({
81
+ id: '__vtc_row_order', label: '', kind: 'rowOrder', width: 72, minWidth: 72,
82
+ sortable: false, resizable: false
83
+ }, 0));
84
+ this.#layout();
85
+ }
86
+
87
+ setRowActions(count) {
88
+ this.rowActionCount = count;
89
+ this.columns = this.columns.filter(column => column.id !== '__vtc_actions');
90
+ if (count) this.columns.push(normalizeColumn({ id: '__vtc_actions', label: '', kind: 'text',
91
+ width: count * 28, minWidth: count * 28, sortable: false, resizable: false, value: () => '' }, 0));
72
92
  this.#layout();
73
93
  }
74
94
 
@@ -83,17 +103,18 @@ export class TreeColumnModel {
83
103
 
84
104
  resizeColumn(id, width) {
85
105
  const column = this.getColumn(id);
86
- if (!column) return false;
106
+ if (!column || column.resizable === false) return false;
87
107
  column.width = Math.max(column.minWidth, width);
88
108
  this.#layout();
89
109
  return true;
90
110
  }
91
111
 
92
112
  moveColumn(id, targetIndex) {
113
+ if (id === '__vtc_row_order' || id === '__vtc_actions') return false;
93
114
  const currentIndex = this.columns.findIndex((column) => column.id === id);
94
115
  if (currentIndex === -1) return false;
95
116
  const [column] = this.columns.splice(currentIndex, 1);
96
- const nextIndex = Math.max(0, Math.min(this.columns.length, targetIndex));
117
+ const nextIndex = Math.max(this.rowReorder ? 1 : 0, Math.min(this.columns.length - (this.rowActionCount ? 1 : 0), targetIndex));
97
118
  this.columns.splice(nextIndex, 0, column);
98
119
  if (!this.columns.some((item) => item.kind === 'tree')) this.columns.unshift(normalizeColumn(builtInColumns.tree, 0));
99
120
  this.#layout();
@@ -107,7 +128,7 @@ export class TreeColumnModel {
107
128
  }
108
129
 
109
130
  getResizeHandleAt(x, tolerance = 5) {
110
- return this.columns.find((column) => Math.abs(x - (column.x + column.width)) <= tolerance) ?? null;
131
+ return this.columns.find((column) => column.resizable !== false && Math.abs(x - (column.x + column.width)) <= tolerance) ?? null;
111
132
  }
112
133
 
113
134
  #layout() {
@@ -144,7 +165,10 @@ function normalizeColumn(column, index) {
144
165
  align: source.align ?? 'left',
145
166
  kind: source.kind ?? (index === 0 ? 'tree' : 'text'),
146
167
  sortable: source.sortable ?? true,
168
+ resizable: source.resizable ?? true,
147
169
  value: source.value ?? ((node) => node[source.id] ?? ''),
170
+ format: source.format,
171
+ valueType: source.valueType,
148
172
  render: source.render,
149
173
  x: 0,
150
174
  };
@@ -56,6 +56,26 @@ export class TreeModel extends EventTarget {
56
56
  this.dispatchEvent(new Event('structurechange'));
57
57
  }
58
58
 
59
+ /** Move one node among siblings without resetting expansion or live values. */
60
+ moveNode(id, targetIndex) {
61
+ const node = this.index.getNode(id);
62
+ if (!node || !Number.isInteger(targetIndex)) return null;
63
+ const parentId = node.parentId ?? null;
64
+ const siblings = this.index.getChildren(parentId);
65
+ const fromIndex = siblings.indexOf(id);
66
+ const toIndex = Math.max(0, Math.min(siblings.length - 1, targetIndex));
67
+ if (fromIndex === toIndex) return null;
68
+ const siblingOrder = siblings.slice();
69
+ siblingOrder.splice(fromIndex, 1);
70
+ siblingOrder.splice(toIndex, 0, id);
71
+ const orderedNodes = siblingOrder.map(nodeId => this.index.getNode(nodeId));
72
+ let position = 0;
73
+ this.nodes = this.nodes.map(item => (item.parentId ?? null) === parentId ? orderedNodes[position++] : item);
74
+ this.index.rebuild(this.nodes);
75
+ this.dispatchEvent(new Event('structurechange'));
76
+ return { nodeId: id, parentId, fromIndex, toIndex, siblingOrder, order: this.nodes.map(item => item.id) };
77
+ }
78
+
59
79
  /** @param {string} id */
60
80
  collapse(id) {
61
81
  this.expanded.delete(id);
package/src/core/types.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * @property {string} [icon]
8
8
  * @property {string} [image]
9
9
  * @property {string[]} [tags]
10
+ * @property {boolean} [reorderable] Allow manual reordering of this node (defaults to true).
10
11
  * @property {any} [data]
11
12
  */
12
13