Widen the IWP screen and add a left work-package navigator

The Work Package form was capped at a 1000px column, which wasted most of a
desktop screen, and the only way to reach another package was to scroll to the
Saved table at the bottom.

- Put the form in a wide two-column shell (max 1760px); ctx-bar, mode-wrap and
  the release banner widened to match.
- Above 1200px the two-up field grids flow to 3-4 columns instead of stretching
  two fields across the whole card. Narrow layouts are unchanged.
- New sticky left rail listing every saved package, grouped by status in field
  order, with WP number, subject, readiness dot and type. Click to open it in
  the form; the package being edited is highlighted. Filter box, + New and
  Dashboard shortcuts, collapsible (state persisted), hidden under 1100px where
  the Saved table still covers navigation.
- The rail re-renders from renderSavedList(), so saves, deletes, splits,
  archive/restore and the project pull all keep it current.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 12:59:34 -07:00
parent 39b48055ff
commit 1d004cab75
3 changed files with 135 additions and 7 deletions

View File

@@ -844,6 +844,7 @@ function setFormChrome(on){
if(save) save.style.display = on ? 'flex' : 'none';
document.body.classList.toggle('has-sticky-save', !!on);
if(on){ buildSectionNav(); updateStickyStatus(); makeCollapsible(); initSectionNavAutoHide(); }
else positionSectionNav();
}
// Make each form card collapsible by clicking its heading (idempotent).
function makeCollapsible(){
@@ -881,6 +882,9 @@ function buildSectionNav(){
function positionSectionNav(){
const nav=document.getElementById('section-nav'), hdr=document.querySelector('.header');
if(nav && hdr) nav.style.top = hdr.offsetHeight + 'px';
// The WP navigator rail sticks below the header + section-nav chrome.
const top=(hdr?hdr.offsetHeight:0)+((nav && nav.style.display!=='none')?nav.offsetHeight:0);
document.documentElement.style.setProperty('--rail-top', top+'px');
}
let _snLastY=0, _snBound=false;
function initSectionNavAutoHide(){
@@ -912,6 +916,7 @@ function saveStore(){ try{ localStorage.setItem(wpKey(STORE_KEY), JSON.stringify
function loadStore(){ try{ const d=JSON.parse(localStorage.getItem(wpKey(STORE_KEY))); savedPackages=Array.isArray(d)?d:[]; }catch(e){ savedPackages=[]; } }
function renderSavedList(){
const card=document.getElementById('saved-card'), body=document.getElementById('saved-body');
renderWpNav();
document.getElementById('saved-count').textContent=savedPackages.length?`(${savedPackages.length})`:'';
if(!savedPackages.length){ card.style.display='none'; return; }
if(document.getElementById('pkg-output').style.display==='none' || document.getElementById('pkg-output').style.display==='') card.style.display='';
@@ -926,6 +931,58 @@ function renderSavedList(){
}
function viewPackage(i){ if(savedPackages[i]) renderPackage(savedPackages[i]); }
// ── WP NAVIGATOR (left rail) ─────────────────────────────────────────────────
// Every saved package on this project, grouped by status, filterable. Clicking a
// row opens it in the form (same path as the Saved table's "edit").
const WPNAV_ORDER=['Issue','In Progress','Issued','QC','Scheduled','Draft','Closed'];
function toggleWpNav(){
document.body.classList.toggle('wp-nav-collapsed');
try{ localStorage.setItem('wp_nav_collapsed', document.body.classList.contains('wp-nav-collapsed')?'1':''); }catch(e){}
}
function renderWpNav(){
const list=document.getElementById('wp-nav-list'); if(!list) return;
const cnt=document.getElementById('wp-nav-count');
if(cnt) cnt.textContent = savedPackages.length ? '('+savedPackages.length+')' : '';
const q=((document.getElementById('wp-nav-search')||{}).value||'').trim().toLowerCase();
// Keep the original index — edit/view act on savedPackages by position.
const rows=savedPackages.map((p,i)=>({p:p,i:i})).filter(r=>{
if(!q) return true;
const p=r.p;
return [p.number,p.subject,p.type,p.location,p.system].some(v=>String(v||'').toLowerCase().includes(q));
});
if(!rows.length){
list.innerHTML='<div class="wp-nav-empty">'+(savedPackages.length?'No packages match “'+esc(q)+'”.':'No work packages saved yet. Fill the form and Save Draft.')+'</div>';
return;
}
const groups={};
rows.forEach(r=>{ const s=r.p.status||'Draft'; (groups[s]=groups[s]||[]).push(r); });
const keys=Object.keys(groups).sort((a,b)=>{
const ia=WPNAV_ORDER.indexOf(a), ib=WPNAV_ORDER.indexOf(b);
return (ia<0?99:ia)-(ib<0?99:ib);
});
list.innerHTML=keys.map(k=>{
const label = k==='Issue' ? 'Issue (Hold)' : k;
return '<div class="wp-nav-group">'+esc(label)+' · '+groups[k].length+'</div>'+groups[k].map(r=>{
const p=r.p, open=(p.constraints||[]).filter(c=>c.status==='open').length;
const dot = p.status==='Issue' ? 'hold' : (open===0 ? 'ok' : 'open');
const state = p.status==='Issue' ? 'on hold' : (open===0 ? 'ready' : open+' open');
const active = (editingId && p.id===editingId) ? ' active' : '';
return '<button type="button" class="wp-nav-item'+active+'" onclick="wpNavOpen('+r.i+')" title="'+esc((p.number||'')+' — '+(p.subject||''))+'">'+
'<span class="wp-nav-num">'+esc(p.number||'(unnumbered)')+'</span>'+
'<span class="wp-nav-subj">'+esc(p.subject||'untitled')+'</span>'+
'<span class="wp-nav-meta"><span class="wp-nav-dot '+dot+'"></span>'+esc(state)+
(p.type?' · '+esc(p.type):'')+'</span></button>';
}).join('');
}).join('');
}
function wpNavOpen(i){
const p=savedPackages[i]; if(!p) return;
editingId=p.id;
loadPackageIntoForm(p); // ends in showForm(), so this also leaves the dashboard / package view
renderWpNav();
track('wp_nav_open');
}
// ── WP HISTORY (audit trail) ─────────────────────────────────────────────────
function showHistoryRow(i){ const p=savedPackages[i]; if(p) showHistory(p.id, p.number||p.subject); }
function showHistoryCurrent(){ showHistory(editingId, (document.getElementById('wp_number')||{}).value); }
@@ -960,7 +1017,7 @@ async function showHistory(wpId, label){
}
function deletePackage(i){ const p=savedPackages[i]; if(!p) return; if(!confirm('Delete work package "'+(p.number||p.subject||'untitled')+'"? This cannot be undone.')) return; const delId=p.id; savedPackages.splice(i,1); saveStore(); renderSavedList(); track('package_deleted'); if(typeof ProjectData!=='undefined' && ProjectData.removeWP) ProjectData.removeWP(delId); }
function clearSaved(){ if(!savedPackages.length) return; if(!confirm('Delete all '+savedPackages.length+' saved packages?')) return; const ids=savedPackages.map(p=>p.id); savedPackages=[]; saveStore(); renderSavedList(); if(typeof ProjectData!=='undefined' && ProjectData.removeWP) ids.forEach(id=>ProjectData.removeWP(id)); }
function editPackage(i){ const p=savedPackages[i]; if(!p) return; editingId=p.id; loadPackageIntoForm(p); }
function editPackage(i){ const p=savedPackages[i]; if(!p) return; editingId=p.id; loadPackageIntoForm(p); renderWpNav(); }
function loadExample(){ loadPackageIntoForm(JSON.parse(JSON.stringify(EXAMPLE_PKG))); editingId=null; toast('Example work package loaded'); track('example_loaded'); }
function loadPackageIntoForm(p){
pkgKind = (p.kind === 'ewp') ? 'ewp' : 'iwp'; // set before type/constraint pickers so they filter correctly
@@ -1055,7 +1112,7 @@ function newPackage(){
pkgHolds=[]; pkgOverrides={};
set_quality_from_sop(); lockQuality('wp_qc'); lockQuality('wp_photo'); lockQuality('wp_hold');
prevStatus='Draft';
updateNumber(); updateReleaseBanner(); showForm(); track('new_package');
updateNumber(); updateReleaseBanner(); showForm(); renderWpNav(); track('new_package');
}
function set_quality_from_sop(){ const set=(id,v)=>{const el=document.getElementById(id); if(el) el.value=v||'';}; set('wp_qc',sopValueFor('wp_qc')); set('wp_photo',sopValueFor('wp_photo')); set('wp_hold',sopValueFor('wp_hold')); }
function exportPackages(){
@@ -1367,7 +1424,9 @@ function bootData(){
bootSOP();
setRadio('status','Draft');
loadMembers();
try{ if(localStorage.getItem('wp_nav_collapsed')) document.body.classList.add('wp-nav-collapsed'); }catch(e){}
renderSavedList();
positionSectionNav();
cmtInit();
// Deep-link: open straight to the dashboard when requested (?view=dashboard or #dashboard).
const p=new URLSearchParams(location.search); if(p.get('view')==='dashboard' || location.hash==='#dashboard'){ showDashboard(); }

View File

@@ -45,6 +45,23 @@
<!-- SECTION NAV (jump links, built from the form cards) -->
<div class="section-nav-bar" id="section-nav"></div>
<div class="wp-layout">
<!-- WP NAVIGATOR (left rail — jump between every work package on this project) -->
<aside class="wp-nav" id="wp-nav" aria-label="Work packages">
<div class="wp-nav-head">
<div class="wp-nav-title">Work Packages <span class="wp-nav-count" id="wp-nav-count"></span></div>
<button class="wp-nav-collapse" id="wp-nav-collapse" onclick="toggleWpNav()" title="Collapse list" aria-label="Collapse list"></button>
</div>
<input type="search" class="wp-nav-search" id="wp-nav-search" placeholder="Filter by number, subject, type…" oninput="renderWpNav()">
<div class="wp-nav-list" id="wp-nav-list"></div>
<div class="wp-nav-foot">
<button class="add-btn" onclick="newPackage()">+ New</button>
<button class="add-btn" onclick="showDashboard()">📊 Dashboard</button>
</div>
</aside>
<button class="wp-nav-reopen" id="wp-nav-reopen" onclick="toggleWpNav()" title="Show work packages" aria-label="Show work packages"></button>
<div class="main">
<!-- PACKAGE KIND (only shown when the project's SOP includes BIM/VDC) -->
@@ -268,6 +285,7 @@
</div>
</div>
</div>
<!-- HOLD LOG MODAL (comment 7) -->
<div class="modal-overlay" id="hold-modal">

View File

@@ -99,8 +99,11 @@
.step-tab.done { color: var(--accent-green); background: var(--accent-green-dim); }
.step-num { display: block; font-size: 9px; opacity: .65; margin-bottom: 2px; }
/* ── MAIN ── */
.main { max-width: 1000px; margin: 0 auto; padding: 28px 32px 64px; }
/* ── MAIN ──
The form sits in a wide two-column shell: a sticky work-package navigator on
the left and the form itself filling the rest of the screen. */
.wp-layout { display: flex; align-items: flex-start; gap: 0; max-width: 1760px; margin: 0 auto; }
.main { flex: 1 1 auto; min-width: 0; max-width: none; margin: 0; padding: 28px 32px 64px; }
.section { display: none; }
.section.active { display: block; animation: fade .25s ease; }
@@ -126,6 +129,11 @@
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 18px; }
.field-grid.col3 { grid-template-columns: 1fr 1fr 1fr; }
.field-grid.col1 { grid-template-columns: 1fr; }
/* On a wide screen let the two-up grids flow into 34 columns instead of
stretching two fields across the whole card. */
@media (min-width: 1200px) {
.field-grid:not(.col1):not(.col3) { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
}
.field { display: flex; flex-direction: column; gap: 6px; }
.field.span2 { grid-column: span 2; }
@@ -427,7 +435,7 @@
border-radius:var(--radius); padding:7px 10px; font-size:11px; }
/* ── CREATION TOOL ───────────────────────────────────────────────── */
.ctx-bar { max-width:1080px; margin:0 auto; padding:12px 28px; display:flex; align-items:center; gap:20px;
.ctx-bar { max-width:1760px; margin:0 auto; padding:12px 28px; display:flex; align-items:center; gap:20px;
border-bottom:1px solid var(--border); background:var(--surface); flex-wrap:wrap; }
.ctx-empty { color:var(--text-muted); font-size:13px; }
.ctx-main .ctx-proj { font-weight:700; color:var(--text); font-size:14px; }
@@ -439,7 +447,7 @@
.ctx-meta code { background:var(--surface2); padding:1px 6px; border-radius:3px; color:var(--accent); }
.link-btn { background:none; border:none; color:var(--accent); cursor:pointer; font-size:inherit; padding:0; text-decoration:underline; }
.mode-wrap { max-width:1080px; margin:0 auto; padding:16px 28px 0; display:flex; align-items:center; gap:16px; }
.mode-wrap { max-width:1760px; margin:0 auto; padding:16px 28px 0; display:flex; align-items:center; gap:16px; }
.mode-toggle { display:inline-flex; border:1px solid var(--border-strong); border-radius:6px; overflow:hidden; }
.mode-btn { padding:8px 18px; font-family:var(--sans); font-size:13px; font-weight:600; border:none; background:var(--surface);
color:var(--text-muted); cursor:pointer; }
@@ -463,7 +471,7 @@
/* ── WORK PACKAGE FORM ───────────────────────────────────────────── */
.sop-hint { color:var(--accent) !important; }
.release-banner { max-width:1080px; margin:0 auto; padding:0 28px; }
.release-banner { max-width:1760px; margin:0 auto; padding:0 28px; }
.release-banner .rb-inner { margin-top:14px; border-radius:var(--radius); padding:11px 16px; font-size:13px; font-weight:600;
display:flex; align-items:center; gap:10px; }
.rb-ready { background:var(--accent-green-dim); color:var(--accent-green); border:1px solid #b6e3c6; }
@@ -580,6 +588,49 @@
border:1px solid var(--border); border-radius:14px; padding:4px 11px; cursor:pointer; white-space:nowrap; }
.sec-chip:hover{ border-color:var(--accent); color:var(--accent); }
/* ── WP NAVIGATOR (left rail) ─────────────────────────────────────────
Sticky list of every work package on the project. Click one to open it in
the form; the one being edited is highlighted. */
.wp-nav { flex:0 0 262px; width:262px; align-self:flex-start; position:sticky; top:var(--rail-top,0px);
height:calc(100vh - var(--rail-top,0px)); display:flex; flex-direction:column;
background:var(--surface); border-right:1px solid var(--border); }
.wp-nav-head { display:flex; align-items:center; gap:8px; padding:14px 14px 8px; }
.wp-nav-title { font-size:12px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--text-muted); }
.wp-nav-count { color:var(--text-dim); font-weight:600; letter-spacing:0; }
.wp-nav-collapse, .wp-nav-reopen { background:transparent; border:1px solid var(--border); border-radius:4px;
color:var(--text-muted); cursor:pointer; font-size:14px; line-height:1; padding:2px 7px; margin-left:auto; }
.wp-nav-collapse:hover, .wp-nav-reopen:hover { border-color:var(--accent); color:var(--accent); }
.wp-nav-search { margin:0 14px 10px; padding:6px 9px; font-size:12px; font-family:inherit;
border:1px solid var(--border); border-radius:4px; background:var(--bg); color:var(--text); }
.wp-nav-search:focus { outline:none; border-color:var(--accent); }
.wp-nav-list { flex:1 1 auto; overflow-y:auto; padding:0 8px 8px; }
.wp-nav-group { font-size:10px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
color:var(--text-dim); padding:10px 6px 5px; }
.wp-nav-item { display:block; width:100%; text-align:left; background:transparent; border:0;
border-left:3px solid transparent; border-radius:4px; padding:6px 8px; cursor:pointer;
font-family:inherit; color:var(--text); }
.wp-nav-item:hover { background:var(--surface2); }
.wp-nav-item.active { background:var(--accent-dim); border-left-color:var(--accent); }
.wp-nav-num { display:block; font-family:var(--mono); font-size:11px; font-weight:600; color:var(--accent); }
.wp-nav-subj { display:block; font-size:12px; color:var(--text-muted); line-height:1.35;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.wp-nav-meta { display:flex; align-items:center; gap:6px; margin-top:3px; font-size:10px; color:var(--text-dim); }
.wp-nav-dot { width:7px; height:7px; border-radius:50%; background:var(--text-dim); flex:0 0 auto; }
.wp-nav-dot.ok { background:var(--accent-green); }
.wp-nav-dot.open { background:var(--accent-amber); }
.wp-nav-dot.hold { background:var(--red); }
.wp-nav-empty { padding:12px 8px; font-size:12px; color:var(--text-dim); }
.wp-nav-foot { border-top:1px solid var(--border); padding:9px 12px; display:flex; gap:8px; flex-wrap:wrap; }
.wp-nav-reopen { display:none; position:sticky; top:var(--rail-top,0px); margin:10px 0 0 8px; align-self:flex-start; z-index:20; }
/* Keep the rail's footer clear of the fixed save bar. */
body.has-sticky-save .wp-nav { height:calc(100vh - var(--rail-top,0px) - 56px); }
body.wp-nav-collapsed .wp-nav { display:none; }
body.wp-nav-collapsed .wp-nav-reopen { display:block; }
@media (max-width: 1100px) {
.wp-nav { display:none; }
.wp-nav-reopen { display:none !important; }
}
/* Sticky save bar */
.sticky-save{ position:fixed; left:0; right:0; bottom:0; z-index:40; display:flex; align-items:center;
justify-content:space-between; gap:14px; padding:10px 20px; background:#fff;