From a98035fc1062ac3d2135ea6ed173d95bd67da974 Mon Sep 17 00:00:00 2001 From: edenliu Date: Fri, 14 Aug 2026 18:30:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=20Mermaid=20=E5=85=A8?= =?UTF-8?q?=E5=B1=8F=E7=BC=A9=E6=94=BE=E9=A2=84=E8=A7=88=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E5=B8=83=201.2.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 + Cargo.lock | 2 +- apps/desktop/native-backend/Cargo.toml | 2 +- apps/desktop/package-lock.json | 4 +- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.lock | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/tauri.conf.json | 2 +- apps/desktop/src/App.tsx | 3 + apps/desktop/src/app/i18n/zh-CN.curated.ts | 5 + .../editor/MermaidFilePreview.test.tsx | 3 + .../features/editor/MermaidFilePreview.tsx | 27 +- .../extensions/livePreviewBlocks.test.ts | 5 + .../editor/extensions/livePreviewBlocks.ts | 4 + .../editor/extensions/livePreviewTheme.ts | 1 + .../mermaid/MermaidFullscreenHost.test.tsx | 99 +++++ .../editor/mermaid/MermaidFullscreenHost.tsx | 352 ++++++++++++++++++ .../editor/mermaid/mermaidFullscreen.ts | 36 ++ apps/desktop/src/index.css | 40 ++ 19 files changed, 583 insertions(+), 14 deletions(-) create mode 100644 apps/desktop/src/features/editor/mermaid/MermaidFullscreenHost.test.tsx create mode 100644 apps/desktop/src/features/editor/mermaid/MermaidFullscreenHost.tsx create mode 100644 apps/desktop/src/features/editor/mermaid/mermaidFullscreen.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f4e29b..b20c5f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,12 @@ refactors, dependency updates, CI changes, and code cleanup do not belong here. --- +## [1.2.6] - 2026-08-14 + +### Added + +- Added a full-screen Mermaid viewer for Markdown and standalone Mermaid previews, with trackpad pinch zoom, native panning and scrolling, zoom controls, reset, Escape-to-close, and a dedicated close button. + ## [1.2.5] - 2026-08-14 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 73737c04..b6ec15e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1845,7 +1845,7 @@ dependencies = [ [[package]] name = "neverwrite-native-backend" -version = "1.2.5" +version = "1.2.6" dependencies = [ "agent-client-protocol", "agent-client-protocol-legacy", diff --git a/apps/desktop/native-backend/Cargo.toml b/apps/desktop/native-backend/Cargo.toml index 35bb4d49..f2292628 100644 --- a/apps/desktop/native-backend/Cargo.toml +++ b/apps/desktop/native-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "neverwrite-native-backend" -version = "1.2.5" +version = "1.2.6" edition = "2021" [dependencies] diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json index 09af4f21..3dbb4bf9 100644 --- a/apps/desktop/package-lock.json +++ b/apps/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "bifrostwrite-desktop", - "version": "1.2.5", + "version": "1.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bifrostwrite-desktop", - "version": "1.2.5", + "version": "1.2.6", "hasInstallScript": true, "dependencies": { "@codemirror/commands": "^6.10.3", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 5c9e1789..2248d2c4 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "bifrostwrite-desktop", "private": true, - "version": "1.2.5", + "version": "1.2.6", "homepage": "https://github.com/bifrost-proxy/BifrostWrite", "type": "module", "engines": { diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 5f32e9b7..7284847b 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -250,7 +250,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bifrostwrite-desktop" -version = "1.2.5" +version = "1.2.6" dependencies = [ "arboard", "base64 0.22.1", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 7ca7f479..84ee1915 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bifrostwrite-desktop" -version = "1.2.5" +version = "1.2.6" edition = "2021" description = "Tauri desktop shell for BifrostWrite" license = "Apache-2.0" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 4190fd1a..93125095 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "BifrostWrite", - "version": "1.2.5", + "version": "1.2.6", "identifier": "com.bifrostwrite.desktop", "build": { "beforeDevCommand": "npm run renderer:dev", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 8d951f1d..9bdba4e7 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -119,6 +119,7 @@ import { useChatFoldersStore } from "./features/ai/store/chatFoldersStore"; import { useTerminalRuntimeStore } from "./features/terminal/terminalRuntimeStore"; import { shouldAllowNativeContextMenu } from "./features/spellcheck/contextMenu"; import { YouTubeModalHost } from "./features/editor/YouTubeModalHost"; +import { MermaidFullscreenHost } from "./features/editor/mermaid/MermaidFullscreenHost"; import { ClipNotification } from "./features/clip/ClipNotification"; import { useClipImportStore } from "./features/clip/clipImportStore"; import { @@ -2328,6 +2329,7 @@ export default function App() { + @@ -2373,6 +2375,7 @@ export default function App() { + diff --git a/apps/desktop/src/app/i18n/zh-CN.curated.ts b/apps/desktop/src/app/i18n/zh-CN.curated.ts index 9cd1d14a..80b051c6 100644 --- a/apps/desktop/src/app/i18n/zh-CN.curated.ts +++ b/apps/desktop/src/app/i18n/zh-CN.curated.ts @@ -328,9 +328,14 @@ export const CURATED_ZH_CN_TRANSLATIONS: Readonly> = { "Map view is unavailable in this hardened build": "此安全构建暂不支持概念图视图", "Mermaid diagram error": "Mermaid 图表错误", + "Mermaid canvas": "Mermaid 画布", "Mermaid file mode": "Mermaid 文件模式", "Mermaid preview": "Mermaid 预览", "Mermaid source is empty.": "Mermaid 源码为空。", + "Open Mermaid fullscreen": "全屏查看 Mermaid", + "Pinch to zoom • Scroll to pan • Esc to close": + "双指捏合缩放 • 滚动平移 • Esc 关闭", + "Reset zoom": "重置缩放", "No backlinks": "没有反向链接", "No concept maps yet.": "还没有概念图。", "No file tab active": "当前没有打开的文件标签页", diff --git a/apps/desktop/src/features/editor/MermaidFilePreview.test.tsx b/apps/desktop/src/features/editor/MermaidFilePreview.test.tsx index 7a3fed75..ca183891 100644 --- a/apps/desktop/src/features/editor/MermaidFilePreview.test.tsx +++ b/apps/desktop/src/features/editor/MermaidFilePreview.test.tsx @@ -42,6 +42,9 @@ A --> B`; expect(screen.getByText("Rendering Mermaid diagram...")).toBeInTheDocument(); expect(await screen.findByText("README Flow")).not.toBeNull(); + expect( + screen.getByRole("button", { name: "Open Mermaid fullscreen" }), + ).toBeInTheDocument(); expect(mockedRenderMermaidDiagram).toHaveBeenCalledWith( source, expect.stringMatching(/^mermaid-file-tab-1-/), diff --git a/apps/desktop/src/features/editor/MermaidFilePreview.tsx b/apps/desktop/src/features/editor/MermaidFilePreview.tsx index dda61431..678a9e75 100644 --- a/apps/desktop/src/features/editor/MermaidFilePreview.tsx +++ b/apps/desktop/src/features/editor/MermaidFilePreview.tsx @@ -4,6 +4,7 @@ import { renderMermaidDiagram, type MermaidRenderResult, } from "./mermaid/mermaidRenderer"; +import { openMermaidFullscreen } from "./mermaid/mermaidFullscreen"; import { parseMermaidSvg } from "./mermaid/mermaidSvg"; type MermaidFilePreviewState = @@ -84,12 +85,26 @@ export function MermaidFilePreview({ source, tabId }: MermaidFilePreviewProps) { ) : null} {state.status === "rendered" ? (
{ - if (!node) return; - node.replaceChildren(state.svg.cloneNode(true)); - }} - /> + className="mermaid-file-preview-body relative flex min-h-full items-start justify-center" + > +
{ + if (!node) return; + node.replaceChildren( + state.svg.cloneNode(true), + ); + }} + /> + +
) : null} {state.status === "error" ? (
{ view.dom.querySelector(".cm-mermaid-preview svg foreignObject") ?.textContent, ).toBe("README\u00a0Diagram"); + expect( + view.dom.querySelector( + 'button[aria-label="Open Mermaid fullscreen"]', + ), + ).not.toBeNull(); expect(view.dom.querySelector(".cm-mermaid-preview-error")).toBeNull(); view.destroy(); diff --git a/apps/desktop/src/features/editor/extensions/livePreviewBlocks.ts b/apps/desktop/src/features/editor/extensions/livePreviewBlocks.ts index bbd18b58..75167739 100644 --- a/apps/desktop/src/features/editor/extensions/livePreviewBlocks.ts +++ b/apps/desktop/src/features/editor/extensions/livePreviewBlocks.ts @@ -54,6 +54,7 @@ import { renderEmbedPreview, } from "./notePreviewSource"; import { renderMermaidDiagram } from "../mermaid/mermaidRenderer"; +import { createMermaidFullscreenButton } from "../mermaid/mermaidFullscreen"; import { parseMermaidSvg } from "../mermaid/mermaidSvg"; import { formatCodeFenceLanguageLabel } from "../codeFencePresentation"; @@ -1029,6 +1030,9 @@ class MermaidDiagramWidget extends WidgetType { const svg = parseMermaidSvg(result.svg); if (svg) { body.appendChild(svg); + body.appendChild( + createMermaidFullscreenButton(result.svg), + ); } else { renderMermaidError( body, diff --git a/apps/desktop/src/features/editor/extensions/livePreviewTheme.ts b/apps/desktop/src/features/editor/extensions/livePreviewTheme.ts index 07c766e8..a67dd25f 100644 --- a/apps/desktop/src/features/editor/extensions/livePreviewTheme.ts +++ b/apps/desktop/src/features/editor/extensions/livePreviewTheme.ts @@ -1069,6 +1069,7 @@ export const livePreviewTheme = EditorView.baseTheme({ fontSize: "0.9em", }, ".cm-mermaid-preview-body-rendered": { + position: "relative", display: "flex", justifyContent: "center", alignItems: "center", diff --git a/apps/desktop/src/features/editor/mermaid/MermaidFullscreenHost.test.tsx b/apps/desktop/src/features/editor/mermaid/MermaidFullscreenHost.test.tsx new file mode 100644 index 00000000..0a277dd1 --- /dev/null +++ b/apps/desktop/src/features/editor/mermaid/MermaidFullscreenHost.test.tsx @@ -0,0 +1,99 @@ +import { act, fireEvent, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { renderComponent } from "../../../test/test-utils"; +import { MermaidFullscreenHost } from "./MermaidFullscreenHost"; +import { + createMermaidFullscreenButton, + openMermaidFullscreen, +} from "./mermaidFullscreen"; + +const TEST_SVG = ` + + Fullscreen diagram + +`; + +describe("MermaidFullscreenHost", () => { + it("opens from a Mermaid preview button and closes from the toolbar", () => { + renderComponent(); + const trigger = createMermaidFullscreenButton(TEST_SVG); + document.body.appendChild(trigger); + + fireEvent.click(trigger); + + expect( + screen.getByRole("dialog", { name: "Mermaid preview" }), + ).toBeInTheDocument(); + expect(screen.getByText("Fullscreen diagram")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Reset zoom" })).toHaveTextContent( + "100%", + ); + + fireEvent.click(screen.getByRole("button", { name: "Close" })); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + trigger.remove(); + }); + + it("supports toolbar and trackpad zoom while leaving regular scrolling native", () => { + renderComponent(); + act(() => openMermaidFullscreen(TEST_SVG)); + + fireEvent.click(screen.getByRole("button", { name: "Zoom in" })); + expect(screen.getByRole("button", { name: "Reset zoom" })).toHaveTextContent( + "125%", + ); + + const canvas = screen.getByLabelText("Mermaid canvas"); + fireEvent.click(screen.getByRole("button", { name: "Reset zoom" })); + const gestureStart = new Event("gesturestart", { + bubbles: true, + cancelable: true, + }); + act(() => canvas.dispatchEvent(gestureStart)); + const gestureChange = new Event("gesturechange", { + bubbles: true, + cancelable: true, + }); + Object.defineProperties(gestureChange, { + scale: { value: 1.5 }, + clientX: { value: 40 }, + clientY: { value: 40 }, + }); + act(() => canvas.dispatchEvent(gestureChange)); + expect(gestureChange.defaultPrevented).toBe(true); + expect(screen.getByRole("button", { name: "Reset zoom" })).toHaveTextContent( + "150%", + ); + + fireEvent.click(screen.getByRole("button", { name: "Reset zoom" })); + const plainWheel = new WheelEvent("wheel", { + bubbles: true, + cancelable: true, + deltaY: 40, + }); + canvas.dispatchEvent(plainWheel); + expect(plainWheel.defaultPrevented).toBe(false); + expect(screen.getByRole("button", { name: "Reset zoom" })).toHaveTextContent( + "100%", + ); + + fireEvent.wheel(canvas, { + ctrlKey: true, + deltaY: -100, + clientX: 40, + clientY: 40, + }); + expect(screen.getByRole("button", { name: "Reset zoom" })).toHaveTextContent( + "125%", + ); + }); + + it("closes on Escape", () => { + renderComponent(); + act(() => openMermaidFullscreen(TEST_SVG)); + + fireEvent.keyDown(window, { key: "Escape" }); + + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); +}); diff --git a/apps/desktop/src/features/editor/mermaid/MermaidFullscreenHost.tsx b/apps/desktop/src/features/editor/mermaid/MermaidFullscreenHost.tsx new file mode 100644 index 00000000..460cb0ce --- /dev/null +++ b/apps/desktop/src/features/editor/mermaid/MermaidFullscreenHost.tsx @@ -0,0 +1,352 @@ +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + isWheelZoomGesture, + useWheelZoomModifier, +} from "../../../app/hooks/useWheelZoomModifier"; +import { translate } from "../../../app/i18n"; +import { formatZoomPercentage } from "../../../app/utils/zoom"; +import { + OPEN_MERMAID_FULLSCREEN_EVENT, + type OpenMermaidFullscreenPayload, +} from "./mermaidFullscreen"; +import { parseMermaidSvg } from "./mermaidSvg"; + +const MIN_ZOOM = 0.25; +const MAX_ZOOM = 8; +const ZOOM_STEP = 1.25; +const PINCH_SENSITIVITY = 0.0025; + +interface ZoomAnchor { + pointerOffsetX: number; + pointerOffsetY: number; + previousZoom: number; + nextZoom: number; +} + +function clampZoom(value: number) { + return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, value)); +} + +function clampScrollOffset(value: number) { + return Number.isFinite(value) ? Math.max(0, value) : 0; +} + +export function MermaidFullscreenHost() { + const [svgSource, setSvgSource] = useState(null); + const [zoom, setZoom] = useState(1); + const viewportRef = useRef(null); + const svgMountRef = useRef(null); + const zoomRef = useRef(1); + const pendingZoomAnchorRef = useRef(null); + const previousFocusRef = useRef(null); + const wheelZoomModifierRef = useWheelZoomModifier(); + const svg = useMemo( + () => (svgSource ? parseMermaidSvg(svgSource) : null), + [svgSource], + ); + + const close = useCallback(() => { + setSvgSource(null); + pendingZoomAnchorRef.current = null; + window.setTimeout(() => previousFocusRef.current?.focus(), 0); + }, []); + + const setZoomAround = useCallback((requestedZoom: number, anchor?: { x: number; y: number }) => { + const viewport = viewportRef.current; + const previousZoom = zoomRef.current; + const nextZoom = clampZoom(requestedZoom); + if (!viewport || nextZoom === previousZoom) return; + + const rect = viewport.getBoundingClientRect(); + pendingZoomAnchorRef.current = { + pointerOffsetX: anchor ? anchor.x - rect.left : viewport.clientWidth / 2, + pointerOffsetY: anchor ? anchor.y - rect.top : viewport.clientHeight / 2, + previousZoom, + nextZoom, + }; + zoomRef.current = nextZoom; + setZoom(nextZoom); + }, []); + + useEffect(() => { + const handleOpen = (event: Event) => { + const detail = (event as CustomEvent) + .detail; + if (!detail?.svg.trim() || !parseMermaidSvg(detail.svg)) return; + + previousFocusRef.current = + document.activeElement instanceof HTMLElement + ? document.activeElement + : null; + zoomRef.current = 1; + pendingZoomAnchorRef.current = null; + setZoom(1); + setSvgSource(detail.svg); + }; + + window.addEventListener(OPEN_MERMAID_FULLSCREEN_EVENT, handleOpen); + return () => + window.removeEventListener( + OPEN_MERMAID_FULLSCREEN_EVENT, + handleOpen, + ); + }, []); + + useEffect(() => { + if (!svgSource) return; + + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + viewportRef.current?.focus(); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + event.preventDefault(); + event.stopPropagation(); + close(); + }; + + window.addEventListener("keydown", handleKeyDown, true); + return () => { + document.body.style.overflow = previousOverflow; + window.removeEventListener("keydown", handleKeyDown, true); + }; + }, [close, svgSource]); + + useEffect(() => { + const viewport = viewportRef.current; + if (!viewport || !svgSource) return; + let gestureStartZoom = zoomRef.current; + + const handleWheel = (event: WheelEvent) => { + if (!isWheelZoomGesture(event, wheelZoomModifierRef)) return; + event.preventDefault(); + event.stopPropagation(); + + setZoomAround( + zoomRef.current * + (1 - event.deltaY * PINCH_SENSITIVITY), + { x: event.clientX, y: event.clientY }, + ); + }; + + const handleGestureStart = (event: Event) => { + event.preventDefault(); + event.stopPropagation(); + gestureStartZoom = zoomRef.current; + }; + + const handleGestureChange = (event: Event) => { + const gesture = event as Event & { + scale?: number; + clientX?: number; + clientY?: number; + }; + if (!Number.isFinite(gesture.scale)) return; + event.preventDefault(); + event.stopPropagation(); + + setZoomAround(gestureStartZoom * (gesture.scale ?? 1), { + x: gesture.clientX ?? viewport.clientWidth / 2, + y: gesture.clientY ?? viewport.clientHeight / 2, + }); + }; + + viewport.addEventListener("wheel", handleWheel, { passive: false }); + viewport.addEventListener("gesturestart", handleGestureStart, { + passive: false, + }); + viewport.addEventListener("gesturechange", handleGestureChange, { + passive: false, + }); + return () => { + viewport.removeEventListener("wheel", handleWheel); + viewport.removeEventListener("gesturestart", handleGestureStart); + viewport.removeEventListener("gesturechange", handleGestureChange); + }; + }, [setZoomAround, svgSource, wheelZoomModifierRef]); + + useLayoutEffect(() => { + const viewport = viewportRef.current; + const pending = pendingZoomAnchorRef.current; + if (!viewport || !pending) return; + + const ratio = pending.nextZoom / pending.previousZoom; + viewport.scrollLeft = clampScrollOffset( + (viewport.scrollLeft + pending.pointerOffsetX) * ratio - + pending.pointerOffsetX, + ); + viewport.scrollTop = clampScrollOffset( + (viewport.scrollTop + pending.pointerOffsetY) * ratio - + pending.pointerOffsetY, + ); + pendingZoomAnchorRef.current = null; + }, [zoom]); + + useLayoutEffect(() => { + const mount = svgMountRef.current; + if (!mount || !svg) return; + + const clone = svg.cloneNode(true) as SVGSVGElement; + clone.style.width = "100%"; + clone.style.maxWidth = "none"; + clone.style.height = "auto"; + clone.style.display = "block"; + mount.replaceChildren(clone); + }, [svg]); + + if (!svgSource || !svg) return null; + + return ( +
+
+ {translate("Mermaid preview")} +
+ +
+ setZoomAround(zoomRef.current / ZOOM_STEP)} + > + − + + + setZoomAround(zoomRef.current * ZOOM_STEP)} + > + + + +
+ +
+ {translate("Pinch to zoom • Scroll to pan • Esc to close")} +
+ +
+
+
+
+
+
+ ); +} + +function ToolbarButton({ + children, + label, + onClick, +}: { + children: string; + label: string; + onClick: () => void; +}) { + return ( + + ); +} + +const toolbarButtonStyle = { + border: "1px solid transparent", + background: "transparent", + color: "var(--text-primary)", + cursor: "pointer", +} as const; diff --git a/apps/desktop/src/features/editor/mermaid/mermaidFullscreen.ts b/apps/desktop/src/features/editor/mermaid/mermaidFullscreen.ts new file mode 100644 index 00000000..1862a7ea --- /dev/null +++ b/apps/desktop/src/features/editor/mermaid/mermaidFullscreen.ts @@ -0,0 +1,36 @@ +import { translate } from "../../../app/i18n"; + +export const OPEN_MERMAID_FULLSCREEN_EVENT = + "bifrostwrite:open-mermaid-fullscreen"; + +export interface OpenMermaidFullscreenPayload { + svg: string; +} + +export function openMermaidFullscreen(svg: string | SVGElement) { + const source = typeof svg === "string" ? svg : svg.outerHTML; + if (!source.trim()) return; + + window.dispatchEvent( + new CustomEvent( + OPEN_MERMAID_FULLSCREEN_EVENT, + { detail: { svg: source } }, + ), + ); +} + +export function createMermaidFullscreenButton(svg: string) { + const button = document.createElement("button"); + const label = translate("Open Mermaid fullscreen"); + button.type = "button"; + button.className = "mermaid-fullscreen-trigger"; + button.setAttribute("aria-label", label); + button.title = label; + button.textContent = "⛶"; + button.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + openMermaidFullscreen(svg); + }); + return button; +} diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index 78587efc..1ce350d6 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -2,6 +2,46 @@ @import "katex/dist/katex.min.css"; @import "react-datasheet-grid/dist/style.css"; +.mermaid-fullscreen-trigger { + position: absolute; + top: 8px; + right: 8px; + z-index: 2; + display: inline-flex; + width: 30px; + height: 30px; + align-items: center; + justify-content: center; + border: 1px solid var(--border); + border-radius: 8px; + background: color-mix(in srgb, var(--bg-primary) 92%, transparent); + color: var(--text-primary); + box-shadow: 0 6px 20px rgb(0 0 0 / 0.14); + cursor: pointer; + font-size: 16px; + line-height: 1; + opacity: 0.82; + transition: + opacity 120ms ease, + background-color 120ms ease, + border-color 120ms ease; +} + +.mermaid-fullscreen-trigger:hover, +.mermaid-fullscreen-trigger:focus-visible { + border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); + background: color-mix(in srgb, var(--accent) 12%, var(--bg-primary)); + opacity: 1; + outline: none; +} + +.mermaid-fullscreen-toolbar-button:hover, +.mermaid-fullscreen-toolbar-button:focus-visible { + border-color: color-mix(in srgb, var(--accent) 35%, var(--border)) !important; + background: color-mix(in srgb, var(--accent) 12%, transparent) !important; + outline: none; +} + @font-face { font-family: "JetBrains Mono"; src: url("./assets/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2")