@gsteja2307/orbit-3d-explorer 1.0.0 → 2.0.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/CHANGELOG.md +72 -0
- package/README.md +88 -18
- package/dist/app.js +63 -31
- package/dist/graph.js +16 -0
- package/dist/index.html +7 -6
- package/dist/model.js +67 -0
- package/dist/sample.json +1019 -0
- package/dist/store.js +12 -0
- package/dist/style.css +1 -2
- package/package.json +41 -10
- package/server.mjs +23 -3
- package/tests/model.test.mjs +24 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {clone,normalizeGraph} from './model.js';
|
|
2
|
+
const KEY='orbit.workspace.v2';
|
|
3
|
+
export class WorkspaceStore {
|
|
4
|
+
constructor(storage,seed){this.storage=storage;this.warning='';this.state={graph:normalizeGraph(seed),audit:[],views:[],bookmarks:[],history:[],revision:0};try{const raw=storage.getItem(KEY);if(raw){const saved=JSON.parse(raw);saved.graph=normalizeGraph(saved.graph);for(const field of ['audit','views','bookmarks','history'])if(!Array.isArray(saved[field]))throw Error('Invalid workspace state');this.state=saved;}}catch(e){this.warning='Saved workspace could not be loaded. Export recovery data before making changes.';this.loadError=true;}}
|
|
5
|
+
persist(next){if(this.loadError)throw Error(this.warning);const raw=this.storage.getItem(KEY);if(raw && (JSON.parse(raw).revision||0)!==(this.state.revision||0))throw Error('Workspace changed in another tab. Reload before saving.');next.revision=(this.state.revision||0)+1;this.storage.setItem(KEY,JSON.stringify(next));this.state=next;}
|
|
6
|
+
commit(graph,action){const next=clone(this.state);next.graph=normalizeGraph(graph);next.history=[...next.history,nextGraphSnapshot(this.state)].slice(-10);next.audit=[{at:new Date().toISOString(),action,actor:'Local browser'},...next.audit].slice(0,200);next.revision=(next.revision||0)+1;this.persist(next);}
|
|
7
|
+
undo(){const next=clone(this.state),previous=next.history.pop();if(!previous)return false;next.graph=normalizeGraph(previous);next.audit.unshift({at:new Date().toISOString(),action:'Restored previous graph snapshot',actor:'Local browser'});next.audit=next.audit.slice(0,200);next.revision++;this.persist(next);return true;}
|
|
8
|
+
preference(key,value){if(!['views','bookmarks'].includes(key))throw Error('Unknown preference');this.persist({...this.state,[key]:clone(value)});}
|
|
9
|
+
reload(){const saved=JSON.parse(this.storage.getItem(KEY));if(saved){saved.graph=normalizeGraph(saved.graph);this.state=saved;}}
|
|
10
|
+
}
|
|
11
|
+
function nextGraphSnapshot(state){return clone(state.graph);}
|
|
12
|
+
export {KEY};
|
package/dist/style.css
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
|
2
|
-
:root{color-scheme:dark;--bg:#0c1016;--panel:#11171f;--line:#252d38;--text:#e7edf4;--muted:#7e8b9d;--teal:#65dfc2;--violet:#a494ff;--amber:#edbb70}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px 'DM Sans',system-ui,sans-serif}button,input,select{font:inherit}button,select{cursor:pointer;color:var(--text);background:#19212c;border:1px solid var(--line);border-radius:7px;padding:9px 12px}button:hover{background:#283446;border-color:#596b80}button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--teal);outline-offset:3px}.rail{position:fixed;width:70px;height:100vh;border-right:1px solid var(--line);display:flex;align-items:center;flex-direction:column;gap:18px;padding:24px 0;background:#0e141c}.logo{font-size:37px;color:var(--teal);text-decoration:none;margin-bottom:39px}.railbtn{font-size:25px;border:0;background:transparent;width:44px;height:45px;padding:0;color:var(--muted)}.railbtn.active{color:var(--teal);background:#18312f}.railbottom{margin-top:auto;border:1px solid #435249;border-radius:50%;padding:9px;color:#b7c8bc;font-size:11px}.shell{margin-left:70px}header{height:82px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 32px}header>div{display:flex;align-items:center;gap:24px}.brand{font:700 29px 'Space Grotesk',sans-serif;letter-spacing:-1.5px}.dot{color:var(--teal)}.divider{height:22px;width:1px;background:var(--line)}.workspace{color:#b1bccb;font-size:12px}.muted{color:var(--muted)}.headerRight{gap:18px}.demo{font-size:9px;letter-spacing:1.2px;color:#90a49d}.avatar{background:#36344d;color:#d7cbff;border:1px solid #514869;border-radius:50%;width:34px;height:34px;display:grid;place-items:center;font-size:11px}main{padding:30px 30px 0}.heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:26px}.eyebrow{font-size:9px;color:var(--teal);letter-spacing:2px;font-weight:700}h1{font:500 30px 'Space Grotesk',sans-serif;letter-spacing:-1px;margin:10px 0}p{color:var(--muted);line-height:1.6;margin:6px 0;font-size:12px}.stats{display:flex;gap:32px}.stats div{display:flex;flex-direction:column;gap:5px;border-left:1px solid var(--line);padding-left:25px}.stats b{font:500 25px 'Space Grotesk',sans-serif}.stats span{font-size:10px;color:var(--muted)}.toolbar{display:flex;gap:12px;margin-bottom:18px;align-items:center}.search{display:flex;align-items:center;gap:10px;flex:1;background:#131b24;border:1px solid var(--line);border-radius:7px;padding:9px 12px;font-size:19px;color:var(--muted)}input{background:transparent;border:0;outline:none;color:var(--text);width:100%;font-size:12px}kbd{font-size:10px;border:1px solid #394353;border-radius:3px;padding:1px 5px}.toolbar select,.toolbar>button{font-size:11px;height:37px}.tabs{display:flex;padding:3px;border:1px solid var(--line);border-radius:7px}.tabs button{font-size:11px;border:0;background:transparent;padding:7px 12px;color:var(--muted)}.tabs button.active{background:#273540;color:white}.content{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:17px;height:calc(100vh - 297px);min-height:510px;max-height:900px}.graph{position:relative;overflow:hidden;border:1px solid var(--line);border-radius:11px;background:radial-gradient(ellipse at center,#19232d 0%,#101720 70%)}#scene,#labels{position:absolute;inset:0}#scene canvas{display:block;width:100%;height:100%}#labels{pointer-events:none;overflow:hidden}.nodeLabel{position:absolute;transform:translate(-50%,-50%);font-size:10px;white-space:nowrap;color:#bdcad6;text-shadow:0 2px 4px #000;pointer-events:auto;cursor:pointer;background:#121a22c9;border:1px solid #34445188;padding:4px 7px;border-radius:5px}.nodeLabel.selected{color:#fff;border-color:var(--teal);background:#1b3c36}.nodeLabel small{display:block;font-size:8px;color:#8496a8;margin-top:2px}.graphTitle{position:absolute;top:20px;left:20px;font-size:11px;display:flex;align-items:center;gap:8px}.graphTitle>span:last-child{font-size:9px;color:var(--muted);border-left:1px solid #394451;padding-left:10px;margin-left:4px}.live{display:inline-block;width:6px;height:6px;background:var(--teal);border-radius:50%}.legend{position:absolute;bottom:55px;left:20px;display:flex;gap:16px;font-size:9px;color:#9aabba}.legend span{display:flex;align-items:center;gap:6px}.legend i{width:7px;height:7px;border-radius:50%}.person{background:var(--teal)}.project{background:var(--violet)}.app{background:var(--amber)}.graphTools{position:absolute;right:15px;bottom:53px;display:flex;gap:4px}.graphTools button{padding:5px 9px;font-size:13px;background:#19232f}.hint{position:absolute;bottom:0;width:100%;text-align:center;padding:13px;border-top:1px solid #26313b;color:#637488;font-size:9px}.hint em{padding:0 12px;font-style:normal}.details{border:1px solid var(--line);border-radius:11px;background:#121922;overflow:auto;padding:22px}.detailTop{display:flex;justify-content:space-between;color:var(--muted);font-size:9px;letter-spacing:1.7px}.detailTop button{padding:0;border:0;background:transparent;font-size:16px}.bigAvatar{margin-top:22px;width:58px;height:58px;border-radius:15px;background:linear-gradient(135deg,#244d4b,#233a47);border:1px solid #436862;display:grid;place-items:center;color:#9ce9d6;font:500 21px 'Space Grotesk',sans-serif}.details h2{font:500 23px 'Space Grotesk',sans-serif;margin:14px 0 4px;letter-spacing:-.7px}.role{font-size:11px;color:#99a8bc}.badge{display:inline-block;padding:4px 7px;margin-top:12px;background:#1a332e;color:#83d3b7;border:1px solid #2c4a40;border-radius:5px;font-size:9px}.meta{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin:20px 0}.meta span{font-size:9px;color:var(--muted);display:block;margin-bottom:5px}.meta b{font-size:10px;font-weight:500}.sectionTitle{display:flex;justify-content:space-between;font-size:10px;margin:23px 0 10px;letter-spacing:.3px}.sectionTitle span{color:var(--muted)}.connection{width:100%;display:flex;align-items:center;gap:10px;text-align:left;margin-top:7px;padding:10px;background:#171f2a;border:1px solid #2a3442}.connection .mini{width:29px;height:29px;background:#283c3d;border-radius:7px;display:grid;place-items:center;color:var(--teal);font-size:10px;flex-shrink:0}.connection.projectLink .mini{background:#302a49;color:var(--violet)}.connection.appLink .mini{background:#413527;color:var(--amber)}.connection strong{display:block;font-size:11px;font-weight:500}.connection small{font-size:9px;color:var(--muted);display:block;margin-top:4px}.connection .arrow{margin-left:auto;color:#6c7a8c}.chips{display:flex;flex-wrap:wrap;gap:5px}.chips span{font-size:9px;border:1px solid #344151;border-radius:5px;padding:5px 7px;color:#a3b2c5}.focus{width:100%;background:#70dec1;color:#10251f;border:0;margin-top:22px;font-size:11px;font-weight:700}.focus:hover{background:#a1efd9}.load{height:4px;border-radius:4px;background:#2b3643;margin:9px 0}.load i{height:100%;display:block;background:var(--teal);border-radius:4px}footer{padding:17px 0;display:flex;justify-content:space-between;color:#647387;font-size:9px}footer .live{width:4px;height:4px;margin-right:6px}#empty{position:absolute;top:45%;left:15%;right:15%;text-align:center;color:var(--muted)}#directory{position:absolute;inset:55px 12px 45px;overflow:auto;background:#111923;padding:10px;z-index:3}.directoryRow{display:flex;justify-content:space-between;width:100%;margin:6px 0;text-align:left;font-size:12px}.directoryRow small{color:var(--muted)}#toast{position:fixed;bottom:25px;left:50%;transform:translateX(-50%);background:#29483d;color:#e0fff2;border-radius:8px;padding:12px 20px;font-size:12px;display:none;z-index:10}a{color:var(--teal)}@media(max-width:1100px){.content{grid-template-columns:minmax(0,1fr) 290px}.tabs button{padding:7px}.workspace,.demo{display:none}.stats{gap:15px}main{padding:25px 18px 0}}@media(max-width:800px){.rail{width:52px}.shell{margin-left:52px}header{padding:0 18px;height:65px}.stats{display:none}.toolbar{flex-wrap:wrap}.search{min-width:100%}.content{grid-template-columns:1fr;height:auto;max-height:none}.graph{height:560px}.details{max-height:600px}.heading h1{font-size:26px}footer{gap:15px}.graphTitle>span:last-child{display:none}.headerRight button{font-size:10px}}
|
|
1
|
+
*{box-sizing:border-box}:root{color-scheme:dark;--bg:#0b1017;--panel:#121b25;--line:#263340;--muted:#8b9bad;--mint:#78e4c4;--text:#e8eff5}body{margin:0;background:var(--bg);color:var(--text);font:13px Inter,Segoe UI,system-ui,sans-serif}button,input,select,textarea{font:inherit}button,select,input,textarea{border:1px solid var(--line);background:#17222e;color:var(--text);border-radius:7px;padding:9px 12px}button,select{cursor:pointer}button:hover{background:#263747}button:disabled{opacity:.4;cursor:not-allowed}button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--mint);outline-offset:3px}button.primary{background:var(--mint);color:#102b25;border-color:var(--mint);font-weight:650}a{color:var(--mint)}.mint{color:var(--mint)}[hidden]{display:none!important}.sidebar{position:fixed;inset:0 auto 0 0;width:224px;padding:27px 18px;border-right:1px solid var(--line);background:#0e151d;display:flex;flex-direction:column}.brand{display:flex;gap:10px;align-items:center;text-decoration:none;font-size:29px;color:var(--mint);font-weight:650;letter-spacing:-1px}.brand>span{color:white}.brand small{font:10px monospace;color:#9bafbf;background:#24313e;padding:3px 5px;border-radius:4px;margin-left:auto}.workspaceBadge{display:flex;gap:10px;align-items:center;border:1px solid var(--line);padding:11px 9px;border-radius:8px;margin:30px 0}.workspaceIcon{background:#253f3e;color:var(--mint);padding:8px;border-radius:6px}.workspaceBadge strong{display:block;font-size:11px;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.workspaceBadge small{display:block;color:var(--muted);font-size:10px;margin-top:4px}.navLabel{font-size:9px;letter-spacing:1.5px;color:#6f8396;margin:7px 8px 13px;display:flex;justify-content:space-between;align-items:center}.navLabel button{padding:0 5px;font-size:15px;border:0;background:transparent}.sidebar nav{display:grid;gap:5px;margin-bottom:30px}.sidebar nav button{display:flex;gap:12px;align-items:center;text-align:left;background:transparent;border:0;color:#9eb0bf;font-size:17px;padding:11px}.sidebar nav button span{font-size:12px}.sidebar nav button.active{background:#1b3533;color:var(--mint)}#savedViews{overflow:auto}.savedView{display:flex;gap:4px;margin:5px 0}.savedView button{border:0;background:transparent;text-align:left;font-size:11px}.savedView button:first-child{flex:1;overflow:hidden;text-overflow:ellipsis}.sidebarFoot{margin-top:auto;display:flex;gap:8px;font-size:10px;color:#afc3c0;padding-top:20px}.sidebarFoot small{display:block;font-size:9px;color:#708494;margin-top:5px}.live{display:inline-block;width:6px;height:6px;background:var(--mint);border-radius:50%;margin-top:4px}.shell{margin-left:224px}header{height:74px;padding:0 30px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}.crumb{font-size:11px;color:var(--muted)}.crumb span{margin:0 15px;color:#43515e}.crumb b{font-weight:500;color:#ced8e3}.headerActions{display:flex;gap:9px;align-items:center}.headerActions button{font-size:11px}#saveStatus{color:#758e8c;font-size:10px;margin-right:10px}main{padding:28px 28px 0}.heading{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.eyebrow{font-size:9px;letter-spacing:1.8px;color:var(--mint)}h1{font-size:28px;font-weight:550;letter-spacing:-1px;margin:10px 0}p{color:var(--muted);font-size:12px;line-height:1.65;margin:8px 0}.versionBadge{font:9px monospace;color:#a5b1c7;letter-spacing:1px;border:1px solid #334256;border-radius:5px;padding:7px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:13px;margin-bottom:23px}.metric{padding:16px 18px;background:linear-gradient(110deg,#15202a,#111b25);border:1px solid var(--line);border-radius:9px}.metricTop{font-size:10px;color:#9bafbd;display:flex;justify-content:space-between}.metric b{display:block;font-size:25px;font-weight:550;margin:9px 0 5px}.metric small{font-size:9px;color:#758c9b}.metric .accent{color:var(--mint)}.toolbar{display:flex;gap:8px;margin-bottom:15px;flex-wrap:wrap}.search{flex:1;min-width:210px;display:flex;align-items:center;gap:8px;border:1px solid var(--line);background:#131e28;padding:0 10px;border-radius:7px;color:#7e95a5}.search input{width:100%;background:transparent;border:0;padding:10px 0;font-size:11px;outline:none}kbd{border:1px solid #334552;padding:1px 4px;border-radius:3px;font-size:10px}.toolbar select,.toolbar button{font-size:10px}.content{display:grid;grid-template-columns:minmax(0,1fr) 305px;gap:15px;min-height:590px;height:calc(100vh - 365px)}.graphPanel{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:#101923;display:flex;flex-direction:column;min-width:0}.panelToolbar{display:flex;align-items:center;gap:9px;padding:12px;border-bottom:1px solid var(--line);flex-wrap:wrap}.panelToolbar button,.panelToolbar select{font-size:10px;padding:6px 8px}.segmented{display:flex;gap:2px;background:#0d151d;border:1px solid var(--line);border-radius:6px;padding:2px}.segmented button{border:0;background:transparent;color:var(--muted)}.segmented button.active{background:#2d414c;color:#e6f3ef}#resultCount{font-size:9px;color:#89a1b1;margin-right:auto}#canvasWrap{position:relative;flex:1;min-height:400px;overflow:hidden;background:radial-gradient(ellipse at center,#1b2a37,#101923 75%)}#scene{position:absolute;inset:0}#scene canvas{display:block}.graphLabels{position:absolute;inset:0;pointer-events:none;overflow:hidden}.nodeLabel{position:absolute;transform:translate(-50%,-50%);white-space:nowrap;font-size:9px;padding:3px 6px;pointer-events:auto;color:#d0dde5;background:#111e28d9;border:1px solid #39506177;text-shadow:0 1px 2px black}.nodeLabel.selected{background:#24534a;border-color:var(--mint);color:white;z-index:3}.nodeLabel.pathNode{border-color:white;color:white}.hint{position:absolute;bottom:0;left:0;right:0;text-align:center;padding:12px;font-size:9px;color:#7890a2;border-top:1px solid #243440;background:#101923b0}#legend{position:absolute;left:15px;bottom:54px;display:flex;gap:12px;flex-wrap:wrap;max-width:60%;pointer-events:none}#legend span{display:flex;gap:5px;align-items:center;color:#9eb0bf;font-size:9px}#legend i{width:6px;height:6px;border-radius:50%}.graphTools{position:absolute;right:12px;bottom:48px;display:flex;gap:3px}.graphTools button{font-size:12px;padding:5px 8px}#empty{position:absolute;top:45%;left:15%;right:15%;text-align:center;color:var(--muted);pointer-events:none}#pathBanner{position:absolute;top:15px;left:15px;right:15px;padding:10px;border:1px solid #4d7269;background:#19362fea;font-size:11px;z-index:4}.details{background:#111b25;border:1px solid var(--line);border-radius:10px;padding:20px;overflow:auto}.detailTop{display:flex;justify-content:space-between;font:9px monospace;letter-spacing:1px;color:#8ca4b3}.detailTop button{padding:2px 6px;border:0;background:transparent;font-size:17px}.bigAvatar{width:52px;height:52px;border:1px solid #496770;background:#24423e;border-radius:12px;display:grid;place-items:center;font-size:20px;color:var(--mint);margin:18px 0 12px}.details h2{font-size:22px;letter-spacing:-.6px;font-weight:550;margin:9px 0}.subtitle{font-size:11px;color:#9cb0c0}.badge{display:inline-block;font-size:9px;padding:4px 7px;background:#263b35;color:#9adac5;border-radius:4px;margin-top:10px}.detailActions{display:flex;gap:6px;margin:17px 0}.detailActions button{font-size:10px;flex:1}.meta{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:20px 0}.meta small{display:block;font-size:9px;color:#8198aa;margin-bottom:5px}.meta span{font-size:11px;overflow-wrap:anywhere}.sectionTitle{font-size:10px;display:flex;justify-content:space-between;margin:23px 0 9px;color:#b7c9d5}.sectionTitle span{color:#718c9e}.connection{width:100%;display:flex;align-items:center;gap:9px;margin:6px 0;text-align:left;padding:10px;background:#172330}.connection strong{font-size:11px;display:block;font-weight:500}.connection small{font-size:9px;color:#849fb2;display:block;margin-top:5px}.mini{width:27px;height:27px;border-radius:6px;display:grid;place-items:center;background:#283c49;color:var(--mint);font-size:10px}.chips{display:flex;gap:5px;flex-wrap:wrap}.chips span{padding:5px 7px;border-radius:5px;border:1px solid #344956;font-size:9px;color:#a7c0ce}.load{height:5px;background:#2b3945;border-radius:4px;margin:10px 0}.load i{display:block;height:100%;border-radius:4px;background:var(--mint)}.danger{color:#f7a8a8!important}.full{width:100%;margin-top:15px}.emptyState{padding:35px 20px;text-align:center;color:var(--muted)}#directory{flex:1;overflow:auto}table{width:100%;border-collapse:collapse;font-size:11px}th,td{padding:13px 15px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}th{font-size:9px;color:#90a8b8;position:sticky;top:0;background:#172531}td button{padding:0;border:0;background:transparent;color:#d8f2e9;text-align:left}td small{display:block;color:#708e9f;margin-top:4px}.tableFoot{padding:12px;color:var(--muted);font-size:10px}.pagePanel{min-height:500px}.panelGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:17px}.card{background:#121e29;border:1px solid var(--line);border-radius:10px;padding:22px}.card h2{font-size:16px;font-weight:550;margin:0 0 13px}.card h3{font-size:12px;font-weight:550}.card button{font-size:11px}.card .bar{display:flex;align-items:center;gap:12px;margin:15px 0;font-size:11px}.bar span{min-width:90px}.bar i{height:6px;background:var(--mint);border-radius:4px}.bar b{margin-left:auto;font-size:10px;color:#94b1bd}.auditRow{display:flex;gap:20px;padding:16px 0;border-bottom:1px solid var(--line)}.auditRow small{min-width:160px;color:#819aab;font-size:10px}.auditRow b{font-weight:500;font-size:12px}.auditRow span{display:block;color:#6f8c9d;font-size:10px;margin-top:5px}footer{display:flex;justify-content:space-between;padding:18px 0;font-size:9px;color:#6f8799}footer b{font-weight:400;color:#9ab1be;margin-left:8px}dialog{color:var(--text);background:#131e29;border:1px solid #405463;border-radius:12px;width:min(640px,95vw);max-height:90vh;padding:23px;box-shadow:0 25px 100px #0008}dialog::backdrop{background:#020810bb;backdrop-filter:blur(3px)}.dialogHead{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-bottom:18px}.dialogHead h2{font-size:20px;font-weight:550;margin:0}.dialogHead button{background:transparent;border:0;font-size:22px;padding:0 8px}.formGrid{display:grid;grid-template-columns:1fr 1fr;gap:15px}label.field{display:flex;flex-direction:column;gap:7px;font-size:11px;color:#b3c8d5}label.field.wide{grid-column:1/-1}label.field input,label.field textarea,label.field select{width:100%;font-size:12px}textarea{resize:vertical;min-height:80px}.dialogActions{display:flex;justify-content:flex-end;gap:8px;margin-top:23px;padding-top:18px;border-top:1px solid var(--line)}#formError{background:#482b32;color:#ffc5cc;white-space:pre-wrap;padding:12px;font:11px/1.7 system-ui;border-radius:6px}#toast{position:fixed;left:50%;bottom:25px;transform:translateX(-50%);background:#265347;color:#dcfff4;border:1px solid #568577;padding:13px 22px;border-radius:8px;z-index:99;display:none;max-width:90vw}.note{padding:12px;border:1px solid #3e505a;background:#1b2c35;border-radius:6px;color:#b4cbd5;font-size:11px;line-height:1.7;margin:10px 0}#warning{padding:13px;background:#46362c;border:1px solid #795638;color:#ffdaa8;margin-bottom:20px;border-radius:7px}.inlineActions{display:flex;gap:8px;flex-wrap:wrap}.relationshipRow{display:flex;align-items:center;gap:5px}.relationshipRow>.connection{flex:1}.relationshipRow>button{padding:5px 7px;font-size:10px}.contentText{white-space:pre-wrap;overflow-wrap:anywhere}.activeFilter{border-color:var(--mint);color:var(--mint)}.scrollBox{overflow:auto;max-height:280px}.pathStep{margin:8px 0;padding:10px;border-left:2px solid var(--mint);background:#1a3035;font-size:12px}@media(min-width:1600px){.content{grid-template-columns:minmax(0,1fr) 345px}}@media(max-width:1150px){.sidebar{width:190px;padding:22px 12px}.shell{margin-left:190px}main{padding:22px 18px 0}.content{grid-template-columns:minmax(0,1fr) 275px}.versionBadge{display:none}header{padding:0 20px}#saveStatus{display:none}.graphTools button{padding:5px}.metrics{gap:9px}.metric{padding:14px}.panelToolbar{gap:5px}}@media(max-width:850px){.sidebar{width:65px;padding:20px 8px}.brand{font-size:27px;justify-content:center}.brand span,.brand small,.workspaceBadge,.navLabel,#savedViews,.sidebarFoot,.sidebar nav button span{display:none}.sidebar nav{margin-top:25px}.sidebar nav button{justify-content:center}.shell{margin-left:65px}.content{grid-template-columns:1fr;height:auto}.graphPanel{height:570px}.details{max-height:600px}.metrics{grid-template-columns:repeat(2,1fr)}.heading h1{font-size:24px}.crumb{display:none}.headerActions{margin-left:auto}.panelGrid{grid-template-columns:1fr}.toolbar .search{min-width:100%}.auditRow{flex-direction:column;gap:8px}}@media(max-width:480px){main{padding:20px 12px 0}.headerActions{gap:5px}.headerActions button{font-size:9px;padding:7px}header{padding:0 10px}.formGrid{grid-template-columns:1fr}.formGrid label{grid-column:1/-1}.graphPanel{height:530px}.graphTools{bottom:70px}#legend{bottom:45px;max-width:90%}.metrics .metric{padding:12px}.metric small{font-size:8px}}
|
package/package.json
CHANGED
|
@@ -1,21 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsteja2307/orbit-3d-explorer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"author": "gsteja2307",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
|
-
"publishConfig": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"orbit-3d": "cli.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/",
|
|
14
|
+
"scripts/",
|
|
15
|
+
"cli.mjs",
|
|
16
|
+
"server.mjs",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"tests/"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"3d",
|
|
23
|
+
"threejs",
|
|
24
|
+
"graph",
|
|
25
|
+
"network-visualization",
|
|
26
|
+
"data-visualization"
|
|
27
|
+
],
|
|
28
|
+
"description": "Editable 3D relationship graph workspace with generic entities, validation, insights and local persistence",
|
|
11
29
|
"type": "module",
|
|
12
|
-
"engines": {
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
13
33
|
"scripts": {
|
|
14
34
|
"prepublishOnly": "npm run build",
|
|
15
35
|
"dev": "node scripts/vendor.mjs && node --watch server.mjs",
|
|
16
36
|
"start": "node scripts/vendor.mjs && node server.mjs",
|
|
17
|
-
"build": "node scripts/vendor.mjs && npm run check",
|
|
18
|
-
"check": "node --check dist/app.js && node --check server.mjs"
|
|
37
|
+
"build": "node scripts/vendor.mjs && npm run check && npm test",
|
|
38
|
+
"check": "node --check dist/app.js && node --check dist/model.js && node --check dist/store.js && node --check dist/graph.js && node --check server.mjs",
|
|
39
|
+
"test": "node --test"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"three": "0.169.0"
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
"./model": "./dist/model.js"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/gsteja2307/orbit-3d-explorer.git"
|
|
19
50
|
},
|
|
20
|
-
"
|
|
51
|
+
"homepage": "https://github.com/gsteja2307/orbit-3d-explorer#readme"
|
|
21
52
|
}
|
package/server.mjs
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
import http from 'node:http';
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
const root=path.join(path.dirname(fileURLToPath(import.meta.url)),'dist');
|
|
6
|
+
const port=Number(process.env.PORT||4173);
|
|
7
|
+
if(!Number.isInteger(port)||port<1||port>65535)throw Error('PORT must be an integer between 1 and 65535.');
|
|
8
|
+
const types={'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json; charset=utf-8','.png':'image/png'};
|
|
9
|
+
const server=http.createServer(async(req,res)=>{
|
|
10
|
+
const headers={'X-Content-Type-Options':'nosniff','Referrer-Policy':'no-referrer','Cache-Control':'no-store'};
|
|
11
|
+
if(!['GET','HEAD'].includes(req.method)){res.writeHead(405,{...headers,Allow:'GET, HEAD'});res.end();return;}
|
|
12
|
+
try{
|
|
13
|
+
const raw=decodeURIComponent((req.url||'/').split('?')[0]);
|
|
14
|
+
const file=path.resolve(root,'.'+(raw==='/'?'/index.html':raw));
|
|
15
|
+
if(!file.startsWith(root+path.sep)){res.writeHead(403,headers);res.end('Forbidden');return;}
|
|
16
|
+
const bytes=await fs.readFile(file);
|
|
17
|
+
res.writeHead(200,{...headers,'Content-Type':types[path.extname(file)]||'application/octet-stream'});
|
|
18
|
+
res.end(req.method==='HEAD'?undefined:bytes);
|
|
19
|
+
}catch(e){res.writeHead(e instanceof URIError?400:404,headers);res.end(e instanceof URIError?'Bad request':'Not found');}
|
|
20
|
+
});
|
|
21
|
+
server.on('error',error=>{console.error(error.code==='EADDRINUSE'?'Port '+port+' is already in use. Stop the previous preview or set PORT to another number.':error.message);process.exitCode=1;});
|
|
22
|
+
server.listen(port,'127.0.0.1',()=>console.log('Orbit 3D Explorer v2 · Local: http://127.0.0.1:'+port));
|
|
23
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {readFile} from 'node:fs/promises';
|
|
4
|
+
import {validateGraph,normalizeGraph,shortestPath,neighbors,insights,removeNode,mergeGraphs,csvExport,escapeHTML,migrateV1} from '../dist/model.js';
|
|
5
|
+
import {WorkspaceStore,KEY} from '../dist/store.js';
|
|
6
|
+
const seed=JSON.parse(await readFile(new URL('../dist/sample.json',import.meta.url),'utf8'));
|
|
7
|
+
const graph=()=>({schemaVersion:2,name:'Test',nodes:['a','b','c'].map(id=>({id,name:id,type:'service',tags:[]})),edges:[{id:'r1',source:'a',target:'b',kind:'depends_on'},{id:'r2',source:'b',target:'c',kind:'depends_on'}]});
|
|
8
|
+
function memory(){const data=new Map();return {getItem:k=>data.get(k)||null,setItem:(k,v)=>data.set(k,v)}}
|
|
9
|
+
test('sample data validates and preserves all relationships',()=>{assert.deepEqual(validateGraph(seed),[]);assert.equal(seed.nodes.length,30);assert.equal(normalizeGraph(seed).edges.length,seed.edges.length);});
|
|
10
|
+
test('reject dangling edges, duplicate IDs and self-links',()=>{const g=graph();g.nodes.push(g.nodes[0]);g.edges.push({id:'r3',source:'bad',target:'c',kind:'uses'},{id:'r4',source:'a',target:'a',kind:'uses'});const errors=validateGraph(g).join(' ');assert.match(errors,/duplicate entity/);assert.match(errors,/endpoint/);assert.match(errors,/self relationships/);});
|
|
11
|
+
test('reject reporting cycles and multiple managers',()=>{const g=graph();g.edges=[{id:'r1',source:'a',target:'b',kind:'reports_to'},{id:'r2',source:'b',target:'a',kind:'reports_to'},{id:'r3',source:'a',target:'c',kind:'reports_to'}];assert.match(validateGraph(g).join(' '),/only one/);g.edges.pop();assert.match(validateGraph(g).join(' '),/cycle/);});
|
|
12
|
+
test('reject unsafe URLs, invalid dates and invalid attributes',()=>{const g=graph();Object.assign(g.nodes[0],{url:'javascript:alert(1)',due:'2026-02-31',attributes:{secret:{nested:true}}});assert.equal(validateGraph(g).length,3);});
|
|
13
|
+
test('BFS finds shortest path, ignores direction, handles disconnected and missing records',()=>{const g=graph();assert.deepEqual(shortestPath(g,'c','a'),['c','b','a']);assert.deepEqual(shortestPath(g,'a','missing'),[]);g.edges=[];assert.deepEqual(shortestPath(g,'a','c'),[]);assert.deepEqual(shortestPath(g,'a','a'),['a']);});
|
|
14
|
+
test('neighborhood depth is bounded',()=>{const g=graph();assert.deepEqual([...neighbors(g,'a',1)],['a','b']);assert.deepEqual([...neighbors(g,'a',2)],['a','b','c']);});
|
|
15
|
+
test('deleting entity removes attached edges only',()=>{const g=removeNode(graph(),'a');assert.equal(g.nodes.length,2);assert.equal(g.edges.length,1);assert.deepEqual(validateGraph(g),[]);});
|
|
16
|
+
test('merge overwrites by ID and rejects duplicate semantic edges',()=>{const g=graph(),incoming=graph();incoming.nodes[0].name='Updated';assert.equal(mergeGraphs(g,incoming).nodes[0].name,'Updated');incoming.edges[0].id='new-id';assert.throws(()=>mergeGraphs(g,incoming),/duplicate relationship/);});
|
|
17
|
+
test('capacity insights sum all works_on allocations and flag overcapacity',()=>{const g=graph();g.edges=[{id:'r1',source:'a',target:'b',kind:'works_on',allocation:70},{id:'r2',source:'a',target:'c',kind:'works_on',allocation:50}];assert.equal(insights(g).allocation.get('a'),120);assert.equal(insights(g).overallocated[0].id,'a');});
|
|
18
|
+
test('CSV neutralizes spreadsheet formulas and HTML is escaped',()=>{assert.match(csvExport([{id:'a',name:'=1+1'}]),/"'=1\+1"/);assert.equal(escapeHTML('<img onerror="x">'),'<img onerror="x">');});
|
|
19
|
+
test('store persists, reloads, audits and restores graph with undo',()=>{const storage=memory(),s=new WorkspaceStore(storage,graph()),g=graph();g.nodes[0].name='Updated';s.commit(g,'Updated A');const restored=new WorkspaceStore(storage,graph());assert.equal(restored.state.graph.nodes[0].name,'Updated');assert.equal(restored.state.audit[0].action,'Updated A');assert.equal(restored.undo(),true);assert.equal(restored.state.graph.nodes[0].name,'a');assert.equal(restored.state.audit.length,2);});
|
|
20
|
+
test('quota failure never mutates current in-memory state',()=>{const storage=memory(),s=new WorkspaceStore(storage,graph());storage.setItem=()=>{throw Error('Quota exceeded')};const g=graph();g.nodes=[];g.edges=[];assert.throws(()=>s.commit(g,'Clear'),/Quota/);assert.equal(s.state.graph.nodes.length,3);});
|
|
21
|
+
test('stale tabs cannot overwrite a newer revision',()=>{const storage=memory(),a=new WorkspaceStore(storage,graph()),b=new WorkspaceStore(storage,graph());a.commit(graph(),'First edit');assert.throws(()=>b.commit(graph(),'Stale edit'),/another tab/);b.reload();b.commit(graph(),'Second edit');assert.equal(b.state.revision,2);});
|
|
22
|
+
test('corrupt storage is protected from overwrite',()=>{const storage=memory();storage.setItem(KEY,'invalid JSON');const s=new WorkspaceStore(storage,graph());assert.equal(s.loadError,true);assert.throws(()=>s.commit(graph(),'overwrite'),/recovery/);assert.equal(storage.getItem(KEY),'invalid JSON');});
|
|
23
|
+
test('legacy import creates manager, ownership and project relationships',()=>{const g=migrateV1({people:[{id:'a',name:'A',type:'person',skills:[]}],projects:[{id:'p',name:'P',type:'project',members:[['a','Lead',50]],apps:[],owner:'a'}],applications:[]});assert.deepEqual(validateGraph(g),[]);assert.deepEqual(g.edges.map(e=>e.kind),['works_on','owns']);});
|
|
24
|
+
test('normalize excludes unknown runtime fields',()=>{const g=graph();g.nodes[0].mesh={danger:'runtime'};assert.equal(normalizeGraph(g).nodes[0].mesh,undefined);});
|