Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,55 @@
overflow: hidden;
}

/* Drag handle that resizes the left sidebar. It sits between the sidebar's
right border and the content area as a narrow invisible strip that lights
up on hover/while dragging. */
#sidebar-resizer {
flex: 0 0 6px;
cursor: col-resize;
background: transparent;
border-left: 1px solid transparent;
transition:
background 0.15s,
border-color 0.15s;
}

#sidebar-resizer:hover,
body.resizing-sidebar #sidebar-resizer {
background: var(--accent-light);
border-left-color: var(--accent);
}

body.resizing-sidebar {
cursor: col-resize;
user-select: none;
}

/* Drag handle that resizes the right outline panel. Mirrors the sidebar
resizer: it sits just left of the outline's border and is hidden whenever
the outline itself is hidden. */
#outline-resizer {
flex: 0 0 6px;
cursor: col-resize;
background: transparent;
border-right: 1px solid transparent;
transition:
background 0.15s,
border-color 0.15s;
display: none;
}

#outline-resizer:hover,
body.resizing-outline #outline-resizer {
background: var(--accent-light);
border-right-color: var(--accent);
}

body.resizing-outline {
cursor: col-resize;
user-select: none;
}

#sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border);
Expand Down Expand Up @@ -1139,7 +1188,8 @@
);
}

html[data-outline="hidden"] #outline {
html[data-outline="hidden"] #outline,
html[data-outline="hidden"] #outline-resizer {
display: none !important;
}

Expand Down Expand Up @@ -1780,6 +1830,8 @@

#sidebar,
#outline,
#sidebar-resizer,
#outline-resizer,
#empty-state,
.copy-btn,
.mermaid-fullscreen-btn,
Expand Down Expand Up @@ -1845,6 +1897,8 @@
/* PDF export mode — applied via JS before createPDF capture */
body.print-mode #sidebar,
body.print-mode #outline,
body.print-mode #sidebar-resizer,
body.print-mode #outline-resizer,
body.print-mode #empty-state,
body.print-mode .copy-btn,
body.print-mode .mermaid-fullscreen-btn {
Expand Down Expand Up @@ -2052,6 +2106,8 @@ <h2 data-i18n="app.name">Markdown Viewer</h2>
<div id="file-list"></div>
</div>

<div id="sidebar-resizer" aria-hidden="true"></div>

<div id="content" class="empty">
<div id="empty-state">
<h2 data-i18n="app.name">Markdown Viewer</h2>
Expand Down Expand Up @@ -2083,6 +2139,8 @@ <h2 data-i18n="app.name">Markdown Viewer</h2>
</div>
</div>

<div id="outline-resizer" aria-hidden="true"></div>

<div id="outline">
<div id="outline-title" data-i18n="outline.title">On this page</div>
<nav id="outline-nav"></nav>
Expand Down
147 changes: 147 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,36 @@ import {
BODY_FONT_KEY,
CODE_FONT_KEY,
CUSTOM_FONT_SIZE_KEY,
DEFAULT_OUTLINE_WIDTH,
DEFAULT_SIDEBAR_WIDTH,
FONTSIZE_KEY,
FONT_WEIGHT_KEY,
MAX_CUSTOM_FONT_SIZE,
MAX_OUTLINE_WIDTH,
MAX_SIDEBAR_WIDTH,
MIN_CUSTOM_FONT_SIZE,
MIN_OUTLINE_WIDTH,
MIN_SIDEBAR_WIDTH,
FINDER_OPEN_KEY,
OUTLINE_PREF_KEY,
OUTLINE_WIDTH_KEY,
SIDEBAR_WIDTH_KEY,
applyBodyFontToDOM,
applyCodeFontToDOM,
applyCustomFontSizeToDOM,
applyFontSizeToDOM,
applyFontWeightToDOM,
applyOutlinePrefToDOM,
applyOutlineWidthToDOM,
applySidebarWidthToDOM,
resolveInitialCustomFontSize,
resolveInitialFontFamily,
resolveInitialFontSize,
resolveInitialFinderOpenPref,
resolveInitialFontWeight,
resolveInitialOutlinePref,
resolveInitialOutlineWidth,
resolveInitialSidebarWidth,
setSegmentActive,
type FinderOpenPref,
type FontSize,
Expand Down Expand Up @@ -158,6 +170,12 @@ async function initTheme(): Promise<void> {
const savedOutline = resolveInitialOutlinePref(
await store.get(OUTLINE_PREF_KEY)
);
const savedSidebarWidth = resolveInitialSidebarWidth(
await store.get(SIDEBAR_WIDTH_KEY)
);
const savedOutlineWidth = resolveInitialOutlineWidth(
await store.get(OUTLINE_WIDTH_KEY)
);

const storeCustoms = sanitizeCustomThemes(
await store.get(CUSTOM_THEMES_KEY)
Expand Down Expand Up @@ -192,6 +210,8 @@ async function initTheme(): Promise<void> {
applyActiveTheme();
applyFontSize(savedFontSize);
applyOutlinePref(savedOutline);
applySidebarWidthToDOM(document.documentElement, savedSidebarWidth);
applyOutlineWidthToDOM(document.documentElement, savedOutlineWidth);
applyBodyFontToDOM(document.documentElement, currentBodyFont);
applyCodeFontToDOM(document.documentElement, currentCodeFont);
applyFontWeightToDOM(document.documentElement, currentFontWeight);
Expand Down Expand Up @@ -432,6 +452,9 @@ const examplesBtn = document.getElementById(
) as HTMLButtonElement;
const outlineEl = document.getElementById("outline") as HTMLDivElement;
const outlineNav = document.getElementById("outline-nav") as HTMLElement;
const outlineResizer = document.getElementById(
"outline-resizer"
) as HTMLDivElement;
const titlebarFilename = document.getElementById(
"titlebar-filename"
) as HTMLSpanElement;
Expand All @@ -457,6 +480,11 @@ const searchOptWholeWord = document.getElementById(
"search-opt-wholeword"
) as HTMLInputElement;

const sidebarEl = document.getElementById("sidebar") as HTMLDivElement;
const sidebarResizer = document.getElementById(
"sidebar-resizer"
) as HTMLDivElement;

const appWindow = getCurrentWindow();

// Keeps the native window title in sync with what the window shows. The title is
Expand Down Expand Up @@ -666,6 +694,121 @@ function initSearch(): void {
document.addEventListener("keydown", handleGlobalSearchShortcut);
}

// --- Sidebar resizing ---

let sidebarDragging = false;
let sidebarDragStartX = 0;
let sidebarDragStartWidth = 0;

function currentSidebarWidth(): number {
return sidebarEl.getBoundingClientRect().width;
}

function setSidebarWidth(px: number): void {
const clamped = Math.min(
MAX_SIDEBAR_WIDTH,
Math.max(MIN_SIDEBAR_WIDTH, px)
);
applySidebarWidthToDOM(document.documentElement, clamped);
}

function initSidebarResizer(): void {
sidebarResizer.addEventListener("pointerdown", (e) => {
e.preventDefault();
// Capture so moves/up keep firing even if the cursor leaves the window.
sidebarResizer.setPointerCapture(e.pointerId);
sidebarDragging = true;
sidebarDragStartX = e.clientX;
sidebarDragStartWidth = currentSidebarWidth();
document.body.classList.add("resizing-sidebar");
});

sidebarResizer.addEventListener("pointermove", (e) => {
if (!sidebarDragging) return;
setSidebarWidth(sidebarDragStartWidth + (e.clientX - sidebarDragStartX));
});

function endSidebarDrag(): void {
if (!sidebarDragging) return;
sidebarDragging = false;
document.body.classList.remove("resizing-sidebar");
}

sidebarResizer.addEventListener("pointerup", async (e) => {
if (!sidebarDragging) return;
sidebarResizer.releasePointerCapture(e.pointerId);
endSidebarDrag();
await savePref(SIDEBAR_WIDTH_KEY, Math.round(currentSidebarWidth()));
});

sidebarResizer.addEventListener("pointercancel", () => {
endSidebarDrag();
});

sidebarResizer.addEventListener("dblclick", async () => {
setSidebarWidth(DEFAULT_SIDEBAR_WIDTH);
await savePref(SIDEBAR_WIDTH_KEY, DEFAULT_SIDEBAR_WIDTH);
});
}

// --- Outline resizing ---

let outlineDragging = false;
let outlineDragStartX = 0;
let outlineDragStartWidth = 0;

function currentOutlineWidth(): number {
return outlineEl.getBoundingClientRect().width;
}

function setOutlineWidth(px: number): void {
const clamped = Math.min(
MAX_OUTLINE_WIDTH,
Math.max(MIN_OUTLINE_WIDTH, px)
);
applyOutlineWidthToDOM(document.documentElement, clamped);
}

function initOutlineResizer(): void {
outlineResizer.addEventListener("pointerdown", (e) => {
e.preventDefault();
// Capture so moves/up keep firing even if the cursor leaves the window.
outlineResizer.setPointerCapture(e.pointerId);
outlineDragging = true;
outlineDragStartX = e.clientX;
outlineDragStartWidth = currentOutlineWidth();
document.body.classList.add("resizing-outline");
});

outlineResizer.addEventListener("pointermove", (e) => {
if (!outlineDragging) return;
// The handle sits on the outline's left edge: dragging right shrinks it.
setOutlineWidth(outlineDragStartWidth - (e.clientX - outlineDragStartX));
});

function endOutlineDrag(): void {
if (!outlineDragging) return;
outlineDragging = false;
document.body.classList.remove("resizing-outline");
}

outlineResizer.addEventListener("pointerup", async (e) => {
if (!outlineDragging) return;
outlineResizer.releasePointerCapture(e.pointerId);
endOutlineDrag();
await savePref(OUTLINE_WIDTH_KEY, Math.round(currentOutlineWidth()));
});

outlineResizer.addEventListener("pointercancel", () => {
endOutlineDrag();
});

outlineResizer.addEventListener("dblclick", async () => {
setOutlineWidth(DEFAULT_OUTLINE_WIDTH);
await savePref(OUTLINE_WIDTH_KEY, DEFAULT_OUTLINE_WIDTH);
});
}

// --- Preferences modal ---

const prefsBackdrop = document.getElementById(
Expand Down Expand Up @@ -1519,6 +1662,8 @@ async function init(): Promise<void> {
await initTheme();
initSearch();
initPreferences();
initSidebarResizer();
initOutlineResizer();

// Runtime opens (hot-start file association, "Open With", CLI events)
appWindow.listen<string>("open-folder", (event) => {
Expand Down Expand Up @@ -1922,10 +2067,12 @@ function buildOutline(): void {

if (headings.length === 0) {
outlineEl.style.display = "none";
outlineResizer.style.display = "none";
return;
}

outlineEl.style.display = "block";
outlineResizer.style.display = "block";
const items: { el: HTMLAnchorElement; heading: Element }[] = [];

for (const h of headings) {
Expand Down
Loading