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:
@@ -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(); }
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 3–4 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;
|
||||
|
||||
Reference in New Issue
Block a user