Drop a .md file anywhere to open it
diff --git a/script.js b/script.js
index 3205a73..3c77fcf 100644
--- a/script.js
+++ b/script.js
@@ -9,6 +9,12 @@ document.addEventListener("DOMContentLoaded", function () {
// View Mode State - Story 1.1
let currentViewMode = 'split'; // 'editor', 'split', or 'preview'
+ let activeModal = null;
+ let lastFocusedElement = null;
+ let isFindModalOpen = false;
+ let findMatches = [];
+ let activeFindIndex = -1;
+ let lastFindQuery = '';
const markdownEditor = document.getElementById("markdown-editor");
const markdownPreview = document.getElementById("markdown-preview");
@@ -31,7 +37,7 @@ document.addEventListener("DOMContentLoaded", function () {
// View Mode Elements - Story 1.1
const contentContainer = document.querySelector(".content-container");
- const viewModeButtons = document.querySelectorAll(".view-mode-btn");
+ const viewModeButtons = document.querySelectorAll(".view-toggle-btn");
// Mobile View Mode Elements - Story 1.4
const mobileViewModeButtons = document.querySelectorAll(".mobile-view-mode-btn");
@@ -72,6 +78,27 @@ document.addEventListener("DOMContentLoaded", function () {
const githubImportError = document.getElementById("github-import-error");
const githubImportCancelBtn = document.getElementById("github-import-cancel");
const githubImportSubmitBtn = document.getElementById("github-import-submit");
+ const editorHighlightLayer = document.getElementById("editor-highlight-layer");
+ const clearFormattingModal = document.getElementById("clear-formatting-modal");
+ const clearFormattingConfirm = document.getElementById("clear-formatting-confirm");
+ const clearFormattingCancel = document.getElementById("clear-formatting-cancel");
+ const clearFormattingClose = document.getElementById("clear-formatting-close");
+ const findReplaceModal = document.getElementById("find-replace-modal");
+ const findReplaceInput = document.getElementById("find-replace-input");
+ const findReplaceWith = document.getElementById("find-replace-with");
+ const findReplaceCount = document.getElementById("find-replace-count");
+ const findReplacePrev = document.getElementById("find-prev");
+ const findReplaceNext = document.getElementById("find-next");
+ const findReplaceCurrent = document.getElementById("find-replace-current");
+ const findReplaceAll = document.getElementById("find-replace-all");
+ const findReplaceClose = document.getElementById("find-replace-close");
+ const findReplaceCloseIcon = document.getElementById("find-replace-close-icon");
+ const helpModal = document.getElementById("help-modal");
+ const helpModalClose = document.getElementById("help-modal-close");
+ const helpModalCloseIcon = document.getElementById("help-modal-close-icon");
+ const aboutModal = document.getElementById("about-modal");
+ const aboutModalClose = document.getElementById("about-modal-close");
+ const aboutModalCloseIcon = document.getElementById("about-modal-close-icon");
// ========================================
// GLOBAL STATE (persisted across reloads)
@@ -1075,6 +1102,7 @@ This is a fully client-side application. Your content never leaves your browser
}
updateDocumentStats();
+ updateFindHighlights();
cleanupImageObjectUrls();
} catch (e) {
console.error("Markdown rendering failed:", e);
@@ -1686,12 +1714,12 @@ This is a fully client-side application. Your content never leaves your browser
// Update button active states (desktop)
viewModeButtons.forEach(btn => {
- const btnMode = btn.getAttribute('data-mode');
+ const btnMode = btn.getAttribute('data-view-mode');
if (btnMode === mode) {
- btn.classList.add('active');
+ btn.classList.add('is-active');
btn.setAttribute('aria-pressed', 'true');
} else {
- btn.classList.remove('active');
+ btn.classList.remove('is-active');
btn.setAttribute('aria-pressed', 'false');
}
});
@@ -2876,30 +2904,315 @@ This is a fully client-side application. Your content never leaves your browser
});
}
- function findInMarkdownEditor() {
- const selected = markdownEditor.value.slice(markdownEditor.selectionStart, markdownEditor.selectionEnd);
- const query = prompt('Find in document', selected);
- if (!query) return;
- const value = markdownEditor.value;
- const fromIndex = markdownEditor.selectionEnd < value.length ? markdownEditor.selectionEnd : 0;
- let foundAt = value.toLowerCase().indexOf(query.toLowerCase(), fromIndex);
- if (foundAt === -1 && fromIndex > 0) {
- foundAt = value.toLowerCase().indexOf(query.toLowerCase(), 0);
+ function escapeRegExp(text) {
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
+
+ function getFocusableElements(container) {
+ return Array.from(container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))
+ .filter(element => !element.disabled && element.offsetParent !== null);
+ }
+
+ function trapFocusInModal(modal, event) {
+ const focusable = getFocusableElements(modal);
+ if (!focusable.length) {
+ event.preventDefault();
+ return;
+ }
+ const first = focusable[0];
+ const last = focusable[focusable.length - 1];
+ if (event.shiftKey && document.activeElement === first) {
+ event.preventDefault();
+ last.focus();
+ } else if (!event.shiftKey && document.activeElement === last) {
+ event.preventDefault();
+ first.focus();
+ }
+ }
+
+ function openAppModal(modal, options = {}) {
+ if (!modal) return;
+ if (activeModal && activeModal !== modal) {
+ closeAppModal(activeModal);
+ }
+ lastFocusedElement = document.activeElement;
+ modal.style.display = 'flex';
+ requestAnimationFrame(function() {
+ modal.classList.add('is-visible');
+ });
+ modal.setAttribute('aria-hidden', 'false');
+ activeModal = modal;
+ const focusTarget = options.focusTarget || getFocusableElements(modal)[0];
+ if (focusTarget) {
+ focusTarget.focus();
+ }
+ const handleKeydown = function(event) {
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ if (options.onClose) {
+ options.onClose();
+ } else {
+ closeAppModal(modal);
+ }
+ } else if (event.key === 'Tab') {
+ trapFocusInModal(modal, event);
+ }
+ };
+ const handlePointerDown = function(event) {
+ if (event.target === modal) {
+ if (options.onClose) {
+ options.onClose();
+ } else {
+ closeAppModal(modal);
+ }
+ }
+ };
+ modal.addEventListener('keydown', handleKeydown);
+ modal.addEventListener('mousedown', handlePointerDown);
+ modal._modalHandlers = { handleKeydown, handlePointerDown };
+ }
+
+ function closeAppModal(modal) {
+ if (!modal) return;
+ modal.classList.remove('is-visible');
+ modal.setAttribute('aria-hidden', 'true');
+ const handlers = modal._modalHandlers || {};
+ if (handlers.handleKeydown) modal.removeEventListener('keydown', handlers.handleKeydown);
+ if (handlers.handlePointerDown) modal.removeEventListener('mousedown', handlers.handlePointerDown);
+ if (activeModal === modal) activeModal = null;
+ window.setTimeout(function() {
+ if (!modal.classList.contains('is-visible')) {
+ modal.style.display = 'none';
+ }
+ }, 200);
+ if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
+ lastFocusedElement.focus();
+ }
+ }
+
+ function updateFindHighlights() {
+ if (!editorHighlightLayer) return;
+ const text = markdownEditor.value || '';
+ const scrollTop = markdownEditor.scrollTop;
+ const scrollLeft = markdownEditor.scrollLeft;
+ if (!isFindModalOpen || !findReplaceInput || !findReplaceInput.value || !findMatches.length) {
+ editorHighlightLayer.textContent = text;
+ editorHighlightLayer.scrollTop = scrollTop;
+ editorHighlightLayer.scrollLeft = scrollLeft;
+ return;
}
- if (foundAt === -1) {
- alert('No matches found.');
+ let html = '';
+ let lastIndex = 0;
+ findMatches.forEach(function(match, index) {
+ html += escapeHtml(text.slice(lastIndex, match.start));
+ const matchText = escapeHtml(text.slice(match.start, match.end));
+ html += '
' + matchText + '';
+ lastIndex = match.end;
+ });
+ html += escapeHtml(text.slice(lastIndex));
+ editorHighlightLayer.innerHTML = html;
+ editorHighlightLayer.scrollTop = scrollTop;
+ editorHighlightLayer.scrollLeft = scrollLeft;
+ }
+
+ function syncHighlightScroll() {
+ if (!editorHighlightLayer) return;
+ editorHighlightLayer.scrollTop = markdownEditor.scrollTop;
+ editorHighlightLayer.scrollLeft = markdownEditor.scrollLeft;
+ }
+
+ function computeFindMatches(value, query) {
+ if (!query) return [];
+ const haystack = value.toLowerCase();
+ const needle = query.toLowerCase();
+ const matches = [];
+ let index = 0;
+ while (needle && (index = haystack.indexOf(needle, index)) !== -1) {
+ matches.push({ start: index, end: index + needle.length });
+ index += needle.length || 1;
+ }
+ return matches;
+ }
+
+ function updateFindControls() {
+ if (!findReplaceCount) return;
+ const total = findMatches.length;
+ const current = total && activeFindIndex >= 0 ? activeFindIndex + 1 : 0;
+ findReplaceCount.textContent = current + ' of ' + total + ' matches';
+ const hasMatches = total > 0;
+ const hasQuery = !!(findReplaceInput && findReplaceInput.value);
+ if (findReplacePrev) findReplacePrev.disabled = !hasMatches;
+ if (findReplaceNext) findReplaceNext.disabled = !hasMatches;
+ if (findReplaceCurrent) findReplaceCurrent.disabled = !hasMatches;
+ if (findReplaceAll) findReplaceAll.disabled = !hasQuery || !hasMatches;
+ }
+
+ function refreshFindMatches(options) {
+ const opts = options || {};
+ const query = findReplaceInput ? findReplaceInput.value : '';
+ if (!isFindModalOpen || !query) {
+ findMatches = [];
+ activeFindIndex = -1;
+ updateFindControls();
+ updateFindHighlights();
return;
}
+ findMatches = computeFindMatches(markdownEditor.value, query);
+ if (opts.resetIndex || query !== lastFindQuery) {
+ activeFindIndex = findMatches.length ? 0 : -1;
+ } else if (activeFindIndex >= findMatches.length) {
+ activeFindIndex = findMatches.length - 1;
+ }
+ lastFindQuery = query;
+ updateFindControls();
+ updateFindHighlights();
+ }
+
+ function selectActiveMatch() {
+ if (!findMatches.length || activeFindIndex < 0) return;
+ const match = findMatches[activeFindIndex];
markdownEditor.focus();
- markdownEditor.setSelectionRange(foundAt, foundAt + query.length);
+ markdownEditor.setSelectionRange(match.start, match.end);
+ }
+
+ function moveFindMatch(direction) {
+ if (!findMatches.length) return;
+ activeFindIndex = (activeFindIndex + direction + findMatches.length) % findMatches.length;
+ updateFindControls();
+ updateFindHighlights();
+ selectActiveMatch();
}
- function showMarkdownToolbarHelp() {
- alert('Use the toolbar to format selected text, add headings and lists, insert links/images/code/tables, switch views, search, or clear simple Markdown syntax.');
+ function openFindReplaceModal() {
+ if (!findReplaceModal || !findReplaceInput) return;
+ isFindModalOpen = true;
+ const selected = markdownEditor.value.slice(markdownEditor.selectionStart, markdownEditor.selectionEnd);
+ if (selected) {
+ findReplaceInput.value = selected;
+ }
+ openAppModal(findReplaceModal, { focusTarget: findReplaceInput, onClose: closeFindReplaceModal });
+ requestAnimationFrame(function() {
+ findReplaceInput.focus();
+ findReplaceInput.select();
+ });
+ refreshFindMatches({ resetIndex: true });
+ if (findMatches.length) {
+ selectActiveMatch();
+ }
}
- function showMarkdownDocumentInfo() {
- alert('Words: ' + wordCountElement.textContent + '\nCharacters: ' + charCountElement.textContent + '\nReading time: ' + readingTimeElement.textContent + ' min');
+ function closeFindReplaceModal() {
+ isFindModalOpen = false;
+ closeAppModal(findReplaceModal);
+ findMatches = [];
+ activeFindIndex = -1;
+ updateFindControls();
+ updateFindHighlights();
+ }
+
+ function replaceCurrentMatch() {
+ if (!findMatches.length) return;
+ const replacement = findReplaceWith ? findReplaceWith.value : '';
+ const match = findMatches[activeFindIndex];
+ replaceEditorRange(match.start, match.end, replacement, match.start, match.start + replacement.length);
+ refreshFindMatches();
+ if (findMatches.length) {
+ selectActiveMatch();
+ }
+ }
+
+ function replaceAllMatches() {
+ const query = findReplaceInput ? findReplaceInput.value : '';
+ if (!query) return;
+ const replacement = findReplaceWith ? findReplaceWith.value : '';
+ const regex = new RegExp(escapeRegExp(query), 'gi');
+ markdownEditor.value = markdownEditor.value.replace(regex, replacement);
+ markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+ refreshFindMatches({ resetIndex: true });
+ if (findMatches.length) {
+ selectActiveMatch();
+ }
+ }
+
+ function openClearFormattingModal() {
+ if (!clearFormattingModal) return;
+ openAppModal(clearFormattingModal, { focusTarget: clearFormattingConfirm || clearFormattingCancel });
+ }
+
+ function applyClearFormatting() {
+ const stripped = stripBasicMarkdown(markdownEditor.value);
+ replaceEditorRange(0, markdownEditor.value.length, stripped, 0, 0);
+ }
+
+ function openHelpModal() {
+ if (!helpModal) return;
+ openAppModal(helpModal, { focusTarget: helpModalClose || helpModalCloseIcon });
+ }
+
+ function openAboutModal() {
+ if (!aboutModal) return;
+ openAppModal(aboutModal, { focusTarget: aboutModalClose || aboutModalCloseIcon });
+ }
+
+ function initFindReplaceModal() {
+ if (!findReplaceModal || !findReplaceInput) return;
+ findReplaceInput.addEventListener('input', function() {
+ refreshFindMatches({ resetIndex: true });
+ });
+ findReplaceInput.addEventListener('keydown', function(event) {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ moveFindMatch(event.shiftKey ? -1 : 1);
+ }
+ });
+ if (findReplaceWith) {
+ findReplaceWith.addEventListener('input', updateFindControls);
+ }
+ if (findReplacePrev) {
+ findReplacePrev.addEventListener('click', function() { moveFindMatch(-1); });
+ }
+ if (findReplaceNext) {
+ findReplaceNext.addEventListener('click', function() { moveFindMatch(1); });
+ }
+ if (findReplaceCurrent) {
+ findReplaceCurrent.addEventListener('click', replaceCurrentMatch);
+ }
+ if (findReplaceAll) {
+ findReplaceAll.addEventListener('click', replaceAllMatches);
+ }
+ if (findReplaceClose) {
+ findReplaceClose.addEventListener('click', closeFindReplaceModal);
+ }
+ if (findReplaceCloseIcon) {
+ findReplaceCloseIcon.addEventListener('click', closeFindReplaceModal);
+ }
+ }
+
+ function initAppModals() {
+ if (clearFormattingConfirm) {
+ clearFormattingConfirm.addEventListener('click', function() {
+ applyClearFormatting();
+ closeAppModal(clearFormattingModal);
+ });
+ }
+ if (clearFormattingCancel) {
+ clearFormattingCancel.addEventListener('click', function() { closeAppModal(clearFormattingModal); });
+ }
+ if (clearFormattingClose) {
+ clearFormattingClose.addEventListener('click', function() { closeAppModal(clearFormattingModal); });
+ }
+ if (helpModalClose) {
+ helpModalClose.addEventListener('click', function() { closeAppModal(helpModal); });
+ }
+ if (helpModalCloseIcon) {
+ helpModalCloseIcon.addEventListener('click', function() { closeAppModal(helpModal); });
+ }
+ if (aboutModalClose) {
+ aboutModalClose.addEventListener('click', function() { closeAppModal(aboutModal); });
+ }
+ if (aboutModalCloseIcon) {
+ aboutModalCloseIcon.addEventListener('click', function() { closeAppModal(aboutModal); });
+ }
}
function runMarkdownTool(action, button) {
@@ -2945,15 +3258,13 @@ This is a fully client-side application. Your content never leaves your browser
else if (action === 'symbols') openSymbolsModal();
else if (action === 'alert') openAlertModal();
else if (action === 'terminal-block') insertMarkdownBlock('```bash\nnpm run dev\n```\n');
- else if (action === 'editor-only') { setViewMode('editor'); saveCurrentTabState(); }
- else if (action === 'split-view') { setViewMode('split'); saveCurrentTabState(); }
else if (action === 'fullscreen') {
if (document.fullscreenElement && document.exitFullscreen) document.exitFullscreen();
else if (document.documentElement.requestFullscreen) document.documentElement.requestFullscreen();
- } else if (action === 'clear-formatting') transformSelectionOrCurrentLine(stripBasicMarkdown);
- else if (action === 'find') findInMarkdownEditor();
- else if (action === 'help') showMarkdownToolbarHelp();
- else if (action === 'info') showMarkdownDocumentInfo();
+ } else if (action === 'clear-formatting') openClearFormattingModal();
+ else if (action === 'find') openFindReplaceModal();
+ else if (action === 'help') openHelpModal();
+ else if (action === 'info') openAboutModal();
}
function initMarkdownFormatToolbar() {
@@ -3121,6 +3432,8 @@ This is a fully client-side application. Your content never leaves your browser
initTabs();
if (loadGlobalState().syncScrollingEnabled === false) toggleSyncScrolling();
updateMobileStats();
+ updateFindHighlights();
+ syncHighlightScroll();
// Initialize resizer - Story 1.3
initResizer();
@@ -3128,8 +3441,12 @@ This is a fully client-side application. Your content never leaves your browser
// View Mode Button Event Listeners - Story 1.1
viewModeButtons.forEach(btn => {
btn.addEventListener('click', function() {
- const mode = this.getAttribute('data-mode');
- setViewMode(mode);
+ const mode = this.getAttribute('data-view-mode');
+ if ((mode === 'editor' || mode === 'preview') && currentViewMode === mode) {
+ setViewMode('split');
+ } else {
+ setViewMode(mode);
+ }
saveCurrentTabState();
});
});
@@ -3148,12 +3465,24 @@ This is a fully client-side application. Your content never leaves your browser
debouncedRender();
clearTimeout(saveTabStateTimeout);
saveTabStateTimeout = setTimeout(saveCurrentTabState, 500);
+ if (isFindModalOpen) {
+ refreshFindMatches();
+ } else {
+ updateFindHighlights();
+ }
});
initMarkdownFormatToolbar();
+ initFindReplaceModal();
+ initAppModals();
// Editor key handlers for list continuation and indentation
markdownEditor.addEventListener("keydown", function(e) {
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'f') {
+ e.preventDefault();
+ openFindReplaceModal();
+ return;
+ }
if (handleListEnter(e)) {
return;
}
@@ -3179,7 +3508,10 @@ This is a fully client-side application. Your content never leaves your browser
}
});
- editorPane.addEventListener("scroll", syncEditorToPreview);
+ editorPane.addEventListener("scroll", function() {
+ syncEditorToPreview();
+ syncHighlightScroll();
+ });
previewPane.addEventListener("scroll", syncPreviewToEditor);
toggleSyncButton.addEventListener("click", toggleSyncScrolling);
themeToggle.addEventListener("click", function () {
diff --git a/styles.css b/styles.css
index 657ef28..ec7952e 100644
--- a/styles.css
+++ b/styles.css
@@ -120,7 +120,7 @@ body {
width: 100%;
height: 100%;
border: none;
- background-color: var(--editor-bg);
+ background-color: transparent;
color: var(--text-color);
resize: none;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
@@ -129,6 +129,8 @@ body {
padding: 10px;
transition: background-color 0.3s ease, color 0.3s ease;
overflow-y: auto;
+ position: relative;
+ z-index: 2;
}
#markdown-editor:focus {
@@ -286,9 +288,23 @@ body {
.toolbar {
display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.toolbar-group {
+ display: inline-flex;
+ align-items: center;
gap: 6px;
}
+.toolbar-divider {
+ width: 1px;
+ height: 20px;
+ background-color: var(--border-color);
+ opacity: 0.7;
+}
+
.tool-button {
background-color: var(--button-bg);
border: 1px solid var(--border-color);
@@ -299,6 +315,7 @@ body {
cursor: pointer;
display: inline-flex;
align-items: center;
+ justify-content: center;
gap: 4px;
transition: all 0.2s ease;
}
@@ -315,10 +332,22 @@ body {
font-size: 15px;
}
+.tool-button.is-active,
+.tool-button.is-active:hover {
+ border-color: var(--accent-color);
+ color: var(--accent-color);
+ background-color: rgba(3, 102, 214, 0.08);
+}
+
.btn-text {
display: none;
}
+.toolbar .tool-button {
+ height: 28px;
+ min-width: 28px;
+}
+
.toolbar .tool-button.sync-active {
border-color: var(--accent-color);
color: var(--accent-color);
@@ -391,12 +420,43 @@ body {
opacity: 0.35;
pointer-events: none;
user-select: none;
+ z-index: 3;
}
.editor-pane:has(#markdown-editor:not(:placeholder-shown)) .drop-hint {
display: none;
}
+.editor-highlight-layer {
+ position: absolute;
+ inset: 20px 0 20px 20px;
+ padding: 10px;
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+ font-size: 14px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ color: transparent;
+ pointer-events: none;
+ overflow: auto;
+ background-color: var(--editor-bg);
+ border-radius: 4px;
+ z-index: 1;
+}
+
+.editor-highlight-layer::-webkit-scrollbar {
+ display: none;
+}
+
+.find-highlight {
+ background-color: rgba(255, 196, 0, 0.35);
+ border-radius: 2px;
+}
+
+.find-highlight.active {
+ background-color: rgba(255, 196, 0, 0.65);
+}
+
/* Dropdown improvements */
.dropdown-menu {
background-color: var(--bg-color);
@@ -970,7 +1030,6 @@ a:focus {
@media (max-width: 1079px) {
/* Override Bootstrap d-md-flex / d-md-none so the breakpoint is 1080px */
.stats-container,
- .view-mode-group,
.toolbar {
display: none !important;
}
@@ -1032,10 +1091,8 @@ a:focus {
}
/* ========================================
- VIEW MODE CONTROLS - Story 1.1
+ HEADER LAYOUT
======================================== */
-
-/* Header layout for three sections */
.header-container {
position: relative;
min-height: 30px;
@@ -1058,59 +1115,6 @@ a:focus {
white-space: nowrap;
}
-/* View Mode Button Group */
-.view-mode-group {
- display: flex;
- gap: 0;
- flex: 1;
- justify-content: center;
-}
-
-.view-mode-btn {
- background-color: var(--button-bg);
- border: 1px solid var(--border-color);
- color: var(--text-color);
- padding: 4px 10px;
- font-size: 13px;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 4px;
- transition: all 0.2s ease;
-}
-
-.view-mode-btn:first-child {
- border-radius: 6px 0 0 6px;
-}
-
-.view-mode-btn:last-child {
- border-radius: 0 6px 6px 0;
-}
-
-.view-mode-btn:not(:last-child) {
- border-right: none;
-}
-
-.view-mode-btn:hover {
- background-color: var(--button-hover);
-}
-
-.view-mode-btn.active {
- background-color: var(--button-bg);
- border-color: var(--accent-color);
- color: var(--accent-color);
- border-width: 2px;
- padding: 3px 9px; /* Adjust for thicker border */
-}
-
-.view-mode-btn.active:not(:last-child) {
- border-right: 2px solid var(--accent-color);
-}
-
-.view-mode-btn i {
- font-size: 15px;
-}
-
/* Pane View States */
.content-container.view-editor-only .preview-pane {
display: none;
@@ -1134,21 +1138,8 @@ a:focus {
flex: 1;
}
-/* Compact desktop (< 1280px): icon-only view mode buttons, compact toolbar */
+/* Compact desktop (< 1280px): compact toolbar */
@media (max-width: 1280px) {
- /* Icon-only view mode buttons at medium widths */
- .view-mode-btn span {
- display: none;
- }
-
- .view-mode-btn {
- padding: 5px 10px;
- }
-
- .view-mode-btn.active {
- padding: 4px 9px;
- }
-
/* Compact toolbar at medium widths */
.toolbar {
gap: 4px;
@@ -1780,6 +1771,17 @@ a:focus {
justify-content: center;
}
+.reset-modal-overlay.modal-overlay {
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s ease, visibility 0.2s ease;
+}
+
+.reset-modal-overlay.modal-overlay.is-visible {
+ opacity: 1;
+ visibility: visible;
+}
+
.reset-modal-box {
background: var(--header-bg);
border: 1px solid var(--border-color);
@@ -1793,6 +1795,165 @@ a:focus {
gap: 16px;
}
+.modal-box {
+ max-height: min(85vh, 760px);
+ opacity: 0;
+ transform: translateY(8px);
+ transition: transform 0.2s ease, opacity 0.2s ease;
+}
+
+.reset-modal-overlay.modal-overlay.is-visible .modal-box {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.modal-header .reset-modal-message {
+ text-align: left;
+ flex: 1;
+}
+
+.modal-close-btn {
+ border: 1px solid var(--border-color);
+ background: var(--button-bg);
+ color: var(--text-color);
+ border-radius: 6px;
+ width: 28px;
+ height: 28px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: background-color 0.15s ease;
+}
+
+.modal-close-btn:hover {
+ background-color: var(--button-hover);
+}
+
+.modal-body {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ max-height: min(60vh, 520px);
+ overflow: auto;
+ padding-right: 4px;
+}
+
+.modal-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.modal-section-title {
+ margin: 0;
+ font-size: 0.95rem;
+ font-weight: 600;
+}
+
+.modal-list {
+ margin: 0;
+ padding-left: 1.1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ font-size: 0.85rem;
+}
+
+.modal-list a {
+ color: var(--accent-color);
+ text-decoration: none;
+}
+
+.modal-list a:hover {
+ text-decoration: underline;
+}
+
+.modal-subtext {
+ margin: 0;
+ font-size: 12px;
+ color: var(--text-secondary, #57606a);
+ line-height: 1.4;
+}
+
+.find-replace-meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.find-match-count {
+ font-size: 12px;
+ color: var(--text-secondary, #57606a);
+}
+
+.find-replace-nav {
+ display: inline-flex;
+ gap: 6px;
+}
+
+.find-nav-btn {
+ width: 28px;
+ height: 28px;
+ padding: 0;
+}
+
+.about-header {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.about-logo {
+ width: 64px;
+ height: 64px;
+ border-radius: 12px;
+ border: 1px solid var(--border-color);
+ object-fit: cover;
+}
+
+.about-details {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.about-title {
+ margin: 0;
+ font-size: 1.05rem;
+ font-weight: 600;
+}
+
+.about-description {
+ margin: 0;
+ font-size: 0.85rem;
+ color: var(--text-secondary, #57606a);
+}
+
+.about-meta {
+ margin: 0;
+ font-size: 0.78rem;
+ color: var(--text-secondary, #57606a);
+}
+
+.modal-body kbd {
+ padding: 2px 6px;
+ border-radius: 4px;
+ background-color: var(--button-bg);
+ border: 1px solid var(--border-color);
+ font-size: 0.75rem;
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+}
+
.reset-modal-box--wide {
width: min(92vw, 640px);
max-width: 640px;
From faa7078ff9a9a8d814ac0a82fdde6485e52d367a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 6 May 2026 20:04:14 +0000
Subject: [PATCH 2/7] Harden render error handling and find navigation
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/1d1f0d43-2348-47f2-9f50-dfd73674eda0
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/js/script.js | 12 ++++++++----
script.js | 12 ++++++++----
2 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 3c77fcf..011f238 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -1106,10 +1106,12 @@ This is a fully client-side application. Your content never leaves your browser
cleanupImageObjectUrls();
} catch (e) {
console.error("Markdown rendering failed:", e);
+ const safeMessage = escapeHtml(e && e.message ? e.message : 'Unknown error');
+ const safeMarkdown = escapeHtml(markdownEditor.value);
markdownPreview.innerHTML = `
- Error rendering markdown: ${e.message}
+ Error rendering markdown: ${safeMessage}
-
${markdownEditor.value}`;
+
${safeMarkdown}`;
}
}
@@ -3076,8 +3078,9 @@ This is a fully client-side application. Your content never leaves your browser
}
function moveFindMatch(direction) {
- if (!findMatches.length) return;
- activeFindIndex = (activeFindIndex + direction + findMatches.length) % findMatches.length;
+ const totalMatches = findMatches.length;
+ if (!totalMatches) return;
+ activeFindIndex = (activeFindIndex + direction + totalMatches) % totalMatches;
updateFindControls();
updateFindHighlights();
selectActiveMatch();
@@ -3117,6 +3120,7 @@ This is a fully client-side application. Your content never leaves your browser
replaceEditorRange(match.start, match.end, replacement, match.start, match.start + replacement.length);
refreshFindMatches();
if (findMatches.length) {
+ activeFindIndex = Math.min(activeFindIndex, findMatches.length - 1);
selectActiveMatch();
}
}
diff --git a/script.js b/script.js
index 3c77fcf..011f238 100644
--- a/script.js
+++ b/script.js
@@ -1106,10 +1106,12 @@ This is a fully client-side application. Your content never leaves your browser
cleanupImageObjectUrls();
} catch (e) {
console.error("Markdown rendering failed:", e);
+ const safeMessage = escapeHtml(e && e.message ? e.message : 'Unknown error');
+ const safeMarkdown = escapeHtml(markdownEditor.value);
markdownPreview.innerHTML = `
- Error rendering markdown: ${e.message}
+ Error rendering markdown: ${safeMessage}
-
${markdownEditor.value}`;
+
${safeMarkdown}`;
}
}
@@ -3076,8 +3078,9 @@ This is a fully client-side application. Your content never leaves your browser
}
function moveFindMatch(direction) {
- if (!findMatches.length) return;
- activeFindIndex = (activeFindIndex + direction + findMatches.length) % findMatches.length;
+ const totalMatches = findMatches.length;
+ if (!totalMatches) return;
+ activeFindIndex = (activeFindIndex + direction + totalMatches) % totalMatches;
updateFindControls();
updateFindHighlights();
selectActiveMatch();
@@ -3117,6 +3120,7 @@ This is a fully client-side application. Your content never leaves your browser
replaceEditorRange(match.start, match.end, replacement, match.start, match.start + replacement.length);
refreshFindMatches();
if (findMatches.length) {
+ activeFindIndex = Math.min(activeFindIndex, findMatches.length - 1);
selectActiveMatch();
}
}
From 2dc31c8b5a46cad3d74aef7f29d0e8e4bfaa4b41 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 6 May 2026 20:06:02 +0000
Subject: [PATCH 3/7] Refine view toggle and version wiring
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/1d1f0d43-2348-47f2-9f50-dfd73674eda0
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/index.html | 2 +-
desktop-app/resources/js/script.js | 26 +++++++++++++++++---------
index.html | 2 +-
script.js | 26 +++++++++++++++++---------
4 files changed, 36 insertions(+), 20 deletions(-)
diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html
index 377e66a..ea14c73 100644
--- a/desktop-app/resources/index.html
+++ b/desktop-app/resources/index.html
@@ -435,7 +435,7 @@
Keyboard shortcuts
Markdown Viewer
A GitHub-style Markdown editor with live preview, diagrams, math, syntax highlighting, and export tools.
-
Version 1.0.0 • Apache License 2.0 • Open source
+
Version • Apache License 2.0 • Open source
diff --git a/script.js b/script.js
index 011f238..1a2e8b6 100644
--- a/script.js
+++ b/script.js
@@ -9,6 +9,7 @@ document.addEventListener("DOMContentLoaded", function () {
// View Mode State - Story 1.1
let currentViewMode = 'split'; // 'editor', 'split', or 'preview'
+ const APP_VERSION = '1.0.0';
let activeModal = null;
let lastFocusedElement = null;
let isFindModalOpen = false;
@@ -99,6 +100,10 @@ document.addEventListener("DOMContentLoaded", function () {
const aboutModal = document.getElementById("about-modal");
const aboutModalClose = document.getElementById("about-modal-close");
const aboutModalCloseIcon = document.getElementById("about-modal-close-icon");
+ const aboutVersion = document.getElementById("about-version");
+ if (aboutVersion) {
+ aboutVersion.textContent = APP_VERSION;
+ }
// ========================================
// GLOBAL STATE (persisted across reloads)
@@ -1756,6 +1761,13 @@ This is a fully client-side application. Your content never leaves your browser
}
}
+ function resolveViewToggleMode(mode) {
+ if ((mode === 'editor' || mode === 'preview') && currentViewMode === mode) {
+ return 'split';
+ }
+ return mode;
+ }
+
// Story 1.2: Update sync toggle visibility
function updateSyncToggleVisibility(mode) {
const isSplitView = mode === 'split';
@@ -3077,7 +3089,7 @@ This is a fully client-side application. Your content never leaves your browser
markdownEditor.setSelectionRange(match.start, match.end);
}
- function moveFindMatch(direction) {
+ function cycleFindMatch(direction) {
const totalMatches = findMatches.length;
if (!totalMatches) return;
activeFindIndex = (activeFindIndex + direction + totalMatches) % totalMatches;
@@ -3166,17 +3178,17 @@ This is a fully client-side application. Your content never leaves your browser
findReplaceInput.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
- moveFindMatch(event.shiftKey ? -1 : 1);
+ cycleFindMatch(event.shiftKey ? -1 : 1);
}
});
if (findReplaceWith) {
findReplaceWith.addEventListener('input', updateFindControls);
}
if (findReplacePrev) {
- findReplacePrev.addEventListener('click', function() { moveFindMatch(-1); });
+ findReplacePrev.addEventListener('click', function() { cycleFindMatch(-1); });
}
if (findReplaceNext) {
- findReplaceNext.addEventListener('click', function() { moveFindMatch(1); });
+ findReplaceNext.addEventListener('click', function() { cycleFindMatch(1); });
}
if (findReplaceCurrent) {
findReplaceCurrent.addEventListener('click', replaceCurrentMatch);
@@ -3446,11 +3458,7 @@ This is a fully client-side application. Your content never leaves your browser
viewModeButtons.forEach(btn => {
btn.addEventListener('click', function() {
const mode = this.getAttribute('data-view-mode');
- if ((mode === 'editor' || mode === 'preview') && currentViewMode === mode) {
- setViewMode('split');
- } else {
- setViewMode(mode);
- }
+ setViewMode(resolveViewToggleMode(mode));
saveCurrentTabState();
});
});
From 5d26c77f14a6fe4e1bc5c669fc95934d74e6d6c4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 6 May 2026 20:07:03 +0000
Subject: [PATCH 4/7] Simplify find match indexing
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/1d1f0d43-2348-47f2-9f50-dfd73674eda0
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/js/script.js | 2 +-
script.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 1a2e8b6..f54f1b2 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -3043,7 +3043,7 @@ This is a fully client-side application. Your content never leaves your browser
let index = 0;
while (needle && (index = haystack.indexOf(needle, index)) !== -1) {
matches.push({ start: index, end: index + needle.length });
- index += needle.length || 1;
+ index += needle.length;
}
return matches;
}
diff --git a/script.js b/script.js
index 1a2e8b6..f54f1b2 100644
--- a/script.js
+++ b/script.js
@@ -3043,7 +3043,7 @@ This is a fully client-side application. Your content never leaves your browser
let index = 0;
while (needle && (index = haystack.indexOf(needle, index)) !== -1) {
matches.push({ start: index, end: index + needle.length });
- index += needle.length || 1;
+ index += needle.length;
}
return matches;
}
From a14a91969f950fba8dc55776d6822b0403522015 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 6 May 2026 20:12:37 +0000
Subject: [PATCH 5/7] Changes before error encountered
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/1d1f0d43-2348-47f2-9f50-dfd73674eda0
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
script.js | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/script.js b/script.js
index f54f1b2..3cfa83d 100644
--- a/script.js
+++ b/script.js
@@ -3015,16 +3015,19 @@ This is a fully client-side application. Your content never leaves your browser
editorHighlightLayer.scrollLeft = scrollLeft;
return;
}
- let html = '';
+ const fragment = document.createDocumentFragment();
let lastIndex = 0;
findMatches.forEach(function(match, index) {
- html += escapeHtml(text.slice(lastIndex, match.start));
- const matchText = escapeHtml(text.slice(match.start, match.end));
- html += '' + matchText + '';
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.start)));
+ const mark = document.createElement('mark');
+ mark.className = 'find-highlight' + (index === activeFindIndex ? ' active' : '');
+ mark.textContent = text.slice(match.start, match.end);
+ fragment.appendChild(mark);
lastIndex = match.end;
});
- html += escapeHtml(text.slice(lastIndex));
- editorHighlightLayer.innerHTML = html;
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
+ editorHighlightLayer.textContent = '';
+ editorHighlightLayer.appendChild(fragment);
editorHighlightLayer.scrollTop = scrollTop;
editorHighlightLayer.scrollLeft = scrollLeft;
}
@@ -3040,10 +3043,10 @@ This is a fully client-side application. Your content never leaves your browser
const haystack = value.toLowerCase();
const needle = query.toLowerCase();
const matches = [];
- let index = 0;
- while (needle && (index = haystack.indexOf(needle, index)) !== -1) {
+ let index = haystack.indexOf(needle);
+ while (index !== -1) {
matches.push({ start: index, end: index + needle.length });
- index += needle.length;
+ index = haystack.indexOf(needle, index + needle.length);
}
return matches;
}
From 4c51778ce8efac260f7cb72624e67c5ea1d88c90 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 7 May 2026 03:22:39 +0000
Subject: [PATCH 6/7] Sync desktop resources
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/0fa74dcb-7602-4367-9e4c-962c09b06b1d
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/js/script.js | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index f54f1b2..3cfa83d 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -3015,16 +3015,19 @@ This is a fully client-side application. Your content never leaves your browser
editorHighlightLayer.scrollLeft = scrollLeft;
return;
}
- let html = '';
+ const fragment = document.createDocumentFragment();
let lastIndex = 0;
findMatches.forEach(function(match, index) {
- html += escapeHtml(text.slice(lastIndex, match.start));
- const matchText = escapeHtml(text.slice(match.start, match.end));
- html += '' + matchText + '';
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.start)));
+ const mark = document.createElement('mark');
+ mark.className = 'find-highlight' + (index === activeFindIndex ? ' active' : '');
+ mark.textContent = text.slice(match.start, match.end);
+ fragment.appendChild(mark);
lastIndex = match.end;
});
- html += escapeHtml(text.slice(lastIndex));
- editorHighlightLayer.innerHTML = html;
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
+ editorHighlightLayer.textContent = '';
+ editorHighlightLayer.appendChild(fragment);
editorHighlightLayer.scrollTop = scrollTop;
editorHighlightLayer.scrollLeft = scrollLeft;
}
@@ -3040,10 +3043,10 @@ This is a fully client-side application. Your content never leaves your browser
const haystack = value.toLowerCase();
const needle = query.toLowerCase();
const matches = [];
- let index = 0;
- while (needle && (index = haystack.indexOf(needle, index)) !== -1) {
+ let index = haystack.indexOf(needle);
+ while (index !== -1) {
matches.push({ start: index, end: index + needle.length });
- index += needle.length;
+ index = haystack.indexOf(needle, index + needle.length);
}
return matches;
}
From d0ce2c119c51e1dfa42c4314701d58a8bbd56c3c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 7 May 2026 04:26:58 +0000
Subject: [PATCH 7/7] Keep sync toggle visible across modes
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/a00bb011-4552-4d75-a3d5-34a6ae5bf099
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/js/script.js | 12 ++++++++----
desktop-app/resources/styles.css | 6 ++++++
script.js | 12 ++++++++----
styles.css | 6 ++++++
4 files changed, 28 insertions(+), 8 deletions(-)
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 3cfa83d..95874a2 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -1774,14 +1774,18 @@ This is a fully client-side application. Your content never leaves your browser
// Desktop sync toggle
if (toggleSyncButton) {
- toggleSyncButton.style.display = isSplitView ? '' : 'none';
- toggleSyncButton.setAttribute('aria-hidden', !isSplitView);
+ toggleSyncButton.style.display = '';
+ toggleSyncButton.disabled = !isSplitView;
+ toggleSyncButton.setAttribute('aria-disabled', String(!isSplitView));
+ toggleSyncButton.removeAttribute('aria-hidden');
}
// Mobile sync toggle
if (mobileToggleSync) {
- mobileToggleSync.style.display = isSplitView ? '' : 'none';
- mobileToggleSync.setAttribute('aria-hidden', !isSplitView);
+ mobileToggleSync.style.display = '';
+ mobileToggleSync.disabled = !isSplitView;
+ mobileToggleSync.setAttribute('aria-disabled', String(!isSplitView));
+ mobileToggleSync.removeAttribute('aria-hidden');
}
}
diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css
index ec7952e..d1c69d1 100644
--- a/desktop-app/resources/styles.css
+++ b/desktop-app/resources/styles.css
@@ -328,6 +328,12 @@ body {
background-color: var(--button-active);
}
+.tool-button:disabled,
+.tool-button[aria-disabled="true"] {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
.tool-button i {
font-size: 15px;
}
diff --git a/script.js b/script.js
index 3cfa83d..95874a2 100644
--- a/script.js
+++ b/script.js
@@ -1774,14 +1774,18 @@ This is a fully client-side application. Your content never leaves your browser
// Desktop sync toggle
if (toggleSyncButton) {
- toggleSyncButton.style.display = isSplitView ? '' : 'none';
- toggleSyncButton.setAttribute('aria-hidden', !isSplitView);
+ toggleSyncButton.style.display = '';
+ toggleSyncButton.disabled = !isSplitView;
+ toggleSyncButton.setAttribute('aria-disabled', String(!isSplitView));
+ toggleSyncButton.removeAttribute('aria-hidden');
}
// Mobile sync toggle
if (mobileToggleSync) {
- mobileToggleSync.style.display = isSplitView ? '' : 'none';
- mobileToggleSync.setAttribute('aria-hidden', !isSplitView);
+ mobileToggleSync.style.display = '';
+ mobileToggleSync.disabled = !isSplitView;
+ mobileToggleSync.setAttribute('aria-disabled', String(!isSplitView));
+ mobileToggleSync.removeAttribute('aria-hidden');
}
}
diff --git a/styles.css b/styles.css
index ec7952e..d1c69d1 100644
--- a/styles.css
+++ b/styles.css
@@ -328,6 +328,12 @@ body {
background-color: var(--button-active);
}
+.tool-button:disabled,
+.tool-button[aria-disabled="true"] {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
.tool-button i {
font-size: 15px;
}