diff --git a/assets/i18n/bg.json b/assets/i18n/bg.json
index 6abd0c0d..7013ac45 100644
--- a/assets/i18n/bg.json
+++ b/assets/i18n/bg.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Разширен изглед",
+ "Exit expanded view": "Изход от разширения изглед",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "Тук целият екран не е наличен. Разширеният изглед оставя контролите на браузъра видими.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Неуспешен изход от целия екран. Опитайте отново или използвайте бутона за изход на браузъра.",
"— Copy/Paste": "— Копиране/поставяне",
"— Open Find & Replace": "— Отваряне на „Търсене и замяна“",
"— Redo": "— Повтаряне",
diff --git a/assets/i18n/de.json b/assets/i18n/de.json
index 31cda7dc..13618cd8 100644
--- a/assets/i18n/de.json
+++ b/assets/i18n/de.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Erweiterte Ansicht",
+ "Exit expanded view": "Erweiterte Ansicht verlassen",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "Vollbild ist hier nicht verfügbar. In der erweiterten Ansicht bleiben die Bedienelemente des Browsers sichtbar.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Vollbild konnte nicht beendet werden. Versuchen Sie es erneut oder verwenden Sie die Beenden-Funktion Ihres Browsers.",
"— Copy/Paste": "– Kopieren/Einfügen",
"— Open Find & Replace": "– Öffnen Sie „Suchen und Ersetzen“.",
"— Redo": "– Wiederholen",
diff --git a/assets/i18n/en.json b/assets/i18n/en.json
index a157a269..bc538970 100644
--- a/assets/i18n/en.json
+++ b/assets/i18n/en.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Expanded view",
+ "Exit expanded view": "Exit expanded view",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "Fullscreen is unavailable here. Expanded view keeps browser controls visible.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Could not exit fullscreen. Try again or use your browser's exit control.",
"— Copy/Paste": "— Copy/Paste",
"— Open Find & Replace": "— Open Find & Replace",
"— Redo": "— Redo",
diff --git a/assets/i18n/es.json b/assets/i18n/es.json
index 7f6f3461..47603ac4 100644
--- a/assets/i18n/es.json
+++ b/assets/i18n/es.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Vista ampliada",
+ "Exit expanded view": "Salir de la vista ampliada",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "La pantalla completa no está disponible aquí. La vista ampliada mantiene visibles los controles del navegador.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "No se pudo salir de la pantalla completa. Inténtalo de nuevo o usa el control de salida del navegador.",
"— Copy/Paste": "— Copiar/Pegar",
"— Open Find & Replace": "— Abrir Buscar y reemplazar",
"— Redo": "— Rehacer",
diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json
index e4690a6c..87ccc818 100644
--- a/assets/i18n/fr.json
+++ b/assets/i18n/fr.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Vue agrandie",
+ "Exit expanded view": "Quitter la vue agrandie",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "Le plein écran n'est pas disponible ici. La vue agrandie conserve les commandes du navigateur visibles.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Impossible de quitter le plein écran. Réessayez ou utilisez la commande de sortie de votre navigateur.",
"— Copy/Paste": "— Copier/Coller",
"— Open Find & Replace": "— Ouvrir Rechercher et remplacer",
"— Redo": "— Refaire",
diff --git a/assets/i18n/it.json b/assets/i18n/it.json
index 26509e69..5c4328a3 100644
--- a/assets/i18n/it.json
+++ b/assets/i18n/it.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Vista espansa",
+ "Exit expanded view": "Esci dalla vista espansa",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "Lo schermo intero non è disponibile qui. La vista espansa mantiene visibili i controlli del browser.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Impossibile uscire dallo schermo intero. Riprova o usa il comando di uscita del browser.",
"— Copy/Paste": "– Copia/Incolla",
"— Open Find & Replace": "– Apri Trova e sostituisci",
"— Redo": "— Ripeti",
diff --git a/assets/i18n/ja.json b/assets/i18n/ja.json
index bf1dc9a4..44ccd8a1 100644
--- a/assets/i18n/ja.json
+++ b/assets/i18n/ja.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "拡大表示",
+ "Exit expanded view": "拡大表示を終了",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "ここでは全画面表示を利用できません。拡大表示ではブラウザーの操作ボタンは表示されたままです。",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "全画面表示を終了できませんでした。もう一度試すか、ブラウザーの終了ボタンを使用してください。",
"— Copy/Paste": "— コピー/ペースト",
"— Open Find & Replace": "— 検索と置換を開く",
"— Redo": "— やり直し",
diff --git a/assets/i18n/ko.json b/assets/i18n/ko.json
index 88e361bd..b9d96358 100644
--- a/assets/i18n/ko.json
+++ b/assets/i18n/ko.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "확장 보기",
+ "Exit expanded view": "확장 보기 종료",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "여기에서는 전체 화면을 사용할 수 없습니다. 확장 보기에서는 브라우저의 조작 버튼이 계속 표시됩니다.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "전체 화면을 종료하지 못했습니다. 다시 시도하거나 브라우저의 종료 버튼을 사용하세요.",
"— Copy/Paste": "— 복사/붙여넣기",
"— Open Find & Replace": "— 찾기 및 바꾸기 열기",
"— Redo": "— 다시 실행",
diff --git a/assets/i18n/pl.json b/assets/i18n/pl.json
index 8922343a..c8993f4c 100644
--- a/assets/i18n/pl.json
+++ b/assets/i18n/pl.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Widok rozszerzony",
+ "Exit expanded view": "Wyjdź z widoku rozszerzonego",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "Pełny ekran jest tutaj niedostępny. Widok rozszerzony pozostawia widoczne elementy sterujące przeglądarki.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Nie udało się wyjść z pełnego ekranu. Spróbuj ponownie lub użyj przycisku wyjścia w przeglądarce.",
"— Copy/Paste": "— Kopiuj/Wklej",
"— Open Find & Replace": "— Otwórz opcję Znajdź i zamień",
"— Redo": "— Wykonaj ponownie",
diff --git a/assets/i18n/pt.json b/assets/i18n/pt.json
index a12d4f8c..43d23891 100644
--- a/assets/i18n/pt.json
+++ b/assets/i18n/pt.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Visualização expandida",
+ "Exit expanded view": "Sair da visualização expandida",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "A tela cheia não está disponível aqui. A visualização expandida mantém os controles do navegador visíveis.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Não foi possível sair da tela cheia. Tente novamente ou use o controle de saída do navegador.",
"— Copy/Paste": "— Copiar/Colar",
"— Open Find & Replace": "- Abra Localizar e Substituir",
"— Redo": "- Refazer",
diff --git a/assets/i18n/ru.json b/assets/i18n/ru.json
index a01902bb..95f79ec4 100644
--- a/assets/i18n/ru.json
+++ b/assets/i18n/ru.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Расширенный вид",
+ "Exit expanded view": "Выйти из расширенного вида",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "Полноэкранный режим здесь недоступен. В расширенном виде элементы управления браузера остаются видимыми.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Не удалось выйти из полноэкранного режима. Повторите попытку или используйте кнопку выхода в браузере.",
"— Copy/Paste": "— Копировать/Вставить",
"— Open Find & Replace": "— открыть «Найти и заменить»",
"— Redo": "— Повторить",
diff --git a/assets/i18n/tr.json b/assets/i18n/tr.json
index 7905c94b..9eaf697b 100644
--- a/assets/i18n/tr.json
+++ b/assets/i18n/tr.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Genişletilmiş görünüm",
+ "Exit expanded view": "Genişletilmiş görünümden çık",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "Tam ekran burada kullanılamıyor. Genişletilmiş görünümde tarayıcı kontrolleri görünür kalır.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Tam ekrandan çıkılamadı. Tekrar deneyin veya tarayıcınızın çıkış kontrolünü kullanın.",
"— Copy/Paste": "— Kopyala/Yapıştır",
"— Open Find & Replace": "— Bul ve Değiştir'i aç",
"— Redo": "— Yinele",
diff --git a/assets/i18n/tw.json b/assets/i18n/tw.json
index 023dee06..379e1b03 100644
--- a/assets/i18n/tw.json
+++ b/assets/i18n/tw.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "展開檢視",
+ "Exit expanded view": "結束展開檢視",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "此處無法使用全螢幕。展開檢視會保留瀏覽器的控制項。",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "無法結束全螢幕。請再試一次,或使用瀏覽器的退出控制項。",
"— Copy/Paste": "— 複製/貼上",
"— Open Find & Replace": "— 開啟尋找與替換",
"— Redo": "— 重做",
diff --git a/assets/i18n/uk.json b/assets/i18n/uk.json
index 840c2733..1afdfb0c 100644
--- a/assets/i18n/uk.json
+++ b/assets/i18n/uk.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "Розширений вигляд",
+ "Exit expanded view": "Вийти з розширеного вигляду",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "Повноекранний режим тут недоступний. У розширеному вигляді елементи керування браузера залишаються видимими.",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "Не вдалося вийти з повноекранного режиму. Спробуйте ще раз або скористайтеся кнопкою виходу браузера.",
"— Copy/Paste": "— Копіювати/Вставити",
"— Open Find & Replace": "— відкрити Знайти й замінити",
"— Redo": "— Повторити",
diff --git a/assets/i18n/zh.json b/assets/i18n/zh.json
index ef34e97d..d6808a2c 100644
--- a/assets/i18n/zh.json
+++ b/assets/i18n/zh.json
@@ -1,4 +1,8 @@
{
+ "Expanded view": "展开视图",
+ "Exit expanded view": "退出展开视图",
+ "Fullscreen is unavailable here. Expanded view keeps browser controls visible.": "此处无法使用全屏。展开视图会保留浏览器的控件。",
+ "Could not exit fullscreen. Try again or use your browser's exit control.": "无法退出全屏。请重试,或使用浏览器的退出控件。",
"— Copy/Paste": "— 复制/粘贴",
"— Open Find & Replace": "— 打开查找和替换",
"— Redo": "— 重做",
diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html
index 3cfcbe2b..dcdebb03 100644
--- a/desktop-app/resources/index.html
+++ b/desktop-app/resources/index.html
@@ -483,7 +483,7 @@
Explorer
-
+
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 36da8191..21e35060 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -11197,8 +11197,8 @@ document.addEventListener("DOMContentLoaded", async function () {
function updateDocumentToolbarAvailability(hasActiveDocument) {
if (markdownFormatToolbar) {
markdownFormatToolbar.classList.toggle('is-no-active-document', !hasActiveDocument);
- markdownFormatToolbar.setAttribute('aria-disabled', hasActiveDocument ? 'false' : 'true');
- updateMissingDocumentControlState(Array.from(markdownFormatToolbar.querySelectorAll('button')), hasActiveDocument);
+ // Fullscreen is a workspace control; its exit must survive closing the last tab.
+ updateMissingDocumentControlState(Array.from(markdownFormatToolbar.querySelectorAll('button:not([data-md-action="fullscreen"])')), hasActiveDocument);
}
updateMissingDocumentControlState([
@@ -22971,6 +22971,124 @@ ${selector} .arrowheadPath {
markdownEditor.addEventListener('scroll', scheduleOutlineHighlight, { passive: true });
}
+ let fullscreenTransitionPending = false;
+ let fullscreenFallbackScroll = null;
+
+ function getNativeFullscreenElement() {
+ return document.fullscreenElement || document.webkitFullscreenElement;
+ }
+
+ function updateFullscreenButtonState() {
+ const fullscreenButton = markdownFormatToolbar && markdownFormatToolbar.querySelector('[data-md-action="fullscreen"]');
+ if (!fullscreenButton) return;
+ const isFallback = document.documentElement.classList.contains('is-fullscreen-fallback');
+ const isFullscreen = Boolean(getNativeFullscreenElement()) || isFallback;
+ const label = isFallback ? 'Exit expanded view' : (isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen');
+ const icon = fullscreenButton.querySelector('i');
+ fullscreenButton.setAttribute('title', label + ' (F11 / Cmd+Option+F)');
+ fullscreenButton.setAttribute('aria-label', label);
+ fullscreenButton.setAttribute('aria-pressed', String(isFullscreen));
+ if (icon) icon.className = isFullscreen ? 'lucide lucide-minimize' : 'lucide lucide-maximize';
+ }
+
+ function updateFullscreenViewport() {
+ if (!document.documentElement.classList.contains('is-fullscreen-fallback')) return;
+ const viewport = window.visualViewport;
+ // Follow the keyboard/browser bars without reflowing the page during pinch zoom.
+ if (viewport && viewport.scale !== 1) return;
+ document.documentElement.style.setProperty('--fullscreen-viewport-height', (viewport ? viewport.height : window.innerHeight) + 'px');
+ }
+
+ function setFullscreenFallback(enabled) {
+ const root = document.documentElement;
+ if (root.classList.contains('is-fullscreen-fallback') === enabled) return;
+ if (enabled) fullscreenFallbackScroll = { x: window.scrollX, y: window.scrollY };
+ root.classList.toggle('is-fullscreen-fallback', enabled);
+ if (enabled) {
+ updateFullscreenViewport();
+ window.scrollTo(0, 0);
+ showAppToast('Fullscreen is unavailable here. Expanded view keeps browser controls visible.', {
+ title: 'Expanded view', tone: 'info', dedupeKey: 'fullscreen-fallback'
+ });
+ } else {
+ root.style.removeProperty('--fullscreen-viewport-height');
+ if (fullscreenFallbackScroll) window.scrollTo(fullscreenFallbackScroll.x, fullscreenFallbackScroll.y);
+ fullscreenFallbackScroll = null;
+ }
+ updateFullscreenButtonState();
+ }
+
+ function changeNativeFullscreen(receiver, method) {
+ return new Promise(function(resolve, reject) {
+ let settled = false;
+ let timeout;
+ function finish(error) {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timeout);
+ ['fullscreenchange', 'webkitfullscreenchange'].forEach(name => document.removeEventListener(name, changed));
+ ['fullscreenerror', 'webkitfullscreenerror'].forEach(name => document.removeEventListener(name, failed));
+ if (error) reject(error);
+ else resolve();
+ }
+ function changed() { finish(); }
+ function failed() { finish(new Error('Fullscreen transition failed')); }
+ ['fullscreenchange', 'webkitfullscreenchange'].forEach(name => document.addEventListener(name, changed));
+ ['fullscreenerror', 'webkitfullscreenerror'].forEach(name => document.addEventListener(name, failed));
+ try {
+ // Call synchronously so Safari retains the click's user activation.
+ const result = method.call(receiver);
+ if (result && typeof result.then === 'function') result.then(changed, failed);
+ // Older Safari returns void; wait for its change/error event, not an immediate await.
+ else if (!settled) timeout = setTimeout(failed, 4000);
+ } catch (error) {
+ finish(error);
+ }
+ });
+ }
+
+ async function toggleFullscreenMode() {
+ if (fullscreenTransitionPending) return;
+ const root = document.documentElement;
+ if (getNativeFullscreenElement()) {
+ fullscreenTransitionPending = true;
+ try {
+ const exit = document.fullscreenElement ? document.exitFullscreen : document.webkitExitFullscreen;
+ await changeNativeFullscreen(document, exit);
+ } catch (error) {
+ showAppToast('Could not exit fullscreen. Try again or use your browser\'s exit control.', { title: 'Fullscreen', tone: 'error' });
+ } finally {
+ fullscreenTransitionPending = false;
+ updateFullscreenButtonState();
+ }
+ return;
+ }
+
+ if (root.classList.contains('is-fullscreen-fallback')) {
+ setFullscreenFallback(false);
+ return;
+ }
+
+ // Prefer the standard API. Use the prefix only on Safari versions without it.
+ const useStandardApi = typeof root.requestFullscreen === 'function';
+ const request = useStandardApi ? root.requestFullscreen : root.webkitRequestFullscreen;
+ const allowed = useStandardApi ? document.fullscreenEnabled : document.webkitFullscreenEnabled;
+ fullscreenTransitionPending = true;
+ try {
+ if (typeof request === 'function' && allowed !== false) {
+ try {
+ await changeNativeFullscreen(root, request);
+ } catch (error) {
+ // A rejected request needs the same fallback as a missing API (e.g. iPhone).
+ }
+ }
+ if (!getNativeFullscreenElement()) setFullscreenFallback(true);
+ } finally {
+ fullscreenTransitionPending = false;
+ updateFullscreenButtonState();
+ }
+ }
+
function runMarkdownTool(action, button) {
if (!canMutateEditor() && isLiveMutatingAction(action)) {
announceToScreenReader(getEditorReadOnlyMessage());
@@ -23027,10 +23145,8 @@ ${selector} .arrowheadPath {
else if (action === 'alert') openAlertModal();
else if (action === 'diagram') openDiagramModal(button);
else if (action === 'terminal-block') insertMarkdownBlock('```bash\nnpm run dev\n```\n');
- else if (action === 'fullscreen') {
- if (document.fullscreenElement && document.exitFullscreen) document.exitFullscreen();
- else if (document.documentElement.requestFullscreen) document.documentElement.requestFullscreen();
- } else if (action === 'outline') setDocumentOutlineOpen(documentOutline.hidden);
+ else if (action === 'fullscreen') toggleFullscreenMode();
+ else if (action === 'outline') setDocumentOutlineOpen(documentOutline.hidden);
else if (action === 'clear-formatting') openClearFormattingModal();
else if (action === 'find') openFindReplaceModal();
else if (action === 'help') openHelpModal();
@@ -24041,16 +24157,22 @@ ${selector} .arrowheadPath {
initDropdownMenuMotion();
initFindReplaceModal();
initAppModals();
- document.addEventListener('fullscreenchange', function() {
- const fullscreenButton = markdownFormatToolbar && markdownFormatToolbar.querySelector('[data-md-action="fullscreen"]');
- if (!fullscreenButton) return;
- const isFullscreen = Boolean(document.fullscreenElement);
- const label = isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen';
- const icon = fullscreenButton.querySelector('i');
- fullscreenButton.setAttribute('title', label + ' (F11 / Cmd+Option+F)');
- fullscreenButton.setAttribute('aria-label', label);
- if (icon) icon.className = isFullscreen ? 'lucide lucide-minimize' : 'lucide lucide-maximize';
+ ['fullscreenchange', 'webkitfullscreenchange'].forEach(function(name) {
+ document.addEventListener(name, function() {
+ if (getNativeFullscreenElement()) setFullscreenFallback(false);
+ updateFullscreenButtonState();
+ });
+ });
+ window.addEventListener('resize', updateFullscreenViewport);
+ if (window.visualViewport) window.visualViewport.addEventListener('resize', updateFullscreenViewport);
+ // Run after dialog/menu handlers so their Escape action takes precedence.
+ window.addEventListener('keydown', function(event) {
+ if (event.key !== 'Escape' || event.defaultPrevented || activeModal || isFindModalOpen) return;
+ if (!document.documentElement.classList.contains('is-fullscreen-fallback')) return;
+ event.preventDefault();
+ setFullscreenFallback(false);
});
+ updateFullscreenButtonState();
const headerAboutButton = document.getElementById('header-about-button');
if (headerAboutButton) {
headerAboutButton.addEventListener('click', openAboutModal);
@@ -27075,9 +27197,8 @@ ${selector} .arrowheadPath {
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.querySelectorAll('button:not([data-md-action="fullscreen"])').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) {
diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css
index 49a6e2f6..0d9c3625 100644
--- a/desktop-app/resources/styles.css
+++ b/desktop-app/resources/styles.css
@@ -178,6 +178,27 @@ body {
overflow: hidden;
}
+/* Keep the expanded view out of print layout and preserve the existing overlay layers. */
+@media screen {
+ html.is-fullscreen-fallback,
+ html.is-fullscreen-fallback body {
+ height: 100%;
+ min-height: 0;
+ overflow: hidden;
+ overscroll-behavior: none;
+ }
+
+ html.is-fullscreen-fallback .app-container {
+ height: 100vh;
+ height: 100dvh;
+ height: var(--fullscreen-viewport-height, 100dvh);
+ }
+
+ html.is-fullscreen-fallback .app-header {
+ display: none;
+ }
+}
+
/* ========================================
DOCUMENT MANAGEMENT SIDEBAR
======================================== */
diff --git a/index.html b/index.html
index bd610001..e52fbb09 100644
--- a/index.html
+++ b/index.html
@@ -580,7 +580,7 @@
Explorer
-
+
diff --git a/script.js b/script.js
index 36da8191..21e35060 100644
--- a/script.js
+++ b/script.js
@@ -11197,8 +11197,8 @@ document.addEventListener("DOMContentLoaded", async function () {
function updateDocumentToolbarAvailability(hasActiveDocument) {
if (markdownFormatToolbar) {
markdownFormatToolbar.classList.toggle('is-no-active-document', !hasActiveDocument);
- markdownFormatToolbar.setAttribute('aria-disabled', hasActiveDocument ? 'false' : 'true');
- updateMissingDocumentControlState(Array.from(markdownFormatToolbar.querySelectorAll('button')), hasActiveDocument);
+ // Fullscreen is a workspace control; its exit must survive closing the last tab.
+ updateMissingDocumentControlState(Array.from(markdownFormatToolbar.querySelectorAll('button:not([data-md-action="fullscreen"])')), hasActiveDocument);
}
updateMissingDocumentControlState([
@@ -22971,6 +22971,124 @@ ${selector} .arrowheadPath {
markdownEditor.addEventListener('scroll', scheduleOutlineHighlight, { passive: true });
}
+ let fullscreenTransitionPending = false;
+ let fullscreenFallbackScroll = null;
+
+ function getNativeFullscreenElement() {
+ return document.fullscreenElement || document.webkitFullscreenElement;
+ }
+
+ function updateFullscreenButtonState() {
+ const fullscreenButton = markdownFormatToolbar && markdownFormatToolbar.querySelector('[data-md-action="fullscreen"]');
+ if (!fullscreenButton) return;
+ const isFallback = document.documentElement.classList.contains('is-fullscreen-fallback');
+ const isFullscreen = Boolean(getNativeFullscreenElement()) || isFallback;
+ const label = isFallback ? 'Exit expanded view' : (isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen');
+ const icon = fullscreenButton.querySelector('i');
+ fullscreenButton.setAttribute('title', label + ' (F11 / Cmd+Option+F)');
+ fullscreenButton.setAttribute('aria-label', label);
+ fullscreenButton.setAttribute('aria-pressed', String(isFullscreen));
+ if (icon) icon.className = isFullscreen ? 'lucide lucide-minimize' : 'lucide lucide-maximize';
+ }
+
+ function updateFullscreenViewport() {
+ if (!document.documentElement.classList.contains('is-fullscreen-fallback')) return;
+ const viewport = window.visualViewport;
+ // Follow the keyboard/browser bars without reflowing the page during pinch zoom.
+ if (viewport && viewport.scale !== 1) return;
+ document.documentElement.style.setProperty('--fullscreen-viewport-height', (viewport ? viewport.height : window.innerHeight) + 'px');
+ }
+
+ function setFullscreenFallback(enabled) {
+ const root = document.documentElement;
+ if (root.classList.contains('is-fullscreen-fallback') === enabled) return;
+ if (enabled) fullscreenFallbackScroll = { x: window.scrollX, y: window.scrollY };
+ root.classList.toggle('is-fullscreen-fallback', enabled);
+ if (enabled) {
+ updateFullscreenViewport();
+ window.scrollTo(0, 0);
+ showAppToast('Fullscreen is unavailable here. Expanded view keeps browser controls visible.', {
+ title: 'Expanded view', tone: 'info', dedupeKey: 'fullscreen-fallback'
+ });
+ } else {
+ root.style.removeProperty('--fullscreen-viewport-height');
+ if (fullscreenFallbackScroll) window.scrollTo(fullscreenFallbackScroll.x, fullscreenFallbackScroll.y);
+ fullscreenFallbackScroll = null;
+ }
+ updateFullscreenButtonState();
+ }
+
+ function changeNativeFullscreen(receiver, method) {
+ return new Promise(function(resolve, reject) {
+ let settled = false;
+ let timeout;
+ function finish(error) {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timeout);
+ ['fullscreenchange', 'webkitfullscreenchange'].forEach(name => document.removeEventListener(name, changed));
+ ['fullscreenerror', 'webkitfullscreenerror'].forEach(name => document.removeEventListener(name, failed));
+ if (error) reject(error);
+ else resolve();
+ }
+ function changed() { finish(); }
+ function failed() { finish(new Error('Fullscreen transition failed')); }
+ ['fullscreenchange', 'webkitfullscreenchange'].forEach(name => document.addEventListener(name, changed));
+ ['fullscreenerror', 'webkitfullscreenerror'].forEach(name => document.addEventListener(name, failed));
+ try {
+ // Call synchronously so Safari retains the click's user activation.
+ const result = method.call(receiver);
+ if (result && typeof result.then === 'function') result.then(changed, failed);
+ // Older Safari returns void; wait for its change/error event, not an immediate await.
+ else if (!settled) timeout = setTimeout(failed, 4000);
+ } catch (error) {
+ finish(error);
+ }
+ });
+ }
+
+ async function toggleFullscreenMode() {
+ if (fullscreenTransitionPending) return;
+ const root = document.documentElement;
+ if (getNativeFullscreenElement()) {
+ fullscreenTransitionPending = true;
+ try {
+ const exit = document.fullscreenElement ? document.exitFullscreen : document.webkitExitFullscreen;
+ await changeNativeFullscreen(document, exit);
+ } catch (error) {
+ showAppToast('Could not exit fullscreen. Try again or use your browser\'s exit control.', { title: 'Fullscreen', tone: 'error' });
+ } finally {
+ fullscreenTransitionPending = false;
+ updateFullscreenButtonState();
+ }
+ return;
+ }
+
+ if (root.classList.contains('is-fullscreen-fallback')) {
+ setFullscreenFallback(false);
+ return;
+ }
+
+ // Prefer the standard API. Use the prefix only on Safari versions without it.
+ const useStandardApi = typeof root.requestFullscreen === 'function';
+ const request = useStandardApi ? root.requestFullscreen : root.webkitRequestFullscreen;
+ const allowed = useStandardApi ? document.fullscreenEnabled : document.webkitFullscreenEnabled;
+ fullscreenTransitionPending = true;
+ try {
+ if (typeof request === 'function' && allowed !== false) {
+ try {
+ await changeNativeFullscreen(root, request);
+ } catch (error) {
+ // A rejected request needs the same fallback as a missing API (e.g. iPhone).
+ }
+ }
+ if (!getNativeFullscreenElement()) setFullscreenFallback(true);
+ } finally {
+ fullscreenTransitionPending = false;
+ updateFullscreenButtonState();
+ }
+ }
+
function runMarkdownTool(action, button) {
if (!canMutateEditor() && isLiveMutatingAction(action)) {
announceToScreenReader(getEditorReadOnlyMessage());
@@ -23027,10 +23145,8 @@ ${selector} .arrowheadPath {
else if (action === 'alert') openAlertModal();
else if (action === 'diagram') openDiagramModal(button);
else if (action === 'terminal-block') insertMarkdownBlock('```bash\nnpm run dev\n```\n');
- else if (action === 'fullscreen') {
- if (document.fullscreenElement && document.exitFullscreen) document.exitFullscreen();
- else if (document.documentElement.requestFullscreen) document.documentElement.requestFullscreen();
- } else if (action === 'outline') setDocumentOutlineOpen(documentOutline.hidden);
+ else if (action === 'fullscreen') toggleFullscreenMode();
+ else if (action === 'outline') setDocumentOutlineOpen(documentOutline.hidden);
else if (action === 'clear-formatting') openClearFormattingModal();
else if (action === 'find') openFindReplaceModal();
else if (action === 'help') openHelpModal();
@@ -24041,16 +24157,22 @@ ${selector} .arrowheadPath {
initDropdownMenuMotion();
initFindReplaceModal();
initAppModals();
- document.addEventListener('fullscreenchange', function() {
- const fullscreenButton = markdownFormatToolbar && markdownFormatToolbar.querySelector('[data-md-action="fullscreen"]');
- if (!fullscreenButton) return;
- const isFullscreen = Boolean(document.fullscreenElement);
- const label = isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen';
- const icon = fullscreenButton.querySelector('i');
- fullscreenButton.setAttribute('title', label + ' (F11 / Cmd+Option+F)');
- fullscreenButton.setAttribute('aria-label', label);
- if (icon) icon.className = isFullscreen ? 'lucide lucide-minimize' : 'lucide lucide-maximize';
+ ['fullscreenchange', 'webkitfullscreenchange'].forEach(function(name) {
+ document.addEventListener(name, function() {
+ if (getNativeFullscreenElement()) setFullscreenFallback(false);
+ updateFullscreenButtonState();
+ });
+ });
+ window.addEventListener('resize', updateFullscreenViewport);
+ if (window.visualViewport) window.visualViewport.addEventListener('resize', updateFullscreenViewport);
+ // Run after dialog/menu handlers so their Escape action takes precedence.
+ window.addEventListener('keydown', function(event) {
+ if (event.key !== 'Escape' || event.defaultPrevented || activeModal || isFindModalOpen) return;
+ if (!document.documentElement.classList.contains('is-fullscreen-fallback')) return;
+ event.preventDefault();
+ setFullscreenFallback(false);
});
+ updateFullscreenButtonState();
const headerAboutButton = document.getElementById('header-about-button');
if (headerAboutButton) {
headerAboutButton.addEventListener('click', openAboutModal);
@@ -27075,9 +27197,8 @@ ${selector} .arrowheadPath {
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.querySelectorAll('button:not([data-md-action="fullscreen"])').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) {
diff --git a/styles.css b/styles.css
index 49a6e2f6..0d9c3625 100644
--- a/styles.css
+++ b/styles.css
@@ -178,6 +178,27 @@ body {
overflow: hidden;
}
+/* Keep the expanded view out of print layout and preserve the existing overlay layers. */
+@media screen {
+ html.is-fullscreen-fallback,
+ html.is-fullscreen-fallback body {
+ height: 100%;
+ min-height: 0;
+ overflow: hidden;
+ overscroll-behavior: none;
+ }
+
+ html.is-fullscreen-fallback .app-container {
+ height: 100vh;
+ height: 100dvh;
+ height: var(--fullscreen-viewport-height, 100dvh);
+ }
+
+ html.is-fullscreen-fallback .app-header {
+ display: none;
+ }
+}
+
/* ========================================
DOCUMENT MANAGEMENT SIDEBAR
======================================== */
diff --git a/tests/e2e/fullscreen.spec.js b/tests/e2e/fullscreen.spec.js
new file mode 100644
index 00000000..2bab04cd
--- /dev/null
+++ b/tests/e2e/fullscreen.spec.js
@@ -0,0 +1,242 @@
+const { test, expect } = require('@playwright/test');
+const { openApp, setEditorContent } = require('../helpers/app');
+
+const fullscreenSelector = '[data-md-action="fullscreen"]';
+test.use({ hasTouch: true });
+
+async function removeFullscreenApi(page) {
+ await page.evaluate(() => {
+ Object.defineProperty(document.documentElement, 'requestFullscreen', { configurable: true, value: undefined });
+ Object.defineProperty(document.documentElement, 'webkitRequestFullscreen', { configurable: true, value: undefined });
+ });
+}
+
+test('mobile fallback can be exited by touch after opening release notes or closing every tab', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await openApp(page, '/', { showReleaseNotes: true });
+ await removeFullscreenApi(page);
+ const toggle = page.locator(fullscreenSelector);
+ await toggle.tap();
+ await expect(page.locator('.app-header')).toBeHidden();
+ await page.locator('#tab-list .release-notes-tab').click();
+ await expect(toggle).toBeEnabled();
+ await toggle.tap();
+ await expect(page.locator('.app-header')).toBeVisible();
+
+ await toggle.tap();
+ while (await page.locator('#tab-list .tab-close-btn').count()) {
+ await page.locator('#tab-list .tab-close-btn').first().click();
+ }
+ await expect(toggle).toBeEnabled();
+ await toggle.tap();
+ await expect(page.locator('.app-header')).toBeVisible();
+});
+
+test('mobile fallback preserves the document, fits rotation, and lets dialogs handle Escape first', async ({ page }, testInfo) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await openApp(page);
+ await removeFullscreenApi(page);
+ const markdown = '# Fullscreen investigation\n\nKeep this content when entering and exiting.';
+ await setEditorContent(page, markdown);
+ const toggle = page.locator(fullscreenSelector);
+ await toggle.click();
+ await expect(toggle).toHaveAttribute('aria-pressed', 'true');
+ await expect(toggle).toHaveAttribute('aria-label', 'Exit expanded view');
+ await expect(page.locator('#app-toast-region')).toContainText('browser controls');
+ await page.getByRole('button', { name: 'Dismiss notification' }).click();
+ await page.screenshot({ path: testInfo.outputPath('expanded-portrait.png') });
+
+ for (const viewport of [{ width: 390, height: 844 }, { width: 844, height: 390 }]) {
+ await page.setViewportSize(viewport);
+ await expect.poll(async () => Math.round((await page.locator('.app-container').boundingBox()).height)).toBe(viewport.height);
+ await expect(page.locator('.app-header')).toBeHidden();
+ await expect(toggle).toBeEnabled();
+ }
+ await page.locator('[data-md-action="link"]').click();
+ await expect(page.locator('#link-modal-url')).toBeFocused();
+ await page.keyboard.press('Escape');
+ await expect(page.locator('#link-modal')).toBeHidden();
+ await expect(toggle).toHaveAttribute('aria-pressed', 'true');
+
+ await page.screenshot({ path: testInfo.outputPath('expanded-landscape.png') });
+ await page.keyboard.press('Escape');
+ await expect(page.locator('.app-header')).toBeVisible();
+ await expect(toggle).toHaveAttribute('aria-pressed', 'false');
+ await expect(page.locator('#markdown-editor')).toHaveValue(markdown);
+});
+
+test('legacy Safari uses its asynchronous prefixed fullscreen API and tracks external exit', async ({ page }) => {
+ await openApp(page);
+ await page.evaluate(() => {
+ const root = document.documentElement;
+ Object.defineProperty(root, 'requestFullscreen', { configurable: true, value: undefined });
+ Object.defineProperty(document, 'webkitFullscreenEnabled', { configurable: true, value: true });
+ let element = null;
+ Object.defineProperty(document, 'webkitFullscreenElement', { configurable: true, get: () => element });
+ window.__exitLegacyFullscreen = () => {
+ element = null;
+ document.dispatchEvent(new Event('webkitfullscreenchange'));
+ };
+ Object.defineProperty(root, 'webkitRequestFullscreen', { configurable: true, value() {
+ if (this !== root) throw new Error('Invalid fullscreen receiver');
+ setTimeout(() => {
+ element = root;
+ document.dispatchEvent(new Event('webkitfullscreenchange'));
+ }, 50);
+ // Older Safari returns void and reports completion only through events.
+ } });
+ Object.defineProperty(document, 'webkitExitFullscreen', { configurable: true, value() {
+ if (this !== document) throw new Error('Invalid exit receiver');
+ setTimeout(window.__exitLegacyFullscreen, 50);
+ } });
+ });
+ const toggle = page.locator(fullscreenSelector);
+ await toggle.click();
+ await expect(toggle).toHaveAttribute('aria-label', 'Exit fullscreen');
+ await expect(page.locator('.app-header')).toBeVisible();
+ await toggle.click();
+ await expect.poll(() => page.evaluate(() => Boolean(document.webkitFullscreenElement))).toBe(false);
+ await expect(toggle).toHaveAttribute('aria-pressed', 'false');
+ await toggle.click();
+ await expect(toggle).toHaveAttribute('aria-pressed', 'true');
+ await page.evaluate(() => window.__exitLegacyFullscreen());
+ await expect(toggle).toHaveAttribute('aria-label', 'Enter fullscreen');
+ await expect(toggle).toHaveAttribute('aria-pressed', 'false');
+});
+
+for (const failure of ['rejection', 'throw', 'disabled', 'webkitfullscreenerror']) {
+ test(`fullscreen ${failure} offers an expanded view without an unhandled error`, async ({ page }) => {
+ const errors = await openApp(page);
+ await page.evaluate(failure => {
+ const root = document.documentElement;
+ window.__fullscreenCalls = 0;
+ Object.defineProperty(document, 'fullscreenEnabled', { configurable: true, value: failure !== 'disabled' });
+ Object.defineProperty(root, 'requestFullscreen', { configurable: true, value: failure === 'webkitfullscreenerror' ? undefined : function() {
+ window.__fullscreenCalls++;
+ if (failure === 'throw') throw new TypeError('Fullscreen is unavailable');
+ return Promise.reject(new TypeError('Fullscreen is not allowed'));
+ } });
+ Object.defineProperty(document, 'webkitFullscreenEnabled', { configurable: true, value: true });
+ Object.defineProperty(document, 'webkitExitFullscreen', { configurable: true, value() {} });
+ Object.defineProperty(root, 'webkitRequestFullscreen', { configurable: true, value() {
+ setTimeout(() => document.dispatchEvent(new Event('webkitfullscreenerror')), 50);
+ } });
+ }, failure);
+ const toggle = page.locator(fullscreenSelector);
+ await toggle.click();
+ await expect(toggle).toHaveAttribute('aria-label', 'Exit expanded view');
+ await expect(page.locator('.app-header')).toBeHidden();
+ if (failure === 'disabled') expect(await page.evaluate(() => window.__fullscreenCalls)).toBe(0);
+ await toggle.click();
+ await expect(toggle).toHaveAttribute('aria-pressed', 'false');
+ await expect(page.locator('.app-header')).toBeVisible();
+ expect(errors).toEqual([]);
+ });
+}
+
+test('repeated clicks cannot leave fallback active after a native fullscreen transition', async ({ page }) => {
+ await openApp(page);
+ await page.evaluate(() => {
+ const root = document.documentElement;
+ let element = null;
+ window.__fullscreenCalls = 0;
+ Object.defineProperty(document, 'fullscreenEnabled', { configurable: true, value: true });
+ Object.defineProperty(document, 'fullscreenElement', { configurable: true, get: () => element });
+ Object.defineProperty(root, 'requestFullscreen', { configurable: true, value() {
+ window.__fullscreenCalls++;
+ return new Promise(resolve => {
+ window.__finishFullscreen = () => {
+ element = root;
+ document.dispatchEvent(new Event('fullscreenchange'));
+ resolve();
+ };
+ });
+ } });
+ Object.defineProperty(document, 'exitFullscreen', { configurable: true, value() {
+ element = null;
+ document.dispatchEvent(new Event('fullscreenchange'));
+ return Promise.resolve();
+ } });
+ });
+ const toggle = page.locator(fullscreenSelector);
+ await toggle.click();
+ await toggle.click();
+ expect(await page.evaluate(() => window.__fullscreenCalls)).toBe(1);
+ await page.evaluate(() => window.__finishFullscreen());
+ await expect(toggle).toHaveAttribute('aria-label', 'Exit fullscreen');
+ await toggle.click();
+ await expect(toggle).toHaveAttribute('aria-pressed', 'false');
+ await expect(page.locator('.app-header')).toBeVisible();
+});
+
+test('printing from expanded view does not clip the page to the screen height', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await openApp(page);
+ await removeFullscreenApi(page);
+ await setEditorContent(page, '# Print\n\n' + 'A paragraph to fill several printed pages.\n\n'.repeat(120));
+ await page.locator(fullscreenSelector).click();
+ await page.emulateMedia({ media: 'print' });
+ await expect(page.locator('html')).toHaveCSS('overflow-y', 'visible');
+ await expect(page.locator('body')).toHaveCSS('overflow-y', 'visible');
+ expect((await page.locator('.app-container').boundingBox()).height).toBeGreaterThan(1000);
+});
+
+test('native fullscreen still enters and exits through the toolbar', async ({ page }) => {
+ await openApp(page);
+ test.skip(!await page.evaluate(() => document.fullscreenEnabled), 'The browser harness does not support native fullscreen.');
+ const toggle = page.locator(fullscreenSelector);
+ await toggle.click();
+ await expect.poll(() => page.evaluate(() => document.fullscreenElement === document.documentElement)).toBe(true);
+ await expect(toggle).toHaveAttribute('aria-label', 'Exit fullscreen');
+ await expect(page.locator('.app-header')).toBeVisible();
+ await toggle.click();
+ await expect.poll(() => page.evaluate(() => Boolean(document.fullscreenElement))).toBe(false);
+ await expect(toggle).toHaveAttribute('aria-pressed', 'false');
+});
+
+test('expanded view follows visual viewport resizing and restores the page size on exit', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await openApp(page);
+ await removeFullscreenApi(page);
+ const initialHeight = (await page.locator('.app-container').boundingBox()).height;
+ await page.locator(fullscreenSelector).click();
+ await page.evaluate(() => {
+ Object.defineProperty(window.visualViewport, 'height', { configurable: true, value: 420 });
+ window.visualViewport.dispatchEvent(new Event('resize'));
+ });
+ await expect.poll(async () => (await page.locator('.app-container').boundingBox()).height).toBe(420);
+ await page.evaluate(() => {
+ Object.defineProperty(window.visualViewport, 'scale', { configurable: true, value: 2 });
+ Object.defineProperty(window.visualViewport, 'height', { configurable: true, value: 210 });
+ window.visualViewport.dispatchEvent(new Event('resize'));
+ });
+ await expect.poll(async () => (await page.locator('.app-container').boundingBox()).height).toBe(420);
+ await page.keyboard.press('Escape');
+ await expect.poll(async () => (await page.locator('.app-container').boundingBox()).height).toBe(initialHeight);
+});
+
+test('a failed native exit leaves an accurate button and can be retried', async ({ page }) => {
+ const errors = await openApp(page);
+ await page.evaluate(() => {
+ const root = document.documentElement;
+ let element = root;
+ let attempts = 0;
+ Object.defineProperty(document, 'fullscreenElement', { configurable: true, get: () => element });
+ Object.defineProperty(document, 'exitFullscreen', { configurable: true, value() {
+ if (++attempts === 1) return Promise.reject(new TypeError('Cannot exit now'));
+ element = null;
+ document.dispatchEvent(new Event('fullscreenchange'));
+ return Promise.resolve();
+ } });
+ document.dispatchEvent(new Event('fullscreenchange'));
+ });
+ const toggle = page.locator(fullscreenSelector);
+ await toggle.click();
+ await expect(page.locator('#app-toast-region')).toContainText('Could not exit fullscreen');
+ await expect(toggle).toHaveAttribute('aria-label', 'Exit fullscreen');
+ await expect(toggle).toHaveAttribute('aria-pressed', 'true');
+ await toggle.click();
+ await expect(toggle).toHaveAttribute('aria-label', 'Enter fullscreen');
+ await expect(toggle).toHaveAttribute('aria-pressed', 'false');
+ expect(errors).toEqual([]);
+});
diff --git a/tests/e2e/smoke.spec.js b/tests/e2e/smoke.spec.js
index 64fe25bf..bd784ca9 100644
--- a/tests/e2e/smoke.spec.js
+++ b/tests/e2e/smoke.spec.js
@@ -65,7 +65,8 @@ test('release notes behave as an application surface instead of a document', asy
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('#markdown-format-toolbar button:not([data-md-action="fullscreen"]):not(:disabled)')).toHaveCount(0);
+ await expect(page.locator('[data-md-action="fullscreen"]')).toBeEnabled();
await expect(page.locator('.app-status-bar')).toBeVisible();
await expect(page.locator('#importDropdown')).toBeVisible();
diff --git a/wiki/Features.md b/wiki/Features.md
index 4563eed6..82f99cf2 100644
--- a/wiki/Features.md
+++ b/wiki/Features.md
@@ -115,7 +115,7 @@ The formatting toolbar inserts or transforms Markdown at the current selection.
- The direction toggle switches between left-to-right and right-to-left content direction.
- Link, image, reference, emoji, symbol, alert, and diagram buttons open focused modals. The table action opens an accessible 8×8 quick selector with pointer and arrow-key navigation; **Custom table** accepts dimensions up to 20×20, with the selected row count including the header.
- Date/time inserts a local timestamp.
-- Fullscreen uses the browser Fullscreen API when available.
+- Fullscreen uses the browser Fullscreen API when available, including the prefixed API on older Safari/iPadOS. If the API is unavailable or the request fails, **Expanded view** hides the app header and fits the workspace to the visible viewport. A notification explains that browser controls remain visible. Use the same button or **Escape** to exit; dialogs handle Escape first. Fullscreen remains available in Release Notes and with no open documents so the exit button stays usable. The expanded layout does not affect printing or document contents. See [Fullscreen on Safari and mobile](Usage-Guide.md#fullscreen-on-safari-and-mobile) for platform limits.
- Find and Replace and Fullscreen are direct formatting-toolbar actions. About Markdown Viewer opens from the header or mobile menu.
View-only Share Snapshot tabs and view-only Live Share participant tabs block mutating tools and announce that the editor is read-only. Non-mutating actions such as fullscreen, find, help, and info remain available.
diff --git a/wiki/Usage-Guide.md b/wiki/Usage-Guide.md
index fedcfd3d..cfcd4537 100644
--- a/wiki/Usage-Guide.md
+++ b/wiki/Usage-Guide.md
@@ -20,6 +20,16 @@ Use the view buttons to switch between Editor, Split, and Preview. Split view is
To compare or edit two files, open a file menu and choose **Open in split view**, then select the second document. A shared control switches both sides between Edit and Preview. Preview mode supports Markdown, math, diagrams, maps, STL, and ABC rendering. Use the combined tab menu to exit this view.
+## Fullscreen on Safari and Mobile
+
+Select **Fullscreen** in the formatting toolbar (scroll the toolbar horizontally on narrow screens), or use **F11** / **Cmd+Option+F** where the browser permits these shortcuts.
+
+- Desktop browsers and supported iPad Safari versions use native fullscreen. Safari versions before 16.4 use its prefixed API.
+- On browsers without page fullscreen, including iPhone Safari, the button opens **Expanded view**. This hides the app header and fits the workspace to the visible viewport as the window, orientation, or keyboard changes. Safari's address and navigation bars remain under the browser's control.
+- Select **Exit expanded view** or press **Escape** to restore the normal layout. If a dialog has focus, Escape closes it first. The exit button stays available after switching to Release Notes or closing all tabs.
+
+Native page fullscreen on iPhone depends on browser support and cannot be enabled by a website fix. WebKit tracks it in [bug 206854](https://bugs.webkit.org/show_bug.cgi?id=206854); [Safari 16.4's release announcement](https://webkit.org/blog/13966/webkit-features-in-safari-16-4/#fullscreen-api) describes support on macOS and iPadOS.
+
## Document Outline
Select the **book-text** button to open **Document Outline**. The formatting toolbar places these controls in order: **Emoji | Find & Replace | Full Screen | Document Outline**. On narrow screens, scroll the toolbar horizontally to reach them. The right sidebar lists the current document's H1–H6 headings in a nested tree, with indentation and subtle hierarchy guides. It updates automatically as you edit, import, or switch documents. Headings inside code blocks do not appear.