diff --git a/desktop-app/resources/js/preview-worker.js b/desktop-app/resources/js/preview-worker.js
index 989b3566..f9e2f6d1 100644
--- a/desktop-app/resources/js/preview-worker.js
+++ b/desktop-app/resources/js/preview-worker.js
@@ -566,7 +566,7 @@ function splitMarkdownBlocks(markdown) {
}
if (!inFence && !inMathBlock && trimmed === "") {
- flush(lineNumber);
+ flush(lineNumber - 1);
startLine = lineNumber + 1;
continue;
}
diff --git a/index.html b/index.html
index c8a6bd5b..f3b2cd8c 100644
--- a/index.html
+++ b/index.html
@@ -542,6 +542,9 @@
0 of 0 matches
+
diff --git a/preview-worker.js b/preview-worker.js
index 989b3566..f9e2f6d1 100644
--- a/preview-worker.js
+++ b/preview-worker.js
@@ -566,7 +566,7 @@ function splitMarkdownBlocks(markdown) {
}
if (!inFence && !inMathBlock && trimmed === "") {
- flush(lineNumber);
+ flush(lineNumber - 1);
startLine = lineNumber + 1;
continue;
}
diff --git a/script.js b/script.js
index e692ed31..6916c73a 100644
--- a/script.js
+++ b/script.js
@@ -269,6 +269,9 @@ document.addEventListener("DOMContentLoaded", async function () {
let findMatches = [];
let activeFindIndex = -1;
let lastFindQuery = '';
+ let isSectionPickMode = false;
+ let sectionLocateMessageTimer = null;
+ let sectionPickEditorMouseDown = null;
// Custom Editor History State Manager variables
const tabHistories = {};
@@ -459,6 +462,7 @@ document.addEventListener("DOMContentLoaded", async function () {
const findReplaceModal = document.getElementById("find-replace-modal");
const findReplaceInput = document.getElementById("find-replace-input");
const findReplaceWith = document.getElementById("find-replace-with");
+ const findSectionButton = document.getElementById("find-section");
const findReplaceCount = document.getElementById("find-replace-count");
const findReplacePrev = document.getElementById("find-prev");
const findReplaceNext = document.getElementById("find-next");
@@ -855,8 +859,13 @@ document.addEventListener("DOMContentLoaded", async function () {
previewSegmentHtmlCache.set(cacheKey, sanitizedBlock);
}
const blockId = block.id || `preview-block-${index}`;
+ const sourceStart = Number(block.startLine) || 0;
+ const sourceEnd = Number(block.endLine) || sourceStart;
+ const sourceAttrs = sourceStart > 0
+ ? ` data-source-line="${sourceStart}" data-source-start="${sourceStart}" data-source-end="${Math.max(sourceStart, sourceEnd)}"`
+ : '';
htmlParts.push(
- ``
+ ``
);
});
@@ -2896,6 +2905,9 @@ document.addEventListener("DOMContentLoaded", async function () {
function switchTab(tabId) {
if (tabId === activeTabId) return;
+ if (isSectionPickMode) {
+ setSectionPickMode(false);
+ }
saveCurrentTabState();
// Clear typing timeout and reset tracking for the new tab
@@ -2942,6 +2954,9 @@ document.addEventListener("DOMContentLoaded", async function () {
}
function closeTab(tabId) {
+ if (isSectionPickMode) {
+ setSectionPickMode(false);
+ }
if (liveCollaboration && liveCollaboration.tabId === tabId) {
if (liveCollaboration.isHost) {
endLiveSessionForEveryone();
@@ -4845,6 +4860,7 @@ ${selector} .arrowheadPath {
previewSegmentHtmlCache.clear();
const patchResult = commitPreviewHtml(sanitizedHtml, referenceData, rawVal, context);
+ applySourceRangesToRenderedPreview(rawVal);
postProcessPreview(rawVal, context, patchResult);
}
@@ -5686,6 +5702,9 @@ ${selector} .arrowheadPath {
announceToScreenReader(getEditorReadOnlyMessage());
}
if (mode === currentViewMode) return;
+ if (isSectionPickMode) {
+ setSectionPickMode(false);
+ }
const previousMode = currentViewMode;
currentViewMode = mode;
@@ -9265,6 +9284,375 @@ ${selector} .arrowheadPath {
syncEditorScrollOverlays();
}
+ function setSectionPickMode(active, message) {
+ isSectionPickMode = Boolean(active);
+ if (findSectionButton) {
+ findSectionButton.classList.toggle('active', isSectionPickMode);
+ findSectionButton.setAttribute('aria-pressed', isSectionPickMode ? 'true' : 'false');
+ }
+ document.body.classList.toggle('section-pick-active', isSectionPickMode);
+ if (isSectionPickMode) {
+ announceToScreenReader('Find Section active. Click content in the editor or preview.');
+ } else if (message !== false) {
+ announceToScreenReader(message || 'Find Section canceled.');
+ }
+ }
+
+ function showSectionLocateMessage(message) {
+ announceToScreenReader(message);
+ const oldToast = document.querySelector('.section-locate-toast');
+ if (oldToast) oldToast.remove();
+ const toast = document.createElement('div');
+ toast.className = 'section-locate-toast';
+ toast.setAttribute('role', 'status');
+ toast.textContent = message;
+ document.body.appendChild(toast);
+ if (sectionLocateMessageTimer) clearTimeout(sectionLocateMessageTimer);
+ sectionLocateMessageTimer = setTimeout(function() {
+ sectionLocateMessageTimer = null;
+ toast.remove();
+ }, 2400);
+ }
+
+ function getLineNumberForIndex(index) {
+ return countLinesBeforeIndex(markdownEditor.value || '', index) + 1;
+ }
+
+ function splitMarkdownSourceBlocksForLocate(markdown) {
+ const lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n');
+ const blocks = [];
+ let buffer = [];
+ let startLine = 1;
+ let inFence = false;
+ let fenceChar = '';
+ let fenceLength = 0;
+ let inMathBlock = false;
+
+ function flush(endLine) {
+ const source = buffer.join('\n').trimEnd();
+ if (source.trim()) {
+ blocks.push({ source, startLine, endLine });
+ }
+ buffer = [];
+ }
+
+ for (let index = 0; index < lines.length; index += 1) {
+ const line = lines[index];
+ const lineNumber = index + 1;
+ const trimmed = line.trim();
+ const fenceMatch = /^ {0,3}(`{3,}|~{3,})/.exec(line);
+
+ if (fenceMatch) {
+ const marker = fenceMatch[1];
+ if (!inFence) {
+ inFence = true;
+ fenceChar = marker[0];
+ fenceLength = marker.length;
+ } else if (marker[0] === fenceChar && marker.length >= fenceLength) {
+ inFence = false;
+ }
+ }
+
+ if (!inFence && trimmed === '$$') {
+ inMathBlock = !inMathBlock;
+ }
+
+ if (!inFence && !inMathBlock && trimmed === '') {
+ flush(lineNumber - 1);
+ startLine = lineNumber + 1;
+ continue;
+ }
+
+ if (buffer.length === 0) startLine = lineNumber;
+ buffer.push(line);
+ }
+
+ flush(lines.length);
+ return blocks;
+ }
+
+ function countRenderedTopLevelElementsForSource(source) {
+ try {
+ const template = document.createElement('template');
+ template.innerHTML = marked.parse(source || '');
+ const count = Array.from(template.content.childNodes).filter(function(node) {
+ return node.nodeType === Node.ELEMENT_NODE || (node.nodeType === Node.TEXT_NODE && node.textContent.trim());
+ }).length;
+ return Math.max(1, count);
+ } catch (error) {
+ return 1;
+ }
+ }
+
+ function applySourceRangesToRenderedPreview(markdown) {
+ if (!markdownPreview) return;
+ const topLevelElements = Array.from(markdownPreview.children).filter(function(element) {
+ return element.nodeType === Node.ELEMENT_NODE && element.id !== 'markdown-preview-skeleton';
+ });
+ if (!topLevelElements.length) return;
+
+ const blocks = splitMarkdownSourceBlocksForLocate(markdown);
+ let elementIndex = 0;
+ blocks.forEach(function(block) {
+ const renderedCount = countRenderedTopLevelElementsForSource(block.source);
+ for (let count = 0; count < renderedCount && elementIndex < topLevelElements.length; count += 1) {
+ const element = topLevelElements[elementIndex];
+ element.setAttribute('data-source-line', String(block.startLine));
+ element.setAttribute('data-source-start', String(block.startLine));
+ element.setAttribute('data-source-end', String(block.endLine));
+ elementIndex += 1;
+ }
+ });
+ }
+
+ function getMarkdownSourceBlockAroundLine(lineNumber) {
+ const lines = (markdownEditor.value || '').replace(/\r\n/g, '\n').split('\n');
+ const targetLine = Math.max(1, Math.min(lines.length, lineNumber));
+ if (!lines[targetLine - 1] || !lines[targetLine - 1].trim()) return null;
+ return splitMarkdownSourceBlocksForLocate(markdownEditor.value || '').find(function(block) {
+ return targetLine >= block.startLine && targetLine <= block.endLine;
+ }) || null;
+ }
+
+ function getEditorSourceRangeFromMouseEvent(event) {
+ if (!event || !markdownEditor) return null;
+ if (!isGeometryInitialized || cachedEditorWidth !== markdownEditor.clientWidth) {
+ refreshEditorWidth();
+ }
+ const rect = markdownEditor.getBoundingClientRect();
+ const styles = window.getComputedStyle(markdownEditor);
+ const paddingTop = parseFloat(styles.paddingTop) || 10;
+ const targetY = event.clientY - rect.top - paddingTop + markdownEditor.scrollTop;
+ if (!Number.isFinite(targetY) || targetY < 0) return null;
+
+ const lines = (markdownEditor.value || '').split('\n');
+ let offset = 0;
+ for (let index = 0; index < lines.length; index += 1) {
+ const lineHeight = getWrappedLineCountMonospace(lines[index] || '', cachedMaxCharsPerLine) * cachedLineHeight;
+ if (targetY <= offset + lineHeight) {
+ return getMarkdownSourceBlockAroundLine(index + 1);
+ }
+ offset += lineHeight;
+ }
+ return null;
+ }
+
+ function getPickedEditorSourceRange(event, preferSelection) {
+ if (!preferSelection) {
+ return getEditorSourceRangeFromMouseEvent(event) || getMarkdownSourceBlockAroundLine(getLineNumberForIndex(markdownEditor.selectionStart || 0));
+ }
+
+ const selectionStart = markdownEditor.selectionStart || 0;
+ const selectionEnd = markdownEditor.selectionEnd || selectionStart;
+ const selectedText = selectionEnd > selectionStart ? markdownEditor.value.slice(selectionStart, selectionEnd) : '';
+ if (!selectedText.trim()) {
+ return getEditorSourceRangeFromMouseEvent(event) || getMarkdownSourceBlockAroundLine(getLineNumberForIndex(selectionStart));
+ }
+ const startLine = getLineNumberForIndex(selectionStart);
+ const effectiveEnd = selectionEnd > selectionStart && markdownEditor.value[selectionEnd - 1] === '\n'
+ ? selectionEnd - 1
+ : selectionEnd;
+ const endLine = getLineNumberForIndex(Math.max(selectionStart, effectiveEnd));
+ const lines = (markdownEditor.value || '').split('\n');
+ let targetLine = startLine;
+ for (let line = startLine; line <= endLine; line += 1) {
+ if ((lines[line - 1] || '').trim()) {
+ targetLine = line;
+ break;
+ }
+ }
+ return getMarkdownSourceBlockAroundLine(targetLine);
+ }
+
+ function getMappedPreviewBlocks() {
+ if (!markdownPreview) return [];
+ return Array.from(markdownPreview.querySelectorAll('[data-source-start], [data-source-line]'))
+ .map(function(element) {
+ const start = parseInt(element.getAttribute('data-source-start') || element.getAttribute('data-source-line'), 10);
+ const end = parseInt(element.getAttribute('data-source-end') || element.getAttribute('data-source-line'), 10);
+ return {
+ element,
+ startLine: Number.isFinite(start) ? start : null,
+ endLine: Number.isFinite(end) ? end : start,
+ };
+ })
+ .filter(function(item) {
+ return item.startLine !== null;
+ });
+ }
+
+ function findPreviewBlockForSourceRange(sourceRange) {
+ if (!sourceRange || !markdownPreview) return null;
+ const mappedBlocks = getMappedPreviewBlocks();
+ let nearest = null;
+ let nearestDistance = Infinity;
+ for (const block of mappedBlocks) {
+ const overlaps = block.startLine <= sourceRange.endLine && block.endLine >= sourceRange.startLine;
+ if (overlaps) return block.element;
+ const distance = Math.min(
+ Math.abs(block.startLine - sourceRange.startLine),
+ Math.abs(block.endLine - sourceRange.endLine)
+ );
+ if (distance < nearestDistance) {
+ nearest = block.element;
+ nearestDistance = distance;
+ }
+ }
+ if (nearest) return nearest;
+ return findPreviewElementBySourceRatio(sourceRange.startLine);
+ }
+
+ function findPreviewElementBySourceRatio(lineNumber) {
+ if (!markdownPreview || !previewPane) return null;
+ const children = Array.from(markdownPreview.children).filter(function(child) {
+ return child.nodeType === Node.ELEMENT_NODE && child.offsetHeight > 0;
+ });
+ if (!children.length) return null;
+ const totalLines = countLinesFast(markdownEditor.value || '');
+ const ratio = totalLines > 1 ? (Math.max(1, lineNumber) - 1) / (totalLines - 1) : 0;
+ const targetTop = ratio * Math.max(0, markdownPreview.scrollHeight - previewPane.clientHeight);
+ return children.reduce(function(best, child) {
+ const distance = Math.abs(child.offsetTop - targetTop);
+ if (!best || distance < best.distance) {
+ return { element: child, distance };
+ }
+ return best;
+ }, null)?.element || null;
+ }
+
+ function getPreviewSourceRangeFromTarget(target) {
+ if (!target || !markdownPreview) return null;
+ const mapped = target.closest('[data-source-start], [data-source-line]');
+ if (mapped && markdownPreview.contains(mapped)) {
+ const start = parseInt(mapped.getAttribute('data-source-start') || mapped.getAttribute('data-source-line'), 10);
+ const end = parseInt(mapped.getAttribute('data-source-end') || mapped.getAttribute('data-source-line'), 10);
+ if (Number.isFinite(start)) {
+ return {
+ element: mapped,
+ sourceRange: {
+ startLine: start,
+ endLine: Number.isFinite(end) ? end : start,
+ },
+ };
+ }
+ }
+
+ const previewBlock = target.closest('.preview-render-block') || target.closest('.markdown-body > *');
+ if (previewBlock && previewPane) {
+ const ratio = previewPane.scrollHeight > previewPane.clientHeight
+ ? Math.max(0, Math.min(1, previewBlock.offsetTop / Math.max(1, previewPane.scrollHeight - previewPane.clientHeight)))
+ : 0;
+ const approximateLine = Math.max(1, Math.round(ratio * Math.max(1, countLinesFast(markdownEditor.value || ''))));
+ const sourceRange = getMarkdownSourceBlockAroundLine(approximateLine) || { startLine: approximateLine, endLine: approximateLine };
+ return { element: previewBlock, sourceRange };
+ }
+
+ return null;
+ }
+
+ function scrollPreviewSectionIntoView(element) {
+ if (!element || !previewPane) return;
+ isProgrammaticScrolling = true;
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ if (window.programmaticScrollTimeout) clearTimeout(window.programmaticScrollTimeout);
+ window.programmaticScrollTimeout = setTimeout(function() {
+ isProgrammaticScrolling = false;
+ }, 900);
+ }
+
+ function highlightPreviewSection(element) {
+ if (!element) return;
+ element.classList.remove('section-locate-preview-highlight');
+ void element.offsetWidth;
+ element.classList.add('section-locate-preview-highlight');
+ setTimeout(function() {
+ element.classList.remove('section-locate-preview-highlight');
+ }, 2300);
+ }
+
+ function getEditorLineBlockMetrics(sourceRange) {
+ if (!sourceRange) return null;
+ if (!isGeometryInitialized || cachedEditorWidth !== markdownEditor.clientWidth) {
+ refreshEditorWidth();
+ }
+ const lines = (markdownEditor.value || '').split('\n');
+ const startLine = Math.max(1, Math.min(lines.length, sourceRange.startLine));
+ const endLine = Math.max(startLine, Math.min(lines.length, sourceRange.endLine));
+ const styles = window.getComputedStyle(markdownEditor);
+ const paddingTop = parseFloat(styles.paddingTop) || 10;
+ let top = paddingTop;
+ for (let index = 0; index < startLine - 1; index += 1) {
+ top += getWrappedLineCountMonospace(lines[index] || '', cachedMaxCharsPerLine) * cachedLineHeight;
+ }
+ let height = 0;
+ for (let index = startLine - 1; index < endLine; index += 1) {
+ height += getWrappedLineCountMonospace(lines[index] || '', cachedMaxCharsPerLine) * cachedLineHeight;
+ }
+ return { top, height: Math.max(cachedLineHeight, height) };
+ }
+
+ function scrollEditorSectionIntoView(sourceRange) {
+ const metrics = getEditorLineBlockMetrics(sourceRange);
+ if (!metrics) return;
+ isProgrammaticScrolling = true;
+ markdownEditor.scrollTo({
+ top: clampEditorScrollTop(metrics.top - (markdownEditor.clientHeight / 2) + (metrics.height / 2)),
+ behavior: 'smooth',
+ });
+ setTimeout(function() {
+ syncEditorScrollOverlays();
+ isProgrammaticScrolling = false;
+ }, 700);
+ }
+
+ function highlightEditorSourceRange(sourceRange) {
+ if (!sourceRange || !editorPaneElement) return;
+ const metrics = getEditorLineBlockMetrics(sourceRange);
+ if (!metrics) return;
+ const styles = window.getComputedStyle(markdownEditor);
+ const paddingLeft = parseFloat(styles.paddingLeft) || cachedPaddingLeft || 10;
+ const highlight = document.createElement('div');
+ highlight.className = 'section-locate-editor-highlight';
+ highlight.style.top = `${markdownEditor.offsetTop + metrics.top - markdownEditor.scrollTop}px`;
+ highlight.style.left = `${markdownEditor.offsetLeft + paddingLeft}px`;
+ highlight.style.height = `${metrics.height}px`;
+ editorPaneElement.appendChild(highlight);
+ setTimeout(function() {
+ highlight.remove();
+ }, 2300);
+ }
+
+ function locateSectionFromEditorPick(event, preferSelection) {
+ const sourceRange = getPickedEditorSourceRange(event, preferSelection);
+ if (!sourceRange) {
+ showSectionLocateMessage('Matching section not found.');
+ return false;
+ }
+ const previewBlock = findPreviewBlockForSourceRange(sourceRange);
+ if (!previewBlock) {
+ showSectionLocateMessage('Matching section not found.');
+ return false;
+ }
+ scrollPreviewSectionIntoView(previewBlock);
+ highlightPreviewSection(previewBlock);
+ setSectionPickMode(false, 'Matching section located.');
+ return true;
+ }
+
+ function locateSectionFromPreviewPick(target) {
+ const match = getPreviewSourceRangeFromTarget(target);
+ if (!match || !match.sourceRange) {
+ showSectionLocateMessage('Matching section not found.');
+ return false;
+ }
+ scrollEditorSectionIntoView(match.sourceRange);
+ setTimeout(function() {
+ highlightEditorSourceRange(match.sourceRange);
+ }, 450);
+ setSectionPickMode(false, 'Matching section located.');
+ return true;
+ }
+
// Class encapsulating Search & Replace Engine
class FindReplaceEngine {
constructor(editor) {
@@ -9816,6 +10204,9 @@ ${selector} .arrowheadPath {
}
function closeFindReplaceModal() {
+ if (isSectionPickMode) {
+ setSectionPickMode(false);
+ }
isFindModalOpen = false;
const panel = document.getElementById('find-replace-modal');
const contentCont = document.querySelector('.content-container');
@@ -10100,6 +10491,12 @@ ${selector} .arrowheadPath {
if (currentBtn) currentBtn.addEventListener('click', replaceCurrentMatch);
if (allBtn) allBtn.addEventListener('click', replaceAllMatches);
+ if (findSectionButton) {
+ findSectionButton.addEventListener('click', function() {
+ setSectionPickMode(!isSectionPickMode);
+ });
+ }
+
// Close buttons
const closeBtn = document.getElementById('find-replace-close');
const closeIcon = document.getElementById('find-replace-close-icon');
@@ -10552,7 +10949,41 @@ ${selector} .arrowheadPath {
markdownEditor.addEventListener('keydown', updateLastCursor);
markdownEditor.addEventListener('keyup', updateLastCursor);
markdownEditor.addEventListener('mousedown', updateLastCursor);
+ markdownEditor.addEventListener('mousedown', function(event) {
+ if (!isSectionPickMode) {
+ sectionPickEditorMouseDown = null;
+ return;
+ }
+ sectionPickEditorMouseDown = {
+ clientX: event.clientX,
+ clientY: event.clientY,
+ selectionStart: markdownEditor.selectionStart || 0,
+ selectionEnd: markdownEditor.selectionEnd || 0,
+ };
+ });
markdownEditor.addEventListener('mouseup', updateLastCursor);
+ markdownEditor.addEventListener('mouseup', function(event) {
+ if (!isSectionPickMode) return;
+ event.preventDefault();
+ event.stopPropagation();
+ const start = sectionPickEditorMouseDown;
+ sectionPickEditorMouseDown = null;
+ const moved = start
+ ? Math.abs(event.clientX - start.clientX) > 4 || Math.abs(event.clientY - start.clientY) > 4
+ : false;
+ const selectionStart = markdownEditor.selectionStart || 0;
+ const selectionEnd = markdownEditor.selectionEnd || selectionStart;
+ const hasSelection = selectionEnd > selectionStart && (markdownEditor.value || '').slice(selectionStart, selectionEnd).trim().length > 0;
+ const selectionChanged = Boolean(start && (selectionStart !== start.selectionStart || selectionEnd !== start.selectionEnd));
+ const preferSelection = hasSelection && (moved || selectionChanged);
+ const pickEvent = {
+ clientX: event.clientX,
+ clientY: event.clientY,
+ };
+ setTimeout(function() {
+ locateSectionFromEditorPick(pickEvent, preferSelection);
+ }, 0);
+ });
markdownEditor.addEventListener('focus', updateLastCursor);
markdownEditor.addEventListener('beforeinput', function(e) {
if (!canMutateEditor()) {
@@ -15094,6 +15525,12 @@ ${selector} .arrowheadPath {
return;
}
+ if (e.key === 'Escape' && isSectionPickMode) {
+ e.preventDefault();
+ setSectionPickMode(false);
+ return;
+ }
+
// Global Escape dismissal for find-replace panel
if (e.key === 'Escape' && isFindModalOpen) {
e.preventDefault();
@@ -17462,6 +17899,13 @@ ${selector} .arrowheadPath {
// Intercept all link clicks in the preview pane to open them securely and prevent page navigation
if (markdownPreview) {
+ markdownPreview.addEventListener('click', function(e) {
+ if (!isSectionPickMode) return;
+ e.preventDefault();
+ e.stopPropagation();
+ locateSectionFromPreviewPick(e.target);
+ }, true);
+
markdownPreview.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (link) {
diff --git a/styles.css b/styles.css
index 488e43c9..79004ed4 100644
--- a/styles.css
+++ b/styles.css
@@ -31,6 +31,8 @@
--fr-btn-active: #0969da;
--fr-btn-active-bg: #ddf4ff;
--fr-match-highlight: #ffdf5d;
+ --section-locate-highlight: rgba(255, 223, 93, 0.48);
+ --section-locate-outline: rgba(9, 105, 218, 0.9);
--fr-match-active: #ff9b30;
--fr-match-text-color: #24292e;
--fr-match-active-text-color: #24292e;
@@ -71,6 +73,8 @@
--fr-btn-active: #2f81f7;
--fr-btn-active-bg: rgba(56, 139, 253, 0.15);
--fr-match-highlight: rgba(187, 128, 9, 0.4);
+ --section-locate-highlight: rgba(187, 128, 9, 0.45);
+ --section-locate-outline: rgba(88, 166, 255, 0.95);
--fr-match-active: #ad6200;
--fr-match-text-color: #c9d1d9;
--fr-match-active-text-color: #ffffff;
@@ -733,6 +737,45 @@ body {
outline-offset: -1px;
}
+.section-locate-editor-highlight {
+ position: absolute;
+ right: 12px;
+ border-radius: 4px;
+ background-color: var(--section-locate-highlight);
+ outline: 2px solid var(--section-locate-outline);
+ outline-offset: -1px;
+ pointer-events: none;
+ z-index: 2;
+ animation: section-locate-fade 2.2s ease-out forwards;
+}
+
+.section-locate-preview-highlight {
+ border-radius: 4px;
+ box-shadow: 0 0 0 2px var(--section-locate-outline), 0 0 0 9999px rgba(255, 223, 93, 0.08) inset;
+ background-color: var(--section-locate-highlight);
+ animation: section-locate-fade 2.2s ease-out forwards;
+}
+
+.section-pick-active #markdown-editor,
+.section-pick-active .preview-pane [data-source-start],
+.section-pick-active .preview-pane [data-source-line],
+.section-pick-active .preview-pane .preview-render-block,
+.section-pick-active .preview-pane .markdown-body > * {
+ cursor: crosshair;
+}
+
+@keyframes section-locate-fade {
+ 0% {
+ opacity: 1;
+ }
+ 70% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
/* Dropdown improvements */
.dropdown-menu {
background-color: var(--bg-color);
@@ -4280,6 +4323,65 @@ html[lang="ko"] .markdown-body h3 {
cursor: not-allowed;
}
+.find-section-btn {
+ background: none;
+ border: 1px solid var(--fr-border);
+ color: var(--text-color);
+ font-size: 12px;
+ cursor: pointer;
+ padding: 2px 8px;
+ border-radius: 4px;
+ transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ white-space: nowrap;
+}
+
+.find-section-btn:hover {
+ background-color: var(--button-hover);
+}
+
+.find-section-btn.active {
+ color: var(--fr-btn-active);
+ border-color: var(--accent-color);
+ background-color: var(--fr-btn-active-bg);
+}
+
+.section-locate-toast {
+ position: fixed;
+ left: 50%;
+ bottom: 24px;
+ transform: translateX(-50%);
+ z-index: 1100;
+ padding: 8px 12px;
+ border-radius: 6px;
+ border: 1px solid var(--fr-border);
+ background-color: var(--fr-bg);
+ color: var(--text-color);
+ box-shadow: var(--fr-shadow);
+ font-size: 13px;
+ pointer-events: none;
+ animation: section-locate-toast-fade 2.4s ease-out forwards;
+}
+
+@keyframes section-locate-toast-fade {
+ 0% {
+ opacity: 0;
+ transform: translate(-50%, 6px);
+ }
+ 12% {
+ opacity: 1;
+ transform: translate(-50%, 0);
+ }
+ 78% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
.find-drawer-toggle-row {
border-top: 1px solid var(--fr-border);
margin-top: 4px;