Jacky Zhao
2025-03-11 87b803790c10dde62cbc5452014c619eced5b36b
fix(mermaid): themechange detector + expand simplification
3 files modified
216 ■■■■■ changed files
quartz/components/scripts/mermaid.inline.ts 108 ●●●● patch | view | raw | blame | history
quartz/components/styles/mermaid.inline.scss 48 ●●●● patch | view | raw | blame | history
quartz/plugins/transformers/ofm.ts 60 ●●●●● patch | view | raw | blame | history
quartz/components/scripts/mermaid.inline.ts
@@ -12,7 +12,8 @@
  private scale = 1
  private readonly MIN_SCALE = 0.5
  private readonly MAX_SCALE = 3
  private readonly ZOOM_SENSITIVITY = 0.001
  cleanups: (() => void)[] = []
  constructor(
    private container: HTMLElement,
@@ -20,19 +21,33 @@
  ) {
    this.setupEventListeners()
    this.setupNavigationControls()
    this.resetTransform()
  }
  private setupEventListeners() {
    // Mouse drag events
    this.container.addEventListener("mousedown", this.onMouseDown.bind(this))
    document.addEventListener("mousemove", this.onMouseMove.bind(this))
    document.addEventListener("mouseup", this.onMouseUp.bind(this))
    const mouseDownHandler = this.onMouseDown.bind(this)
    const mouseMoveHandler = this.onMouseMove.bind(this)
    const mouseUpHandler = this.onMouseUp.bind(this)
    const resizeHandler = this.resetTransform.bind(this)
    // Wheel zoom events
    this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false })
    this.container.addEventListener("mousedown", mouseDownHandler)
    document.addEventListener("mousemove", mouseMoveHandler)
    document.addEventListener("mouseup", mouseUpHandler)
    window.addEventListener("resize", resizeHandler)
    // Reset on window resize
    window.addEventListener("resize", this.resetTransform.bind(this))
    this.cleanups.push(
      () => this.container.removeEventListener("mousedown", mouseDownHandler),
      () => document.removeEventListener("mousemove", mouseMoveHandler),
      () => document.removeEventListener("mouseup", mouseUpHandler),
      () => window.removeEventListener("resize", resizeHandler),
    )
  }
  cleanup() {
    for (const cleanup of this.cleanups) {
      cleanup()
    }
  }
  private setupNavigationControls() {
@@ -84,26 +99,6 @@
    this.container.style.cursor = "grab"
  }
  private onWheel(e: WheelEvent) {
    e.preventDefault()
    const delta = -e.deltaY * this.ZOOM_SENSITIVITY
    const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
    // Calculate mouse position relative to content
    const rect = this.content.getBoundingClientRect()
    const mouseX = e.clientX - rect.left
    const mouseY = e.clientY - rect.top
    // Adjust pan to zoom around mouse position
    const scaleDiff = newScale - this.scale
    this.currentPan.x -= mouseX * scaleDiff
    this.currentPan.y -= mouseY * scaleDiff
    this.scale = newScale
    this.updateTransform()
  }
  private zoom(delta: number) {
    const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
@@ -126,7 +121,11 @@
  private resetTransform() {
    this.scale = 1
    this.currentPan = { x: 0, y: 0 }
    const svg = this.content.querySelector("svg")!
    this.currentPan = {
      x: svg.getBoundingClientRect().width / 2,
      y: svg.getBoundingClientRect().height / 2,
    }
    this.updateTransform()
  }
}
@@ -149,20 +148,35 @@
  const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement>
  if (nodes.length === 0) return
  const computedStyleMap = cssVars.reduce(
    (acc, key) => {
      acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key)
      return acc
    },
    {} as Record<(typeof cssVars)[number], string>,
  )
  mermaidImport ||= await import(
    // @ts-ignore
    "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs"
  )
  const mermaid = mermaidImport.default
  const textMapping: WeakMap<HTMLElement, string> = new WeakMap()
  for (const node of nodes) {
    textMapping.set(node, node.innerText)
  }
  async function renderMermaid() {
    // de-init any other diagrams
    for (const node of nodes) {
      node.removeAttribute("data-processed")
      const oldText = textMapping.get(node)
      if (oldText) {
        node.innerHTML = oldText
      }
    }
    const computedStyleMap = cssVars.reduce(
      (acc, key) => {
        acc[key] = window.getComputedStyle(document.documentElement).getPropertyValue(key)
        return acc
      },
      {} as Record<(typeof cssVars)[number], string>,
    )
  const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
  mermaid.initialize({
    startOnLoad: false,
@@ -180,7 +194,13 @@
      edgeLabelBackground: computedStyleMap["--highlight"],
    },
  })
  await mermaid.run({ nodes })
  }
  await renderMermaid()
  document.addEventListener("themechange", renderMermaid)
  window.addCleanup(() => document.removeEventListener("themechange", renderMermaid))
  for (let i = 0; i < nodes.length; i++) {
    const codeBlock = nodes[i] as HTMLElement
@@ -203,7 +223,6 @@
    if (!popupContainer) return
    let panZoom: DiagramPanZoom | null = null
    function showMermaid() {
      const container = popupContainer.querySelector("#mermaid-space") as HTMLElement
      const content = popupContainer.querySelector(".mermaid-content") as HTMLElement
@@ -224,24 +243,15 @@
    function hideMermaid() {
      popupContainer.classList.remove("active")
      panZoom?.cleanup()
      panZoom = null
    }
    function handleEscape(e: any) {
      if (e.key === "Escape") {
        hideMermaid()
      }
    }
    const closeBtn = popupContainer.querySelector(".close-button") as HTMLButtonElement
    closeBtn.addEventListener("click", hideMermaid)
    expandBtn.addEventListener("click", showMermaid)
    registerEscapeHandler(popupContainer, hideMermaid)
    document.addEventListener("keydown", handleEscape)
    window.addCleanup(() => {
      closeBtn.removeEventListener("click", hideMermaid)
      panZoom?.cleanup()
      expandBtn.removeEventListener("click", showMermaid)
    })
  }
quartz/components/styles/mermaid.inline.scss
@@ -53,46 +53,16 @@
  }
  & > #mermaid-space {
    display: grid;
    width: 90%;
    height: 90vh;
    margin: 5vh auto;
    background: var(--light);
    box-shadow:
      0 14px 50px rgba(27, 33, 48, 0.12),
      0 10px 30px rgba(27, 33, 48, 0.16);
    border: 1px solid var(--lightgray);
    background-color: var(--light);
    border-radius: 5px;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    height: 80vh;
    width: 80vw;
    overflow: hidden;
    position: relative;
    & > .mermaid-header {
      display: flex;
      justify-content: flex-end;
      padding: 1rem;
      border-bottom: 1px solid var(--lightgray);
      background: var(--light);
      z-index: 2;
      max-height: fit-content;
      & > .close-button {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 32px;
        height: 32px;
        padding: 0;
        background: transparent;
        border: none;
        border-radius: 4px;
        color: var(--darkgray);
        cursor: pointer;
        transition: all 0.2s ease;
        &:hover {
          background: var(--lightgray);
          color: var(--dark);
        }
      }
    }
    & > .mermaid-content {
      padding: 2rem;
quartz/plugins/transformers/ofm.ts
@@ -675,7 +675,6 @@
                    properties: {
                      className: ["expand-button"],
                      "aria-label": "Expand mermaid diagram",
                      "aria-hidden": "true",
                      "data-view-component": true,
                    },
                    children: [
@@ -706,7 +705,7 @@
                  {
                    type: "element",
                    tagName: "div",
                    properties: { id: "mermaid-container" },
                    properties: { id: "mermaid-container", role: "dialog" },
                    children: [
                      {
                        type: "element",
@@ -716,63 +715,6 @@
                          {
                            type: "element",
                            tagName: "div",
                            properties: { className: ["mermaid-header"] },
                            children: [
                              {
                                type: "element",
                                tagName: "button",
                                properties: {
                                  className: ["close-button"],
                                  "aria-label": "close button",
                                },
                                children: [
                                  {
                                    type: "element",
                                    tagName: "svg",
                                    properties: {
                                      "aria-hidden": "true",
                                      xmlns: "http://www.w3.org/2000/svg",
                                      width: 24,
                                      height: 24,
                                      viewBox: "0 0 24 24",
                                      fill: "none",
                                      stroke: "currentColor",
                                      "stroke-width": "2",
                                      "stroke-linecap": "round",
                                      "stroke-linejoin": "round",
                                    },
                                    children: [
                                      {
                                        type: "element",
                                        tagName: "line",
                                        properties: {
                                          x1: 18,
                                          y1: 6,
                                          x2: 6,
                                          y2: 18,
                                        },
                                        children: [],
                                      },
                                      {
                                        type: "element",
                                        tagName: "line",
                                        properties: {
                                          x1: 6,
                                          y1: 6,
                                          x2: 18,
                                          y2: 18,
                                        },
                                        children: [],
                                      },
                                    ],
                                  },
                                ],
                              },
                            ],
                          },
                          {
                            type: "element",
                            tagName: "div",
                            properties: { className: ["mermaid-content"] },
                            children: [],
                          },