Skip to content

Commit 92861b0

Browse files
committed
Sync minimap UI improvements
1 parent c66d4cd commit 92861b0

3 files changed

Lines changed: 199 additions & 19 deletions

File tree

tools/minimap/ui/app.js

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
const FIXED_SECTIONS = ["Summary", "Why", "In Scope", "Out of Scope", "Done When", "Notes"];
22
const 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

48
const 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");
1722
const boardGroupsElement = document.querySelector("#board-groups");
1823
const scopePanelElement = document.querySelector("#scope-panel");
1924
const scopeContentElement = document.querySelector("#scope-content");
25+
const scopeResizerElement = document.querySelector("#scope-resizer");
2026
const scopeToggleButton = document.querySelector("#scope-toggle");
2127
const jumpToBoardButton = document.querySelector("#jump-to-board");
2228
const jumpToEditorButton = document.querySelector("#jump-to-editor");
@@ -37,6 +43,7 @@ const extraSectionsElement = document.querySelector("#extra-sections");
3743
const modeButtons = Array.from(document.querySelectorAll("[data-editor-mode]"));
3844
const modePanes = Array.from(document.querySelectorAll("[data-mode-pane]"));
3945
const stackedLayoutMedia = window.matchMedia("(max-width: 980px)");
46+
const desktopScopeLayoutMedia = window.matchMedia("(min-width: 1321px)");
4047

4148
const 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+
72100
function 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+
236268
function scrollPanelIntoView(element) {
237269
element?.scrollIntoView({ behavior: "smooth", block: "start" });
238270
}
@@ -244,11 +276,19 @@ function syncMobileNavigation() {
244276
}
245277

246278
function 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

254294
function 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+
273344
function toggleGroup(name) {
274345
if (state.collapsedGroups.has(name)) {
275346
state.collapsedGroups.delete(name);
@@ -414,7 +485,8 @@ function renderBoard() {
414485
}
415486

416487
function 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

420492
function 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+
700776
function 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+
765843
jumpToBoardButton.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+
796883
resetEditor();
797884
renderScopeChrome();
798885
applyEditorMode();

tools/minimap/ui/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ <h3>Raw Markdown</h3>
164164
</div>
165165
</section>
166166

167+
<div class="scope-resizer" id="scope-resizer" role="separator" aria-orientation="vertical" aria-label="Resize scope panel"></div>
168+
167169
<section class="panel scope-panel" id="scope-panel">
168170
<div class="panel-header">
169171
<div class="panel-header-main">

tools/minimap/ui/styles.css

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
--shadow-soft: 0 10px 22px rgba(29, 49, 89, 0.07);
1616
--radius: 24px;
1717
--radius-small: 18px;
18+
--scope-width: 272px;
19+
--scope-width-collapsed: 92px;
20+
--divider-width: 8px;
1821
--font-display: "Aptos Display", "Segoe UI Variable Display", "Inter", "Segoe UI", sans-serif;
1922
--font-body: "Aptos", "Segoe UI Variable Text", "Inter", "Segoe UI", sans-serif;
2023
}
@@ -49,6 +52,11 @@ button {
4952
outline: none;
5053
}
5154

55+
body.is-resizing-scope {
56+
cursor: col-resize;
57+
user-select: none;
58+
}
59+
5260
::-webkit-scrollbar {
5361
width: 12px;
5462
height: 12px;
@@ -246,14 +254,15 @@ h3 {
246254

247255
.layout {
248256
display: grid;
249-
grid-template-columns: minmax(290px, 340px) minmax(760px, 1fr) minmax(220px, 272px);
250-
grid-template-areas: "board editor scope";
251-
gap: 14px;
257+
grid-template-columns: minmax(290px, 340px) minmax(520px, 1fr) var(--divider-width) minmax(220px, var(--scope-width));
258+
grid-template-areas: "board editor divider scope";
259+
column-gap: 8px;
260+
row-gap: 14px;
252261
align-items: start;
253262
}
254263

255264
.layout[data-scope-collapsed="true"] {
256-
grid-template-columns: minmax(290px, 340px) minmax(840px, 1fr) 92px;
265+
grid-template-columns: minmax(290px, 340px) minmax(620px, 1fr) 0px var(--scope-width-collapsed);
257266
}
258267

259268
.panel {
@@ -279,6 +288,33 @@ h3 {
279288
grid-area: editor;
280289
}
281290

291+
.scope-resizer {
292+
grid-area: divider;
293+
position: sticky;
294+
top: 18px;
295+
height: calc(100vh - 36px);
296+
align-self: stretch;
297+
border: none;
298+
padding: 0;
299+
background: transparent;
300+
cursor: col-resize;
301+
}
302+
303+
.scope-resizer::before {
304+
content: "";
305+
display: block;
306+
width: 4px;
307+
height: 100%;
308+
margin: 0 auto;
309+
border-radius: 999px;
310+
background: linear-gradient(180deg, rgba(44, 108, 255, 0.08), rgba(44, 108, 255, 0.18), rgba(44, 108, 255, 0.08));
311+
box-shadow: inset 0 0 0 1px rgba(63, 82, 118, 0.08);
312+
}
313+
314+
.scope-resizer[hidden] {
315+
display: none;
316+
}
317+
282318
.scope-panel {
283319
grid-area: scope;
284320
position: sticky;
@@ -508,13 +544,57 @@ h3 {
508544
.scope-content {
509545
margin: 0;
510546
padding: 16px 18px;
511-
white-space: pre-wrap;
512547
overflow-wrap: anywhere;
513548
font-family: var(--font-body);
514-
line-height: 1.56;
549+
line-height: 1.6;
515550
font-size: 0.92rem;
516551
}
517552

553+
.scope-content > :first-child {
554+
margin-top: 0;
555+
}
556+
557+
.scope-content > :last-child {
558+
margin-bottom: 0;
559+
}
560+
561+
.scope-content p,
562+
.scope-content ul,
563+
.scope-content ol,
564+
.scope-content pre,
565+
.scope-content h1,
566+
.scope-content h2,
567+
.scope-content h3,
568+
.scope-content h4 {
569+
margin: 0 0 0.85rem;
570+
}
571+
572+
.scope-content ul,
573+
.scope-content ol {
574+
padding-left: 1.25rem;
575+
}
576+
577+
.scope-content code {
578+
padding: 0.1rem 0.34rem;
579+
border-radius: 999px;
580+
background: rgba(21, 32, 51, 0.08);
581+
font-size: 0.92em;
582+
}
583+
584+
.scope-content pre {
585+
overflow: auto;
586+
padding: 0.86rem 0.92rem;
587+
border-radius: 14px;
588+
background: #142033;
589+
color: #f4f7ff;
590+
}
591+
592+
.scope-content pre code {
593+
padding: 0;
594+
background: transparent;
595+
color: inherit;
596+
}
597+
518598
.scope-panel.scope-collapsed .panel-header {
519599
align-items: stretch;
520600
flex-direction: column;
@@ -535,6 +615,10 @@ h3 {
535615
min-height: auto;
536616
}
537617

618+
.layout[data-scope-collapsed="true"] .scope-resizer {
619+
display: none;
620+
}
621+
538622
.scope-panel.scope-collapsed h2 {
539623
font-size: 1rem;
540624
}
@@ -882,9 +966,12 @@ textarea {
882966
}
883967

884968
@media (max-width: 1380px) {
885-
.layout,
969+
.layout {
970+
grid-template-columns: minmax(260px, 300px) minmax(480px, 1fr) var(--divider-width) minmax(210px, var(--scope-width));
971+
}
972+
886973
.layout[data-scope-collapsed="true"] {
887-
grid-template-columns: minmax(260px, 300px) minmax(0, 1fr) minmax(210px, 236px);
974+
grid-template-columns: minmax(260px, 300px) minmax(560px, 1fr) 0px var(--scope-width-collapsed);
888975
}
889976
}
890977

@@ -897,6 +984,10 @@ textarea {
897984
"scope scope";
898985
}
899986

987+
.scope-resizer {
988+
display: none;
989+
}
990+
900991
.panel {
901992
min-height: auto;
902993
}

0 commit comments

Comments
 (0)