diff --git a/packages/app/src/styles.css b/packages/app/src/styles.css index bace42ce..a19df011 100644 --- a/packages/app/src/styles.css +++ b/packages/app/src/styles.css @@ -4102,6 +4102,10 @@ box-shadow: none; } + .markdown-paper .cm-markra-code-top-gap:has(+ .cm-line .markra-code-block[data-mermaid-mode="preview"]) { + display: none; + } + .markdown-paper .markra-code-block[data-mermaid-mode="preview"] .markra-code-line-numbers, .markdown-paper .markra-code-block[data-mermaid-mode="preview"] pre, .markdown-paper .markra-code-block[data-mermaid-mode="preview"] .markra-code-copy-button, @@ -4210,6 +4214,21 @@ min-height: min(28rem, 88vh); } + .markra-mermaid-zoom-dialog[data-fullscreen="true"] { + padding: 0; + } + + .markra-mermaid-zoom-dialog[data-fullscreen="true"] .markra-mermaid-zoom-panel { + width: 100%; + height: 100%; + max-width: none; + max-height: none; + min-height: 0; + border-width: 0; + border-radius: 0; + box-shadow: none; + } + .markra-mermaid-zoom-toolbar { @apply relative z-[2] flex items-center justify-end gap-1 border-b border-(--border-default) px-3 py-2; background: color-mix(in srgb, var(--editor-paper-bg, var(--bg-primary)) 94%, transparent); diff --git a/packages/app/src/styles.test.ts b/packages/app/src/styles.test.ts index d6cc77e0..b3dc87b7 100644 --- a/packages/app/src/styles.test.ts +++ b/packages/app/src/styles.test.ts @@ -1131,6 +1131,10 @@ describe("editor stylesheet", () => { expect(mermaidFoldStyles).toContain("background: transparent"); expect(mermaidFoldStyles).toContain("border-color: transparent"); expect(mermaidFoldStyles).toContain(".markra-code-language-control"); + expect(styles).toContain( + ".markdown-paper .cm-markra-code-top-gap:has(+ .cm-line .markra-code-block[data-mermaid-mode=\"preview\"]) {\n" + + " display: none;", + ); expect(styles).toContain(".markdown-paper .markra-mermaid-preview-button"); expect(styles).toContain(".markdown-paper .markra-mermaid-zoom-button"); expect(mermaidZoomButtonStyles).toContain("top-4"); @@ -1142,6 +1146,7 @@ describe("editor stylesheet", () => { expect(mermaidZoomCloseHoverStyles).not.toContain(":focus-visible"); expect(mermaidZoomCloseHoverStyles).not.toContain("box-shadow"); expect(styles).toContain(".markra-mermaid-zoom-dialog"); + expect(styles).toContain(".markra-mermaid-zoom-dialog[data-fullscreen=\"true\"]"); expect(styles).toContain(".markra-mermaid-zoom-toolbar"); expect(styles).toContain(".markra-mermaid-zoom-control-button"); expect(styles).toContain(".markra-mermaid-zoom-content[data-dragging=\"true\"]"); diff --git a/packages/editor/src/codemirror/code-block.test.ts b/packages/editor/src/codemirror/code-block.test.ts index 46dd1c8b..52d5eed6 100644 --- a/packages/editor/src/codemirror/code-block.test.ts +++ b/packages/editor/src/codemirror/code-block.test.ts @@ -433,7 +433,18 @@ describe("codeBlockPreviewPlugin", () => { zoomButton?.closest(".markra-code-block")?.dataset.mermaidMode, ).toBe("preview"); expect(zoomButton?.parentElement).not.toBe(preview); - expect(zoomButton?.querySelector(".markra-mermaid-zoom-icon")).not.toBeNull(); + const zoomIcon = zoomButton?.querySelector(".markra-mermaid-zoom-icon"); + const iconPaths = (icon: Element | null | undefined) => ( + Array.from(icon?.querySelectorAll("path") ?? []) + .map((path) => path.getAttribute("d")) + ); + expect(zoomIcon).not.toBeNull(); + expect(iconPaths(zoomIcon)).toEqual([ + "M15 3h6v6", + "m21 3-7 7", + "M9 21H3v-6", + "m3 21 7-7", + ]); expect(zoomButton?.textContent).toBe(""); zoomButton?.click(); @@ -446,12 +457,22 @@ describe("codeBlockPreviewPlugin", () => { ".markra-mermaid-zoom-out-button", ".markra-mermaid-zoom-in-button", ".markra-mermaid-zoom-reset-button", + ".markra-mermaid-zoom-fullscreen-button", ".markra-mermaid-zoom-close-button", ]) { const button = dialog?.querySelector(className); expect(button?.querySelector("svg")).not.toBeNull(); expect(button?.textContent).toBe(""); } + const fullscreenIcon = dialog?.querySelector( + ".markra-mermaid-zoom-fullscreen-icon", + ); + expect(iconPaths(fullscreenIcon)).toEqual([ + "M8 3H5a2 2 0 0 0-2 2v3", + "M21 8V5a2 2 0 0 0-2-2h-3", + "M3 16v3a2 2 0 0 0 2 2h3", + "M16 21h3a2 2 0 0 0 2-2v-3", + ]); dialog?.querySelector(".markra-mermaid-zoom-in-button")?.click(); expect( dialog?.querySelector(".markra-mermaid-zoom-canvas")?.style.transform, @@ -488,6 +509,32 @@ describe("codeBlockPreviewPlugin", () => { dialog?.querySelector(".markra-mermaid-zoom-reset-button")?.click(); expect(canvas?.style.transform).toBe("translate(0px, 0px) scale(1)"); + const fullscreen = dialog?.querySelector( + ".markra-mermaid-zoom-fullscreen-button", + ); + fullscreen?.click(); + expect(dialog?.dataset.fullscreen).toBe("true"); + expect(fullscreen?.ariaPressed).toBe("true"); + expect(fullscreen?.ariaLabel).toBe("Exit full screen"); + expect(iconPaths(dialog?.querySelector(".markra-mermaid-zoom-fullscreen-icon"))).toEqual([ + "M8 3v3a2 2 0 0 1-2 2H3", + "M21 8h-3a2 2 0 0 1-2-2V3", + "M3 16h3a2 2 0 0 1 2 2v3", + "M16 21v-3a2 2 0 0 1 2-2h3", + ]); + + fullscreen?.click(); + expect(dialog?.dataset.fullscreen).toBeUndefined(); + expect(fullscreen?.ariaPressed).toBe("false"); + expect(fullscreen?.ariaLabel).toBe("Enter full screen"); + + fullscreen?.click(); + document.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Escape" })); + expect(document.querySelector(".markra-mermaid-zoom-dialog")).toBe(dialog); + expect(dialog?.dataset.fullscreen).toBeUndefined(); + expect(fullscreen?.ariaPressed).toBe("false"); + expect(fullscreen?.ariaLabel).toBe("Enter full screen"); + document.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Escape" })); expect(document.querySelector(".markra-mermaid-zoom-dialog")).toBeNull(); }); diff --git a/packages/editor/src/codemirror/code-block.ts b/packages/editor/src/codemirror/code-block.ts index 7e7235ba..7cb59336 100644 --- a/packages/editor/src/codemirror/code-block.ts +++ b/packages/editor/src/codemirror/code-block.ts @@ -148,13 +148,27 @@ const checkIconChildren = [ }, ] as const; -const expandIconChildren = [ +const enlargeIconChildren = [ { tag: "path", attributes: { d: "M15 3h6v6" } }, { tag: "path", attributes: { d: "m21 3-7 7" } }, { tag: "path", attributes: { d: "M9 21H3v-6" } }, { tag: "path", attributes: { d: "m3 21 7-7" } }, ] as const; +const fullscreenIconChildren = [ + { tag: "path", attributes: { d: "M8 3H5a2 2 0 0 0-2 2v3" } }, + { tag: "path", attributes: { d: "M21 8V5a2 2 0 0 0-2-2h-3" } }, + { tag: "path", attributes: { d: "M3 16v3a2 2 0 0 0 2 2h3" } }, + { tag: "path", attributes: { d: "M16 21h3a2 2 0 0 0 2-2v-3" } }, +] as const; + +const exitFullscreenIconChildren = [ + { tag: "path", attributes: { d: "M8 3v3a2 2 0 0 1-2 2H3" } }, + { tag: "path", attributes: { d: "M21 8h-3a2 2 0 0 1-2-2V3" } }, + { tag: "path", attributes: { d: "M3 16h3a2 2 0 0 1 2 2v3" } }, + { tag: "path", attributes: { d: "M16 21v-3a2 2 0 0 1 2-2h3" } }, +] as const; + const closeIconChildren = [ { tag: "path", attributes: { d: "M18 6 6 18" } }, { tag: "path", attributes: { d: "m6 6 12 12" } }, @@ -598,6 +612,13 @@ class MermaidPreviewWidget extends WidgetType { "markra-mermaid-zoom-reset-icon", resetViewIconChildren, ); + const fullscreen = makeButton( + "markra-mermaid-zoom-control-button markra-mermaid-zoom-fullscreen-button", + "Enter full screen", + "markra-mermaid-zoom-fullscreen-icon", + fullscreenIconChildren, + ); + fullscreen.ariaPressed = "false"; const close = makeButton( "markra-mermaid-zoom-close-button", "Close enlarged Mermaid diagram", @@ -638,6 +659,24 @@ class MermaidPreviewWidget extends WidgetType { this.zoomScale = Math.max(0.25, Math.min(6, scale)); syncZoom(); }; + const setFullscreen = (isFullscreen: boolean) => { + if (isFullscreen) { + dialog.dataset.fullscreen = "true"; + } else { + delete dialog.dataset.fullscreen; + } + const label = isFullscreen ? "Exit full screen" : "Enter full screen"; + fullscreen.ariaLabel = label; + fullscreen.title = label; + fullscreen.ariaPressed = String(isFullscreen); + fullscreen.replaceChildren( + createCodeControlIcon( + document, + "markra-mermaid-zoom-fullscreen-icon", + isFullscreen ? exitFullscreenIconChildren : fullscreenIconChildren, + ), + ); + }; zoomOut.addEventListener("click", () => setZoom(this.zoomScale - 0.25)); zoomIn.addEventListener("click", () => setZoom(this.zoomScale + 0.25)); reset.addEventListener("click", () => { @@ -646,6 +685,9 @@ class MermaidPreviewWidget extends WidgetType { translateY = 0; syncZoom(); }); + fullscreen.addEventListener("click", () => { + setFullscreen(dialog.dataset.fullscreen !== "true"); + }); close.addEventListener("click", () => this.closeZoom(trigger)); content.addEventListener("wheel", (event) => { event.preventDefault(); @@ -689,11 +731,17 @@ class MermaidPreviewWidget extends WidgetType { this.zoomKeyDown = (event) => { if (event.key !== "Escape") return; event.preventDefault(); + // Preserve the enlarged view on the first Escape by restoring the + // regular dialog before the next Escape closes it. + if (dialog.dataset.fullscreen === "true") { + setFullscreen(false); + return; + } this.closeZoom(trigger); }; document.addEventListener("keydown", this.zoomKeyDown, true); - toolbar.append(zoomOut, zoomValue, zoomIn, reset, close); + toolbar.append(zoomOut, zoomValue, zoomIn, reset, fullscreen, close); content.append(canvas); panel.append(toolbar, content); dialog.append(panel); @@ -719,7 +767,7 @@ class MermaidPreviewWidget extends WidgetType { createCodeControlIcon( view.dom.ownerDocument, "markra-mermaid-zoom-icon", - expandIconChildren, + enlargeIconChildren, ), ); button.addEventListener("mousedown", (event) => event.stopPropagation());