Open demo

CSS code

HTML
<section class="rp" id="release-planner" aria-labelledby="rp-title"><header class="rp-head"><div><p class="rp-kicker">SHIPROOM / PRODUCT OPERATIONS</p><h2 class="" id="rp-title">A roadmap that reaches release.</h2><p class="rp-muted">Dependencies first. Notes with every change. A clear record of what shipped.</p></div><button type="button" class="rp-button primary" id="rp-publish">Publish demo release</button></header><div class="rp-tools"><div class="rp-field"><label class="" for="rp-version">Release</label><select class="" id="rp-version"><option value="1.8">1.8 · In progress</option><option value="1.9">1.9 · Planned</option><option value="1.7">1.7 · Shipped</option></select></div><label class="rp-check"><input class="" type="checkbox" id="rp-unblocked"> Show unblocked work only</label><span class="rp-muted" id="rp-progress" role="status"></span></div><div class="rp-scroll" role="region" tabindex="0" aria-label="Release work by status"><div class="rp-board" id="rp-board"></div></div><div class="rp-release" id="rp-release"></div><p class="rp-summary">Fictional product roadmap. Changes and demo releases save only in this browser; no repository or deployment is connected.</p><p class="rp-status" id="rp-status" role="status" aria-live="polite"></p><dialog class="" id="rp-dialog" aria-labelledby="rp-dialog-title"><h3 class="" id="rp-dialog-title">Update work item</h3><form id="rp-form"><div class="rp-field"><label class="" for="rp-state">Status</label><select class="" id="rp-state"><option>Planned</option><option>In progress</option><option>Review</option><option>Shipped</option></select></div><div class="rp-field"><label class="" for="rp-note">Release note</label><textarea class="" id="rp-note" maxlength="500" required></textarea></div><p class="rp-muted" id="rp-dependencies"></p><p class="rp-error" id="rp-error" role="alert"></p><div class="rp-actions"><button class="rp-button primary" type="submit">Save work item</button><button class="rp-button" type="button" id="rp-cancel">Cancel</button></div></form></dialog></section>
CSS
.rp{--rp-ink:#1e293b;--rp-muted:#536176;--rp-accent:#5041a1;--rp-line:#d9dfeb;color:var(--rp-ink);font:13px/1.5 system-ui,sans-serif;background:white;border:1px solid var(--rp-line);border-radius:13px;overflow:hidden}.rp *{box-sizing:border-box}.rp h2,.rp h3,.rp p{margin:0}.rp h2{font-size:22px;font-weight:700;letter-spacing:-.5px}.rp h3{font-size:14px;font-weight:650}.rp-head{display:flex;justify-content:space-between;gap:18px;align-items:center;padding:24px}.rp-kicker{font-size:10px;font-weight:750;letter-spacing:1.6px;color:var(--rp-accent);margin-bottom:7px!important}.rp-muted{font-size:12px;color:var(--rp-muted)}.rp-tools{padding:16px 24px;display:flex;align-items:end;gap:20px;flex-wrap:wrap;border-block:1px solid var(--rp-line);background:#f7f8fc}.rp-field{min-width:150px}.rp label{display:block;font-size:11px;font-weight:650;margin-bottom:5px}.rp input:not([type=checkbox]),.rp select,.rp textarea{display:block;width:100%;border:1px solid #96a2b6;background:white;border-radius:6px;color:var(--rp-ink);padding:9px 10px;font:inherit}.rp input[type=checkbox]{accent-color:var(--rp-accent);width:16px;height:16px}.rp textarea{min-height:100px}.rp-button{display:inline-flex;align-items:center;justify-content:center;border:1px solid #96a2b6;background:#fff;color:var(--rp-ink);border-radius:6px;padding:8px 12px;font-size:12px;font-weight:600;cursor:pointer;min-height:36px}.rp-button.primary{background:var(--rp-accent);color:white;border-color:var(--rp-accent)}.rp-button:disabled{opacity:.5;cursor:not-allowed}.rp-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.rp-check{display:flex!important;align-items:center;gap:8px;padding-bottom:10px}.rp-scroll{overflow:auto;max-width:100%}.rp-board{display:grid;grid-template-columns:repeat(4,minmax(190px,1fr));gap:14px;min-width:850px;padding:22px;background:#f7f8fc}.rp-column{border:1px solid var(--rp-line);border-radius:8px;padding:13px;min-height:220px;background:#f0f2f7}.rp-column h3{font-size:11px;text-transform:uppercase;letter-spacing:.6px;margin-bottom:13px}.rp-card{display:block;width:100%;text-align:left;background:white;border:1px solid var(--rp-line);border-radius:7px;padding:14px;margin-bottom:12px;color:var(--rp-ink);cursor:pointer}.rp-card strong{display:block;font-size:12px;line-height:1.5;margin:10px 0}.rp-card small{display:block;color:var(--rp-muted);font-size:10px;line-height:1.6}.rp-tag{display:inline-block;border-radius:4px;padding:3px 6px;background:#e9e6f6;color:#5041a1;font-size:10px;font-weight:650}.rp-tag.blocked{background:#fff0d5;color:#7b4911}.rp-release{padding:24px;display:grid;grid-template-columns:minmax(0,1fr) minmax(180px,.8fr);gap:24px;border-top:1px solid var(--rp-line)}.rp-release ul{padding-left:19px;margin:10px 0}.rp-release li{padding:5px 0;font-size:12px}.rp-release h3{font-size:16px;margin-bottom:10px}.rp-summary{padding:0 24px 18px;font-size:11px;color:var(--rp-muted)}.rp-status{padding:0 24px 18px;color:var(--rp-accent);font-size:12px}.rp-status:empty{display:none}.rp dialog{background:white;color:var(--rp-ink);border:1px solid var(--rp-line);border-radius:12px;padding:24px;width:min(480px,calc(100% - 30px))}.rp dialog::backdrop{background:#15213780}.rp dialog .rp-field{margin:16px 0}.rp dialog h3{font-size:20px}.rp-error{font-size:12px;color:#9b2937;margin:12px 0!important}.rp button:focus-visible,.rp input:focus-visible,.rp select:focus-visible,.rp textarea:focus-visible,.rp [tabindex]:focus-visible{outline:3px solid #5041a1;outline-offset:2px}@media(max-width:600px){.rp-head{align-items:start;flex-direction:column;padding:18px}.rp-tools{padding:16px}.rp-release{grid-template-columns:1fr;padding:18px}.rp-summary,.rp-status{padding-inline:18px}}
JavaScript
(()=>{'use strict';const root=document.querySelector('#release-planner');if(!root)return;const $=s=>root.querySelector(s),$$=s=>[...root.querySelectorAll(s)];const KEY='uic-release-planning-23';const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));const FLOW=['Planned','In progress','Review','Shipped'];const initial={items:[{id:'R101',title:'Workspace permissions',version:'1.7',status:'Shipped',deps:[],note:'Workspace owners can assign editor and viewer access.'},{id:'R102',title:'Billing export',version:'1.7',status:'Shipped',deps:['R101'],note:'Export the workspace billing list as CSV.'},{id:'R103',title:'Saved project filters',version:'1.8',status:'In progress',deps:['R101'],note:'Keep named project views with search and status filters.'},{id:'R104',title:'Batch edit projects',version:'1.8',status:'Planned',deps:['R103'],note:'Update selected projects from the saved view.'},{id:'R105',title:'Project audit trail',version:'1.8',status:'Review',deps:['R103'],note:'See a chronological record of project changes.'},{id:'R106',title:'Offline saved views',version:'1.9',status:'Planned',deps:['R104'],note:'Read recently opened saved views while offline.'},{id:'R107',title:'Mobile project list',version:'1.9',status:'Planned',deps:['R103'],note:'Use the project list comfortably on a phone.'}],releases:[{version:'1.7',published:true,date:'2026-10-12',notes:[{title:'Workspace permissions',note:'Workspace owners can assign editor and viewer access.'},{title:'Billing export',note:'Export the workspace billing list as CSV.'}]},{version:'1.8',published:false,date:'2026-10-27',notes:[]},{version:'1.9',published:false,date:'2026-11-10',notes:[]}]};let db=structuredClone(initial);try{const value=JSON.parse(localStorage.getItem(KEY));if(value&&Array.isArray(value.items)&&Array.isArray(value.releases))db=value;}catch{}const say=s=>$('#rp-status').textContent=s;const persist=()=>{try{localStorage.setItem(KEY,JSON.stringify(db));return true;}catch{say('Storage unavailable. Changes last for this tab only.');return false;}};const item=id=>db.items.find(i=>i.id===id);const blocked=i=>i.deps.some(id=>item(id)?.status!=='Shipped');let current=null;
function render(){const version=$('#rp-version').value,release=db.releases.find(r=>r.version===version),all=db.items.filter(i=>i.version===version),visible=all.filter(i=>!$('#rp-unblocked').checked||!blocked(i));$('#rp-progress').textContent=all.filter(i=>i.status==='Shipped').length+' / '+all.length+' work items shipped';$('#rp-publish').disabled=release.published;$('#rp-publish').textContent=release.published?'Demo release published':'Publish demo release';$('#rp-board').innerHTML=FLOW.map(status=>'<section class="rp-column"><h3 class="">'+status+' · '+visible.filter(i=>i.status===status).length+'</h3>'+visible.filter(i=>i.status===status).map(i=>'<button type="button" class="rp-card" data-item="'+i.id+'"><span class="">'+i.id+'</span><strong class="">'+esc(i.title)+'</strong><span class="rp-tag '+(blocked(i)?'blocked':'')+'">'+(blocked(i)?'Dependency blocked':release.published?'Released':'Unblocked')+'</span><small class="">'+(i.deps.length?'Needs: '+i.deps.map(id=>esc(item(id)?.title||id)).join(', '):'No dependencies')+'</small></button>').join('')+(visible.some(i=>i.status===status)?'':'<p class="rp-muted">No work here.</p>')+'</section>').join('');$('#rp-release').innerHTML='<section><h3 class="">Release '+version+' notes</h3><p class="rp-muted">'+(release.published?'Published demo snapshot · '+release.date:'Draft notes · target '+release.date)+'</p><ul class="">'+(release.published?release.notes:all.map(i=>({title:i.title,note:i.note}))).map(n=>'<li class=""><strong>'+esc(n.title)+':</strong> '+esc(n.note)+'</li>').join('')+'</ul></section><aside><h3 class="">Release checks</h3><p class="rp-muted">'+all.filter(blocked).length+' dependency-blocked item(s).<br>'+all.filter(i=>i.status!=='Shipped').length+' item(s) still need to ship.<br>Published snapshots are locked.</p></aside>';$$('[data-item]').forEach(b=>b.onclick=()=>{current=item(b.dataset.item);const r=db.releases.find(r=>r.version===current.version);$('#rp-dialog-title').textContent=current.title;$('#rp-state').value=current.status;$('#rp-note').value=current.note;$('#rp-error').textContent='';$('#rp-dependencies').textContent=current.deps.length?'Dependencies: '+current.deps.map(id=>item(id).title+' ('+item(id).status+')').join(', '):'No dependencies.';$('#rp-state').disabled=r.published;$('#rp-note').readOnly=r.published;$('#rp-form button[type=submit]').disabled=r.published;$('#rp-dialog').showModal();});}
$('#rp-form').onsubmit=e=>{e.preventDefault();if(!e.currentTarget.reportValidity()||!current)return;const release=db.releases.find(r=>r.version===current.version),status=$('#rp-state').value,note=$('#rp-note').value.trim();if(release.published){$('#rp-error').textContent='Published release snapshots are locked.';return;}if(!note){$('#rp-error').textContent='Enter a release note.';return;}if(status==='Shipped'&&blocked(current)){$('#rp-error').textContent='Ship every dependency before shipping this work item.';return;}if(current.status==='Shipped'&&status!=='Shipped'&&db.items.some(i=>i.status==='Shipped'&&i.deps.includes(current.id))){$('#rp-error').textContent='A shipped dependent needs this item. Move dependent work back first.';return;}current.status=status;current.note=note;const saved=persist();$('#rp-dialog').close();render();say(saved?'Work item saved in this browser.':'Work item updated for this tab only.');};$('#rp-cancel').onclick=()=>$('#rp-dialog').close();$('#rp-version').onchange=render;$('#rp-unblocked').onchange=render;$('#rp-publish').onclick=()=>{const version=$('#rp-version').value,release=db.releases.find(r=>r.version===version),all=db.items.filter(i=>i.version===version);if(release.published)return;if(!all.length||all.some(i=>i.status!=='Shipped'||blocked(i))){say('Ship every work item and its dependencies before publishing this demo release.');return;}release.published=true;release.date='2026-10-27';release.notes=all.map(i=>({title:i.title,note:i.note}));const saved=persist();render();say(saved?'Demo release snapshot saved in this browser. No deployment was made.':'Demo release saved for this tab only. No deployment was made.');};render();})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<section class="rp card" id="release-planner" aria-labelledby="rp-title"><header class="rp-head"><div><p class="rp-kicker">SHIPROOM / PRODUCT OPERATIONS</p><h2 class="" id="rp-title">A roadmap that reaches release.</h2><p class="rp-muted">Dependencies first. Notes with every change. A clear record of what shipped.</p></div><button type="button" class="rp-button primary" id="rp-publish">Publish demo release</button></header><div class="rp-tools"><div class="rp-field"><label class="" for="rp-version">Release</label><select class="form-select" id="rp-version"><option value="1.8">1.8 · In progress</option><option value="1.9">1.9 · Planned</option><option value="1.7">1.7 · Shipped</option></select></div><label class="rp-check"><input class="form-check-input" type="checkbox" id="rp-unblocked"> Show unblocked work only</label><span class="rp-muted" id="rp-progress" role="status"></span></div><div class="rp-scroll" role="region" tabindex="0" aria-label="Release work by status"><div class="rp-board" id="rp-board"></div></div><div class="rp-release" id="rp-release"></div><p class="rp-summary">Fictional product roadmap. Changes and demo releases save only in this browser; no repository or deployment is connected.</p><p class="rp-status" id="rp-status" role="status" aria-live="polite"></p><dialog class="" id="rp-dialog" aria-labelledby="rp-dialog-title"><h3 class="" id="rp-dialog-title">Update work item</h3><form id="rp-form"><div class="rp-field"><label class="" for="rp-state">Status</label><select class="form-select" id="rp-state"><option>Planned</option><option>In progress</option><option>Review</option><option>Shipped</option></select></div><div class="rp-field"><label class="" for="rp-note">Release note</label><textarea class="" id="rp-note" maxlength="500" required></textarea></div><p class="rp-muted" id="rp-dependencies"></p><p class="rp-error" id="rp-error" role="alert"></p><div class="rp-actions"><button class="rp-button primary" type="submit">Save work item</button><button class="rp-button" type="button" id="rp-cancel">Cancel</button></div></form></dialog></section>
CSS
.rp{--rp-ink:#1e293b;--rp-muted:#536176;--rp-accent:#5041a1;--rp-line:#d9dfeb;color:var(--rp-ink);font:13px/1.5 system-ui,sans-serif;background:white;border:1px solid var(--rp-line);border-radius:13px;overflow:hidden}.rp *{box-sizing:border-box}.rp h2,.rp h3,.rp p{margin:0}.rp h2{font-size:22px;font-weight:700;letter-spacing:-.5px}.rp h3{font-size:14px;font-weight:650}.rp-head{display:flex;justify-content:space-between;gap:18px;align-items:center;padding:24px}.rp-kicker{font-size:10px;font-weight:750;letter-spacing:1.6px;color:var(--rp-accent);margin-bottom:7px!important}.rp-muted{font-size:12px;color:var(--rp-muted)}.rp-tools{padding:16px 24px;display:flex;align-items:end;gap:20px;flex-wrap:wrap;border-block:1px solid var(--rp-line);background:#f7f8fc}.rp-field{min-width:150px}.rp label{display:block;font-size:11px;font-weight:650;margin-bottom:5px}.rp input:not([type=checkbox]),.rp select,.rp textarea{display:block;width:100%;border:1px solid #96a2b6;background:white;border-radius:6px;color:var(--rp-ink);padding:9px 10px;font:inherit}.rp input[type=checkbox]{accent-color:var(--rp-accent);width:16px;height:16px}.rp textarea{min-height:100px}.rp-button{display:inline-flex;align-items:center;justify-content:center;border:1px solid #96a2b6;background:#fff;color:var(--rp-ink);border-radius:6px;padding:8px 12px;font-size:12px;font-weight:600;cursor:pointer;min-height:36px}.rp-button.primary{background:var(--rp-accent);color:white;border-color:var(--rp-accent)}.rp-button:disabled{opacity:.5;cursor:not-allowed}.rp-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.rp-check{display:flex!important;align-items:center;gap:8px;padding-bottom:10px}.rp-scroll{overflow:auto;max-width:100%}.rp-board{display:grid;grid-template-columns:repeat(4,minmax(190px,1fr));gap:14px;min-width:850px;padding:22px;background:#f7f8fc}.rp-column{border:1px solid var(--rp-line);border-radius:8px;padding:13px;min-height:220px;background:#f0f2f7}.rp-column h3{font-size:11px;text-transform:uppercase;letter-spacing:.6px;margin-bottom:13px}.rp-card{display:block;width:100%;text-align:left;background:white;border:1px solid var(--rp-line);border-radius:7px;padding:14px;margin-bottom:12px;color:var(--rp-ink);cursor:pointer}.rp-card strong{display:block;font-size:12px;line-height:1.5;margin:10px 0}.rp-card small{display:block;color:var(--rp-muted);font-size:10px;line-height:1.6}.rp-tag{display:inline-block;border-radius:4px;padding:3px 6px;background:#e9e6f6;color:#5041a1;font-size:10px;font-weight:650}.rp-tag.blocked{background:#fff0d5;color:#7b4911}.rp-release{padding:24px;display:grid;grid-template-columns:minmax(0,1fr) minmax(180px,.8fr);gap:24px;border-top:1px solid var(--rp-line)}.rp-release ul{padding-left:19px;margin:10px 0}.rp-release li{padding:5px 0;font-size:12px}.rp-release h3{font-size:16px;margin-bottom:10px}.rp-summary{padding:0 24px 18px;font-size:11px;color:var(--rp-muted)}.rp-status{padding:0 24px 18px;color:var(--rp-accent);font-size:12px}.rp-status:empty{display:none}.rp dialog{background:white;color:var(--rp-ink);border:1px solid var(--rp-line);border-radius:12px;padding:24px;width:min(480px,calc(100% - 30px))}.rp dialog::backdrop{background:#15213780}.rp dialog .rp-field{margin:16px 0}.rp dialog h3{font-size:20px}.rp-error{font-size:12px;color:#9b2937;margin:12px 0!important}.rp button:focus-visible,.rp input:focus-visible,.rp select:focus-visible,.rp textarea:focus-visible,.rp [tabindex]:focus-visible{outline:3px solid #5041a1;outline-offset:2px}@media(max-width:600px){.rp-head{align-items:start;flex-direction:column;padding:18px}.rp-tools{padding:16px}.rp-release{grid-template-columns:1fr;padding:18px}.rp-summary,.rp-status{padding-inline:18px}}
JavaScript
(()=>{'use strict';const root=document.querySelector('#release-planner');if(!root)return;const $=s=>root.querySelector(s),$$=s=>[...root.querySelectorAll(s)];const KEY='uic-release-planning-23';const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));const FLOW=['Planned','In progress','Review','Shipped'];const initial={items:[{id:'R101',title:'Workspace permissions',version:'1.7',status:'Shipped',deps:[],note:'Workspace owners can assign editor and viewer access.'},{id:'R102',title:'Billing export',version:'1.7',status:'Shipped',deps:['R101'],note:'Export the workspace billing list as CSV.'},{id:'R103',title:'Saved project filters',version:'1.8',status:'In progress',deps:['R101'],note:'Keep named project views with search and status filters.'},{id:'R104',title:'Batch edit projects',version:'1.8',status:'Planned',deps:['R103'],note:'Update selected projects from the saved view.'},{id:'R105',title:'Project audit trail',version:'1.8',status:'Review',deps:['R103'],note:'See a chronological record of project changes.'},{id:'R106',title:'Offline saved views',version:'1.9',status:'Planned',deps:['R104'],note:'Read recently opened saved views while offline.'},{id:'R107',title:'Mobile project list',version:'1.9',status:'Planned',deps:['R103'],note:'Use the project list comfortably on a phone.'}],releases:[{version:'1.7',published:true,date:'2026-10-12',notes:[{title:'Workspace permissions',note:'Workspace owners can assign editor and viewer access.'},{title:'Billing export',note:'Export the workspace billing list as CSV.'}]},{version:'1.8',published:false,date:'2026-10-27',notes:[]},{version:'1.9',published:false,date:'2026-11-10',notes:[]}]};let db=structuredClone(initial);try{const value=JSON.parse(localStorage.getItem(KEY));if(value&&Array.isArray(value.items)&&Array.isArray(value.releases))db=value;}catch{}const say=s=>$('#rp-status').textContent=s;const persist=()=>{try{localStorage.setItem(KEY,JSON.stringify(db));return true;}catch{say('Storage unavailable. Changes last for this tab only.');return false;}};const item=id=>db.items.find(i=>i.id===id);const blocked=i=>i.deps.some(id=>item(id)?.status!=='Shipped');let current=null;
function render(){const version=$('#rp-version').value,release=db.releases.find(r=>r.version===version),all=db.items.filter(i=>i.version===version),visible=all.filter(i=>!$('#rp-unblocked').checked||!blocked(i));$('#rp-progress').textContent=all.filter(i=>i.status==='Shipped').length+' / '+all.length+' work items shipped';$('#rp-publish').disabled=release.published;$('#rp-publish').textContent=release.published?'Demo release published':'Publish demo release';$('#rp-board').innerHTML=FLOW.map(status=>'<section class="rp-column"><h3 class="">'+status+' · '+visible.filter(i=>i.status===status).length+'</h3>'+visible.filter(i=>i.status===status).map(i=>'<button type="button" class="rp-card" data-item="'+i.id+'"><span class="">'+i.id+'</span><strong class="">'+esc(i.title)+'</strong><span class="rp-tag '+(blocked(i)?'blocked':'')+'">'+(blocked(i)?'Dependency blocked':release.published?'Released':'Unblocked')+'</span><small class="">'+(i.deps.length?'Needs: '+i.deps.map(id=>esc(item(id)?.title||id)).join(', '):'No dependencies')+'</small></button>').join('')+(visible.some(i=>i.status===status)?'':'<p class="rp-muted">No work here.</p>')+'</section>').join('');$('#rp-release').innerHTML='<section><h3 class="">Release '+version+' notes</h3><p class="rp-muted">'+(release.published?'Published demo snapshot · '+release.date:'Draft notes · target '+release.date)+'</p><ul class="">'+(release.published?release.notes:all.map(i=>({title:i.title,note:i.note}))).map(n=>'<li class=""><strong>'+esc(n.title)+':</strong> '+esc(n.note)+'</li>').join('')+'</ul></section><aside><h3 class="">Release checks</h3><p class="rp-muted">'+all.filter(blocked).length+' dependency-blocked item(s).<br>'+all.filter(i=>i.status!=='Shipped').length+' item(s) still need to ship.<br>Published snapshots are locked.</p></aside>';$$('[data-item]').forEach(b=>b.onclick=()=>{current=item(b.dataset.item);const r=db.releases.find(r=>r.version===current.version);$('#rp-dialog-title').textContent=current.title;$('#rp-state').value=current.status;$('#rp-note').value=current.note;$('#rp-error').textContent='';$('#rp-dependencies').textContent=current.deps.length?'Dependencies: '+current.deps.map(id=>item(id).title+' ('+item(id).status+')').join(', '):'No dependencies.';$('#rp-state').disabled=r.published;$('#rp-note').readOnly=r.published;$('#rp-form button[type=submit]').disabled=r.published;$('#rp-dialog').showModal();});}
$('#rp-form').onsubmit=e=>{e.preventDefault();if(!e.currentTarget.reportValidity()||!current)return;const release=db.releases.find(r=>r.version===current.version),status=$('#rp-state').value,note=$('#rp-note').value.trim();if(release.published){$('#rp-error').textContent='Published release snapshots are locked.';return;}if(!note){$('#rp-error').textContent='Enter a release note.';return;}if(status==='Shipped'&&blocked(current)){$('#rp-error').textContent='Ship every dependency before shipping this work item.';return;}if(current.status==='Shipped'&&status!=='Shipped'&&db.items.some(i=>i.status==='Shipped'&&i.deps.includes(current.id))){$('#rp-error').textContent='A shipped dependent needs this item. Move dependent work back first.';return;}current.status=status;current.note=note;const saved=persist();$('#rp-dialog').close();render();say(saved?'Work item saved in this browser.':'Work item updated for this tab only.');};$('#rp-cancel').onclick=()=>$('#rp-dialog').close();$('#rp-version').onchange=render;$('#rp-unblocked').onchange=render;$('#rp-publish').onclick=()=>{const version=$('#rp-version').value,release=db.releases.find(r=>r.version===version),all=db.items.filter(i=>i.version===version);if(release.published)return;if(!all.length||all.some(i=>i.status!=='Shipped'||blocked(i))){say('Ship every work item and its dependencies before publishing this demo release.');return;}release.published=true;release.date='2026-10-27';release.notes=all.map(i=>({title:i.title,note:i.note}));const saved=persist();render();say(saved?'Demo release snapshot saved in this browser. No deployment was made.':'Demo release saved for this tab only. No deployment was made.');};render();})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<section class="overflow-hidden rounded-[13px] border border-[#d9dfeb] bg-white font-sans text-[13px] leading-[1.5] text-[#1e293b] [&_button]:cursor-pointer [&_button:disabled]:opacity-50 [&_button:disabled]:cursor-not-allowed [&_button:focus-visible]:outline-3 [&_button:focus-visible]:outline-offset-2 [&_button:focus-visible]:outline-[#5041a1] [&_input:focus-visible]:outline-3 [&_input:focus-visible]:outline-offset-2 [&_input:focus-visible]:outline-[#5041a1] [&_select:focus-visible]:outline-3 [&_select:focus-visible]:outline-offset-2 [&_select:focus-visible]:outline-[#5041a1] [&_textarea:focus-visible]:outline-3 [&_textarea:focus-visible]:outline-offset-2 [&_textarea:focus-visible]:outline-[#5041a1] [&_[tabindex]:focus-visible]:outline-3 [&_[tabindex]:focus-visible]:outline-offset-[-3px] [&_[tabindex]:focus-visible]:outline-[#5041a1]" id="release-planner" aria-labelledby="rp-title"><header class="flex flex-col items-start justify-between gap-[18px] p-[18px] sm:flex-row sm:items-center sm:p-6"><div><p class="mb-[7px] text-[10px] font-bold tracking-[1.6px] text-[#5041a1]">SHIPROOM / PRODUCT OPERATIONS</p><h2 class="m-0 text-[22px] leading-normal font-bold tracking-[-.5px]" id="rp-title">A roadmap that reaches release.</h2><p class="text-xs text-[#536176]">Dependencies first. Notes with every change. A clear record of what shipped.</p></div><button type="button" class="inline-flex min-h-9 items-center justify-center rounded-md border border-[#5041a1] bg-[#5041a1] px-3 py-2 text-xs leading-normal font-semibold text-white" id="rp-publish">Publish demo release</button></header><div class="flex flex-wrap items-end gap-5 border-y border-[#d9dfeb] bg-[#f7f8fc] p-4 sm:px-6"><div class="min-w-[150px]"><label class="mb-[5px] block text-[11px] font-semibold" for="rp-version">Release</label><select class="block w-full rounded-md border border-[#96a2b6] bg-white px-2.5 py-[9px] text-[13px] leading-normal text-[#1e293b]" id="rp-version"><option value="1.8">1.8 · In progress</option><option value="1.9">1.9 · Planned</option><option value="1.7">1.7 · Shipped</option></select></div><label class="flex items-center gap-2 pb-2.5 text-[11px] font-semibold"><input class="h-4 w-4 accent-[#5041a1]" type="checkbox" id="rp-unblocked"> Show unblocked work only</label><span class="text-xs text-[#536176]" id="rp-progress" role="status"></span></div><div class="max-w-full overflow-auto" role="region" tabindex="0" aria-label="Release work by status"><div class="grid min-w-[850px] grid-cols-4 gap-[14px] bg-[#f7f8fc] p-[22px]" id="rp-board"></div></div><div class="grid grid-cols-1 gap-6 border-t border-[#d9dfeb] p-[18px] sm:grid-cols-[minmax(0,1fr)_minmax(180px,.8fr)] sm:p-6" id="rp-release"></div><p class="px-[18px] pb-[18px] text-[11px] text-[#536176] sm:px-6">Fictional product roadmap. Changes and demo releases save only in this browser; no repository or deployment is connected.</p><p class="px-[18px] pb-[18px] text-xs text-[#5041a1] empty:hidden sm:px-6" id="rp-status" role="status" aria-live="polite"></p><dialog class="m-auto w-[min(480px,calc(100%-30px))] max-w-none rounded-xl border border-[#d9dfeb] bg-white p-6 text-[#1e293b] backdrop:bg-[#15213780]" id="rp-dialog" aria-labelledby="rp-dialog-title"><h3 class="text-xl font-semibold" id="rp-dialog-title">Update work item</h3><form id="rp-form"><div class="my-4 min-w-[150px]"><label class="mb-[5px] block text-[11px] font-semibold" for="rp-state">Status</label><select class="block w-full rounded-md border border-[#96a2b6] bg-white px-2.5 py-[9px] text-[13px] leading-normal text-[#1e293b]" id="rp-state"><option>Planned</option><option>In progress</option><option>Review</option><option>Shipped</option></select></div><div class="my-4 min-w-[150px]"><label class="mb-[5px] block text-[11px] font-semibold" for="rp-note">Release note</label><textarea class="block min-h-[100px] w-full rounded-md border border-[#96a2b6] bg-white px-2.5 py-[9px] text-[13px] text-[#1e293b]" id="rp-note" maxlength="500" required></textarea></div><p class="text-xs text-[#536176]" id="rp-dependencies"></p><p class="my-3 text-xs text-[#9b2937]" id="rp-error" role="alert"></p><div class="flex flex-wrap items-center gap-2"><button class="inline-flex min-h-9 items-center justify-center rounded-md border border-[#5041a1] bg-[#5041a1] px-3 py-2 text-xs leading-normal font-semibold text-white" type="submit">Save work item</button><button class="inline-flex min-h-9 items-center justify-center rounded-md border border-[#96a2b6] bg-white px-3 py-2 text-xs leading-normal font-semibold text-[#1e293b]" type="button" id="rp-cancel">Cancel</button></div></form></dialog></section>
JavaScript
(()=>{'use strict';const root=document.querySelector('#release-planner');if(!root)return;const $=s=>root.querySelector(s),$$=s=>[...root.querySelectorAll(s)];const KEY='uic-release-planning-23';const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));const FLOW=['Planned','In progress','Review','Shipped'];const initial={items:[{id:'R101',title:'Workspace permissions',version:'1.7',status:'Shipped',deps:[],note:'Workspace owners can assign editor and viewer access.'},{id:'R102',title:'Billing export',version:'1.7',status:'Shipped',deps:['R101'],note:'Export the workspace billing list as CSV.'},{id:'R103',title:'Saved project filters',version:'1.8',status:'In progress',deps:['R101'],note:'Keep named project views with search and status filters.'},{id:'R104',title:'Batch edit projects',version:'1.8',status:'Planned',deps:['R103'],note:'Update selected projects from the saved view.'},{id:'R105',title:'Project audit trail',version:'1.8',status:'Review',deps:['R103'],note:'See a chronological record of project changes.'},{id:'R106',title:'Offline saved views',version:'1.9',status:'Planned',deps:['R104'],note:'Read recently opened saved views while offline.'},{id:'R107',title:'Mobile project list',version:'1.9',status:'Planned',deps:['R103'],note:'Use the project list comfortably on a phone.'}],releases:[{version:'1.7',published:true,date:'2026-10-12',notes:[{title:'Workspace permissions',note:'Workspace owners can assign editor and viewer access.'},{title:'Billing export',note:'Export the workspace billing list as CSV.'}]},{version:'1.8',published:false,date:'2026-10-27',notes:[]},{version:'1.9',published:false,date:'2026-11-10',notes:[]}]};let db=structuredClone(initial);try{const value=JSON.parse(localStorage.getItem(KEY));if(value&&Array.isArray(value.items)&&Array.isArray(value.releases))db=value;}catch{}const say=s=>$('#rp-status').textContent=s;const persist=()=>{try{localStorage.setItem(KEY,JSON.stringify(db));return true;}catch{say('Storage unavailable. Changes last for this tab only.');return false;}};const item=id=>db.items.find(i=>i.id===id);const blocked=i=>i.deps.some(id=>item(id)?.status!=='Shipped');let current=null;
function render(){const version=$('#rp-version').value,release=db.releases.find(r=>r.version===version),all=db.items.filter(i=>i.version===version),visible=all.filter(i=>!$('#rp-unblocked').checked||!blocked(i));$('#rp-progress').textContent=all.filter(i=>i.status==='Shipped').length+' / '+all.length+' work items shipped';$('#rp-publish').disabled=release.published;$('#rp-publish').textContent=release.published?'Demo release published':'Publish demo release';$('#rp-board').innerHTML=FLOW.map(status=>'<section class="min-h-[220px] rounded-lg border border-[#d9dfeb] bg-[#f0f2f7] p-[13px]"><h3 class="mb-[13px] text-[11px] font-semibold tracking-[.6px] uppercase">'+status+' · '+visible.filter(i=>i.status===status).length+'</h3>'+visible.filter(i=>i.status===status).map(i=>'<button type="button" class="mb-3 block w-full rounded-[7px] border border-[#d9dfeb] bg-white p-[14px] text-left text-[#1e293b]" data-item="'+i.id+'"><span class="font-mono text-[10px] text-[#536176]">'+i.id+'</span><strong class="my-2.5 block text-xs leading-[1.5] font-semibold">'+esc(i.title)+'</strong><span class="inline-block rounded px-1.5 py-[3px] text-[10px] leading-normal font-semibold '+(blocked(i)?'bg-[#fff0d5] text-[#7b4911]':'bg-[#e9e6f6] text-[#5041a1]')+'">'+(blocked(i)?'Dependency blocked':release.published?'Released':'Unblocked')+'</span><small class="mt-2 block text-[10px] leading-[1.6] text-[#536176]">'+(i.deps.length?'Needs: '+i.deps.map(id=>esc(item(id)?.title||id)).join(', '):'No dependencies')+'</small></button>').join('')+(visible.some(i=>i.status===status)?'':'<p class="text-xs text-[#536176]">No work here.</p>')+'</section>').join('');$('#rp-release').innerHTML='<section><h3 class="mb-2.5 text-base font-semibold">Release '+version+' notes</h3><p class="text-xs text-[#536176]">'+(release.published?'Published demo snapshot · '+release.date:'Draft notes · target '+release.date)+'</p><ul class="my-2.5 list-disc pl-[19px] text-xs">'+(release.published?release.notes:all.map(i=>({title:i.title,note:i.note}))).map(n=>'<li class="py-[5px]"><strong>'+esc(n.title)+':</strong> '+esc(n.note)+'</li>').join('')+'</ul></section><aside><h3 class="mb-2.5 text-base font-semibold">Release checks</h3><p class="text-xs text-[#536176]">'+all.filter(blocked).length+' dependency-blocked item(s).<br>'+all.filter(i=>i.status!=='Shipped').length+' item(s) still need to ship.<br>Published snapshots are locked.</p></aside>';$$('[data-item]').forEach(b=>b.onclick=()=>{current=item(b.dataset.item);const r=db.releases.find(r=>r.version===current.version);$('#rp-dialog-title').textContent=current.title;$('#rp-state').value=current.status;$('#rp-note').value=current.note;$('#rp-error').textContent='';$('#rp-dependencies').textContent=current.deps.length?'Dependencies: '+current.deps.map(id=>item(id).title+' ('+item(id).status+')').join(', '):'No dependencies.';$('#rp-state').disabled=r.published;$('#rp-note').readOnly=r.published;$('#rp-form button[type=submit]').disabled=r.published;$('#rp-dialog').showModal();});}
$('#rp-form').onsubmit=e=>{e.preventDefault();if(!e.currentTarget.reportValidity()||!current)return;const release=db.releases.find(r=>r.version===current.version),status=$('#rp-state').value,note=$('#rp-note').value.trim();if(release.published){$('#rp-error').textContent='Published release snapshots are locked.';return;}if(!note){$('#rp-error').textContent='Enter a release note.';return;}if(status==='Shipped'&&blocked(current)){$('#rp-error').textContent='Ship every dependency before shipping this work item.';return;}if(current.status==='Shipped'&&status!=='Shipped'&&db.items.some(i=>i.status==='Shipped'&&i.deps.includes(current.id))){$('#rp-error').textContent='A shipped dependent needs this item. Move dependent work back first.';return;}current.status=status;current.note=note;const saved=persist();$('#rp-dialog').close();render();say(saved?'Work item saved in this browser.':'Work item updated for this tab only.');};$('#rp-cancel').onclick=()=>$('#rp-dialog').close();$('#rp-version').onchange=render;$('#rp-unblocked').onchange=render;$('#rp-publish').onclick=()=>{const version=$('#rp-version').value,release=db.releases.find(r=>r.version===version),all=db.items.filter(i=>i.version===version);if(release.published)return;if(!all.length||all.some(i=>i.status!=='Shipped'||blocked(i))){say('Ship every work item and its dependencies before publishing this demo release.');return;}release.published=true;release.date='2026-10-27';release.notes=all.map(i=>({title:i.title,note:i.note}));const saved=persist();render();say(saved?'Demo release snapshot saved in this browser. No deployment was made.':'Demo release saved for this tab only. No deployment was made.');};render();})();

About this set

A product roadmap that connects planned work with release notes and dependency checks. Choose a release, inspect the four status columns and open any work card to change its status or release note. A dependent item cannot move to Shipped until all its prerequisites have shipped. Moving a shipped prerequisite backwards is also blocked while shipped dependent work still needs it. The unblocked-only filter helps separate work that can move now from work waiting on another item. Publish demo release remains available as a check action, but creates a browser-local snapshot only when every item in that version is shipped and unblocked. The snapshot keeps its release notes and locks the published items, so later draft edits cannot rewrite the shipped record. A previous release demonstrates the locked state and a later release shows dependencies across versions. Use the component for product planning screens, release dashboards and changelog workflows. Replace the local data and persistence helper with a shared backend for real teams; no repository or deployment is connected.

What’s included

  • Four-column roadmap grouped by selected release
  • Dependency checks before shipping work
  • Guard against demoting a prerequisite of shipped work
  • Per-item release notes and unblocked-only filtering
  • Locked release-note snapshots after demo publication
  • Browser-local state with a tab-only fallback

Accessibility

Work cards and actions are native buttons. The status editor uses a labelled native dialog, select and textarea with Escape support. The roadmap scroll region is named and keyboard focusable, dependency errors use an alert, and progress and actions use polite status messages. All controls have visible focus outlines and nothing animates.

Customise it

Edit the items, dependency IDs and releases in the inline JavaScript. CSS and Bootstrap colors use --rp-accent, --rp-ink and --rp-line; Tailwind uses matching color and spacing utilities in the component markup.