diff --git a/assets/lucide-icons.css b/assets/lucide-icons.css index dda69892..618e4baa 100644 --- a/assets/lucide-icons.css +++ b/assets/lucide-icons.css @@ -21,6 +21,8 @@ pointer-events: none; } +.lucide-clipboard { --lucide-icon: url("data:image/svg+xml;base64,PCEtLSBAbGljZW5zZSBsdWNpZGUtc3RhdGljIHYxLjI1LjAgLSBJU0MgLS0+PHN2ZyBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1jbGlwYm9hcmQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiID48cmVjdCB3aWR0aD0iOCIgaGVpZ2h0PSI0IiB4PSI4IiB5PSIyIiByeD0iMSIgcnk9IjEiIC8+PHBhdGggZD0iTTE2IDRoMmEyIDIgMCAwIDEgMiAydjE0YTIgMiAwIDAgMS0yIDJINmEyIDIgMCAwIDEtMi0yVjZhMiAyIDAgMCAxIDItMmgyIiAvPjwvc3ZnPg=="); } + @media (forced-colors: active) { .lucide { background-color: ButtonText; } } diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index ef73df36..e1780dbe 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -41,7 +41,6 @@
-

Markdown Viewer - Online Markdown Editor with Live Preview

@@ -53,11 +52,21 @@

Markdown Viewer - Online Markd

- - -
+
-
-
+
+
+
+ + +
-
- - - - - +
+ +
-
+
+ + -
- - -
+
+
+ + + + +
-
- - - - -
- - - -
-
@@ -791,8 +789,8 @@
  • Use the view buttons in the toolbar to switch between Editor, Split, and Preview modes.
  • Sync scrolling is available in Split view to keep the editor and preview aligned.
  • Review opens a read-only preview where you can comment on or suggest changes to headings, paragraphs, code blocks, and diagrams.
  • -
  • New, Share, Live Share, Report an Issue, About, and Settings are available in the header toolbar.
  • -
  • Sync, Copy, Review, view modes, and Export stay with the document formatting toolbar.
  • +
  • View modes, New, Copy, Sync, Review, Share, Live Share, and Export are grouped in the header toolbar.
  • +
  • Report an Issue, About, and Settings follow the document actions in the header.
  • @@ -1505,10 +1503,10 @@

    Comments & suggestions

    -
    - - - +
    + + +
    '; - } else { - const duplicateAction = isShareSnapshotTab(tab) - ? '' - : ''; - const downloadAction = isShareSnapshotTab(tab) - ? '' - : ''; - const favoriteAction = isShareSnapshotTab(tab) - ? '' - : ''; - const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); - const splitAction = isCombinedSplitTab - ? '' - : tabs.length > 1 - ? '' - : ''; - dropdown.innerHTML = - '' + - duplicateAction + - favoriteAction + - splitAction + - downloadAction + - '' + - ''; - } + const duplicateAction = isShareSnapshotTab(tab) + ? '' + : ''; + const downloadAction = isShareSnapshotTab(tab) + ? '' + : ''; + const favoriteAction = isShareSnapshotTab(tab) + ? '' + : ''; + const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); + const splitAction = isCombinedSplitTab + ? '' + : tabs.length > 1 + ? '' + : ''; + dropdown.innerHTML = + '' + + duplicateAction + + favoriteAction + + splitAction + + downloadAction + + '' + + ''; menuBtn.addEventListener('click', function(e) { e.preventDefault(); @@ -8196,7 +8234,7 @@ document.addEventListener("DOMContentLoaded", async function () { }, { id: 'all', icon: 'lucide-square-x', label: 'Close all' }]; - const actions = isReleaseNotesTab(tab) ? closeActions : [{ + const actions = isReleaseNotesTab(tab) ? closeActions.slice(0, 1) : [{ id: isCombinedSplitTab ? 'split-close' : 'split', icon: isCombinedSplitTab ? 'lucide-panel-right-close' : 'lucide-columns-2', label: isCombinedSplitTab ? 'Exit split view' : 'Open in split view', @@ -8333,7 +8371,9 @@ document.addEventListener("DOMContentLoaded", async function () { titleSpan.title = getDocumentTabHoverTitle(tab); } - const tabMenu = createTabActionMenu(tab, { menuIdPrefix: 'desktop-tab-menu' }); + const tabMenu = isReleaseNotesTab(tab) + ? null + : createTabActionMenu(tab, { menuIdPrefix: 'desktop-tab-menu' }); const closeButton = document.createElement('button'); closeButton.type = 'button'; @@ -8351,7 +8391,7 @@ document.addEventListener("DOMContentLoaded", async function () { item.appendChild(fileIcon); item.appendChild(titleSpan); - item.appendChild(tabMenu.button); + if (tabMenu) item.appendChild(tabMenu.button); item.appendChild(closeButton); item.addEventListener('contextmenu', function(event) { @@ -8913,6 +8953,7 @@ document.addEventListener("DOMContentLoaded", async function () { saveSecondarySplitState(); closeReviewComposer(); clearReviewDecorations(); + if (isReleaseNotesTab(tab) && reviewModeActive) setReviewMode(false); // Clear typing timeout and reset tracking for the new tab if (typingTimeout) { @@ -8923,11 +8964,14 @@ document.addEventListener("DOMContentLoaded", async function () { pendingState = null; activeTabId = tabId; + const releaseNotesActive = isReleaseNotesTab(tab); + document.body.classList.toggle('release-notes-active', releaseNotesActive); if (swapSplitPanes) secondarySplitTabId = previousActiveTabId; saveActiveTabId(activeTabId); updateNoOpenDocumentState(); - selectedDocumentId = tabId; - setSingleDocumentTreeSelection('document', tabId); + selectedDocumentId = releaseNotesActive ? null : tabId; + if (releaseNotesActive) clearDocumentTreeSelection({ announce: false }); + else setSingleDocumentTreeSelection('document', tabId); tab.lastOpenedAt = Date.now(); if (!isTemporaryDocument(tab)) saveTabsToStorage(tabs, [tab.id]); markdownEditor.value = tab.content; @@ -18764,6 +18808,55 @@ ${selector} .arrowheadPath { }); } + function initDropdownMenuMotion() { + if (!window.bootstrap || !window.bootstrap.Dropdown) return; + + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + const closeDurationMs = 120; + + document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(function(toggle) { + if (toggle.dataset.dropdownMotionReady === 'true') return; + const dropdown = toggle.closest('.dropdown'); + const menu = dropdown && Array.from(dropdown.children).find(function(child) { + return child.classList && child.classList.contains('dropdown-menu'); + }); + if (!dropdown || !menu) return; + + toggle.dataset.dropdownMotionReady = 'true'; + const instance = window.bootstrap.Dropdown.getOrCreateInstance(toggle); + let closeTimer = null; + let allowImmediateHide = false; + + toggle.addEventListener('show.bs.dropdown', function() { + if (closeTimer !== null) window.clearTimeout(closeTimer); + closeTimer = null; + allowImmediateHide = false; + menu.classList.remove('is-closing'); + }); + + toggle.addEventListener('hide.bs.dropdown', function(event) { + if (allowImmediateHide || reducedMotion.matches) return; + event.preventDefault(); + if (closeTimer !== null) return; + + menu.classList.add('is-closing'); + closeTimer = window.setTimeout(function() { + closeTimer = null; + allowImmediateHide = true; + instance.hide(); + allowImmediateHide = false; + }, closeDurationMs); + }); + + toggle.addEventListener('hidden.bs.dropdown', function() { + if (closeTimer !== null) window.clearTimeout(closeTimer); + closeTimer = null; + allowImmediateHide = false; + menu.classList.remove('is-closing'); + }); + }); + } + // Story 1.3: Resize Divider Functions function initResizer() { if (!resizeDivider) return; @@ -19387,6 +19480,7 @@ ${selector} .arrowheadPath { initMarkdownFormatToolbar(); initToolbarDropdownPortals(); + initDropdownMenuMotion(); initFindReplaceModal(); initAppModals(); document.addEventListener('fullscreenchange', function() { @@ -21679,6 +21773,7 @@ ${selector} .arrowheadPath { }); copyMarkdownButton.addEventListener("click", async function () { + if (isReleaseNotesActive()) return; if (blockShareSnapshotSourceAccess()) return; try { await copyTextToClipboard(markdownEditor.value); @@ -22335,6 +22430,9 @@ ${selector} .arrowheadPath { function updateReleaseNotesActionAvailability(releaseNotesActive) { const actions = [ + toggleSyncButton, + copyMarkdownButton, + reviewToggle, exportDropdown, exportMd, exportHtml, @@ -22347,10 +22445,21 @@ ${selector} .arrowheadPath { mobileExportHtml, mobileExportPdf, mobileExportPng, + mobileToggleSyncButton, + mobileCopyMarkdownButton, + mobileReviewToggle, mobileShareButton, mobileLiveShareButton ]; + viewModeButtons.forEach(function(action) { actions.push(action); }); + mobileViewModeButtons.forEach(function(action) { actions.push(action); }); + if (markdownFormatToolbar) { + markdownFormatToolbar.querySelectorAll('button').forEach(function(action) { actions.push(action); }); + markdownFormatToolbar.classList.toggle('is-release-notes-disabled', releaseNotesActive); + markdownFormatToolbar.setAttribute('aria-disabled', releaseNotesActive ? 'true' : 'false'); + } + actions.forEach(function(action) { if (!action) return; if (releaseNotesActive) { @@ -22361,11 +22470,15 @@ ${selector} .arrowheadPath { ? action.getAttribute('tabindex') : '__missing__'; } + action.dataset.releaseNotesOriginalTitle = action.hasAttribute('title') + ? action.getAttribute('title') + : '__missing__'; } action.dataset.releaseNotesDisabled = 'true'; if ('disabled' in action) action.disabled = true; action.classList.add('disabled'); action.setAttribute('aria-disabled', 'true'); + action.setAttribute('title', 'Unavailable in Release Notes'); if (action.tagName === 'A') action.setAttribute('tabindex', '-1'); return; } @@ -22381,9 +22494,16 @@ ${selector} .arrowheadPath { const remainsDisabled = wasDisabled || hasAnotherDisabledState; delete action.dataset.releaseNotesDisabled; delete action.dataset.releaseNotesWasDisabled; + const originalTitle = action.dataset.releaseNotesOriginalTitle; + delete action.dataset.releaseNotesOriginalTitle; if ('disabled' in action) action.disabled = remainsDisabled; action.classList.toggle('disabled', remainsDisabled); action.setAttribute('aria-disabled', remainsDisabled ? 'true' : 'false'); + if (originalTitle && originalTitle !== '__missing__') { + action.setAttribute('title', originalTitle); + } else { + action.removeAttribute('title'); + } if (action.tagName === 'A') { const originalTabindex = action.dataset.releaseNotesOriginalTabindex; delete action.dataset.releaseNotesOriginalTabindex; @@ -22413,6 +22533,8 @@ ${selector} .arrowheadPath { const liveShareDocumentActive = isLiveShareDocumentActive(); const liveShareGuestDocumentActive = liveShareDocumentActive && !isLiveShareHostDocumentActive(); const viewOnly = isLiveViewOnlyParticipant() || snapshotViewOnly || releaseNotesActive; + document.body.classList.toggle('release-notes-active', releaseNotesActive); + if (!releaseNotesActive) updateReleaseNotesActionAvailability(false); const sourceReadOnly = viewOnly || reviewModeActive; if (markdownEditor) { markdownEditor.readOnly = sourceReadOnly; @@ -22531,9 +22653,10 @@ ${selector} .arrowheadPath { if (markdownFormatToolbar) { markdownFormatToolbar.classList.toggle('is-live-view-only', sourceReadOnly); - markdownFormatToolbar.querySelectorAll('[data-md-action]').forEach(function(button) { + markdownFormatToolbar.querySelectorAll('[data-md-action], [data-toolbar-menu-toggle]').forEach(function(button) { const action = button.getAttribute('data-md-action'); - const shouldDisable = sourceReadOnly && isLiveMutatingAction(action); + const isMenuToggle = button.hasAttribute('data-toolbar-menu-toggle'); + const shouldDisable = sourceReadOnly && (isMenuToggle || isLiveMutatingAction(action)); if (shouldDisable) { button.dataset.liveViewOnlyDisabled = 'true'; button.disabled = true; @@ -22550,8 +22673,9 @@ ${selector} .arrowheadPath { updateUndoRedoButtons(); } } + updateSyncToggleVisibility(currentViewMode); updateDocumentToolbarAvailability(hasActiveOpenDocument()); - updateReleaseNotesActionAvailability(releaseNotesActive); + if (releaseNotesActive) updateReleaseNotesActionAvailability(true); } function getLiveRoomSocketUrl(roomId, secret, auth) { @@ -24464,6 +24588,10 @@ ${selector} .arrowheadPath { if (isCmdOrCtrl && e.shiftKey && !e.altKey && key === 'c') { e.preventDefault(); + if (isReleaseNotesActive()) { + announceToScreenReader('Release notes do not expose Markdown source.'); + return; + } const focusedEditor = document.activeElement === documentSplitEditor ? documentSplitEditor : markdownEditor; const selected = focusedEditor.value.slice(focusedEditor.selectionStart, focusedEditor.selectionEnd); copyTextToClipboard(selected || focusedEditor.value) diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css index fd95dc38..98aab8dc 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -360,11 +360,10 @@ body { cursor: pointer; } -.document-sidebar-filters { +.segmented-control { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2px; - margin: 0 8px 7px; padding: 3px; border: 1px solid var(--border-color); border-radius: 6px; @@ -372,6 +371,10 @@ body { box-shadow: inset 0 1px 1px rgba(31, 35, 40, 0.03); } +.document-sidebar-filters { + margin: 0 8px 7px; +} + /* Bootstrap Icons 1.11.3 GitHub mark: the only branded, non-Lucide UI icon. */ .bi { display: inline-block; @@ -394,7 +397,7 @@ body { mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0iYmkgYmktZ2l0aHViIiB2aWV3Qm94PSIwIDAgMTYgMTYiPgogIDxwYXRoIGQ9Ik04IDBDMy41OCAwIDAgMy41OCAwIDhjMCAzLjU0IDIuMjkgNi41MyA1LjQ3IDcuNTkuNC4wNy41NS0uMTcuNTUtLjM4IDAtLjE5LS4wMS0uODItLjAxLTEuNDktMi4wMS4zNy0yLjUzLS40OS0yLjY5LS45NC0uMDktLjIzLS40OC0uOTQtLjgyLTEuMTMtLjI4LS4xNS0uNjgtLjUyLS4wMS0uNTMuNjMtLjAxIDEuMDguNTggMS4yMy44Mi43MiAxLjIxIDEuODcuODcgMi4zMy42Ni4wNy0uNTIuMjgtLjg3LjUxLTEuMDctMS43OC0uMi0zLjY0LS44OS0zLjY0LTMuOTUgMC0uODcuMzEtMS41OS44Mi0yLjE1LS4wOC0uMi0uMzYtMS4wMi4wOC0yLjEyIDAgMCAuNjctLjIxIDIuMi44Mi42NC0uMTggMS4zMi0uMjcgMi0uMjdzMS4zNi4wOSAyIC4yN2MxLjUzLTEuMDQgMi4yLS44MiAyLjItLjgyLjQ0IDEuMS4xNiAxLjkyLjA4IDIuMTIuNTEuNTYuODIgMS4yNy44MiAyLjE1IDAgMy4wNy0xLjg3IDMuNzUtMy42NSAzLjk1LjI5LjI1LjU0LjczLjU0IDEuNDggMCAxLjA3LS4wMSAxLjkzLS4wMSAyLjIgMCAuMjEuMTUuNDYuNTUuMzhBOC4wMSA4LjAxIDAgMCAwIDE2IDhjMC00LjQyLTMuNTgtOC04LTgiLz4KPC9zdmc+"); } -.document-filter-btn { +.segmented-control-btn { display: flex; align-items: center; justify-content: center; @@ -403,12 +406,22 @@ body { min-height: 24px; gap: 5px; padding: 3px 6px; + border: 1px solid transparent; border-radius: 4px; + background: transparent; color: var(--text-secondary); + cursor: pointer; + font: inherit; font-size: 11px; line-height: 1.2; text-align: center; white-space: nowrap; + transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease; +} + +.segmented-control-btn:hover:not(.is-active) { + background: var(--button-hover); + color: var(--text-color); } .document-filter-btn i { @@ -418,7 +431,7 @@ body { text-align: center; } -.document-filter-btn.is-active { +.segmented-control-btn.is-active { background: var(--bg-color); color: var(--text-color); font-weight: 500; @@ -2218,10 +2231,6 @@ body.document-sidebar-collapsed .document-sidebar { gap: 5px; } -.header-right .header-command-btn .btn-text { - display: inline; -} - .header-command-btn { width: auto; padding-inline: 10px; @@ -2235,6 +2244,19 @@ body.document-sidebar-collapsed .document-sidebar { display: inline-flex; } +.header-icon-dropdown .dropdown-toggle::after { + display: none; +} + +.header-toolbar-divider { + width: 1px; + height: 20px; + flex: 0 0 1px; + margin-inline: 2px; + background: var(--border-color); + opacity: 0.72; +} + .app-command-menu.header-new-menu, .app-command-menu.header-export-menu { width: max-content; @@ -2283,6 +2305,71 @@ body.document-sidebar-collapsed .document-sidebar { line-height: 1; } +.header-icon-command.is-active, +.header-icon-command.is-active:hover, +.header-icon-command.is-live-active, +.header-icon-command.is-live-active:hover, +.header-icon-command.sync-active, +.header-icon-command.sync-active:hover, +.header-icon-dropdown.show > .header-icon-command:not(:disabled):not([aria-disabled="true"]), +.header-icon-dropdown > .header-icon-command.show:not(:disabled):not([aria-disabled="true"]), +.settings-dropdown.show > .header-icon-command:not(:disabled):not([aria-disabled="true"]), +.settings-dropdown > .header-icon-command.show:not(:disabled):not([aria-disabled="true"]) { + border-color: color-mix(in srgb, var(--accent-color) 28%, var(--border-color)); + background: color-mix(in srgb, var(--accent-color) 10%, transparent); + color: var(--accent-color); +} + +.header-icon-command.sync-active:disabled, +.header-icon-command.sync-active[aria-disabled="true"] { + border-color: transparent; + background: transparent; + color: var(--text-color); + box-shadow: none; +} + +.header-icon-command#review-toggle:has(.review-toolbar-count:not([hidden])) { + width: auto; + padding-inline: 6px; +} + +.header-view-toolbar { + flex: 0 0 auto; +} + +.header-view-toolbar .view-toggle-btn { + width: 27px; + min-width: 27px; + height: 26px; + padding: 0; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--text-secondary); + box-shadow: none; +} + +.header-view-toolbar .view-toggle-btn:hover, +.header-view-toolbar .view-toggle-btn:focus-visible { + border: 0; + background: var(--toolbar-control-hover); + color: var(--text-color); +} + +.header-view-toolbar .view-toggle-btn.is-active, +.header-view-toolbar .view-toggle-btn.is-active:hover { + border: 0; + background: var(--segmented-surface); + color: var(--accent-color); + box-shadow: var(--segmented-shadow); +} + +.header-view-toolbar .view-toggle-btn:disabled, +.header-view-toolbar .view-toggle-btn[aria-disabled="true"] { + background: transparent; + box-shadow: none; +} + .settings-dropdown { display: inline-flex; } @@ -2433,8 +2520,8 @@ body.document-sidebar-collapsed .document-sidebar { .settings-switch { position: relative; - width: 32px; - height: 18px; + width: 40px; + height: 22px; border-radius: 999px; background: var(--button-active); transition: background-color 0.16s ease; @@ -2444,20 +2531,46 @@ body.document-sidebar-collapsed .document-sidebar { position: absolute; top: 3px; left: 3px; - width: 12px; - height: 12px; + width: 16px; + height: 16px; border-radius: 50%; background: var(--text-secondary); transition: transform 0.16s ease, background-color 0.16s ease; } +.themed-settings-switch > span { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-color); + background: var(--bg-color); + transition: color 0.16s ease, transform 0.16s ease; +} + +.settings-switch-icon { + width: 12px; + height: 12px; + color: currentColor; + font-size: 12px; + filter: + drop-shadow(0.35px 0 0 currentColor) + drop-shadow(-0.35px 0 0 currentColor) + drop-shadow(0 0.35px 0 currentColor) + drop-shadow(0 -0.35px 0 currentColor); +} + .settings-menu-item.is-active .settings-switch { background: var(--accent-color); } .settings-menu-item.is-active .settings-switch > span { background: #fff; - transform: translateX(14px); + transform: translateX(18px); +} + +.settings-menu-item.is-active .themed-settings-switch > span { + color: var(--text-color); + background: var(--bg-color); } .settings-language-dropdown { @@ -2800,46 +2913,10 @@ body.document-sidebar-collapsed .document-sidebar { flex-shrink: 0; } -.markdown-toolbar-group--utilities { - position: sticky; - z-index: 6; - inset-inline-end: 0; - margin-inline-start: auto; - border-right: 0; - background: var(--toolbar-surface); -} - -.markdown-toolbar-group--document { - gap: 2px; - padding-inline-start: 6px; -} - .workspace-format-actions { padding-inline: 5px; } -.markdown-toolbar-group--document .tool-button { - flex: 0 0 auto; - width: 30px; - height: 30px; - min-width: 30px; - padding: 0; - border-color: transparent; - border-radius: 7px; - background: transparent; -} - -.markdown-toolbar-group--document .tool-button:hover, -.markdown-toolbar-group--document .tool-button:focus-visible { - border-color: transparent; - background: var(--toolbar-control-hover); -} - -.markdown-toolbar-group--document #review-toggle:has(.review-toolbar-count:not([hidden])) { - width: auto; - padding-inline: 5px; -} - .document-actions-menu { width: 280px; min-width: 280px; @@ -2860,14 +2937,6 @@ body.document-sidebar-collapsed .document-sidebar { z-index: var(--layer-menu); } -.document-command-divider { - width: 1px; - height: 18px; - margin-inline: 4px; - background: var(--border-color); - opacity: 0.75; -} - .markdown-view-toolbar { display: inline-flex; align-items: center; @@ -2879,44 +2948,6 @@ body.document-sidebar-collapsed .document-sidebar { background: var(--segmented-bg); } -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn { - width: 27px; - height: 26px; - min-width: 27px; - border: 0; - border-radius: 7px; - background: transparent; - color: var(--text-secondary); - box-shadow: none; -} - -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn:hover, -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn:focus-visible { - border: 0; - background: var(--toolbar-control-hover); - color: var(--text-color); -} - -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn.is-active, -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn.is-active:hover { - border: 0; - background: var(--segmented-surface); - color: var(--accent-color); - box-shadow: var(--segmented-shadow); -} - -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn:disabled, -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn[aria-disabled="true"] { - background: transparent; - box-shadow: none; -} - -.markdown-toolbar-group--utilities .tool-button { - flex: 0 0 auto; - height: 30px; - min-width: 30px; -} - .markdown-toolbar-group:first-child { padding-left: 0; } @@ -3017,17 +3048,6 @@ body.document-sidebar-collapsed .document-sidebar { gap: 5px; } -.markdown-tool-select--insert { - width: 30px; - min-width: 30px; - gap: 0; - padding: 0; -} - -.markdown-tool-select--insert .lucide-ellipsis { - font-size: var(--ui-icon-size-lg); -} - .markdown-tool-select-label { overflow: hidden; max-width: 48px; @@ -3680,12 +3700,6 @@ a:focus { min-width: 44px; } - .markdown-tool-select--insert { - width: 30px !important; - min-width: 30px; - padding-inline: 0 !important; - } - .markdown-format-toolbar { height: 40px !important; } @@ -3699,23 +3713,6 @@ a:focus { height: 40px !important; } - .markdown-toolbar-group--document .tool-button { - width: 30px; - height: 30px; - min-width: 30px; - padding: 0; - } - - .markdown-view-toolbar { - height: 32px; - } - - .markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn { - width: 27px; - height: 26px; - min-width: 27px; - } - .document-action-item { min-height: 44px; } @@ -3812,16 +3809,6 @@ a:focus { gap: 8px; } -.app-brand-icon { - display: block; - flex: 0 0 32px; - width: 32px; - height: 32px; - border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); - border-radius: 8px; - object-fit: cover; -} - .app-brand-copy { display: flex; min-width: 0; @@ -4061,15 +4048,6 @@ a:focus { scroll-padding-inline: 6px; } - .markdown-toolbar-group--utilities { - position: static; - margin-inline-start: 0; - } - - .markdown-tool-select--insert { - min-width: 30px; - } - .markdown-tool-menu-item, .tab-menu-item, .header-command-menu .app-menu-item { @@ -7794,26 +7772,72 @@ html[lang="ko"] .markdown-body h3 { margin-bottom: 0.6em; } -/* Consistent, position-safe menu motion. Popper owns menu transforms. */ +/* Consistent, position-safe menu motion. Popper owns transform; the individual + translate/scale properties compose with it without disturbing placement. */ @keyframes app-menu-fade { from { opacity: 0; } to { opacity: 1; } } .dropdown-menu { + --app-menu-shift-x: 0px; + --app-menu-shift-y: -4px; opacity: 0; display: block; visibility: hidden; - transition: opacity 0.15s ease-out, visibility 0.15s ease-out; + pointer-events: none; + translate: var(--app-menu-shift-x) var(--app-menu-shift-y); + scale: 0.985; + transform-origin: top center; + transition: + opacity 0.18s cubic-bezier(0.16, 1, 0.3, 1), + translate 0.18s cubic-bezier(0.16, 1, 0.3, 1), + scale 0.18s cubic-bezier(0.16, 1, 0.3, 1), + visibility 0s linear 0.18s; +} + +.dropdown-menu[data-popper-placement^="top"] { + --app-menu-shift-y: 4px; + transform-origin: bottom center; +} + +.dropdown-menu[data-popper-placement^="left"] { + --app-menu-shift-x: 4px; + --app-menu-shift-y: 0px; + transform-origin: center right; +} + +.dropdown-menu[data-popper-placement^="right"] { + --app-menu-shift-x: -4px; + --app-menu-shift-y: 0px; + transform-origin: center left; } .dropdown-menu.show { opacity: 1; visibility: visible; + pointer-events: auto; + translate: 0 0; + scale: 1; + transition-delay: 0s; +} + +.dropdown-menu.show.is-closing { + opacity: 0; + visibility: visible; + pointer-events: none; + translate: var(--app-menu-shift-x) var(--app-menu-shift-y); + scale: 0.985; + transition-duration: 0.12s; + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); } @media (prefers-reduced-motion: reduce) { - .dropdown-menu { + .dropdown-menu, + .dropdown-menu.show, + .dropdown-menu.show.is-closing { + translate: 0 0; + scale: 1; transition: none; } @@ -9808,9 +9832,11 @@ html[data-theme="dark"] .mermaid svg { flex-direction: column; overflow: hidden; color: var(--text-color); - background: var(--bg-color); - border-inline-start: 1px solid var(--border-color); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25); + background: var(--menu-surface); + border-inline-start: 1px solid var(--menu-border); + box-shadow: var(--menu-shadow); + font-size: var(--ui-font-md); + animation: review-panel-enter 180ms ease-out; } .review-panel-backdrop { @@ -9820,25 +9846,27 @@ html[data-theme="dark"] .mermaid svg { width: 100%; height: 100%; border: 0; - background: rgba(31, 35, 40, 0.38); + background: color-mix(in srgb, var(--text-color) 36%, transparent); cursor: default; + animation: review-backdrop-enter 140ms ease-out; } .review-panel-header { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; - gap: 16px; - padding: 16px; + gap: 12px; + min-height: 50px; + padding: 7px 9px 7px 12px; border-bottom: 1px solid var(--border-color); } .review-panel-eyebrow { - margin: 0 0 3px; - color: var(--accent-color); - font-size: 0.7rem; + margin: 0 0 4px; + color: var(--text-secondary); + font-size: var(--ui-font-sm); font-weight: 700; - letter-spacing: 0.08em; + letter-spacing: 0.06em; text-transform: uppercase; } @@ -9851,18 +9879,20 @@ html[data-theme="dark"] .mermaid svg { } .review-panel-header h2 { - font-size: 1rem; + font-size: var(--ui-font-lg); + line-height: 1.25; } .review-panel-summary { - margin: 4px 0 0; + margin: 2px 0 0; color: var(--text-secondary); - font-size: 0.78rem; + font-size: var(--ui-font-sm); + line-height: 1.35; } .review-icon-btn { - width: 32px; - height: 32px; + width: 30px; + height: 30px; flex: 0 0 auto; display: inline-flex; align-items: center; @@ -9870,13 +9900,18 @@ html[data-theme="dark"] .mermaid svg { color: var(--text-secondary); background: transparent; border: 1px solid transparent; - border-radius: 6px; + border-radius: var(--menu-item-radius); cursor: pointer; + transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease; +} + +.review-icon-btn i { + font-size: var(--ui-icon-size-md); } .review-icon-btn:hover:not(:disabled) { color: var(--text-color); - background: var(--button-hover); + background: var(--toolbar-control-hover); border-color: var(--border-color); } @@ -9887,7 +9922,8 @@ html[data-theme="dark"] .mermaid svg { } .review-icon-btn:disabled { - opacity: 0.45; + color: color-mix(in srgb, var(--text-secondary) 50%, transparent); + opacity: 0.55; cursor: not-allowed; } @@ -9895,8 +9931,9 @@ html[data-theme="dark"] .mermaid svg { display: flex; align-items: center; justify-content: space-between; - gap: 12px; - padding: 10px 14px; + gap: 8px; + padding: 7px 8px; + background: var(--header-bg); border-bottom: 1px solid var(--border-color); } @@ -9912,47 +9949,66 @@ html[data-theme="dark"] .mermaid svg { display: flex; flex-direction: column; overflow-y: auto; + background: var(--menu-surface); +} + +.review-panel-body:has(.review-composer:not([hidden])) .review-empty-state { + display: none; +} + +.review-filter-group { + width: 180px; + flex: 0 0 auto; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 3px; + margin: 0; + padding: 3px; +} + +.review-filter-btn { + width: 100%; + min-width: 0; + padding-inline: 0; } -.review-filter-group, .review-kind-group { display: inline-flex; align-items: center; padding: 2px; - background: var(--button-bg); + background: var(--segmented-bg); border: 1px solid var(--border-color); - border-radius: 8px; + border-radius: var(--menu-item-radius); } -.review-filter-btn, .review-kind-btn { color: var(--text-secondary); background: transparent; border: 0; - border-radius: 6px; + border-radius: calc(var(--menu-item-radius) - 2px); cursor: pointer; font-weight: 600; + transition: color 160ms ease, background-color 160ms ease, box-shadow 160ms ease; } -.review-filter-btn { - padding: 5px 10px; - font-size: 0.74rem; +.review-kind-btn:hover:not(.is-active) { + color: var(--text-color); + background: var(--toolbar-control-hover); } -.review-filter-btn.is-active, .review-kind-btn.is-active { - color: var(--accent-color); - background: color-mix(in srgb, var(--accent-color) 8%, transparent); - box-shadow: 0 1px 3px rgba(31, 35, 40, 0.12); + color: var(--bg-color); + background: var(--accent-color); + box-shadow: var(--segmented-shadow); } .review-composer { flex: 0 0 auto; - margin: 14px 14px 4px; - padding: 14px; - background: var(--header-bg); - border: 1px solid var(--border-color); - border-radius: 8px; + margin: 12px 12px 2px; + padding: 13px; + background: var(--menu-surface); + border: 1px solid var(--menu-border); + border-radius: var(--menu-radius); + box-shadow: 0 2px 8px color-mix(in srgb, var(--text-color) 10%, transparent); } .review-composer-heading { @@ -9963,15 +10019,20 @@ html[data-theme="dark"] .mermaid svg { } .review-composer-heading h3 { - font-size: 0.9rem; + font-size: var(--ui-font-lg); + line-height: 1.35; } .review-anchor-preview { display: -webkit-box; margin: 10px 0 12px; + padding: 8px 9px; overflow: hidden; color: var(--text-secondary); - font-size: 0.78rem; + background: var(--button-bg); + border-inline-start: 2px solid var(--border-color); + border-radius: 0 var(--menu-item-radius) var(--menu-item-radius) 0; + font-size: var(--ui-font-md); line-height: 1.45; -webkit-box-orient: vertical; -webkit-line-clamp: 2; @@ -9984,15 +10045,21 @@ html[data-theme="dark"] .mermaid svg { .review-kind-btn { flex: 1; + min-height: 32px; padding: 7px 8px; - font-size: 0.76rem; + font-size: var(--ui-font-md); +} + +.review-kind-btn i { + margin-inline-end: 4px; + font-size: var(--ui-icon-size-sm); } .review-feedback-label { display: block; margin-bottom: 6px; color: var(--text-color); - font-size: 0.76rem; + font-size: var(--ui-font-md); font-weight: 650; } @@ -10003,11 +10070,12 @@ html[data-theme="dark"] .mermaid svg { resize: vertical; color: var(--text-color); background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 7px; + border: 1px solid var(--menu-border); + border-radius: var(--menu-item-radius); font: inherit; - font-size: 0.82rem; + font-size: var(--ui-font-md); line-height: 1.45; + transition: border-color 160ms ease, box-shadow 160ms ease; } .review-feedback-input:focus { @@ -10026,47 +10094,69 @@ html[data-theme="dark"] .mermaid svg { .review-feedback-count { color: var(--text-secondary); - font-size: 0.7rem; + font-size: var(--ui-font-sm); + font-variant-numeric: tabular-nums; } .review-primary-btn { - min-height: 32px; - padding: 6px 12px; - color: var(--accent-color); - background: var(--button-bg); + min-height: 34px; + padding: 7px 13px; + color: var(--bg-color); + background: var(--accent-color); border: 1px solid var(--accent-color); - border-radius: 6px; + border-radius: var(--menu-item-radius); cursor: pointer; - font-size: 0.76rem; + font-size: var(--ui-font-md); font-weight: 650; + transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease; } .review-primary-btn:hover:not(:disabled) { - background: var(--button-hover); + background: color-mix(in srgb, var(--accent-color) 88%, var(--text-color)); + border-color: color-mix(in srgb, var(--accent-color) 88%, var(--text-color)); } .review-primary-btn:disabled { - opacity: 0.5; + color: var(--text-secondary); + background: var(--button-bg); + border-color: var(--border-color); + opacity: 0.62; cursor: not-allowed; } .review-list { flex: 0 0 auto; + display: grid; + gap: 10px; overflow: visible; - padding: 10px 14px 18px; + padding: 10px 12px 16px; } .review-thread { - margin-bottom: 10px; - padding: 12px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-inline-start: 3px solid var(--accent-color); - border-radius: 8px; + position: relative; + margin: 0; + padding: 0; + overflow: hidden; + background: var(--menu-surface); + border: 1px solid var(--menu-border); + border-radius: var(--menu-radius); + box-shadow: 0 1px 2px color-mix(in srgb, var(--text-color) 8%, transparent); + transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease; +} + +@media (hover: hover) { + .review-thread:hover { + border-color: color-mix(in srgb, var(--text-secondary) 42%, var(--menu-border)); + box-shadow: 0 3px 10px color-mix(in srgb, var(--text-color) 9%, transparent); + } } .review-thread.is-resolved { - opacity: 0.72; + background: color-mix(in srgb, var(--button-bg) 52%, var(--menu-surface)); +} + +.review-thread.is-resolved .review-thread-body { + color: var(--text-secondary); } .review-thread.is-orphaned { @@ -10081,8 +10171,12 @@ html[data-theme="dark"] .mermaid svg { } .review-thread-header { + min-height: 40px; justify-content: space-between; gap: 8px; + padding: 6px 7px 6px 11px; + background: color-mix(in srgb, var(--header-bg) 68%, var(--menu-surface)); + border-bottom: 1px solid var(--border-color); } .review-thread-meta { @@ -10090,47 +10184,62 @@ html[data-theme="dark"] .mermaid svg { gap: 6px; } +.review-thread-content { + padding: 10px 12px 11px; +} + .review-kind-label, .review-status-label { display: inline-flex; align-items: center; gap: 4px; - border-radius: 999px; - font-size: 0.67rem; - font-weight: 700; + font-size: var(--ui-font-sm); + font-weight: 650; line-height: 1; } .review-kind-label { - padding: 5px 7px; - color: var(--text-secondary); - background: var(--button-bg); - border: 1px solid var(--border-color); + color: var(--text-color); +} + +.review-kind-label i { + font-size: var(--ui-icon-size-sm); } .review-status-label { + padding-inline-start: 7px; color: var(--text-secondary); + border-inline-start: 1px solid var(--border-color); +} + +.review-status-label i { + font-size: var(--ui-icon-size-sm); } .review-thread-anchor { width: 100%; - margin: 9px 0 8px; - padding: 0; + margin: 0 0 10px; + padding: 7px 9px; overflow: hidden; color: var(--text-secondary); - background: transparent; - border: 0; + background: var(--button-bg); + border: 1px solid transparent; + border-inline-start-color: var(--border-color); + border-inline-start-width: 2px; + border-radius: 0 var(--menu-item-radius) var(--menu-item-radius) 0; cursor: pointer; - font-size: 0.72rem; - line-height: 1.35; + font-size: var(--ui-font-md); + line-height: 1.4; text-align: start; text-overflow: ellipsis; white-space: nowrap; + transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease; } .review-thread-anchor:hover:not(:disabled) { - color: var(--accent-color); - text-decoration: underline; + color: var(--text-color); + background: var(--button-hover); + border-color: var(--border-color); } .review-thread-anchor:disabled { @@ -10141,7 +10250,7 @@ html[data-theme="dark"] .mermaid svg { .review-thread-body { margin: 0; color: var(--text-color); - font-size: 0.82rem; + font-size: var(--ui-font-md); line-height: 1.5; overflow-wrap: anywhere; white-space: pre-wrap; @@ -10150,37 +10259,64 @@ html[data-theme="dark"] .mermaid svg { .review-thread-dates { display: flex; flex-wrap: wrap; - gap: 4px 12px; - margin-top: 9px; + gap: 6px 12px; + margin-top: 11px; + padding-top: 9px; + border-top: 1px solid var(--border-color); } .review-thread-time { + display: inline-flex; + align-items: center; + gap: 4px; color: var(--text-secondary); - font-size: 0.67rem; + font-size: var(--ui-font-sm); + line-height: 1.3; +} + +.review-thread-time i { + flex: 0 0 auto; + font-size: var(--ui-icon-size-xs); } .review-thread-actions { + flex: 0 0 auto; justify-content: flex-end; - gap: 4px; - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid var(--border-color); + gap: 2px; + margin: 0; + padding: 2px; + background: var(--menu-surface); + border: 1px solid var(--border-color); + border-radius: var(--menu-item-radius); } .review-thread-action { - padding: 5px 7px; + width: 24px; + height: 24px; + min-height: 24px; + flex: 0 0 24px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; color: var(--text-secondary); background: transparent; - border: 0; - border-radius: 5px; + border: 1px solid transparent; + border-radius: calc(var(--menu-item-radius) - 3px); cursor: pointer; - font-size: 0.7rem; - font-weight: 600; + font-size: var(--ui-icon-size-sm); + line-height: 1; + transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease; +} + +.review-thread-action i { + font-size: var(--ui-icon-size-md); } .review-thread-action:hover { color: var(--text-color); - background: var(--button-hover); + background: var(--toolbar-control-hover); + border-color: transparent; } .review-thread-action.is-danger:hover { @@ -10189,27 +10325,34 @@ html[data-theme="dark"] .mermaid svg { .review-empty-state { margin: auto; - padding: 30px 26px; + padding: 36px 26px; color: var(--text-secondary); text-align: center; } .review-empty-state i { - display: block; - margin-bottom: 10px; - color: var(--accent-color); - font-size: 1.7rem; + width: 42px; + height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; + color: var(--text-secondary); + background: var(--button-bg); + border: 1px solid var(--border-color); + border-radius: 50%; + font-size: var(--ui-icon-size-xl); } .review-empty-state h3 { - font-size: 0.9rem; + font-size: var(--ui-font-md); } .review-empty-state p { margin: 7px auto 0; max-width: 270px; - font-size: 0.76rem; - line-height: 1.45; + font-size: var(--ui-font-md); + line-height: 1.5; } #markdown-preview .review-target { @@ -10300,6 +10443,24 @@ html[data-theme="dark"] .mermaid svg { .review-thread.is-review-thread-active { border-color: var(--accent-color); + background: color-mix(in srgb, var(--accent-color) 7%, var(--menu-surface)); + box-shadow: inset 3px 0 0 var(--accent-color); +} + +@keyframes review-panel-enter { + from { + opacity: 0; + transform: translateX(12px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes review-backdrop-enter { + from { opacity: 0; } + to { opacity: 1; } } .review-filter-btn:focus-visible, @@ -10354,6 +10515,17 @@ html[data-theme="dark"] .mermaid svg { .review-thread-action { min-height: 40px; } + + .review-thread-action { + min-width: 40px; + width: 40px; + height: 40px; + flex-basis: 40px; + } + + .review-thread-actions { + gap: 6px; + } } @media (max-width: 767px) { @@ -10364,7 +10536,7 @@ html[data-theme="dark"] .mermaid svg { height: min(68%, 620px); border-inline-start: 0; border-top: 1px solid var(--border-color); - box-shadow: 0 -12px 32px rgba(31, 35, 40, 0.2); + box-shadow: var(--menu-shadow); } .content-container.is-reviewing .preview-pane { @@ -10390,6 +10562,12 @@ html[data-theme="dark"] .mermaid svg { height: 44px; } + .review-panel-header .review-icon-btn, + .review-composer-heading .review-icon-btn { + width: 44px; + height: 44px; + } + .review-filter-btn, .review-kind-btn, .review-primary-btn, @@ -10407,7 +10585,14 @@ html[data-theme="dark"] .mermaid svg { } .review-thread-action { - padding-inline: 10px; + min-width: 44px; + width: 44px; + height: 44px; + flex-basis: 44px; + } + + .review-thread-actions { + gap: 8px; } .review-target-button { @@ -10437,7 +10622,10 @@ html[data-theme="dark"] .mermaid svg { } @media (prefers-reduced-motion: reduce) { + .review-panel, + .review-panel-backdrop, .review-target-button { + animation: none; transition: none; } } @@ -11177,7 +11365,22 @@ html[data-theme="dark"] .mermaid svg { .mobile-settings-toggle.is-active .settings-switch > span { background: #fff; - transform: translateX(14px); + transform: translateX(18px); +} + +.mobile-settings-toggle.is-active .themed-settings-switch > span { + color: var(--text-color); + background: var(--bg-color); +} + +html[data-theme="dark"] .settings-menu-item.is-active .settings-switch, +html[data-theme="dark"] .mobile-settings-toggle.is-active .settings-switch { + background: var(--button-active); +} + +#private-mode-toggle.is-active .settings-switch, +#mobile-private-mode-toggle.is-active .settings-switch { + background: var(--accent-color); } .mobile-menu-command.is-active .mobile-menu-item-label > i, @@ -11749,6 +11952,18 @@ html[data-theme="dark"] .mermaid svg { /* ======================================== VERSIONED RELEASE NOTES ======================================== */ +body.release-notes-active #review-panel, +body.release-notes-active #review-panel-backdrop, +body.release-notes-active #review-composer, +body.release-notes-active #review-pins-layer { + display: none !important; +} + +body.release-notes-active .app-status-bar, +body.release-notes-active .mobile-stats-container { + opacity: 0.5; +} + .tab-item.release-notes-tab { width: max-content; min-width: max-content; diff --git a/index.html b/index.html index b57ca427..43c7c263 100644 --- a/index.html +++ b/index.html @@ -134,7 +134,6 @@
    -
    - - -
    +
    -
    -
    +
    +
    +
    + + +
    -
    - - - - - +
    + +
    -
    +
    + + -
    - - -
    +
    +
    + + + + +
    -
    - - - - -
    - - - -
    -
    @@ -884,8 +882,8 @@
  • Use the view buttons in the toolbar to switch between Editor, Split, and Preview modes.
  • Sync scrolling is available in Split view to keep the editor and preview aligned.
  • Review opens a read-only preview where you can comment on or suggest changes to headings, paragraphs, code blocks, and diagrams.
  • -
  • New, Share, Live Share, Report an Issue, About, and Settings are available in the header toolbar.
  • -
  • Sync, Copy, Review, view modes, and Export stay with the document formatting toolbar.
  • +
  • View modes, New, Copy, Sync, Review, Share, Live Share, and Export are grouped in the header toolbar.
  • +
  • Report an Issue, About, and Settings follow the document actions in the header.
  • @@ -1598,10 +1596,10 @@

    Comments & suggestions

    -
    - - - +
    + + +
    '; - } else { - const duplicateAction = isShareSnapshotTab(tab) - ? '' - : ''; - const downloadAction = isShareSnapshotTab(tab) - ? '' - : ''; - const favoriteAction = isShareSnapshotTab(tab) - ? '' - : ''; - const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); - const splitAction = isCombinedSplitTab - ? '' - : tabs.length > 1 - ? '' - : ''; - dropdown.innerHTML = - '' + - duplicateAction + - favoriteAction + - splitAction + - downloadAction + - '' + - ''; - } + const duplicateAction = isShareSnapshotTab(tab) + ? '' + : ''; + const downloadAction = isShareSnapshotTab(tab) + ? '' + : ''; + const favoriteAction = isShareSnapshotTab(tab) + ? '' + : ''; + const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); + const splitAction = isCombinedSplitTab + ? '' + : tabs.length > 1 + ? '' + : ''; + dropdown.innerHTML = + '' + + duplicateAction + + favoriteAction + + splitAction + + downloadAction + + '' + + ''; menuBtn.addEventListener('click', function(e) { e.preventDefault(); @@ -8196,7 +8234,7 @@ document.addEventListener("DOMContentLoaded", async function () { }, { id: 'all', icon: 'lucide-square-x', label: 'Close all' }]; - const actions = isReleaseNotesTab(tab) ? closeActions : [{ + const actions = isReleaseNotesTab(tab) ? closeActions.slice(0, 1) : [{ id: isCombinedSplitTab ? 'split-close' : 'split', icon: isCombinedSplitTab ? 'lucide-panel-right-close' : 'lucide-columns-2', label: isCombinedSplitTab ? 'Exit split view' : 'Open in split view', @@ -8333,7 +8371,9 @@ document.addEventListener("DOMContentLoaded", async function () { titleSpan.title = getDocumentTabHoverTitle(tab); } - const tabMenu = createTabActionMenu(tab, { menuIdPrefix: 'desktop-tab-menu' }); + const tabMenu = isReleaseNotesTab(tab) + ? null + : createTabActionMenu(tab, { menuIdPrefix: 'desktop-tab-menu' }); const closeButton = document.createElement('button'); closeButton.type = 'button'; @@ -8351,7 +8391,7 @@ document.addEventListener("DOMContentLoaded", async function () { item.appendChild(fileIcon); item.appendChild(titleSpan); - item.appendChild(tabMenu.button); + if (tabMenu) item.appendChild(tabMenu.button); item.appendChild(closeButton); item.addEventListener('contextmenu', function(event) { @@ -8913,6 +8953,7 @@ document.addEventListener("DOMContentLoaded", async function () { saveSecondarySplitState(); closeReviewComposer(); clearReviewDecorations(); + if (isReleaseNotesTab(tab) && reviewModeActive) setReviewMode(false); // Clear typing timeout and reset tracking for the new tab if (typingTimeout) { @@ -8923,11 +8964,14 @@ document.addEventListener("DOMContentLoaded", async function () { pendingState = null; activeTabId = tabId; + const releaseNotesActive = isReleaseNotesTab(tab); + document.body.classList.toggle('release-notes-active', releaseNotesActive); if (swapSplitPanes) secondarySplitTabId = previousActiveTabId; saveActiveTabId(activeTabId); updateNoOpenDocumentState(); - selectedDocumentId = tabId; - setSingleDocumentTreeSelection('document', tabId); + selectedDocumentId = releaseNotesActive ? null : tabId; + if (releaseNotesActive) clearDocumentTreeSelection({ announce: false }); + else setSingleDocumentTreeSelection('document', tabId); tab.lastOpenedAt = Date.now(); if (!isTemporaryDocument(tab)) saveTabsToStorage(tabs, [tab.id]); markdownEditor.value = tab.content; @@ -18764,6 +18808,55 @@ ${selector} .arrowheadPath { }); } + function initDropdownMenuMotion() { + if (!window.bootstrap || !window.bootstrap.Dropdown) return; + + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + const closeDurationMs = 120; + + document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(function(toggle) { + if (toggle.dataset.dropdownMotionReady === 'true') return; + const dropdown = toggle.closest('.dropdown'); + const menu = dropdown && Array.from(dropdown.children).find(function(child) { + return child.classList && child.classList.contains('dropdown-menu'); + }); + if (!dropdown || !menu) return; + + toggle.dataset.dropdownMotionReady = 'true'; + const instance = window.bootstrap.Dropdown.getOrCreateInstance(toggle); + let closeTimer = null; + let allowImmediateHide = false; + + toggle.addEventListener('show.bs.dropdown', function() { + if (closeTimer !== null) window.clearTimeout(closeTimer); + closeTimer = null; + allowImmediateHide = false; + menu.classList.remove('is-closing'); + }); + + toggle.addEventListener('hide.bs.dropdown', function(event) { + if (allowImmediateHide || reducedMotion.matches) return; + event.preventDefault(); + if (closeTimer !== null) return; + + menu.classList.add('is-closing'); + closeTimer = window.setTimeout(function() { + closeTimer = null; + allowImmediateHide = true; + instance.hide(); + allowImmediateHide = false; + }, closeDurationMs); + }); + + toggle.addEventListener('hidden.bs.dropdown', function() { + if (closeTimer !== null) window.clearTimeout(closeTimer); + closeTimer = null; + allowImmediateHide = false; + menu.classList.remove('is-closing'); + }); + }); + } + // Story 1.3: Resize Divider Functions function initResizer() { if (!resizeDivider) return; @@ -19387,6 +19480,7 @@ ${selector} .arrowheadPath { initMarkdownFormatToolbar(); initToolbarDropdownPortals(); + initDropdownMenuMotion(); initFindReplaceModal(); initAppModals(); document.addEventListener('fullscreenchange', function() { @@ -21679,6 +21773,7 @@ ${selector} .arrowheadPath { }); copyMarkdownButton.addEventListener("click", async function () { + if (isReleaseNotesActive()) return; if (blockShareSnapshotSourceAccess()) return; try { await copyTextToClipboard(markdownEditor.value); @@ -22335,6 +22430,9 @@ ${selector} .arrowheadPath { function updateReleaseNotesActionAvailability(releaseNotesActive) { const actions = [ + toggleSyncButton, + copyMarkdownButton, + reviewToggle, exportDropdown, exportMd, exportHtml, @@ -22347,10 +22445,21 @@ ${selector} .arrowheadPath { mobileExportHtml, mobileExportPdf, mobileExportPng, + mobileToggleSyncButton, + mobileCopyMarkdownButton, + mobileReviewToggle, mobileShareButton, mobileLiveShareButton ]; + viewModeButtons.forEach(function(action) { actions.push(action); }); + mobileViewModeButtons.forEach(function(action) { actions.push(action); }); + if (markdownFormatToolbar) { + markdownFormatToolbar.querySelectorAll('button').forEach(function(action) { actions.push(action); }); + markdownFormatToolbar.classList.toggle('is-release-notes-disabled', releaseNotesActive); + markdownFormatToolbar.setAttribute('aria-disabled', releaseNotesActive ? 'true' : 'false'); + } + actions.forEach(function(action) { if (!action) return; if (releaseNotesActive) { @@ -22361,11 +22470,15 @@ ${selector} .arrowheadPath { ? action.getAttribute('tabindex') : '__missing__'; } + action.dataset.releaseNotesOriginalTitle = action.hasAttribute('title') + ? action.getAttribute('title') + : '__missing__'; } action.dataset.releaseNotesDisabled = 'true'; if ('disabled' in action) action.disabled = true; action.classList.add('disabled'); action.setAttribute('aria-disabled', 'true'); + action.setAttribute('title', 'Unavailable in Release Notes'); if (action.tagName === 'A') action.setAttribute('tabindex', '-1'); return; } @@ -22381,9 +22494,16 @@ ${selector} .arrowheadPath { const remainsDisabled = wasDisabled || hasAnotherDisabledState; delete action.dataset.releaseNotesDisabled; delete action.dataset.releaseNotesWasDisabled; + const originalTitle = action.dataset.releaseNotesOriginalTitle; + delete action.dataset.releaseNotesOriginalTitle; if ('disabled' in action) action.disabled = remainsDisabled; action.classList.toggle('disabled', remainsDisabled); action.setAttribute('aria-disabled', remainsDisabled ? 'true' : 'false'); + if (originalTitle && originalTitle !== '__missing__') { + action.setAttribute('title', originalTitle); + } else { + action.removeAttribute('title'); + } if (action.tagName === 'A') { const originalTabindex = action.dataset.releaseNotesOriginalTabindex; delete action.dataset.releaseNotesOriginalTabindex; @@ -22413,6 +22533,8 @@ ${selector} .arrowheadPath { const liveShareDocumentActive = isLiveShareDocumentActive(); const liveShareGuestDocumentActive = liveShareDocumentActive && !isLiveShareHostDocumentActive(); const viewOnly = isLiveViewOnlyParticipant() || snapshotViewOnly || releaseNotesActive; + document.body.classList.toggle('release-notes-active', releaseNotesActive); + if (!releaseNotesActive) updateReleaseNotesActionAvailability(false); const sourceReadOnly = viewOnly || reviewModeActive; if (markdownEditor) { markdownEditor.readOnly = sourceReadOnly; @@ -22531,9 +22653,10 @@ ${selector} .arrowheadPath { if (markdownFormatToolbar) { markdownFormatToolbar.classList.toggle('is-live-view-only', sourceReadOnly); - markdownFormatToolbar.querySelectorAll('[data-md-action]').forEach(function(button) { + markdownFormatToolbar.querySelectorAll('[data-md-action], [data-toolbar-menu-toggle]').forEach(function(button) { const action = button.getAttribute('data-md-action'); - const shouldDisable = sourceReadOnly && isLiveMutatingAction(action); + const isMenuToggle = button.hasAttribute('data-toolbar-menu-toggle'); + const shouldDisable = sourceReadOnly && (isMenuToggle || isLiveMutatingAction(action)); if (shouldDisable) { button.dataset.liveViewOnlyDisabled = 'true'; button.disabled = true; @@ -22550,8 +22673,9 @@ ${selector} .arrowheadPath { updateUndoRedoButtons(); } } + updateSyncToggleVisibility(currentViewMode); updateDocumentToolbarAvailability(hasActiveOpenDocument()); - updateReleaseNotesActionAvailability(releaseNotesActive); + if (releaseNotesActive) updateReleaseNotesActionAvailability(true); } function getLiveRoomSocketUrl(roomId, secret, auth) { @@ -24464,6 +24588,10 @@ ${selector} .arrowheadPath { if (isCmdOrCtrl && e.shiftKey && !e.altKey && key === 'c') { e.preventDefault(); + if (isReleaseNotesActive()) { + announceToScreenReader('Release notes do not expose Markdown source.'); + return; + } const focusedEditor = document.activeElement === documentSplitEditor ? documentSplitEditor : markdownEditor; const selected = focusedEditor.value.slice(focusedEditor.selectionStart, focusedEditor.selectionEnd); copyTextToClipboard(selected || focusedEditor.value) diff --git a/styles.css b/styles.css index fd95dc38..98aab8dc 100644 --- a/styles.css +++ b/styles.css @@ -360,11 +360,10 @@ body { cursor: pointer; } -.document-sidebar-filters { +.segmented-control { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2px; - margin: 0 8px 7px; padding: 3px; border: 1px solid var(--border-color); border-radius: 6px; @@ -372,6 +371,10 @@ body { box-shadow: inset 0 1px 1px rgba(31, 35, 40, 0.03); } +.document-sidebar-filters { + margin: 0 8px 7px; +} + /* Bootstrap Icons 1.11.3 GitHub mark: the only branded, non-Lucide UI icon. */ .bi { display: inline-block; @@ -394,7 +397,7 @@ body { mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0iYmkgYmktZ2l0aHViIiB2aWV3Qm94PSIwIDAgMTYgMTYiPgogIDxwYXRoIGQ9Ik04IDBDMy41OCAwIDAgMy41OCAwIDhjMCAzLjU0IDIuMjkgNi41MyA1LjQ3IDcuNTkuNC4wNy41NS0uMTcuNTUtLjM4IDAtLjE5LS4wMS0uODItLjAxLTEuNDktMi4wMS4zNy0yLjUzLS40OS0yLjY5LS45NC0uMDktLjIzLS40OC0uOTQtLjgyLTEuMTMtLjI4LS4xNS0uNjgtLjUyLS4wMS0uNTMuNjMtLjAxIDEuMDguNTggMS4yMy44Mi43MiAxLjIxIDEuODcuODcgMi4zMy42Ni4wNy0uNTIuMjgtLjg3LjUxLTEuMDctMS43OC0uMi0zLjY0LS44OS0zLjY0LTMuOTUgMC0uODcuMzEtMS41OS44Mi0yLjE1LS4wOC0uMi0uMzYtMS4wMi4wOC0yLjEyIDAgMCAuNjctLjIxIDIuMi44Mi42NC0uMTggMS4zMi0uMjcgMi0uMjdzMS4zNi4wOSAyIC4yN2MxLjUzLTEuMDQgMi4yLS44MiAyLjItLjgyLjQ0IDEuMS4xNiAxLjkyLjA4IDIuMTIuNTEuNTYuODIgMS4yNy44MiAyLjE1IDAgMy4wNy0xLjg3IDMuNzUtMy42NSAzLjk1LjI5LjI1LjU0LjczLjU0IDEuNDggMCAxLjA3LS4wMSAxLjkzLS4wMSAyLjIgMCAuMjEuMTUuNDYuNTUuMzhBOC4wMSA4LjAxIDAgMCAwIDE2IDhjMC00LjQyLTMuNTgtOC04LTgiLz4KPC9zdmc+"); } -.document-filter-btn { +.segmented-control-btn { display: flex; align-items: center; justify-content: center; @@ -403,12 +406,22 @@ body { min-height: 24px; gap: 5px; padding: 3px 6px; + border: 1px solid transparent; border-radius: 4px; + background: transparent; color: var(--text-secondary); + cursor: pointer; + font: inherit; font-size: 11px; line-height: 1.2; text-align: center; white-space: nowrap; + transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease; +} + +.segmented-control-btn:hover:not(.is-active) { + background: var(--button-hover); + color: var(--text-color); } .document-filter-btn i { @@ -418,7 +431,7 @@ body { text-align: center; } -.document-filter-btn.is-active { +.segmented-control-btn.is-active { background: var(--bg-color); color: var(--text-color); font-weight: 500; @@ -2218,10 +2231,6 @@ body.document-sidebar-collapsed .document-sidebar { gap: 5px; } -.header-right .header-command-btn .btn-text { - display: inline; -} - .header-command-btn { width: auto; padding-inline: 10px; @@ -2235,6 +2244,19 @@ body.document-sidebar-collapsed .document-sidebar { display: inline-flex; } +.header-icon-dropdown .dropdown-toggle::after { + display: none; +} + +.header-toolbar-divider { + width: 1px; + height: 20px; + flex: 0 0 1px; + margin-inline: 2px; + background: var(--border-color); + opacity: 0.72; +} + .app-command-menu.header-new-menu, .app-command-menu.header-export-menu { width: max-content; @@ -2283,6 +2305,71 @@ body.document-sidebar-collapsed .document-sidebar { line-height: 1; } +.header-icon-command.is-active, +.header-icon-command.is-active:hover, +.header-icon-command.is-live-active, +.header-icon-command.is-live-active:hover, +.header-icon-command.sync-active, +.header-icon-command.sync-active:hover, +.header-icon-dropdown.show > .header-icon-command:not(:disabled):not([aria-disabled="true"]), +.header-icon-dropdown > .header-icon-command.show:not(:disabled):not([aria-disabled="true"]), +.settings-dropdown.show > .header-icon-command:not(:disabled):not([aria-disabled="true"]), +.settings-dropdown > .header-icon-command.show:not(:disabled):not([aria-disabled="true"]) { + border-color: color-mix(in srgb, var(--accent-color) 28%, var(--border-color)); + background: color-mix(in srgb, var(--accent-color) 10%, transparent); + color: var(--accent-color); +} + +.header-icon-command.sync-active:disabled, +.header-icon-command.sync-active[aria-disabled="true"] { + border-color: transparent; + background: transparent; + color: var(--text-color); + box-shadow: none; +} + +.header-icon-command#review-toggle:has(.review-toolbar-count:not([hidden])) { + width: auto; + padding-inline: 6px; +} + +.header-view-toolbar { + flex: 0 0 auto; +} + +.header-view-toolbar .view-toggle-btn { + width: 27px; + min-width: 27px; + height: 26px; + padding: 0; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--text-secondary); + box-shadow: none; +} + +.header-view-toolbar .view-toggle-btn:hover, +.header-view-toolbar .view-toggle-btn:focus-visible { + border: 0; + background: var(--toolbar-control-hover); + color: var(--text-color); +} + +.header-view-toolbar .view-toggle-btn.is-active, +.header-view-toolbar .view-toggle-btn.is-active:hover { + border: 0; + background: var(--segmented-surface); + color: var(--accent-color); + box-shadow: var(--segmented-shadow); +} + +.header-view-toolbar .view-toggle-btn:disabled, +.header-view-toolbar .view-toggle-btn[aria-disabled="true"] { + background: transparent; + box-shadow: none; +} + .settings-dropdown { display: inline-flex; } @@ -2433,8 +2520,8 @@ body.document-sidebar-collapsed .document-sidebar { .settings-switch { position: relative; - width: 32px; - height: 18px; + width: 40px; + height: 22px; border-radius: 999px; background: var(--button-active); transition: background-color 0.16s ease; @@ -2444,20 +2531,46 @@ body.document-sidebar-collapsed .document-sidebar { position: absolute; top: 3px; left: 3px; - width: 12px; - height: 12px; + width: 16px; + height: 16px; border-radius: 50%; background: var(--text-secondary); transition: transform 0.16s ease, background-color 0.16s ease; } +.themed-settings-switch > span { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-color); + background: var(--bg-color); + transition: color 0.16s ease, transform 0.16s ease; +} + +.settings-switch-icon { + width: 12px; + height: 12px; + color: currentColor; + font-size: 12px; + filter: + drop-shadow(0.35px 0 0 currentColor) + drop-shadow(-0.35px 0 0 currentColor) + drop-shadow(0 0.35px 0 currentColor) + drop-shadow(0 -0.35px 0 currentColor); +} + .settings-menu-item.is-active .settings-switch { background: var(--accent-color); } .settings-menu-item.is-active .settings-switch > span { background: #fff; - transform: translateX(14px); + transform: translateX(18px); +} + +.settings-menu-item.is-active .themed-settings-switch > span { + color: var(--text-color); + background: var(--bg-color); } .settings-language-dropdown { @@ -2800,46 +2913,10 @@ body.document-sidebar-collapsed .document-sidebar { flex-shrink: 0; } -.markdown-toolbar-group--utilities { - position: sticky; - z-index: 6; - inset-inline-end: 0; - margin-inline-start: auto; - border-right: 0; - background: var(--toolbar-surface); -} - -.markdown-toolbar-group--document { - gap: 2px; - padding-inline-start: 6px; -} - .workspace-format-actions { padding-inline: 5px; } -.markdown-toolbar-group--document .tool-button { - flex: 0 0 auto; - width: 30px; - height: 30px; - min-width: 30px; - padding: 0; - border-color: transparent; - border-radius: 7px; - background: transparent; -} - -.markdown-toolbar-group--document .tool-button:hover, -.markdown-toolbar-group--document .tool-button:focus-visible { - border-color: transparent; - background: var(--toolbar-control-hover); -} - -.markdown-toolbar-group--document #review-toggle:has(.review-toolbar-count:not([hidden])) { - width: auto; - padding-inline: 5px; -} - .document-actions-menu { width: 280px; min-width: 280px; @@ -2860,14 +2937,6 @@ body.document-sidebar-collapsed .document-sidebar { z-index: var(--layer-menu); } -.document-command-divider { - width: 1px; - height: 18px; - margin-inline: 4px; - background: var(--border-color); - opacity: 0.75; -} - .markdown-view-toolbar { display: inline-flex; align-items: center; @@ -2879,44 +2948,6 @@ body.document-sidebar-collapsed .document-sidebar { background: var(--segmented-bg); } -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn { - width: 27px; - height: 26px; - min-width: 27px; - border: 0; - border-radius: 7px; - background: transparent; - color: var(--text-secondary); - box-shadow: none; -} - -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn:hover, -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn:focus-visible { - border: 0; - background: var(--toolbar-control-hover); - color: var(--text-color); -} - -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn.is-active, -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn.is-active:hover { - border: 0; - background: var(--segmented-surface); - color: var(--accent-color); - box-shadow: var(--segmented-shadow); -} - -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn:disabled, -.markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn[aria-disabled="true"] { - background: transparent; - box-shadow: none; -} - -.markdown-toolbar-group--utilities .tool-button { - flex: 0 0 auto; - height: 30px; - min-width: 30px; -} - .markdown-toolbar-group:first-child { padding-left: 0; } @@ -3017,17 +3048,6 @@ body.document-sidebar-collapsed .document-sidebar { gap: 5px; } -.markdown-tool-select--insert { - width: 30px; - min-width: 30px; - gap: 0; - padding: 0; -} - -.markdown-tool-select--insert .lucide-ellipsis { - font-size: var(--ui-icon-size-lg); -} - .markdown-tool-select-label { overflow: hidden; max-width: 48px; @@ -3680,12 +3700,6 @@ a:focus { min-width: 44px; } - .markdown-tool-select--insert { - width: 30px !important; - min-width: 30px; - padding-inline: 0 !important; - } - .markdown-format-toolbar { height: 40px !important; } @@ -3699,23 +3713,6 @@ a:focus { height: 40px !important; } - .markdown-toolbar-group--document .tool-button { - width: 30px; - height: 30px; - min-width: 30px; - padding: 0; - } - - .markdown-view-toolbar { - height: 32px; - } - - .markdown-toolbar-group--document .markdown-view-toolbar .view-toggle-btn { - width: 27px; - height: 26px; - min-width: 27px; - } - .document-action-item { min-height: 44px; } @@ -3812,16 +3809,6 @@ a:focus { gap: 8px; } -.app-brand-icon { - display: block; - flex: 0 0 32px; - width: 32px; - height: 32px; - border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); - border-radius: 8px; - object-fit: cover; -} - .app-brand-copy { display: flex; min-width: 0; @@ -4061,15 +4048,6 @@ a:focus { scroll-padding-inline: 6px; } - .markdown-toolbar-group--utilities { - position: static; - margin-inline-start: 0; - } - - .markdown-tool-select--insert { - min-width: 30px; - } - .markdown-tool-menu-item, .tab-menu-item, .header-command-menu .app-menu-item { @@ -7794,26 +7772,72 @@ html[lang="ko"] .markdown-body h3 { margin-bottom: 0.6em; } -/* Consistent, position-safe menu motion. Popper owns menu transforms. */ +/* Consistent, position-safe menu motion. Popper owns transform; the individual + translate/scale properties compose with it without disturbing placement. */ @keyframes app-menu-fade { from { opacity: 0; } to { opacity: 1; } } .dropdown-menu { + --app-menu-shift-x: 0px; + --app-menu-shift-y: -4px; opacity: 0; display: block; visibility: hidden; - transition: opacity 0.15s ease-out, visibility 0.15s ease-out; + pointer-events: none; + translate: var(--app-menu-shift-x) var(--app-menu-shift-y); + scale: 0.985; + transform-origin: top center; + transition: + opacity 0.18s cubic-bezier(0.16, 1, 0.3, 1), + translate 0.18s cubic-bezier(0.16, 1, 0.3, 1), + scale 0.18s cubic-bezier(0.16, 1, 0.3, 1), + visibility 0s linear 0.18s; +} + +.dropdown-menu[data-popper-placement^="top"] { + --app-menu-shift-y: 4px; + transform-origin: bottom center; +} + +.dropdown-menu[data-popper-placement^="left"] { + --app-menu-shift-x: 4px; + --app-menu-shift-y: 0px; + transform-origin: center right; +} + +.dropdown-menu[data-popper-placement^="right"] { + --app-menu-shift-x: -4px; + --app-menu-shift-y: 0px; + transform-origin: center left; } .dropdown-menu.show { opacity: 1; visibility: visible; + pointer-events: auto; + translate: 0 0; + scale: 1; + transition-delay: 0s; +} + +.dropdown-menu.show.is-closing { + opacity: 0; + visibility: visible; + pointer-events: none; + translate: var(--app-menu-shift-x) var(--app-menu-shift-y); + scale: 0.985; + transition-duration: 0.12s; + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); } @media (prefers-reduced-motion: reduce) { - .dropdown-menu { + .dropdown-menu, + .dropdown-menu.show, + .dropdown-menu.show.is-closing { + translate: 0 0; + scale: 1; transition: none; } @@ -9808,9 +9832,11 @@ html[data-theme="dark"] .mermaid svg { flex-direction: column; overflow: hidden; color: var(--text-color); - background: var(--bg-color); - border-inline-start: 1px solid var(--border-color); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25); + background: var(--menu-surface); + border-inline-start: 1px solid var(--menu-border); + box-shadow: var(--menu-shadow); + font-size: var(--ui-font-md); + animation: review-panel-enter 180ms ease-out; } .review-panel-backdrop { @@ -9820,25 +9846,27 @@ html[data-theme="dark"] .mermaid svg { width: 100%; height: 100%; border: 0; - background: rgba(31, 35, 40, 0.38); + background: color-mix(in srgb, var(--text-color) 36%, transparent); cursor: default; + animation: review-backdrop-enter 140ms ease-out; } .review-panel-header { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; - gap: 16px; - padding: 16px; + gap: 12px; + min-height: 50px; + padding: 7px 9px 7px 12px; border-bottom: 1px solid var(--border-color); } .review-panel-eyebrow { - margin: 0 0 3px; - color: var(--accent-color); - font-size: 0.7rem; + margin: 0 0 4px; + color: var(--text-secondary); + font-size: var(--ui-font-sm); font-weight: 700; - letter-spacing: 0.08em; + letter-spacing: 0.06em; text-transform: uppercase; } @@ -9851,18 +9879,20 @@ html[data-theme="dark"] .mermaid svg { } .review-panel-header h2 { - font-size: 1rem; + font-size: var(--ui-font-lg); + line-height: 1.25; } .review-panel-summary { - margin: 4px 0 0; + margin: 2px 0 0; color: var(--text-secondary); - font-size: 0.78rem; + font-size: var(--ui-font-sm); + line-height: 1.35; } .review-icon-btn { - width: 32px; - height: 32px; + width: 30px; + height: 30px; flex: 0 0 auto; display: inline-flex; align-items: center; @@ -9870,13 +9900,18 @@ html[data-theme="dark"] .mermaid svg { color: var(--text-secondary); background: transparent; border: 1px solid transparent; - border-radius: 6px; + border-radius: var(--menu-item-radius); cursor: pointer; + transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease; +} + +.review-icon-btn i { + font-size: var(--ui-icon-size-md); } .review-icon-btn:hover:not(:disabled) { color: var(--text-color); - background: var(--button-hover); + background: var(--toolbar-control-hover); border-color: var(--border-color); } @@ -9887,7 +9922,8 @@ html[data-theme="dark"] .mermaid svg { } .review-icon-btn:disabled { - opacity: 0.45; + color: color-mix(in srgb, var(--text-secondary) 50%, transparent); + opacity: 0.55; cursor: not-allowed; } @@ -9895,8 +9931,9 @@ html[data-theme="dark"] .mermaid svg { display: flex; align-items: center; justify-content: space-between; - gap: 12px; - padding: 10px 14px; + gap: 8px; + padding: 7px 8px; + background: var(--header-bg); border-bottom: 1px solid var(--border-color); } @@ -9912,47 +9949,66 @@ html[data-theme="dark"] .mermaid svg { display: flex; flex-direction: column; overflow-y: auto; + background: var(--menu-surface); +} + +.review-panel-body:has(.review-composer:not([hidden])) .review-empty-state { + display: none; +} + +.review-filter-group { + width: 180px; + flex: 0 0 auto; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 3px; + margin: 0; + padding: 3px; +} + +.review-filter-btn { + width: 100%; + min-width: 0; + padding-inline: 0; } -.review-filter-group, .review-kind-group { display: inline-flex; align-items: center; padding: 2px; - background: var(--button-bg); + background: var(--segmented-bg); border: 1px solid var(--border-color); - border-radius: 8px; + border-radius: var(--menu-item-radius); } -.review-filter-btn, .review-kind-btn { color: var(--text-secondary); background: transparent; border: 0; - border-radius: 6px; + border-radius: calc(var(--menu-item-radius) - 2px); cursor: pointer; font-weight: 600; + transition: color 160ms ease, background-color 160ms ease, box-shadow 160ms ease; } -.review-filter-btn { - padding: 5px 10px; - font-size: 0.74rem; +.review-kind-btn:hover:not(.is-active) { + color: var(--text-color); + background: var(--toolbar-control-hover); } -.review-filter-btn.is-active, .review-kind-btn.is-active { - color: var(--accent-color); - background: color-mix(in srgb, var(--accent-color) 8%, transparent); - box-shadow: 0 1px 3px rgba(31, 35, 40, 0.12); + color: var(--bg-color); + background: var(--accent-color); + box-shadow: var(--segmented-shadow); } .review-composer { flex: 0 0 auto; - margin: 14px 14px 4px; - padding: 14px; - background: var(--header-bg); - border: 1px solid var(--border-color); - border-radius: 8px; + margin: 12px 12px 2px; + padding: 13px; + background: var(--menu-surface); + border: 1px solid var(--menu-border); + border-radius: var(--menu-radius); + box-shadow: 0 2px 8px color-mix(in srgb, var(--text-color) 10%, transparent); } .review-composer-heading { @@ -9963,15 +10019,20 @@ html[data-theme="dark"] .mermaid svg { } .review-composer-heading h3 { - font-size: 0.9rem; + font-size: var(--ui-font-lg); + line-height: 1.35; } .review-anchor-preview { display: -webkit-box; margin: 10px 0 12px; + padding: 8px 9px; overflow: hidden; color: var(--text-secondary); - font-size: 0.78rem; + background: var(--button-bg); + border-inline-start: 2px solid var(--border-color); + border-radius: 0 var(--menu-item-radius) var(--menu-item-radius) 0; + font-size: var(--ui-font-md); line-height: 1.45; -webkit-box-orient: vertical; -webkit-line-clamp: 2; @@ -9984,15 +10045,21 @@ html[data-theme="dark"] .mermaid svg { .review-kind-btn { flex: 1; + min-height: 32px; padding: 7px 8px; - font-size: 0.76rem; + font-size: var(--ui-font-md); +} + +.review-kind-btn i { + margin-inline-end: 4px; + font-size: var(--ui-icon-size-sm); } .review-feedback-label { display: block; margin-bottom: 6px; color: var(--text-color); - font-size: 0.76rem; + font-size: var(--ui-font-md); font-weight: 650; } @@ -10003,11 +10070,12 @@ html[data-theme="dark"] .mermaid svg { resize: vertical; color: var(--text-color); background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 7px; + border: 1px solid var(--menu-border); + border-radius: var(--menu-item-radius); font: inherit; - font-size: 0.82rem; + font-size: var(--ui-font-md); line-height: 1.45; + transition: border-color 160ms ease, box-shadow 160ms ease; } .review-feedback-input:focus { @@ -10026,47 +10094,69 @@ html[data-theme="dark"] .mermaid svg { .review-feedback-count { color: var(--text-secondary); - font-size: 0.7rem; + font-size: var(--ui-font-sm); + font-variant-numeric: tabular-nums; } .review-primary-btn { - min-height: 32px; - padding: 6px 12px; - color: var(--accent-color); - background: var(--button-bg); + min-height: 34px; + padding: 7px 13px; + color: var(--bg-color); + background: var(--accent-color); border: 1px solid var(--accent-color); - border-radius: 6px; + border-radius: var(--menu-item-radius); cursor: pointer; - font-size: 0.76rem; + font-size: var(--ui-font-md); font-weight: 650; + transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease; } .review-primary-btn:hover:not(:disabled) { - background: var(--button-hover); + background: color-mix(in srgb, var(--accent-color) 88%, var(--text-color)); + border-color: color-mix(in srgb, var(--accent-color) 88%, var(--text-color)); } .review-primary-btn:disabled { - opacity: 0.5; + color: var(--text-secondary); + background: var(--button-bg); + border-color: var(--border-color); + opacity: 0.62; cursor: not-allowed; } .review-list { flex: 0 0 auto; + display: grid; + gap: 10px; overflow: visible; - padding: 10px 14px 18px; + padding: 10px 12px 16px; } .review-thread { - margin-bottom: 10px; - padding: 12px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-inline-start: 3px solid var(--accent-color); - border-radius: 8px; + position: relative; + margin: 0; + padding: 0; + overflow: hidden; + background: var(--menu-surface); + border: 1px solid var(--menu-border); + border-radius: var(--menu-radius); + box-shadow: 0 1px 2px color-mix(in srgb, var(--text-color) 8%, transparent); + transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease; +} + +@media (hover: hover) { + .review-thread:hover { + border-color: color-mix(in srgb, var(--text-secondary) 42%, var(--menu-border)); + box-shadow: 0 3px 10px color-mix(in srgb, var(--text-color) 9%, transparent); + } } .review-thread.is-resolved { - opacity: 0.72; + background: color-mix(in srgb, var(--button-bg) 52%, var(--menu-surface)); +} + +.review-thread.is-resolved .review-thread-body { + color: var(--text-secondary); } .review-thread.is-orphaned { @@ -10081,8 +10171,12 @@ html[data-theme="dark"] .mermaid svg { } .review-thread-header { + min-height: 40px; justify-content: space-between; gap: 8px; + padding: 6px 7px 6px 11px; + background: color-mix(in srgb, var(--header-bg) 68%, var(--menu-surface)); + border-bottom: 1px solid var(--border-color); } .review-thread-meta { @@ -10090,47 +10184,62 @@ html[data-theme="dark"] .mermaid svg { gap: 6px; } +.review-thread-content { + padding: 10px 12px 11px; +} + .review-kind-label, .review-status-label { display: inline-flex; align-items: center; gap: 4px; - border-radius: 999px; - font-size: 0.67rem; - font-weight: 700; + font-size: var(--ui-font-sm); + font-weight: 650; line-height: 1; } .review-kind-label { - padding: 5px 7px; - color: var(--text-secondary); - background: var(--button-bg); - border: 1px solid var(--border-color); + color: var(--text-color); +} + +.review-kind-label i { + font-size: var(--ui-icon-size-sm); } .review-status-label { + padding-inline-start: 7px; color: var(--text-secondary); + border-inline-start: 1px solid var(--border-color); +} + +.review-status-label i { + font-size: var(--ui-icon-size-sm); } .review-thread-anchor { width: 100%; - margin: 9px 0 8px; - padding: 0; + margin: 0 0 10px; + padding: 7px 9px; overflow: hidden; color: var(--text-secondary); - background: transparent; - border: 0; + background: var(--button-bg); + border: 1px solid transparent; + border-inline-start-color: var(--border-color); + border-inline-start-width: 2px; + border-radius: 0 var(--menu-item-radius) var(--menu-item-radius) 0; cursor: pointer; - font-size: 0.72rem; - line-height: 1.35; + font-size: var(--ui-font-md); + line-height: 1.4; text-align: start; text-overflow: ellipsis; white-space: nowrap; + transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease; } .review-thread-anchor:hover:not(:disabled) { - color: var(--accent-color); - text-decoration: underline; + color: var(--text-color); + background: var(--button-hover); + border-color: var(--border-color); } .review-thread-anchor:disabled { @@ -10141,7 +10250,7 @@ html[data-theme="dark"] .mermaid svg { .review-thread-body { margin: 0; color: var(--text-color); - font-size: 0.82rem; + font-size: var(--ui-font-md); line-height: 1.5; overflow-wrap: anywhere; white-space: pre-wrap; @@ -10150,37 +10259,64 @@ html[data-theme="dark"] .mermaid svg { .review-thread-dates { display: flex; flex-wrap: wrap; - gap: 4px 12px; - margin-top: 9px; + gap: 6px 12px; + margin-top: 11px; + padding-top: 9px; + border-top: 1px solid var(--border-color); } .review-thread-time { + display: inline-flex; + align-items: center; + gap: 4px; color: var(--text-secondary); - font-size: 0.67rem; + font-size: var(--ui-font-sm); + line-height: 1.3; +} + +.review-thread-time i { + flex: 0 0 auto; + font-size: var(--ui-icon-size-xs); } .review-thread-actions { + flex: 0 0 auto; justify-content: flex-end; - gap: 4px; - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid var(--border-color); + gap: 2px; + margin: 0; + padding: 2px; + background: var(--menu-surface); + border: 1px solid var(--border-color); + border-radius: var(--menu-item-radius); } .review-thread-action { - padding: 5px 7px; + width: 24px; + height: 24px; + min-height: 24px; + flex: 0 0 24px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; color: var(--text-secondary); background: transparent; - border: 0; - border-radius: 5px; + border: 1px solid transparent; + border-radius: calc(var(--menu-item-radius) - 3px); cursor: pointer; - font-size: 0.7rem; - font-weight: 600; + font-size: var(--ui-icon-size-sm); + line-height: 1; + transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease; +} + +.review-thread-action i { + font-size: var(--ui-icon-size-md); } .review-thread-action:hover { color: var(--text-color); - background: var(--button-hover); + background: var(--toolbar-control-hover); + border-color: transparent; } .review-thread-action.is-danger:hover { @@ -10189,27 +10325,34 @@ html[data-theme="dark"] .mermaid svg { .review-empty-state { margin: auto; - padding: 30px 26px; + padding: 36px 26px; color: var(--text-secondary); text-align: center; } .review-empty-state i { - display: block; - margin-bottom: 10px; - color: var(--accent-color); - font-size: 1.7rem; + width: 42px; + height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; + color: var(--text-secondary); + background: var(--button-bg); + border: 1px solid var(--border-color); + border-radius: 50%; + font-size: var(--ui-icon-size-xl); } .review-empty-state h3 { - font-size: 0.9rem; + font-size: var(--ui-font-md); } .review-empty-state p { margin: 7px auto 0; max-width: 270px; - font-size: 0.76rem; - line-height: 1.45; + font-size: var(--ui-font-md); + line-height: 1.5; } #markdown-preview .review-target { @@ -10300,6 +10443,24 @@ html[data-theme="dark"] .mermaid svg { .review-thread.is-review-thread-active { border-color: var(--accent-color); + background: color-mix(in srgb, var(--accent-color) 7%, var(--menu-surface)); + box-shadow: inset 3px 0 0 var(--accent-color); +} + +@keyframes review-panel-enter { + from { + opacity: 0; + transform: translateX(12px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes review-backdrop-enter { + from { opacity: 0; } + to { opacity: 1; } } .review-filter-btn:focus-visible, @@ -10354,6 +10515,17 @@ html[data-theme="dark"] .mermaid svg { .review-thread-action { min-height: 40px; } + + .review-thread-action { + min-width: 40px; + width: 40px; + height: 40px; + flex-basis: 40px; + } + + .review-thread-actions { + gap: 6px; + } } @media (max-width: 767px) { @@ -10364,7 +10536,7 @@ html[data-theme="dark"] .mermaid svg { height: min(68%, 620px); border-inline-start: 0; border-top: 1px solid var(--border-color); - box-shadow: 0 -12px 32px rgba(31, 35, 40, 0.2); + box-shadow: var(--menu-shadow); } .content-container.is-reviewing .preview-pane { @@ -10390,6 +10562,12 @@ html[data-theme="dark"] .mermaid svg { height: 44px; } + .review-panel-header .review-icon-btn, + .review-composer-heading .review-icon-btn { + width: 44px; + height: 44px; + } + .review-filter-btn, .review-kind-btn, .review-primary-btn, @@ -10407,7 +10585,14 @@ html[data-theme="dark"] .mermaid svg { } .review-thread-action { - padding-inline: 10px; + min-width: 44px; + width: 44px; + height: 44px; + flex-basis: 44px; + } + + .review-thread-actions { + gap: 8px; } .review-target-button { @@ -10437,7 +10622,10 @@ html[data-theme="dark"] .mermaid svg { } @media (prefers-reduced-motion: reduce) { + .review-panel, + .review-panel-backdrop, .review-target-button { + animation: none; transition: none; } } @@ -11177,7 +11365,22 @@ html[data-theme="dark"] .mermaid svg { .mobile-settings-toggle.is-active .settings-switch > span { background: #fff; - transform: translateX(14px); + transform: translateX(18px); +} + +.mobile-settings-toggle.is-active .themed-settings-switch > span { + color: var(--text-color); + background: var(--bg-color); +} + +html[data-theme="dark"] .settings-menu-item.is-active .settings-switch, +html[data-theme="dark"] .mobile-settings-toggle.is-active .settings-switch { + background: var(--button-active); +} + +#private-mode-toggle.is-active .settings-switch, +#mobile-private-mode-toggle.is-active .settings-switch { + background: var(--accent-color); } .mobile-menu-command.is-active .mobile-menu-item-label > i, @@ -11749,6 +11952,18 @@ html[data-theme="dark"] .mermaid svg { /* ======================================== VERSIONED RELEASE NOTES ======================================== */ +body.release-notes-active #review-panel, +body.release-notes-active #review-panel-backdrop, +body.release-notes-active #review-composer, +body.release-notes-active #review-pins-layer { + display: none !important; +} + +body.release-notes-active .app-status-bar, +body.release-notes-active .mobile-stats-container { + opacity: 0.5; +} + .tab-item.release-notes-tab { width: max-content; min-width: max-content; diff --git a/tests/e2e/responsive-storage.spec.js b/tests/e2e/responsive-storage.spec.js index c5638b2a..15c4341f 100644 --- a/tests/e2e/responsive-storage.spec.js +++ b/tests/e2e/responsive-storage.spec.js @@ -8,15 +8,49 @@ const { waitForAppReady } = require('../helpers/app'); +async function resolveCssColor(page, variableName) { + return page.evaluate(name => { + const probe = document.createElement('span'); + probe.style.color = `var(${name})`; + document.body.appendChild(probe); + const color = getComputedStyle(probe).color; + probe.remove(); + return color; + }, variableName); +} + test('theme switching stores and restores the selected theme', async ({ page }) => { await openApp(page); const initialTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme')); + const initialThemeIcon = initialTheme === 'dark' ? 'lucide-moon' : 'lucide-sun'; + const toggledThemeIcon = initialTheme === 'dark' ? 'lucide-sun' : 'lucide-moon'; await page.getByRole('button', { name: 'Open workspace settings' }).click(); - await page.locator('#theme-toggle').click(); + const themeToggle = page.locator('#theme-toggle'); + const themeSwitch = themeToggle.locator('.settings-switch'); + const privateModeSwitch = page.locator('#private-mode-toggle .settings-switch'); + + await expect(themeToggle).toHaveClass(/settings-menu-item--toggle/); + await expect(themeToggle).toHaveAttribute('aria-pressed', String(initialTheme === 'dark')); + await expect(themeSwitch).toBeVisible(); + await expect(themeSwitch).toHaveCSS('width', '40px'); + await expect(themeSwitch).toHaveCSS('height', '22px'); + await expect(page.locator('#theme-switch-icon')).toHaveClass(new RegExp(`\\b${initialThemeIcon}\\b`)); + await expect(page.locator('#theme-switch-icon')).not.toHaveCSS('mask-image', 'none'); + await expect(themeSwitch).toHaveCSS('width', await privateModeSwitch.evaluate(element => getComputedStyle(element).width)); + await expect(themeSwitch).toHaveCSS('height', await privateModeSwitch.evaluate(element => getComputedStyle(element).height)); + await expect(themeSwitch.locator(':scope > span')).toHaveCSS('border-radius', '50%'); + await expect(themeSwitch.locator(':scope > span')).toHaveCSS('background-color', await page.locator('body').evaluate(element => getComputedStyle(element).backgroundColor)); + await expect(page.locator('#theme-switch-icon')).toHaveCSS('color', await page.locator('body').evaluate(element => getComputedStyle(element).color)); + + await themeToggle.click(); const toggledTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme')); expect(toggledTheme).not.toBe(initialTheme); + await expect(themeToggle).toHaveAttribute('aria-pressed', String(toggledTheme === 'dark')); + await expect(page.locator('#theme-switch-icon')).toHaveClass(new RegExp(`\\b${toggledThemeIcon}\\b`)); + await expect(themeSwitch.locator(':scope > span')).toHaveCSS('background-color', await page.locator('body').evaluate(element => getComputedStyle(element).backgroundColor)); + await expect(page.locator('#theme-switch-icon')).toHaveCSS('color', await page.locator('body').evaluate(element => getComputedStyle(element).color)); await expect.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem('markdownViewerGlobalState') || '{}').theme)).toBe(toggledTheme); await page.reload(); @@ -24,6 +58,44 @@ test('theme switching stores and restores the selected theme', async ({ page }) await expect.poll(() => page.evaluate(() => document.documentElement.getAttribute('data-theme'))).toBe(toggledTheme); }); +test('dark mode keeps the active theme switch dark and the private mode switch blue', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'dark' }); + await openApp(page); + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); + + await page.getByRole('button', { name: 'Open workspace settings' }).click(); + const themeSwitch = page.locator('#theme-toggle .settings-switch'); + const privateModeToggle = page.locator('#private-mode-toggle'); + const privateModeSwitch = privateModeToggle.locator('.settings-switch'); + const darkTrackColor = await privateModeSwitch.evaluate(element => getComputedStyle(element).backgroundColor); + const accentTrackColor = await resolveCssColor(page, '--accent-color'); + + await expect(themeSwitch).toHaveCSS('background-color', darkTrackColor); + await privateModeToggle.click(); + await expect(privateModeToggle).toHaveAttribute('aria-pressed', 'true'); + await expect(privateModeSwitch).toHaveCSS('background-color', accentTrackColor); + await expect(page.locator('#theme-switch-icon')).toHaveCSS('width', '12px'); + await expect(page.locator('#theme-switch-icon')).toHaveCSS('height', '12px'); + await expect(page.locator('#theme-switch-icon')).not.toHaveCSS('filter', 'none'); + await expect(privateModeSwitch.locator('.lucide')).toHaveCount(0); +}); + +test('light mode uses the accent blue for active private mode without an inner icon', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'light' }); + await openApp(page); + await expect(page.locator('html')).toHaveAttribute('data-theme', 'light'); + + await page.getByRole('button', { name: 'Open workspace settings' }).click(); + const privateModeToggle = page.locator('#private-mode-toggle'); + const privateModeSwitch = privateModeToggle.locator('.settings-switch'); + const accentTrackColor = await resolveCssColor(page, '--accent-color'); + + await privateModeToggle.click(); + await expect(privateModeToggle).toHaveAttribute('aria-pressed', 'true'); + await expect(privateModeSwitch).toHaveCSS('background-color', accentTrackColor); + await expect(privateModeSwitch.locator('.lucide')).toHaveCount(0); +}); + test('document tabs persist across reload in normal mode', async ({ page }) => { await openApp(page); await setEditorContent(page, '# Persistence Check\n\nSaved locally.'); @@ -364,7 +436,10 @@ test('mobile layout exposes menu controls at 375px width', async ({ page }) => { await expect(page.locator('#mobile-review-toggle')).toBeVisible(); await expect(page.locator('#mobile-private-mode-toggle')).toBeVisible(); await expect(page.locator('#mobile-theme-toggle .settings-switch')).toBeVisible(); + await expect(page.locator('#mobile-theme-switch-icon')).toHaveClass(/lucide-(sun|moon)/); + await expect(page.locator('#mobile-theme-switch-icon')).not.toHaveCSS('mask-image', 'none'); await expect(page.locator('#mobile-private-mode-toggle .settings-switch')).toBeVisible(); + await expect(page.locator('#mobile-private-mode-toggle .settings-switch .lucide')).toHaveCount(0); const settingsOrder = await page.locator('#mobile-menu-settings-panel > *').evaluateAll(elements => elements.filter(element => element.matches('button, .mobile-menu-language')).map(element => element.id || element.className) ); diff --git a/tests/e2e/review-design.spec.js b/tests/e2e/review-design.spec.js new file mode 100644 index 00000000..9f448f1b --- /dev/null +++ b/tests/e2e/review-design.spec.js @@ -0,0 +1,222 @@ +const { test, expect } = require('@playwright/test'); +const { + openApp, + setEditorContent, + stubLazyRendererLibraries +} = require('../helpers/app'); + +const reviewMarkdown = `# Review design + +This paragraph is ready for focused feedback.`; + +test.beforeEach(async ({ page }) => { + await stubLazyRendererLibraries(page); + await openApp(page); + await setEditorContent(page, reviewMarkdown); + await expect(page.locator('#markdown-preview h1')).toHaveText('Review design'); +}); + +test('matches sidebar filters and keeps review cards compact', async ({ page }) => { + await page.locator('#review-toggle').click(); + + const neutralState = await page.evaluate(() => { + const activeFilter = document.querySelector('.review-filter-btn.is-active'); + const sidebarFilter = document.querySelector('.document-filter-btn.is-active'); + const inactiveFilter = document.querySelector('.review-filter-btn:not(.is-active)'); + const emptyIcon = document.querySelector('.review-empty-state i'); + const submit = document.querySelector('#review-feedback-submit'); + const panelHeader = document.querySelector('.review-panel-header'); + const readFilterStyle = (element) => { + const style = getComputedStyle(element); + return { + backgroundColor: style.backgroundColor, + color: style.color, + fontSize: style.fontSize, + fontWeight: style.fontWeight, + borderRadius: style.borderRadius + }; + }; + return { + reviewFilter: readFilterStyle(activeFilter), + sidebarFilter: readFilterStyle(sidebarFilter), + inactiveFilter: getComputedStyle(inactiveFilter).backgroundColor, + emptyIcon: getComputedStyle(emptyIcon).color, + disabledSubmit: getComputedStyle(submit).backgroundColor, + panelHeaderHeight: panelHeader.getBoundingClientRect().height + }; + }); + + expect(neutralState.reviewFilter).toEqual(neutralState.sidebarFilter); + expect(neutralState.inactiveFilter).not.toBe(neutralState.reviewFilter.backgroundColor); + expect(neutralState.panelHeaderHeight).toBeLessThanOrEqual(52); + + const filterGeometry = await page.locator('.review-filter-group').evaluate((group) => { + const style = getComputedStyle(group); + const buttons = Array.from(group.querySelectorAll('.review-filter-btn')); + return { + gap: style.columnGap, + padding: [style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft], + widths: buttons.map((button) => button.getBoundingClientRect().width) + }; + }); + expect(filterGeometry.gap).toBe('3px'); + expect(filterGeometry.padding).toEqual(['3px', '3px', '3px', '3px']); + expect(Math.max(...filterGeometry.widths) - Math.min(...filterGeometry.widths)).toBeLessThan(0.5); + + const activeWidths = []; + for (const filter of ['resolved', 'all', 'open']) { + await page.locator(`[data-review-filter="${filter}"]`).click(); + activeWidths.push(await page.locator('.review-filter-btn.is-active').evaluate((button) => ( + button.getBoundingClientRect().width + ))); + } + expect(Math.max(...activeWidths) - Math.min(...activeWidths)).toBeLessThan(0.5); + + await page.locator('#review-pins-layer .review-target-button[data-review-anchor^="heading:"]').click(); + await expect(page.locator('#review-composer')).toBeVisible(); + + const selectedKindColor = await page.locator('.review-kind-btn.is-active').evaluate((button) => ( + getComputedStyle(button).backgroundColor + )); + expect(neutralState.reviewFilter.backgroundColor).not.toBe(selectedKindColor); + expect(neutralState.emptyIcon).not.toBe(selectedKindColor); + expect(neutralState.disabledSubmit).not.toBe(selectedKindColor); + + await page.locator('#review-feedback-input').fill('Make the heading more specific.'); + await expect(page.locator('#review-feedback-submit')).toBeEnabled(); + await expect.poll(() => page.locator('#review-feedback-submit').evaluate((button) => ( + getComputedStyle(button).backgroundColor + ))).toBe(selectedKindColor); + await page.locator('#review-feedback-submit').click(); + + const threadState = await page.locator('.review-thread').evaluate((thread) => { + const header = thread.querySelector('.review-thread-header'); + const meta = thread.querySelector('.review-thread-meta'); + const actions = thread.querySelector('.review-thread-actions'); + const content = thread.querySelector('.review-thread-content'); + const buttons = Array.from(actions.querySelectorAll('.review-thread-action')); + const threadRect = thread.getBoundingClientRect(); + const metaRect = meta.getBoundingClientRect(); + const actionsRect = actions.getBoundingClientRect(); + return { + border: getComputedStyle(thread).borderInlineStartColor, + bodyFontSize: getComputedStyle(thread.querySelector('.review-thread-body')).fontSize, + anchorFontSize: getComputedStyle(thread.querySelector('.review-thread-anchor')).fontSize, + hasStructuredContent: content?.parentElement === thread, + headerContainsActions: actions.parentElement === header, + actionCount: buttons.length, + actionLabels: buttons.map((button) => button.getAttribute('aria-label')), + actionText: buttons.map((button) => button.textContent.trim()), + actionIcons: buttons.map((button) => button.querySelector('i')?.className || ''), + dateIcons: Array.from(thread.querySelectorAll('.review-thread-time i'), (icon) => icon.className), + actionsRightAligned: actionsRect.left > metaRect.right && actionsRect.right <= threadRect.right + }; + }); + expect(threadState.border).not.toBe(selectedKindColor); + expect(threadState.bodyFontSize).toBe(threadState.anchorFontSize); + expect(threadState.hasStructuredContent).toBe(true); + expect(threadState.headerContainsActions).toBe(true); + expect(threadState.actionCount).toBe(3); + expect(threadState.actionLabels).toEqual(['Edit comment', 'Resolve comment', 'Delete comment']); + expect(threadState.actionText).toEqual(['', '', '']); + expect(threadState.actionIcons).toEqual([ + 'lucide lucide-square-pen', + 'lucide lucide-check', + 'lucide lucide-trash-2' + ]); + expect(threadState.dateIcons).toEqual(['lucide lucide-clock-3', 'lucide lucide-circle']); + expect(threadState.actionsRightAligned).toBe(true); + + await page.locator('[data-review-action="toggle-resolved"]').click(); + await page.locator('[data-review-filter="resolved"]').click(); + await expect(page.locator('.review-status-label')).toContainText('Resolved'); + await expect(page.locator('.review-status-label i')).toHaveClass('lucide lucide-check'); + await expect(page.locator('[data-review-action="toggle-resolved"]')).toHaveAttribute('aria-label', 'Reopen comment'); + await expect(page.locator('[data-review-action="toggle-resolved"] i')).toHaveClass('lucide lucide-refresh-cw'); + + await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark')); + await page.waitForTimeout(180); + const darkState = await page.evaluate(() => { + const activeFilter = document.querySelector('.review-filter-btn.is-active'); + const sidebarFilter = document.querySelector('.document-filter-btn.is-active'); + const thread = document.querySelector('.review-thread'); + return { + reviewFilter: getComputedStyle(activeFilter).backgroundColor, + sidebarFilter: getComputedStyle(sidebarFilter).backgroundColor, + threadBorder: getComputedStyle(thread).borderInlineStartColor + }; + }); + const colorChannels = (value) => value.match(/[\d.]+/g).slice(0, 3).map(Number); + expect(colorChannels(darkState.reviewFilter)).toEqual(colorChannels(darkState.sidebarFilter)); + expect(darkState.threadBorder).not.toBe(selectedKindColor); +}); + +test('keeps the review sheet touch-friendly and stable in narrow layouts', async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.setViewportSize({ width: 375, height: 667 }); + await page.locator('#mobile-menu-toggle').click(); + await page.locator('#mobile-review-toggle').click(); + await expect(page.locator('#review-panel')).toBeVisible(); + + const portraitMetrics = await page.locator('#review-panel').evaluate((panel) => { + const rect = panel.getBoundingClientRect(); + const targets = panel.querySelectorAll( + '.review-panel-header .review-icon-btn, .review-panel-actions .review-icon-btn, .review-filter-btn' + ); + return { + left: rect.left, + right: rect.right, + viewportWidth: window.innerWidth, + scrollWidth: panel.scrollWidth, + clientWidth: panel.clientWidth, + animationName: getComputedStyle(panel).animationName, + targetHeights: Array.from(targets, (target) => target.getBoundingClientRect().height) + }; + }); + + expect(portraitMetrics.left).toBeGreaterThanOrEqual(0); + expect(portraitMetrics.right).toBeLessThanOrEqual(portraitMetrics.viewportWidth); + expect(portraitMetrics.scrollWidth).toBeLessThanOrEqual(portraitMetrics.clientWidth); + expect(portraitMetrics.animationName).toBe('none'); + portraitMetrics.targetHeights.forEach((height) => expect(height).toBeGreaterThanOrEqual(44)); + + await page.locator('#review-pins-layer .review-target-button[data-review-anchor^="heading:"]').evaluate((button) => button.click()); + await page.locator('#review-feedback-input').fill('Keep the mobile review card compact.'); + await page.locator('#review-feedback-submit').click(); + const mobileThreadMetrics = await page.locator('.review-thread').evaluate((thread) => { + const header = thread.querySelector('.review-thread-header'); + const buttons = Array.from(thread.querySelectorAll('.review-thread-action')); + return { + headerScrollWidth: header.scrollWidth, + headerClientWidth: header.clientWidth, + targets: buttons.map((button) => { + const rect = button.getBoundingClientRect(); + return { width: rect.width, height: rect.height }; + }) + }; + }); + expect(mobileThreadMetrics.headerScrollWidth).toBeLessThanOrEqual(mobileThreadMetrics.headerClientWidth); + mobileThreadMetrics.targets.forEach(({ width, height }) => { + expect(width).toBeGreaterThanOrEqual(44); + expect(height).toBeGreaterThanOrEqual(44); + }); + + await page.setViewportSize({ width: 667, height: 375 }); + const landscapeMetrics = await page.locator('#review-panel').evaluate((panel) => { + const rect = panel.getBoundingClientRect(); + return { + left: rect.left, + right: rect.right, + bottom: rect.bottom, + viewportWidth: window.innerWidth, + viewportHeight: window.innerHeight, + scrollWidth: panel.scrollWidth, + clientWidth: panel.clientWidth + }; + }); + + expect(landscapeMetrics.left).toBeGreaterThanOrEqual(0); + expect(landscapeMetrics.right).toBeLessThanOrEqual(landscapeMetrics.viewportWidth); + expect(landscapeMetrics.bottom).toBeLessThanOrEqual(landscapeMetrics.viewportHeight); + expect(landscapeMetrics.scrollWidth).toBeLessThanOrEqual(landscapeMetrics.clientWidth); +}); diff --git a/tests/e2e/review-mode.spec.js b/tests/e2e/review-mode.spec.js index 4d61f102..3a55cd4a 100644 --- a/tests/e2e/review-mode.spec.js +++ b/tests/e2e/review-mode.spec.js @@ -167,7 +167,7 @@ test('resolves feedback and retains an orphaned thread when its block changes', await page.locator('#review-feedback-input').fill('Rework this heading.'); await page.locator('#review-feedback-submit').click(); - await page.locator('.review-thread-action', { hasText: 'Resolve' }).click(); + await page.locator('[data-review-action="toggle-resolved"]').click(); await expect(page.locator('#review-toolbar-count')).toBeHidden(); await page.locator('[data-review-filter="resolved"]').click(); await expect(page.locator('.review-thread')).toContainText('Resolved'); diff --git a/tests/e2e/share-live.spec.js b/tests/e2e/share-live.spec.js index 67ed6956..369023a6 100644 --- a/tests/e2e/share-live.spec.js +++ b/tests/e2e/share-live.spec.js @@ -29,6 +29,14 @@ test('Share Snapshot creates a view-only hash link that opens without permanent await expect(sharedPage.locator('.content-container')).toHaveClass(/view-preview-only/); await expect(sharedPage.locator('#markdown-editor')).toHaveJSProperty('readOnly', true); await expect(sharedPage.locator('#markdown-preview')).toContainText('This text should survive a snapshot round trip.'); + await expect(sharedPage.locator('#toggle-sync')).toBeDisabled(); + await expect(sharedPage.locator('#toggle-sync')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + + const formatMenuToggles = sharedPage.locator('#markdown-format-toolbar [data-toolbar-menu-toggle]'); + await expect(formatMenuToggles).toHaveCount(3); + await expect(sharedPage.locator('#markdown-format-toolbar [data-toolbar-menu-toggle]:not(:disabled)')).toHaveCount(0); + await expect(sharedPage.locator('[data-toolbar-menu-toggle="heading"]')).toHaveCSS('opacity', '0.4'); + await expect(sharedPage.locator('[data-toolbar-menu-toggle="case"]')).toHaveCSS('opacity', '0.4'); const storedTabs = JSON.stringify(await storedDocuments(sharedPage)); expect(storedTabs).not.toContain('This text should survive a snapshot round trip.'); diff --git a/tests/e2e/smoke.spec.js b/tests/e2e/smoke.spec.js index aff00d74..64fe25bf 100644 --- a/tests/e2e/smoke.spec.js +++ b/tests/e2e/smoke.spec.js @@ -1,6 +1,6 @@ const { test, expect } = require('@playwright/test'); const { execFileSync } = require('node:child_process'); -const { openApp } = require('../helpers/app'); +const { openApp, stubClipboard } = require('../helpers/app'); test('static build smoke check passes', () => { const output = execFileSync('node', ['tests/helpers/static-build-check.mjs'], { @@ -30,3 +30,87 @@ test('first run opens release notes in the background without stealing editor fo await expect(page.locator('#markdown-editor')).toBeEditable(); expect(criticalErrors).toEqual([]); }); + +test('release notes behave as an application surface instead of a document', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await openApp(page); + await stubClipboard(page); + + await page.locator('#review-toggle').click(); + await expect(page.locator('#review-panel')).toBeVisible(); + await page.locator('#header-about-button').click(); + await page.getByRole('button', { name: 'Show Release Notes' }).click(); + + const releaseTab = page.locator('#tab-list .release-notes-tab'); + await expect(releaseTab).toHaveClass(/active/); + await expect(page.locator('body')).toHaveClass(/release-notes-active/); + await expect(page.locator('#document-tree [aria-current="page"]')).toHaveCount(0); + await expect(page.locator('#review-panel')).toBeHidden(); + await expect(page.locator('.review-target-actions')).toHaveCount(0); + + await expect(page.locator('.header-view-toolbar')).toBeVisible(); + await expect(page.locator('.header-view-toolbar button')).toHaveCount(3); + await expect(page.locator('.header-view-toolbar button:not(:disabled)')).toHaveCount(0); + await expect(page.locator('#toggle-sync')).toBeVisible(); + await expect(page.locator('#toggle-sync')).toBeDisabled(); + await expect(page.locator('#toggle-sync')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + await expect(page.locator('#copy-markdown-button')).toBeVisible(); + await expect(page.locator('#copy-markdown-button')).toBeDisabled(); + await expect(page.locator('#exportDropdown')).toBeVisible(); + await expect(page.locator('#exportDropdown')).toBeDisabled(); + await expect(page.locator('#share-button')).toBeVisible(); + await expect(page.locator('#share-button')).toBeDisabled(); + await expect(page.locator('#live-share-button')).toBeVisible(); + await expect(page.locator('#live-share-button')).toBeDisabled(); + await expect(page.locator('#review-toggle')).toBeVisible(); + await expect(page.locator('#review-toggle')).toBeDisabled(); + await expect(page.locator('#markdown-format-toolbar')).toBeVisible(); + await expect(page.locator('#markdown-format-toolbar button:not(:disabled)')).toHaveCount(0); + await expect(page.locator('.app-status-bar')).toBeVisible(); + + await expect(page.locator('#importDropdown')).toBeVisible(); + await expect(page.locator('[aria-label="Report an issue"]')).toBeVisible(); + await expect(page.locator('#header-about-button')).toBeVisible(); + await expect(page.locator('#workspaceSettingsDropdown')).toBeVisible(); + + await page.keyboard.press('Control+Shift+C'); + await expect.poll(() => page.evaluate(() => window.__copiedText)).toBe(''); + await page.evaluate(() => document.getElementById('review-toggle').click()); + await expect(page.locator('#review-panel')).toBeHidden(); + + await expect(releaseTab.locator('.tab-menu-btn')).toHaveCount(0); + await expect(releaseTab.locator('.tab-close-btn')).toBeVisible(); + + await page.setViewportSize({ width: 375, height: 812 }); + await page.locator('#mobile-menu-toggle').click(); + await expect(page.locator('.mobile-menu-overview')).toBeVisible(); + await expect(page.locator('.mobile-view-mode-btn:not(:disabled)')).toHaveCount(0); + await expect(page.locator('#mobile-document-tools')).toBeVisible(); + await expect(page.locator('#mobile-document-tools button:not(:disabled)')).toHaveCount(0); + await expect(page.locator('#mobile-export-section')).toBeVisible(); + await expect(page.locator('[aria-controls="mobile-menu-export-panel"]')).toBeDisabled(); + await expect(page.locator('#mobile-share-button')).toBeVisible(); + await expect(page.locator('#mobile-share-button')).toBeDisabled(); + await expect(page.locator('#mobile-live-share-button')).toBeVisible(); + await expect(page.locator('#mobile-live-share-button')).toBeDisabled(); + await expect(page.locator('[aria-controls="mobile-menu-new-panel"]')).toBeVisible(); + await page.locator('#close-mobile-menu').click(); + + await page.locator('#tab-list .tab-item:not(.release-notes-tab)').first().click(); + await page.setViewportSize({ width: 1440, height: 900 }); + await expect(page.locator('body')).not.toHaveClass(/release-notes-active/); + await expect(page.locator('#tab-list .tab-item:not(.release-notes-tab)').first().locator('.tab-menu-btn')).toBeVisible(); + await expect(page.locator('.header-view-toolbar button[data-view-mode="split"]')).toBeEnabled(); + await expect(page.locator('#toggle-sync')).toBeEnabled(); + await expect(page.locator('#copy-markdown-button')).toBeVisible(); + await expect(page.locator('#copy-markdown-button')).toBeEnabled(); + await expect(page.locator('#exportDropdown')).toBeEnabled(); + await expect(page.locator('#share-button')).toBeEnabled(); + await expect(page.locator('#live-share-button')).toBeEnabled(); + await expect(page.locator('#review-toggle')).toBeVisible(); + await expect(page.locator('#review-toggle')).toBeEnabled(); + await expect(page.locator('#markdown-format-toolbar')).toBeVisible(); + await expect(page.locator('#markdown-format-toolbar [data-md-action="bold"]')).toBeEnabled(); + await expect(page.locator('.app-status-bar')).toBeVisible(); + await expect(page.locator('#document-tree [aria-current="page"]')).toHaveCount(1); +}); diff --git a/tests/e2e/status-toolbar-followup.spec.js b/tests/e2e/status-toolbar-followup.spec.js index d8c97873..fa7c51ba 100644 --- a/tests/e2e/status-toolbar-followup.spec.js +++ b/tests/e2e/status-toolbar-followup.spec.js @@ -5,27 +5,49 @@ test.beforeEach(async ({ page }) => { await openApp(page); }); -test('header and formatting toolbar expose the requested actions in order', async ({ page }) => { +test('header consolidates icon document actions in the requested order', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); const header = page.locator('.header-right'); await expect(page.locator('.app-header h1')).toBeVisible(); expect(await page.locator('.app-header h1').evaluate(title => title.getBoundingClientRect().width)).toBeGreaterThan(115); expect(await page.locator('#document-sidebar').evaluate(sidebar => sidebar.getBoundingClientRect().width)).toBeGreaterThanOrEqual(220); - for (const selector of ['#importDropdown', '#share-button', '#live-share-button', '#header-about-button', '#workspaceSettingsDropdown']) { + for (const selector of ['#importDropdown', '#copy-markdown-button', '#toggle-sync', '#review-toggle', '#share-button', '#live-share-button', '#exportDropdown', '#header-about-button', '#workspaceSettingsDropdown']) { await expect(header.locator(selector)).toBeVisible(); } await expect(header.locator('[aria-label="Report an issue"]')).toBeVisible(); - await expect(header.locator('#exportDropdown')).toBeVisible(); - await expect(header.locator('#toggle-sync, #copy-markdown-button')).toHaveCount(0); + await expect(header.locator('.header-view-toolbar .view-toggle-btn')).toHaveCount(3); + await expect(header.locator('#importDropdown .btn-text, #share-button .btn-text, #live-share-button .btn-text, #exportDropdown .btn-text')).toHaveCount(0); + await expect(header.locator('#importDropdown i')).toHaveClass('lucide lucide-plus'); + await expect(header.locator('#copy-markdown-button i')).toHaveClass('lucide lucide-clipboard'); + expect(await header.locator('#copy-markdown-button i').evaluate(icon => getComputedStyle(icon).maskImage)).not.toBe('none'); + + const headerOrder = await header.locator(':scope > button, :scope > a, :scope > .dropdown > button').evaluateAll(elements => + elements.map(element => element.id || element.getAttribute('aria-label')) + ); + expect(headerOrder).toEqual([ + 'toggle-sync', + 'importDropdown', + 'copy-markdown-button', + 'exportDropdown', + 'share-button', + 'live-share-button', + 'review-toggle', + 'Report an issue', + 'header-about-button', + 'workspaceSettingsDropdown' + ]); const toolbar = page.locator('#markdown-format-toolbar'); await expect(toolbar.locator('.workspace-format-actions [data-md-action="find"]')).toBeVisible(); await expect(toolbar.locator('.workspace-format-actions [data-md-action="fullscreen"]')).toBeVisible(); await expect(toolbar.locator('.workspace-format-actions [data-md-action="find"] i')).toHaveClass(/lucide-search/); await expect(toolbar.locator('.workspace-format-actions [data-md-action="fullscreen"] i')).toHaveClass(/lucide-maximize/); - await expect(toolbar.locator('#review-toggle')).toBeVisible(); - await expect(toolbar.locator('#toggle-sync, #copy-markdown-button')).toHaveCount(2); + await expect(toolbar.locator('#review-toggle, #toggle-sync, #copy-markdown-button')).toHaveCount(0); await expect(toolbar.locator('#exportDropdown')).toHaveCount(0); + await expect(toolbar.locator('.markdown-tool-select--insert, [data-toolbar-menu="insert"]')).toHaveCount(0); + await expect(toolbar.locator('.markdown-toolbar-group--content > .markdown-tool-btn')).toHaveCount(4); + await expect(toolbar.locator('.markdown-toolbar-group--technical > .markdown-tool-btn')).toHaveCount(3); + await expect(toolbar.locator('.markdown-toolbar-group--advanced > .markdown-tool-btn')).toHaveCount(5); await toolbar.locator('.workspace-format-actions [data-md-action="find"]').click(); await expect(page.locator('#find-replace-modal')).toBeVisible(); @@ -39,13 +61,13 @@ test('header and formatting toolbar expose the requested actions in order', asyn await fullscreen.click(); await expect.poll(() => page.evaluate(() => Boolean(document.fullscreenElement))).toBe(false); - await toolbar.locator('#toggle-sync').click(); - await expect(toolbar.locator('#toggle-sync')).toHaveAttribute('aria-pressed', 'false'); - await toolbar.locator('#toggle-sync').click(); - await expect(toolbar.locator('#toggle-sync')).toHaveAttribute('aria-pressed', 'true'); + await header.locator('#toggle-sync').click(); + await expect(header.locator('#toggle-sync')).toHaveAttribute('aria-pressed', 'false'); + await header.locator('#toggle-sync').click(); + await expect(header.locator('#toggle-sync')).toHaveAttribute('aria-pressed', 'true'); await stubClipboard(page); - await toolbar.locator('#copy-markdown-button').click(); - await expect(toolbar.locator('#copy-markdown-button')).toHaveAttribute('aria-label', 'Copied'); + await header.locator('#copy-markdown-button').click(); + await expect(header.locator('#copy-markdown-button')).toHaveAttribute('aria-label', 'Copied'); await expect.poll(() => page.evaluate(() => window.__copiedText)).not.toBe(''); await header.locator('#importDropdown').click(); diff --git a/tests/e2e/tab-split-sidebar-update.spec.js b/tests/e2e/tab-split-sidebar-update.spec.js index 75fbe452..8abaacba 100644 --- a/tests/e2e/tab-split-sidebar-update.spec.js +++ b/tests/e2e/tab-split-sidebar-update.spec.js @@ -119,7 +119,7 @@ test('split view uses one combined tab and offers only edit or preview modes', a await expect(page.locator('.view-toolbar [data-view-mode="split"]')).toBeEnabled(); }); -test('header keeps global actions concise and settings groups workspace preferences', async ({ page }) => { +test('header groups document actions before application preferences', async ({ page }) => { const header = page.locator('.header-right'); await expect(header.locator('#importDropdown')).toBeVisible(); await expect(header.locator('#share-button')).toBeVisible(); @@ -128,41 +128,39 @@ test('header keeps global actions concise and settings groups workspace preferen await expect(header.locator('#header-about-button')).toBeVisible(); await expect(header.locator('#workspaceSettingsDropdown')).toBeVisible(); await expect(header.locator('#exportDropdown')).toBeVisible(); - await expect(header.locator('#toggle-sync, #review-toggle, #copy-markdown-button')).toHaveCount(0); + await expect(header.locator('#toggle-sync, #review-toggle, #copy-markdown-button')).toHaveCount(3); + await expect(header.locator('.header-view-toolbar .view-toggle-btn')).toHaveCount(3); const headerActions = await header.locator(':scope > button, :scope > a, :scope > .dropdown > button').evaluateAll(elements => elements.map(element => element.id || element.getAttribute('aria-label')) ); expect(headerActions).toEqual([ + 'toggle-sync', 'importDropdown', + 'copy-markdown-button', 'exportDropdown', 'share-button', 'live-share-button', + 'review-toggle', 'Report an issue', 'header-about-button', 'workspaceSettingsDropdown' ]); const formatToolbar = page.locator('#markdown-format-toolbar'); - await expect(formatToolbar.locator('#toggle-sync')).toBeVisible(); - await expect(formatToolbar.locator('#copy-markdown-button')).toBeVisible(); - await expect(formatToolbar.locator('#review-toggle')).toBeVisible(); + await expect(formatToolbar.locator('#toggle-sync, #copy-markdown-button, #review-toggle')).toHaveCount(0); await expect(formatToolbar.locator('#exportDropdown')).toHaveCount(0); await expect(formatToolbar.locator('#documentActionsDropdown')).toHaveCount(0); await expect(page.locator('.app-brand-logo')).toHaveCount(0); - const utilityOrder = await formatToolbar.locator('.markdown-toolbar-group--utilities').evaluate(group => - Array.from(group.children).map(element => element.id || element.className) - ); - expect(utilityOrder.slice(0, 4)).toEqual(['toggle-sync', 'copy-markdown-button', 'review-toggle', 'document-command-divider']); - await formatToolbar.locator('#toggle-sync').click(); - await expect(formatToolbar.locator('#toggle-sync')).toHaveAttribute('aria-pressed', 'false'); - await expect(formatToolbar.locator('#toggle-sync')).toHaveAttribute('aria-label', 'Enable synchronized scrolling'); - await formatToolbar.locator('#toggle-sync').click(); - await expect(formatToolbar.locator('#toggle-sync')).toHaveAttribute('aria-pressed', 'true'); + await header.locator('#toggle-sync').click(); + await expect(header.locator('#toggle-sync')).toHaveAttribute('aria-pressed', 'false'); + await expect(header.locator('#toggle-sync')).toHaveAttribute('aria-label', 'Enable synchronized scrolling'); + await header.locator('#toggle-sync').click(); + await expect(header.locator('#toggle-sync')).toHaveAttribute('aria-pressed', 'true'); await stubClipboard(page); - await formatToolbar.locator('#copy-markdown-button').click(); - await expect(formatToolbar.locator('#copy-markdown-button')).toHaveAttribute('aria-label', 'Copied'); - await expect(formatToolbar.locator('#copy-markdown-button i')).toHaveClass(/lucide-check/); + await header.locator('#copy-markdown-button').click(); + await expect(header.locator('#copy-markdown-button')).toHaveAttribute('aria-label', 'Copied'); + await expect(header.locator('#copy-markdown-button i')).toHaveClass(/lucide-check/); await expect.poll(() => page.evaluate(() => window.__copiedText)).not.toBe(''); const initialTabCount = await page.locator('#tab-list .tab-item').count(); @@ -188,14 +186,10 @@ test('header keeps global actions concise and settings groups workspace preferen await expect(settings.locator('#theme-toggle')).toContainText('Appearance'); await page.locator('#workspaceSettingsDropdown').click(); - const insertToggle = formatToolbar.locator('.markdown-tool-select--insert'); - await expect(insertToggle).toHaveAttribute('aria-label', 'More tools'); - await expect(insertToggle).not.toContainText('Insert'); - await expect(formatToolbar.locator('[data-md-action="diagram"]')).toHaveCount(1); - await expect(formatToolbar.locator('[data-toolbar-menu="insert"] [data-md-action="diagram"]')).toHaveCount(0); - await insertToggle.press('Enter'); - await expect(formatToolbar.locator('[data-toolbar-menu="insert"]')).toHaveClass(/open/); - await page.keyboard.press('Escape'); + await expect(formatToolbar.locator('.markdown-tool-select--insert, [data-toolbar-menu="insert"]')).toHaveCount(0); + await expect(formatToolbar.locator('.markdown-toolbar-group--content > .markdown-tool-btn')).toHaveCount(4); + await expect(formatToolbar.locator('.markdown-toolbar-group--technical > .markdown-tool-btn')).toHaveCount(3); + await expect(formatToolbar.locator('.markdown-toolbar-group--advanced > .markdown-tool-btn')).toHaveCount(5); await header.locator('#exportDropdown').click(); const exportMenu = page.locator('[aria-labelledby="exportDropdown"]'); @@ -203,8 +197,8 @@ test('header keeps global actions concise and settings groups workspace preferen await expect(exportMenu).not.toHaveClass(/toolbar-portal-menu/); await expect(exportMenu).toBeInViewport(); - await expect(formatToolbar.locator('.markdown-view-toolbar .view-toggle-btn')).toHaveCount(3); - expect(await formatToolbar.locator('.markdown-view-toolbar .view-toggle-btn').evaluateAll(buttons => + await expect(header.locator('.header-view-toolbar .view-toggle-btn')).toHaveCount(3); + expect(await header.locator('.header-view-toolbar .view-toggle-btn').evaluateAll(buttons => buttons.map(button => button.getAttribute('data-view-mode')) )).toEqual(['editor', 'split', 'preview']); const activeStyle = await page.locator('.view-toggle-btn.is-active').evaluate(button => ({ @@ -282,7 +276,10 @@ test('format toolbar consolidates heading, case, alignment, and insert actions', await expect(page.locator('[data-toolbar-menu-toggle="heading"]')).toBeVisible(); await expect(page.locator('[data-toolbar-menu-toggle="case"]')).toBeVisible(); await expect(page.locator('[data-toolbar-menu-toggle="alignment"]')).toBeVisible(); - await expect(page.locator('[data-toolbar-menu-toggle="insert"]')).toBeVisible(); + await expect(page.locator('[data-toolbar-menu-toggle="insert"], [data-toolbar-menu="insert"]')).toHaveCount(0); + await expect(page.locator('.markdown-toolbar-group--content > .markdown-tool-btn')).toHaveCount(4); + await expect(page.locator('.markdown-toolbar-group--technical > .markdown-tool-btn')).toHaveCount(3); + await expect(page.locator('.markdown-toolbar-group--advanced > .markdown-tool-btn')).toHaveCount(5); await setEditorContent(page, 'toolbar heading'); await page.locator('[data-toolbar-menu-toggle="heading"]').click(); @@ -468,19 +465,9 @@ test('toolbar stays usable on phone and landscape widths', async ({ page }) => { await page.locator('[data-toolbar-menu-toggle="heading"]').click(); await expect(page.locator('[data-toolbar-menu="heading"]')).toBeVisible(); await page.keyboard.press('Escape'); - await page.locator('#markdown-format-toolbar').evaluate(toolbar => { toolbar.scrollLeft = toolbar.scrollWidth; }); - const utilityPlacement = await page.evaluate(() => { - const toolbar = document.querySelector('#markdown-format-toolbar').getBoundingClientRect(); - const utilities = document.querySelector('.markdown-toolbar-group--utilities').getBoundingClientRect(); - return { - toolbarLeft: Math.round(toolbar.left), - toolbarRight: Math.round(toolbar.right), - utilitiesRight: Math.round(utilities.right), - utilitiesLeft: Math.round(utilities.left) - }; - }); - expect(utilityPlacement.utilitiesRight).toBeLessThanOrEqual(utilityPlacement.toolbarRight); - expect(utilityPlacement.utilitiesLeft).toBeGreaterThanOrEqual(utilityPlacement.toolbarLeft); + const toolbar = page.locator('#markdown-format-toolbar'); + await toolbar.evaluate(element => { element.scrollLeft = element.scrollWidth; }); + await expect(toolbar.locator('.markdown-toolbar-group--advanced [data-md-action="alert"]')).toBeInViewport(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); await page.setViewportSize({ width: 844, height: 390 }); diff --git a/tests/e2e/ui-toolbar-redesign.spec.js b/tests/e2e/ui-toolbar-redesign.spec.js index eb8f19c3..0425199f 100644 --- a/tests/e2e/ui-toolbar-redesign.spec.js +++ b/tests/e2e/ui-toolbar-redesign.spec.js @@ -30,18 +30,15 @@ test('Explorer labels and sidebar toggle describe the next action', async ({ pag test('application header keeps its height while showing the compact product identity', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); - await expect(page.locator('.app-brand-icon')).toHaveAttribute('src', 'assets/icon.jpg'); + await expect(page.locator('.app-brand-icon')).toHaveCount(0); await expect(page.locator('.app-brand-title-row h1')).toContainText('Markdown Viewer'); await expect(page.locator('.app-brand-subtitle')).toHaveText('Write. Preview. Share.'); const geometry = await page.evaluate(() => ({ - headerHeight: document.querySelector('.app-header').getBoundingClientRect().height, - iconWidth: document.querySelector('.app-brand-icon').getBoundingClientRect().width, - iconHeight: document.querySelector('.app-brand-icon').getBoundingClientRect().height + headerHeight: document.querySelector('.app-header').getBoundingClientRect().height })); expect(geometry.headerHeight).toBeGreaterThanOrEqual(40); expect(geometry.headerHeight).toBeLessThanOrEqual(45); - expect([geometry.iconWidth, geometry.iconHeight]).toEqual([32, 32]); await expect(page.locator('.app-brand .github-link i')).toHaveCSS('font-size', '20px'); await expect(page.locator('#tab-new-btn')).toHaveCSS('width', '25px'); await expect(page.locator('#tab-new-btn')).toHaveCSS('height', '25px'); @@ -71,8 +68,7 @@ test('shared application overlays use compact type, spacing, and surface styling await expect(page.locator('.about-support')).toContainText('Developed and maintained by ThisIs-Developer.'); await page.locator('#about-modal-close').click(); - await page.locator('.markdown-tool-select--insert').click(); - await page.locator('[data-toolbar-menu="insert"] [data-md-action="alert"]').click(); + await page.locator('.markdown-toolbar-group--advanced [data-md-action="alert"]').click(); await expect(page.locator('#alert-modal')).toHaveClass(/is-visible/); await expect(page.locator('#alert-modal .alert-option').first()).toBeFocused(); await page.keyboard.press('Escape'); @@ -110,36 +106,44 @@ test('every application dialog uses the shared alert modal shell', async ({ page expect(dialogShells.filter(dialog => !dialog.sharedShell || !dialog.header || !dialog.close || !dialog.footer || dialog.padding !== '0px')).toEqual([]); }); -test('toolbar groups preserve actions while simplifying Insert and promoting diagrams', async ({ page }) => { +test('header orders document actions and formatting toolbar follows the recommended grouping', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); + const header = page.locator('.header-right'); const toolbar = page.locator('#markdown-format-toolbar'); - const utilityOrder = await toolbar.locator('.markdown-toolbar-group--utilities').evaluate(group => - Array.from(group.children).map(element => element.id || element.className) + const documentActionOrder = await header.locator(':scope > button, :scope > .dropdown > button').evaluateAll(elements => + elements.map(element => element.id) ); - expect(utilityOrder.slice(0, 4)).toEqual([ + expect(documentActionOrder.slice(0, 7)).toEqual([ 'toggle-sync', + 'importDropdown', 'copy-markdown-button', - 'review-toggle', - 'document-command-divider' + 'exportDropdown', + 'share-button', + 'live-share-button', + 'review-toggle' ]); - const insertToggle = toolbar.locator('.markdown-tool-select--insert'); - await expect(insertToggle).toHaveText(''); - await expect(insertToggle).toHaveAttribute('title', 'More tools'); - await expect(insertToggle.locator('.lucide-ellipsis')).toHaveCount(1); - await expect(toolbar.locator('[data-md-action="diagram"]')).toHaveCount(1); - await expect(toolbar.locator('[data-toolbar-menu="insert"] [data-md-action="diagram"]')).toHaveCount(0); - - await insertToggle.press('Enter'); - const insertMenu = toolbar.locator('[data-toolbar-menu="insert"]'); - await expect(insertMenu).toHaveClass(/open/); - await expect(insertMenu.locator('.markdown-tool-menu-item').first()).toBeFocused(); - await page.keyboard.press('Escape'); - await expect(insertMenu).not.toHaveClass(/open/); - await expect(insertToggle).toBeFocused(); + const toolbarGroups = await toolbar.locator(':scope > .markdown-toolbar-group').evaluateAll(groups => + groups.map(group => ({ + label: group.getAttribute('aria-label'), + actions: Array.from(group.children).map(child => { + const control = child.matches('button') ? child : child.querySelector(':scope > button'); + return control?.getAttribute('data-md-action') || control?.getAttribute('data-toolbar-menu-toggle'); + }).filter(Boolean) + })) + ); + expect(toolbarGroups).toEqual([ + { label: 'History', actions: ['undo', 'redo'] }, + { label: 'Text formatting', actions: ['heading', 'bold', 'italic', 'strike', 'inline-code', 'case'] }, + { label: 'Paragraph formatting', actions: ['quote', 'unordered-list', 'ordered-list', 'alignment'] }, + { label: 'Content insertion', actions: ['link', 'image', 'table', 'reference'] }, + { label: 'Technical content', actions: ['code-block', 'terminal-block', 'diagram'] }, + { label: 'Additional insertion', actions: ['horizontal-rule', 'alert', 'date-time', 'symbols', 'emoji'] }, + { label: 'Workspace actions', actions: ['find', 'fullscreen'] } + ]); + await expect(toolbar.locator('.markdown-tool-select--insert, [data-toolbar-menu="insert"]')).toHaveCount(0); - await insertToggle.click(); - await insertMenu.locator('[data-md-action="emoji"]').click(); + await toolbar.locator('[data-md-action="emoji"]').click(); await expect(page.locator('#emoji-modal')).toBeVisible(); await page.locator('#emoji-modal-search').press('Escape'); await expect(page.locator('#emoji-modal')).toBeHidden(); @@ -227,22 +231,39 @@ test('shared interface roles use the application type and icon scale', async ({ expect(sizing.mobileIcons).toEqual(['14px']); }); -test('New, Export, and formatting menus share one visual system and keyboard dismissal', async ({ page }) => { +test('New and Export menus share one visual system and keyboard dismissal', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); + const activeSyncStyle = await page.locator('#toggle-sync').evaluate(button => { + const style = getComputedStyle(button); + return [style.backgroundColor, style.borderColor, style.color]; + }); + const expectActiveDropdownStyle = async locator => { + await expect.poll(() => locator.evaluate(button => { + const style = getComputedStyle(button); + return [style.backgroundColor, style.borderColor, style.color]; + })).toEqual(activeSyncStyle); + }; + await page.locator('#importDropdown').click(); const newMenu = page.locator('[aria-labelledby="importDropdown"]'); await expect(newMenu).toBeVisible(); + await expect(page.locator('#importDropdown')).toHaveAttribute('aria-expanded', 'true'); + await expectActiveDropdownStyle(page.locator('#importDropdown')); const newSurface = await newMenu.evaluate(menu => { const style = getComputedStyle(menu); return [style.backgroundColor, style.borderColor, style.borderRadius, style.boxShadow]; }); await page.keyboard.press('Escape'); + await expect(newMenu).toHaveClass(/is-closing/); + await expect(newMenu).toHaveCSS('pointer-events', 'none'); await expect(newMenu).toBeHidden(); await page.locator('#exportDropdown').click(); const exportMenu = page.locator('[aria-labelledby="exportDropdown"]'); await expect(exportMenu).toBeVisible(); + await expect(page.locator('#exportDropdown')).toHaveAttribute('aria-expanded', 'true'); + await expectActiveDropdownStyle(page.locator('#exportDropdown')); const exportSurface = await exportMenu.evaluate(menu => { const style = getComputedStyle(menu); return [style.backgroundColor, style.borderColor, style.borderRadius, style.boxShadow]; @@ -250,16 +271,26 @@ test('New, Export, and formatting menus share one visual system and keyboard dis expect(exportSurface).toEqual(newSurface); await page.keyboard.press('Escape'); - const insertToggle = page.locator('.markdown-tool-select--insert'); - await insertToggle.click(); - const insertMenu = page.locator('[data-toolbar-menu="insert"]'); - const insertSurface = await insertMenu.evaluate(menu => { - const style = getComputedStyle(menu); - return [style.backgroundColor, style.borderColor, style.borderRadius, style.boxShadow]; - }); - expect(insertSurface).toEqual(newSurface); - await page.locator('#markdown-editor').click(); - await expect(insertMenu).not.toHaveClass(/open/); + await page.locator('#workspaceSettingsDropdown').click(); + await expect(page.locator('#workspaceSettingsDropdown')).toHaveAttribute('aria-expanded', 'true'); + await expectActiveDropdownStyle(page.locator('#workspaceSettingsDropdown')); + await page.keyboard.press('Escape'); + + await expect(page.locator('.markdown-tool-select--insert, [data-toolbar-menu="insert"]')).toHaveCount(0); +}); + +test('dropdown motion respects reduced-motion preferences', async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'reduce' }); + const toggle = page.locator('#importDropdown'); + const menu = page.locator('[aria-labelledby="importDropdown"]'); + + await toggle.click(); + await expect(menu).toBeVisible(); + await expect(menu).toHaveCSS('transition-duration', '0s'); + + await page.keyboard.press('Escape'); + await expect(menu).toBeHidden(); + await expect(menu).not.toHaveClass(/is-closing/); }); test('toolbar uses theme surfaces and remains usable at desktop and phone widths', async ({ page }) => { @@ -269,8 +300,8 @@ test('toolbar uses theme surfaces and remains usable at desktop and phone widths const desktopSizing = await page.evaluate(() => { const toolbar = document.querySelector('#markdown-format-toolbar'); const formatButton = toolbar.querySelector('.markdown-tool-btn'); - const utilityButton = toolbar.querySelector('.markdown-toolbar-group--document .tool-button'); - const viewButton = toolbar.querySelector('.markdown-view-toolbar .view-toggle-btn'); + const utilityButton = document.querySelector('.header-right #copy-markdown-button'); + const viewButton = document.querySelector('.header-view-toolbar .view-toggle-btn'); return { toolbarHeight: toolbar.getBoundingClientRect().height, formatButton: [formatButton.getBoundingClientRect().width, formatButton.getBoundingClientRect().height], @@ -294,14 +325,6 @@ test('toolbar uses theme surfaces and remains usable at desktop and phone widths const button = toolbar.querySelector('.markdown-tool-btn'); return [button.getBoundingClientRect().width, button.getBoundingClientRect().height]; })(), - utilityButton: (() => { - const button = toolbar.querySelector('.markdown-toolbar-group--document .tool-button'); - return [button.getBoundingClientRect().width, button.getBoundingClientRect().height]; - })(), - viewButton: (() => { - const button = toolbar.querySelector('.markdown-view-toolbar .view-toggle-btn'); - return [button.getBoundingClientRect().width, button.getBoundingClientRect().height]; - })(), minimumTarget: Math.min(...buttons.map(button => button.getBoundingClientRect().height)) }; }); @@ -309,8 +332,8 @@ test('toolbar uses theme surfaces and remains usable at desktop and phone widths expect(mobileSizing.toolbarScrollable).toBe(true); expect(mobileSizing.toolbarHeight).toBe(desktopSizing.toolbarHeight); expect(mobileSizing.formatButton).toEqual(desktopSizing.formatButton); - expect(mobileSizing.utilityButton).toEqual(desktopSizing.utilityButton); - expect(mobileSizing.viewButton).toEqual(desktopSizing.viewButton); + expect(desktopSizing.utilityButton).toEqual([30, 30]); + expect(desktopSizing.viewButton).toEqual([27, 26]); expect(mobileSizing.minimumTarget).toBeGreaterThanOrEqual(26); await page.evaluate(() => { document.documentElement.style.fontSize = '20px'; });