11const FIXED_SECTIONS = [ "Summary" , "Why" , "In Scope" , "Out of Scope" , "Done When" , "Notes" ] ;
22const SCOPE_STORAGE_KEY = "roadmap-ui.scope-collapsed" ;
3+ const SCOPE_WIDTH_STORAGE_KEY = "roadmap-ui.scope-width" ;
4+ const DEFAULT_SCOPE_WIDTH = 272 ;
5+ const MIN_SCOPE_WIDTH = 240 ;
6+ const MAX_SCOPE_WIDTH = 440 ;
37
48const state = {
59 workspace : null ,
610 selectedItemId : null ,
711 currentItem : null ,
812 collapsedGroups : new Set ( ) ,
913 scopeCollapsed : loadStoredScopePreference ( ) ,
14+ scopeWidth : loadStoredScopeWidth ( ) ,
1015 editorMode : "preview" ,
1116 dirtyStructured : false ,
1217 dirtyRaw : false ,
@@ -17,6 +22,7 @@ const boardPanelElement = document.querySelector("#board-panel");
1722const boardGroupsElement = document . querySelector ( "#board-groups" ) ;
1823const scopePanelElement = document . querySelector ( "#scope-panel" ) ;
1924const scopeContentElement = document . querySelector ( "#scope-content" ) ;
25+ const scopeResizerElement = document . querySelector ( "#scope-resizer" ) ;
2026const scopeToggleButton = document . querySelector ( "#scope-toggle" ) ;
2127const jumpToBoardButton = document . querySelector ( "#jump-to-board" ) ;
2228const jumpToEditorButton = document . querySelector ( "#jump-to-editor" ) ;
@@ -37,6 +43,7 @@ const extraSectionsElement = document.querySelector("#extra-sections");
3743const modeButtons = Array . from ( document . querySelectorAll ( "[data-editor-mode]" ) ) ;
3844const modePanes = Array . from ( document . querySelectorAll ( "[data-mode-pane]" ) ) ;
3945const stackedLayoutMedia = window . matchMedia ( "(max-width: 980px)" ) ;
46+ const desktopScopeLayoutMedia = window . matchMedia ( "(min-width: 1321px)" ) ;
4047
4148const fields = {
4249 id : document . querySelector ( "#field-id" ) ,
@@ -69,6 +76,27 @@ function persistScopePreference() {
6976 }
7077}
7178
79+ function clampScopeWidth ( width ) {
80+ return Math . max ( MIN_SCOPE_WIDTH , Math . min ( MAX_SCOPE_WIDTH , Math . round ( width ) ) ) ;
81+ }
82+
83+ function loadStoredScopeWidth ( ) {
84+ try {
85+ const rawValue = Number ( window . localStorage . getItem ( SCOPE_WIDTH_STORAGE_KEY ) ) ;
86+ return Number . isFinite ( rawValue ) && rawValue > 0 ? clampScopeWidth ( rawValue ) : DEFAULT_SCOPE_WIDTH ;
87+ } catch {
88+ return DEFAULT_SCOPE_WIDTH ;
89+ }
90+ }
91+
92+ function persistScopeWidth ( ) {
93+ try {
94+ window . localStorage . setItem ( SCOPE_WIDTH_STORAGE_KEY , String ( state . scopeWidth ) ) ;
95+ } catch {
96+ // Ignore storage failures; resize still works for the session.
97+ }
98+ }
99+
72100function setBanner ( message , tone = "info" ) {
73101 if ( ! message ) {
74102 statusBanner . hidden = true ;
@@ -233,6 +261,10 @@ function isStackedLayout() {
233261 return stackedLayoutMedia . matches ;
234262}
235263
264+ function isDesktopScopeLayout ( ) {
265+ return desktopScopeLayoutMedia . matches ;
266+ }
267+
236268function scrollPanelIntoView ( element ) {
237269 element ?. scrollIntoView ( { behavior : "smooth" , block : "start" } ) ;
238270}
@@ -244,11 +276,19 @@ function syncMobileNavigation() {
244276}
245277
246278function renderScopeChrome ( ) {
279+ const showResizer = ! state . scopeCollapsed && isDesktopScopeLayout ( ) ;
280+
247281 layoutElement . dataset . scopeCollapsed = String ( state . scopeCollapsed ) ;
282+ layoutElement . style . setProperty ( "--scope-width" , `${ state . scopeWidth } px` ) ;
248283 scopePanelElement . classList . toggle ( "scope-collapsed" , state . scopeCollapsed ) ;
249284 scopeToggleButton . textContent = state . scopeCollapsed ? "Expand" : "Collapse" ;
250285 scopeToggleButton . setAttribute ( "aria-expanded" , state . scopeCollapsed ? "false" : "true" ) ;
251286 scopeToggleButton . setAttribute ( "aria-label" , state . scopeCollapsed ? "Expand scope panel" : "Collapse scope panel" ) ;
287+ scopeResizerElement . hidden = ! showResizer ;
288+ scopeResizerElement . setAttribute ( "aria-hidden" , showResizer ? "false" : "true" ) ;
289+ scopeResizerElement . setAttribute ( "aria-valuemin" , String ( MIN_SCOPE_WIDTH ) ) ;
290+ scopeResizerElement . setAttribute ( "aria-valuemax" , String ( MAX_SCOPE_WIDTH ) ) ;
291+ scopeResizerElement . setAttribute ( "aria-valuenow" , String ( state . scopeWidth ) ) ;
252292}
253293
254294function renderEditorChrome ( ) {
@@ -270,6 +310,37 @@ function toggleScopePanel() {
270310 renderScopeChrome ( ) ;
271311}
272312
313+ function beginScopeResize ( event ) {
314+ if ( ! isDesktopScopeLayout ( ) || state . scopeCollapsed ) {
315+ return ;
316+ }
317+
318+ event . preventDefault ( ) ;
319+ const startX = event . clientX ;
320+ const startWidth = state . scopeWidth ;
321+ document . body . classList . add ( "is-resizing-scope" ) ;
322+
323+ function handlePointerMove ( moveEvent ) {
324+ const nextWidth = clampScopeWidth ( startWidth + ( startX - moveEvent . clientX ) ) ;
325+ if ( nextWidth !== state . scopeWidth ) {
326+ state . scopeWidth = nextWidth ;
327+ renderScopeChrome ( ) ;
328+ }
329+ }
330+
331+ function stopResize ( ) {
332+ document . body . classList . remove ( "is-resizing-scope" ) ;
333+ window . removeEventListener ( "pointermove" , handlePointerMove ) ;
334+ window . removeEventListener ( "pointerup" , stopResize ) ;
335+ window . removeEventListener ( "pointercancel" , stopResize ) ;
336+ persistScopeWidth ( ) ;
337+ }
338+
339+ window . addEventListener ( "pointermove" , handlePointerMove ) ;
340+ window . addEventListener ( "pointerup" , stopResize ) ;
341+ window . addEventListener ( "pointercancel" , stopResize ) ;
342+ }
343+
273344function toggleGroup ( name ) {
274345 if ( state . collapsedGroups . has ( name ) ) {
275346 state . collapsedGroups . delete ( name ) ;
@@ -414,7 +485,8 @@ function renderBoard() {
414485}
415486
416487function renderScope ( ) {
417- scopeContentElement . textContent = state . workspace ?. scopeText ?? "" ;
488+ const scopeHtml = renderMarkdownToHtml ( state . workspace ?. scopeText ?? "" ) ;
489+ scopeContentElement . innerHTML = scopeHtml || '<p class="muted">No scope notes yet.</p>' ;
418490}
419491
420492function setDirtyState ( kind , value ) {
@@ -548,19 +620,14 @@ function renderPreview() {
548620 </section>
549621 ` )
550622 . join ( "" ) ;
623+ const previewBadges = [ metadata . status , metadata . priority , metadata . commitment , metadata . milestone ]
624+ . filter ( Boolean )
625+ . map ( ( value ) => `<span class="badge">${ escapeHtml ( value ) } </span>` )
626+ . join ( "" ) ;
551627
552628 previewElement . className = "preview-surface" ;
553629 previewElement . innerHTML = `
554- <header class="preview-header">
555- <div>
556- <p class="eyebrow preview-eyebrow">Item preview</p>
557- <h2>${ escapeHtml ( metadata . title || state . currentItem . metadata . title || state . currentItem . id ) } </h2>
558- <p class="muted">${ escapeHtml ( state . currentItem . filePath ) } </p>
559- </div>
560- <div class="preview-meta">
561- ${ [ metadata . status , metadata . priority , metadata . commitment , metadata . milestone ] . filter ( Boolean ) . map ( ( value ) => `<span class="badge">${ escapeHtml ( value ) } </span>` ) . join ( "" ) }
562- </div>
563- </header>
630+ ${ previewBadges ? `<div class="preview-meta">${ previewBadges } </div>` : "" }
564631 <div class="preview-body">${ sectionHtml } </div>
565632 ` ;
566633}
@@ -697,6 +764,15 @@ function applyEditorMode() {
697764 }
698765}
699766
767+ desktopScopeLayoutMedia . addEventListener ( "change" , ( ) => {
768+ renderScopeChrome ( ) ;
769+ } ) ;
770+
771+ stackedLayoutMedia . addEventListener ( "change" , ( ) => {
772+ renderScopeChrome ( ) ;
773+ syncMobileNavigation ( ) ;
774+ } ) ;
775+
700776function switchEditorMode ( nextMode ) {
701777 if ( nextMode === state . editorMode ) {
702778 return ;
@@ -762,6 +838,8 @@ scopeToggleButton.addEventListener("click", () => {
762838 toggleScopePanel ( ) ;
763839} ) ;
764840
841+ scopeResizerElement . addEventListener ( "pointerdown" , beginScopeResize ) ;
842+
765843jumpToBoardButton . addEventListener ( "click" , ( ) => {
766844 scrollPanelIntoView ( boardPanelElement ) ;
767845} ) ;
@@ -793,6 +871,15 @@ for (const button of modeButtons) {
793871 } ) ;
794872}
795873
874+ desktopScopeLayoutMedia . addEventListener ( "change" , ( ) => {
875+ renderScopeChrome ( ) ;
876+ } ) ;
877+
878+ stackedLayoutMedia . addEventListener ( "change" , ( ) => {
879+ renderScopeChrome ( ) ;
880+ syncMobileNavigation ( ) ;
881+ } ) ;
882+
796883resetEditor ( ) ;
797884renderScopeChrome ( ) ;
798885applyEditorMode ( ) ;
0 commit comments