(()=>{'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>
(()=>{'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();})();
(()=>{'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.