@floless/app 0.30.0 → 0.31.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.
@@ -52835,7 +52835,7 @@ function appVersion() {
52835
52835
  return resolveVersion({
52836
52836
  isSea: isSea2(),
52837
52837
  sqVersionXml: readSqVersionXml(),
52838
- define: true ? "0.30.0" : void 0,
52838
+ define: true ? "0.31.0" : void 0,
52839
52839
  pkgVersion: readPkgVersion()
52840
52840
  });
52841
52841
  }
@@ -52845,7 +52845,7 @@ function resolveChannel(s) {
52845
52845
  return "dev";
52846
52846
  }
52847
52847
  function appChannel() {
52848
- return resolveChannel({ isSea: isSea2(), define: true ? "0.30.0" : void 0 });
52848
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.31.0" : void 0 });
52849
52849
  }
52850
52850
 
52851
52851
  // oauth-presets.ts
@@ -61,12 +61,30 @@
61
61
  rect.dethot{cursor:pointer;fill:rgba(168,85,247,.16);stroke:#a855f7;stroke-width:1;stroke-dasharray:3 3;vector-effect:non-scaling-stroke}
62
62
  rect.dethot:hover{fill:rgba(168,85,247,.34)}
63
63
  text.dettx{fill:#ddd6fe;font:bold 11px system-ui;text-anchor:middle;pointer-events:none}
64
- #detailsModal,#framesModal,#rfiModal,#askAiModal{position:fixed;inset:0;z-index:20;display:none;align-items:center;justify-content:center}
64
+ #detailsModal,#framesModal,#rfiModal,#confModal,#askAiModal{position:fixed;inset:0;z-index:20;display:none;align-items:center;justify-content:center}
65
65
  #askAiDrop:hover{border-color:var(--brand);color:var(--text)} #askAiDrop.has{border-style:solid;border-color:var(--brand)}
66
66
  .aithumb{position:relative;display:inline-flex} .aithumb img{height:60px;border-radius:4px;border:1px solid var(--line);background:#fff;display:block}
67
67
  .aithumb button{position:absolute;top:-5px;right:-5px;width:16px;height:16px;padding:0;border-radius:8px;font-size:10px;line-height:16px;text-align:center;background:#7f1d1d;border-color:#991b1b;color:#fecaca}
68
68
  #saveStat.dirty{color:#fbbf24} #saveStat.ok{color:#86efac} #saveStat.err{color:#fca5a5}
69
69
  #rfiStat{cursor:pointer;text-decoration:underline dotted} #rfiStat:hover b{color:#fff}
70
+ #confStat{cursor:pointer;text-decoration:underline dotted} #confStat:hover b{filter:brightness(1.25)}
71
+ .conf-cats{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;padding:14px;border-bottom:1px solid var(--line)}
72
+ .ccard{background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:10px 12px}
73
+ .ccard.click{cursor:pointer} .ccard.click:hover{border-color:var(--brand)} .ccard.on{border-color:var(--brand)}
74
+ .ccard .cc-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);margin-bottom:4px}
75
+ .ccard .cc-score{font-size:22px;font-weight:700;line-height:1;margin-bottom:6px}
76
+ .ccard .cc-bar{height:4px;border-radius:2px;background:var(--line);margin-bottom:6px;overflow:hidden}
77
+ .ccard .cc-fill{height:100%;border-radius:2px}
78
+ .ccard .cc-counts{font-size:11px;color:var(--mut);display:flex;gap:8px;flex-wrap:wrap}
79
+ .conf-filter{display:flex;gap:12px;flex-wrap:wrap;padding:12px 14px;border-bottom:1px solid var(--line);align-items:center}
80
+ .conf-filter .glab{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut)}
81
+ .conf-filter .grp{display:inline-flex;border:1px solid var(--line);border-radius:6px;overflow:hidden}
82
+ .conf-filter .grp button{border:0;border-radius:0;background:transparent;color:var(--mut);font-size:11px;padding:4px 9px;cursor:pointer}
83
+ .conf-filter .grp button.on{background:var(--brand);color:#fff}
84
+ .conf-body{padding:0 14px 14px;overflow:auto;flex:1;min-height:0}
85
+ .chip{display:inline-block;padding:1px 6px;border-radius:4px;font-size:10px;border:1px solid var(--line);color:var(--mut);margin:0 3px 3px 0}
86
+ .chip.assumed,.chip.weak{border-color:#713f12;color:#fbbf24} .chip.fail{border-color:#7f1d1d;color:#fca5a5}
87
+ tr.confrow{cursor:pointer} .conf-expand{color:var(--mut);font-size:12px;line-height:1.6;background:#0b1220}
70
88
  .ftab td .combo{padding:4px 6px} .ftab .rea{color:var(--mut);font-size:12px} .rfichip{display:inline-block;min-width:18px;height:18px;line-height:18px;text-align:center;background:#7f1d1d;color:#fecaca;border-radius:9px;font-size:11px;padding:0 5px}
71
89
  .mbackdrop{position:absolute;inset:0;background:rgba(2,8,23,.62)}
72
90
  .mpanel{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:10px;width:min(880px,92vw);max-height:86vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.6)}
@@ -115,6 +133,7 @@
115
133
  <span class=stat>Members <b id=mc>0</b></span><span class=stat>Weight <b id=wt>0</b> tons · <b id=wtlb>0</b> lb</span>
116
134
  <span class=stat id=rfiStat title="Click to list unresolved members (RFI)">RFI <b id=rc>0</b></span>
117
135
  <span class=stat id=dupStat title="Overlapping/duplicate members (same geometry)">Dup <b id=dpc>0</b></span>
136
+ <span class=stat id=confStat title="Confidence report — score each AI-read element by evidence" style="display:none">Confidence <b id=confPct>—</b></span>
118
137
  <span class=stat id=saveStat title="Edits auto-save in this browser (localStorage)">Saved</span><span style="flex:1"></span>
119
138
  <button id=undoB title="Undo (Ctrl+Z)">↶</button>
120
139
  <button id=redoB title="Redo (Ctrl+Y / Ctrl+Shift+Z)">↷</button>
@@ -160,6 +179,11 @@
160
179
  <div id=rfiModal><div class=mbackdrop id=rfiBackdrop></div>
161
180
  <div class=mpanel><div class=mhead><b>Unresolved members — RFI</b><button id=rfiClose>✕</button></div>
162
181
  <div id=rfiGrid style="padding:14px;overflow:auto"></div></div></div>
182
+ <div id=confModal><div class=mbackdrop id=confBackdrop></div>
183
+ <div class=mpanel><div class=mhead><b>Confidence report</b><button id=confClose>✕</button></div>
184
+ <div id=confCats class=conf-cats></div>
185
+ <div id=confFilter class=conf-filter></div>
186
+ <div id=confBody class=conf-body></div></div></div>
163
187
  <div id=askAiModal><div class=mbackdrop id=askAiBackdrop></div>
164
188
  <div class=mpanel style="width:min(560px,92vw)">
165
189
  <div class=mhead><b>Ask the AI</b><button id=askAiClose>✕</button></div>
@@ -429,7 +453,7 @@ function render(){
429
453
  selM.forEach((m,idx)=>{const c=mid(m),k=Math.round(c[0]/8)+','+Math.round(c[1]/8);(grp[k]=grp[k]||[]).push({idx,c});});
430
454
  for(const k in grp){const a=grp[k],n=a.length;a.forEach((it,j)=>{const x=it.c[0]+(j-(n-1)/2)*R*2,y=it.c[1];const d=`data-bx="${it.c[0]}" data-fi="${j}" data-gn="${n}"`;
431
455
  s+=`<circle class=numbg ${d} cx="${x}" cy="${y}" r="${R}"/><text class=numtx ${d} x="${x}" y="${y}" style="font-size:${F}px">${it.idx+1}</text>`;});}}
432
- svg.innerHTML=s; document.getElementById('profiles').innerHTML=profs.map(p=>`<option value="${esc(p)}">`).join(''); document.getElementById('details').innerHTML=(P.details||[]).map(d=>`<option value="${esc(d.text)}">`).join(''); stats(); panel(); updUR(); updDup();
456
+ svg.innerHTML=s; document.getElementById('profiles').innerHTML=profs.map(p=>`<option value="${esc(p)}">`).join(''); document.getElementById('details').innerHTML=(P.details||[]).map(d=>`<option value="${esc(d.text)}">`).join(''); stats(); panel(); updUR(); updDup(); updConf();
433
457
  }
434
458
  function updDup(){const n=redundantDups().length;
435
459
  document.getElementById('dpc').textContent=n;document.getElementById('dupStat').classList.toggle('has',n>0);
@@ -500,6 +524,7 @@ function panel(){
500
524
  <div class=row><label>Profile</label><div style="display:flex;gap:6px"><input id=pf class=combo data-src=profiles value="${esc(m.profile)}" style="flex:1" autocomplete=off><button id=pickProf class="ghost${(picking&&pickKind==='profile')?' on':''}" title="Pick profile by clicking a label in the drawing">⌖ pick</button></div>${(picking&&pickKind==='profile')?'<div class="hint" style="margin-top:4px;font-style:italic;color:var(--brand)">Click a profile label in the drawing…</div>':(picking&&pickKind==='detail')?'<div class="hint" style="margin-top:4px;font-style:italic;color:#a855f7">Click a detail callout in the drawing…</div>':''}</div>
501
525
  <div class="seg2 f"><button id=rBeam class="${col?'':'on'}">Beam</button><button id=rCol class="${col?'on':''}">Column</button></div>
502
526
  <div class="row hint">Length <b>${L} ft</b> · ${wpf==null?'<span class=pill style="background:#7f1d1d">RFI — size unresolved</span>':'Weight <b>'+(len(m.wp[0],m.wp[1])/FT*wpf).toFixed(0)+' lb</b> · '+wpf+' lb/ft'}</div>
527
+ <div class=row><button class=ghostw id=verifyBtn${m.verified?' style="border-color:#166534;color:#86efac"':''} title="Mark this member human-confirmed → 100% in the confidence report">${m.verified?'✓ Verified — human-confirmed':'Mark verified'}</button></div>
503
528
  ${mfSug.length?`<div class="row" style="border:1px solid #a855f7;border-radius:6px;padding:7px 8px;background:rgba(168,85,247,.07)"><div class=elab style="color:#c4b5fd;margin:0">Moment-frame girder · ${_lvl==='roof'?'roof':'2nd floor'} (from Frames)</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-top:5px">${mfSug.map(s=>`<button class="ghost mfsug${s===m.profile?' on':''}" data-s="${esc(s)}">${esc(s)}</button>`).join('')}</div></div>`:''}
504
529
  ${elev}
505
530
  <div class=divrow><hr><span class=sect style="margin:0">Modify geometry</span><hr></div>
@@ -510,7 +535,7 @@ function panel(){
510
535
  document.getElementById('pickProf').onclick=()=>{if(picking&&pickKind==='profile'){picking=false;}else{picking=true;pickKind='profile';pickEnd=null;}render();};
511
536
  document.getElementById('rBeam').onclick=()=>{if(col)edit(()=>{m.role='beam';});};
512
537
  document.getElementById('rCol').onclick=()=>{if(!col)edit(()=>{m.role='column';});};
513
- document.querySelectorAll('.mfsug').forEach(b=>b.onclick=()=>edit(()=>{m.profile=b.dataset.s;m.rfi=(WT[m.profile]==null);if(!profs.includes(m.profile)){profs.push(m.profile);profs.sort();}}));
538
+ document.querySelectorAll('.mfsug').forEach(b=>b.onclick=()=>edit(()=>{m.profile=b.dataset.s;m.mf=true;m.rfi=(WT[m.profile]==null);if(!profs.includes(m.profile)){profs.push(m.profile);profs.sort();}})); // record the moment-frame provenance (drives the confidence "schedule-resolved" factor)
514
539
  const _gel=document.getElementById('geoEL'),_gsp=document.getElementById('geoSplit');
515
540
  if(_gel)_gel.onclick=()=>{geoMode=(geoMode==='el'?null:'el');setGeo();render();};
516
541
  if(_gsp)_gsp.onclick=()=>{geoMode=(geoMode==='split'?null:'split');setGeo();render();};
@@ -534,6 +559,7 @@ function panel(){
534
559
  document.getElementById('matchEnds').onclick=()=>edit(()=>{m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail};});
535
560
  }
536
561
  document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(x=>x.id!==m.id);selIds.clear();});
562
+ {const vb=document.getElementById('verifyBtn');if(vb)vb.onclick=()=>edit(()=>{m.verified=!m.verified;});}
537
563
  }
538
564
  function addFromSeg(sid){const sg=P.segments.find(s=>s.id===sid);if(!sg)return;const P=addProfile;const id='m'+Date.now();const pv=snapshot();
539
565
  P.members.push(ensureMeta({id,profile:P,wp:[sg.a.slice(),sg.b.slice()],angle:sg.o,rfi:(WT[P]==null)}));
@@ -643,6 +669,7 @@ addEventListener('keydown',e=>{
643
669
  if(e.key==='Escape'&&detailsOpen()){closeDetails();return;}
644
670
  if(e.key==='Escape'&&framesOpen()){closeFrames();return;}
645
671
  if(e.key==='Escape'&&rfiOpen()){closeRFI();return;}
672
+ if(e.key==='Escape'&&confOpen()){closeConf();return;}
646
673
  if(e.key==='Home'){e.preventDefault();fitToWindow();return;}
647
674
  if(e.key==='Escape'){if(geoMode){geoMode=null;setGeo();render();return;}if(mode==='add'){mode='sel';setMode();}picking=false;pickKind='profile';pickEnd=null;selIds.clear();render();return;}
648
675
  if((e.key==='Delete'||e.key==='Backspace')&&selIds.size&&!inForm){e.preventDefault();edit(()=>{P.members=P.members.filter(m=>!selIds.has(m.id));selIds.clear();});return;}
@@ -780,6 +807,96 @@ function rfiOpen(){return document.getElementById('rfiModal').style.display==='f
780
807
  document.getElementById('rfiStat').onclick=openRFI;
781
808
  document.getElementById('rfiClose').onclick=closeRFI;
782
809
  document.getElementById('rfiBackdrop').onclick=closeRFI;
810
+ // ===== Confidence report — evidence-based score (MIRROR of server/steel-confidence.ts; keep in sync) =====
811
+ // Deterministic: scores ONLY observable evidence already in the contract, never a model self-report.
812
+ const BANDW={verified:1,high:.9,med:.65,low:.35,rfi:0};
813
+ const BANDLAB={verified:'Verified',high:'High',med:'Medium',low:'Low',rfi:'RFI'};
814
+ const BANDC={verified:'#86efac',high:'#4ade80',med:'#fbbf24',low:'#fca5a5',rfi:'#fca5a5'};
815
+ const BANDPILL={verified:'background:#166534;color:#86efac',high:'background:#14532d;color:#4ade80',med:'background:#713f12;color:#fbbf24',low:'background:#7f1d1d;color:#fca5a5',rfi:'background:#7f1d1d;color:#fca5a5'};
816
+ function _wt(profile){if(!WT||!profile)return null;if(profile in WT)return WT[profile]==null?null:WT[profile];
817
+ const h=Object.entries(WT).find(([k])=>k.toUpperCase()===profile.toUpperCase());return h?(h[1]==null?null:h[1]):null;}
818
+ const _isMf=p=>!!p&&/(^|[^A-Z])MF($|[^A-Z])/i.test(p);
819
+ function _confDupIds(members){const g={};const key=m=>{if(!m.wp||m.wp.length<2)return null;const r=p=>Math.round(p[0]/3)+','+Math.round(p[1]/3);const a=r(m.wp[0]),b=r(m.wp[1]);return a<b?a+'|'+b:b+'|'+a;};
820
+ const rank=m=>{let s=0;if(m.profile&&!_isMf(m.profile))s++;if(m.profile&&m.profile.trim()!=='')s++;return s;};
821
+ for(const m of members){const k=key(m);if(!k)continue;(g[k]=g[k]||[]).push(m);}
822
+ const out=new Set();for(const k in g){const grp=g[k];if(grp.length<2)continue;grp.sort((a,b)=>rank(b)-rank(a));for(let i=1;i<grp.length;i++)out.add(grp[i].id);}return out;}
823
+ function _elevAssumed(m){if(m.role==='column')return !(m.col&&m.col.tosDef===false);const en=m.ends||[];if(!en.length)return true;return en.some(e=>e.tosDef!==false);}
824
+ function _scoreMember(m,dup){const plf=_wt(m.profile);const F=[];
825
+ if(plf==null){F.push({key:'profile',label:'Profile',state:'fail',detail:(!m.profile||!m.profile.trim())?'no profile assigned':_isMf(m.profile)?('unresolved mark "'+m.profile+'" — not an AISC size'):('"'+m.profile+'" not in the AISC weight table')});return {band:'rfi',factors:F};}
826
+ const sched=m.mf===true;F.push({key:'profile',label:'Profile',state:sched?'assumed':'ok',detail:sched?(m.profile+' — resolved from the frame schedule ('+plf+' plf)'):(m.profile+' ('+plf+' plf, AISC)')});
827
+ const asm=_elevAssumed(m);F.push({key:'elevation',label:'Elevation',state:asm?'assumed':'ok',detail:asm?'plan default (UNO) — no local callout':'from a drawing callout'});
828
+ const isd=dup.has(m.id);if(isd)F.push({key:'dup',label:'Duplicate',state:'fail',detail:'coincident with a kept member — deleting dedupes the BOM'});
829
+ if(m.verified===true)F.push({key:'verified',label:'Verified',state:'ok',detail:'human-confirmed'});
830
+ const band=m.verified===true?'verified':isd?'low':(sched||asm)?'med':'high';return {band,factors:F};}
831
+ function _mTons(m,ptPerFt){const plf=_wt(m.profile);if(plf==null)return 0;let ft;
832
+ if(m.role==='column'){if(!m.col||m.col.tos==null||m.col.bos==null)return 0;ft=Math.abs(m.col.tos-m.col.bos)/12;} // column length = height (TOS−BOS), inches→ft
833
+ else{if(!m.wp||m.wp.length<2)return 0;ft=Math.hypot(m.wp[0][0]-m.wp[1][0],m.wp[0][1]-m.wp[1][1])/(ptPerFt>0?ptPerFt:1);}
834
+ return ft*plf/2000;}
835
+ function _detSheet(t){const m=String(t||'').toUpperCase().match(/S-?\s?\d{2,3}/);return m?m[0].replace(/\s/g,''):null;}
836
+ const _cnt0=()=>({verified:0,high:0,med:0,low:0,rfi:0});
837
+ function scoreContractJS(){const plans=C.plans||[];const byMember=[],byDetail=[];let segs=0;
838
+ const known=new Set();for(const p of plans)if(p.sheet)known.add(p.sheet.toUpperCase());for(const s of Object.keys(C.detail_bubbles||{}))known.add(s.toUpperCase());
839
+ plans.forEach((plan,pi)=>{segs+=(plan.segments||[]).length;const ms=plan.members||[];const dup=_confDupIds(ms);
840
+ for(const m of ms){const r=_scoreMember(m,dup);byMember.push({id:m.id,planIdx:pi,sheet:plan.sheet||'',role:m.role==='column'?'column':'beam',profile:m.profile||'',band:r.band,tons:r.band==='rfi'?0:_mTons(m,plan.pt_per_ft),factors:r.factors});}
841
+ for(const d of (plan.details||[])){const sh=_detSheet(d.text);const ok=sh!=null&&known.has(sh.toUpperCase());byDetail.push({text:d.text||'',planIdx:pi,sheet:plan.sheet||'',band:ok?'high':'low',reason:ok?('references '+sh+' (in the set)'):sh?('references '+sh+' — not found in the set'):'no sheet reference parsed'});}});
842
+ const roll=(arr,cw)=>{const c=_cnt0();let n=0,d=0,t=0;for(const m of arr){c[m.band]++;if(m.band==='rfi')continue;const w=cw?1:m.tons;n+=w*BANDW[m.band];d+=w;t+=m.tons;}return {score:d>0?Math.round(n/d*100):null,tons:t,counts:c};};
843
+ const dRoll=arr=>{const c=_cnt0();let n=0,d=0;for(const x of arr){c[x.band]++;n+=BANDW[x.band];d++;}return {score:d>0?Math.round(n/d*100):null,tons:0,counts:c};};
844
+ const beams=roll(byMember.filter(m=>m.role==='beam'));const columns=roll(byMember.filter(m=>m.role==='column'));const details=dRoll(byDetail);
845
+ const sc=byMember.filter(m=>m.band!=='rfi');const n=sc.reduce((s,m)=>s+m.tons*BANDW[m.band],0),d=sc.reduce((s,m)=>s+m.tons,0);
846
+ return {byMember,byDetail,byCategory:{beams,columns,details,connections:{score:null,note:'Not scored yet (reserved slot)',counts:_cnt0()}},coverage:{members:byMember.length,segments:segs,note:'Approximate in v1 — precise per-segment binding is Slice 2.'},overall:{score:d>0?Math.round(n/d*100):null,tons:d,rfiCount:byMember.filter(m=>m.band==='rfi').length}};}
847
+ function bandColorForPct(p){return p==null?'var(--mut)':p>=95?BANDC.verified:p>=80?BANDC.high:p>=50?BANDC.med:BANDC.low;}
848
+ function updConf(){const el=document.getElementById('confStat');if(!el)return;const s=scoreContractJS();
849
+ if(!s.byMember.length){el.style.display='none';return;}el.style.display='';
850
+ const b=document.getElementById('confPct');b.textContent=s.overall.score==null?'—':s.overall.score+'%';b.style.color=bandColorForPct(s.overall.score);
851
+ el.title='Confidence — '+s.overall.tons.toFixed(1)+' t scored · '+s.overall.rfiCount+' RFI. Click for the report.';}
852
+ // --- the report modal ---
853
+ let confBand='all',confCat='all',confExpand=null;
854
+ function openConf(){confExpand=null;renderConf();document.getElementById('confModal').style.display='flex';}
855
+ function closeConf(){document.getElementById('confModal').style.display='none';}
856
+ function confOpen(){return document.getElementById('confModal').style.display==='flex';}
857
+ function _bar(pct){return '<div class=cc-bar><div class=cc-fill style="width:'+(pct||0)+'%;background:'+bandColorForPct(pct)+';'+(pct==null?'opacity:0':'')+'"></div></div>';}
858
+ function _countsHtml(c){const out=['verified','high','med','low','rfi'].filter(k=>c[k]>0).map(k=>'<span style="color:'+BANDC[k]+'">'+BANDLAB[k][0]+' <b>'+c[k]+'</b></span>').join('');return out||'<span>—</span>';}
859
+ function renderConf(){const s=scoreContractJS();const cat=s.byCategory;
860
+ const card=(key,label,cs,stub)=>'<div class="ccard'+(stub?' stub':' click')+(confCat===key?' on':'')+'" data-cat="'+key+'"'+(stub?' title="not scored yet"':'')+'>'+
861
+ '<div class=cc-label>'+label+'</div>'+
862
+ '<div class=cc-score style="color:'+(stub?'var(--mut)':bandColorForPct(cs.score))+'">'+(stub||cs.score==null?'—':cs.score+'%')+'</div>'+
863
+ _bar(stub?null:cs.score)+
864
+ '<div class=cc-counts>'+(stub?'<span>reserved slot</span>':_countsHtml(cs.counts))+'</div></div>';
865
+ document.getElementById('confCats').innerHTML=card('beams','Beams',cat.beams)+card('columns','Columns',cat.columns)+card('details','Details',cat.details)+card('connections','Connections',cat.connections,true);
866
+ const bands=['all','verified','high','med','low','rfi'],cats=['all','beams','columns','details','connections'];
867
+ document.getElementById('confFilter').innerHTML=
868
+ '<span class=glab>Band</span><span class=grp>'+bands.map(b=>'<button data-band="'+b+'" class="'+(confBand===b?'on':'')+'">'+(b==='all'?'All':BANDLAB[b])+'</button>').join('')+'</span>'+
869
+ '<span class=glab>Category</span><span class=grp>'+cats.map(c=>'<button data-fcat="'+c+'" class="'+(confCat===c?'on':'')+'">'+(c==='all'?'All':c[0].toUpperCase()+c.slice(1))+'</button>').join('')+'</span>'+
870
+ '<span class=glab style="margin-left:auto">Coverage: '+s.coverage.members+' members · '+s.coverage.segments+' segments (approx, v1)</span>';
871
+ document.getElementById('confBody').innerHTML=confBodyHtml(s);
872
+ wireConf();}
873
+ function confBodyHtml(s){
874
+ if(!s.byMember.length&&!s.byDetail.length)return '<div class=hint style="padding:14px 0">No members in the takeoff yet.</div>';
875
+ if(confCat==='connections')return '<div class=hint style="padding:14px 0">Connection scoring is not built yet (reserved slot).</div>';
876
+ let mem=s.byMember.slice();
877
+ if(confCat==='beams')mem=mem.filter(m=>m.role==='beam');else if(confCat==='columns')mem=mem.filter(m=>m.role==='column');else if(confCat==='details')mem=[];
878
+ if(confBand!=='all')mem=mem.filter(m=>m.band===confBand);
879
+ let det=(confCat==='all'||confCat==='details')?s.byDetail.slice():[];
880
+ if(confBand!=='all')det=det.filter(d=>d.band===confBand);
881
+ let h='<table class=ftab><thead><tr><th>Band</th><th>Element</th><th>Sheet</th><th>Tons</th><th>Evidence</th><th></th></tr></thead><tbody>';
882
+ mem.forEach(m=>{const chips=m.factors.map(f=>'<span class="chip '+(f.state==='ok'?'':f.state)+'">'+esc(f.label)+(f.state==='ok'?' ✓':f.state==='fail'?' ✗':' ●')+'</span>').join('');
883
+ h+='<tr class=confrow data-mid="'+esc(m.id)+'" data-pi="'+m.planIdx+'"><td><span class=pill style="'+BANDPILL[m.band]+'">'+BANDLAB[m.band]+'</span></td>'+
884
+ '<td>'+(esc(m.profile)||'<span style="color:var(--mut)">(no profile)</span>')+' <span style="color:var(--mut);font-size:11px">'+esc(m.role)+'</span></td>'+
885
+ '<td>'+esc(m.sheet)+'</td><td>'+(m.tons?m.tons.toFixed(2):'—')+'</td><td>'+chips+'</td>'+
886
+ '<td><button class=ghost data-loc="'+esc(m.id)+'" data-pi="'+m.planIdx+'">Locate</button></td></tr>';
887
+ if(confExpand===m.id)h+='<tr><td></td><td colspan=5 class=conf-expand>'+m.factors.map(f=>'<div>• <b>'+esc(f.label)+':</b> '+esc(f.detail)+'</div>').join('')+'</td></tr>';});
888
+ det.forEach(d=>{h+='<tr><td><span class=pill style="'+BANDPILL[d.band]+'">'+BANDLAB[d.band]+'</span></td><td>'+(esc(d.text)||'(detail)')+' <span style="color:var(--mut);font-size:11px">detail</span></td><td>'+esc(d.sheet)+'</td><td>—</td><td><span class=hint>'+esc(d.reason)+'</span></td><td></td></tr>';});
889
+ if(!mem.length&&!det.length)h+='<tr><td colspan=6><div class=hint style="padding:14px 0">Nothing matches this filter.</div></td></tr>';
890
+ h+='</tbody></table>';return h;}
891
+ function wireConf(){
892
+ document.querySelectorAll('#confCats .ccard.click').forEach(c=>c.onclick=()=>{confCat=(confCat===c.dataset.cat?'all':c.dataset.cat);renderConf();});
893
+ document.querySelectorAll('#confFilter [data-band]').forEach(b=>b.onclick=()=>{confBand=b.dataset.band;renderConf();});
894
+ document.querySelectorAll('#confFilter [data-fcat]').forEach(b=>b.onclick=()=>{confCat=b.dataset.fcat;renderConf();});
895
+ document.querySelectorAll('#confBody tr.confrow').forEach(r=>r.onclick=e=>{if(e.target.closest('button'))return;const id=r.dataset.mid;confExpand=confExpand===id?null:id;renderConf();});
896
+ document.querySelectorAll('#confBody button[data-loc]').forEach(b=>b.onclick=e=>{e.stopPropagation();const pi=+b.dataset.pi;if(pi!==C.active)setPlan(pi);const m=byId(b.dataset.loc);if(!m)return;selIds=new Set([m.id]);geoMode=null;setGeo();closeConf();render();zoomToMember(m);});}
897
+ document.getElementById('confStat').onclick=openConf;
898
+ document.getElementById('confClose').onclick=closeConf;
899
+ document.getElementById('confBackdrop').onclick=closeConf;
783
900
  // --- detail preview lightbox (click a card / an end's ⤢ to enlarge) ---
784
901
  const SHEET_PREV=C.sheet_previews||{}, DBUB=C.detail_bubbles||{};
785
902
  function sheetOf(text){const m=/(S-?\w+)/.exec(String(text||'').replace(/^\w+-/,''));if(!m)return null;const s=m[1].toUpperCase();return s.startsWith('S-')?s:'S-'+s.slice(1);}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {