fix: expose mermaid raw edit action

This commit is contained in:
lucaronin
2026-06-05 23:32:01 +02:00
parent 401087a8ef
commit cc4203ab1c
3 changed files with 73 additions and 4 deletions

View File

@@ -321,16 +321,25 @@
background: var(--surface-editor);
}
.editor__blocknote-container .mermaid-diagram__edit-button,
.editor__blocknote-container .mermaid-diagram__expand-button {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
opacity: 0;
background: var(--surface-editor);
box-shadow: 0 6px 16px rgb(15 23 42 / 0.14);
}
.editor__blocknote-container .mermaid-diagram__edit-button {
right: 44px;
}
.editor__blocknote-container .mermaid-diagram__expand-button {
right: 8px;
}
.editor__blocknote-container .mermaid-diagram:is(:hover, :focus-within) .mermaid-diagram__edit-button,
.editor__blocknote-container .mermaid-diagram:is(:hover, :focus-within) .mermaid-diagram__expand-button {
opacity: 1;
}
@@ -349,6 +358,7 @@
}
@media (hover: none) {
.editor__blocknote-container .mermaid-diagram__edit-button,
.editor__blocknote-container .mermaid-diagram__expand-button {
opacity: 1;
}

View File

@@ -1,5 +1,6 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { APP_COMMAND_EVENT_NAME, APP_COMMAND_IDS } from '../hooks/appCommandDispatcher'
import { RUNTIME_STYLE_NONCE } from '../lib/runtimeStyleNonce'
import { MermaidDiagram } from './MermaidDiagram'
@@ -59,15 +60,40 @@ describe('MermaidDiagram', () => {
expect(screen.getByTestId('mermaid-diagram-dialog-viewport').querySelector('svg')).not.toBeNull()
})
it('offers a raw editor action for editing Mermaid source immediately', () => {
const commands: string[] = []
const handleCommand = (event: Event) => {
if (event instanceof CustomEvent && typeof event.detail === 'string') {
commands.push(event.detail)
}
}
window.addEventListener(APP_COMMAND_EVENT_NAME, handleCommand)
try {
render(
<MermaidDiagram
diagram={'flowchart LR\nA --> B'}
source={'```mermaid\nflowchart LR\nA --> B\n```'}
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'Open the raw editor' }))
expect(commands).toEqual([APP_COMMAND_IDS.editToggleRawEditor])
} finally {
window.removeEventListener(APP_COMMAND_EVENT_NAME, handleCommand)
}
})
it('keeps rendered SVG pointer events inside the Mermaid block', async () => {
const onBlockPointer = vi.fn()
render(
<button type="button" onClick={onBlockPointer} onMouseDown={onBlockPointer}>
<div onClick={onBlockPointer} onMouseDown={onBlockPointer}>
<MermaidDiagram
diagram={'flowchart LR\nA --> B'}
source={'```mermaid\nflowchart LR\nA --> B\n```'}
/>
</button>,
</div>,
)
await waitFor(() => {

View File

@@ -1,4 +1,4 @@
import { ArrowsOut as Maximize2 } from '@phosphor-icons/react'
import { ArrowsOut as Maximize2, PencilSimpleLine } from '@phosphor-icons/react'
import { useEffect, useId, useMemo, useState, type SyntheticEvent } from 'react'
import { Button } from '@/components/ui/button'
import {
@@ -8,6 +8,9 @@ import {
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { APP_COMMAND_EVENT_NAME, APP_COMMAND_IDS } from '../hooks/appCommandDispatcher'
import { translate } from '../lib/i18n'
import { trackEvent } from '../lib/telemetry'
import { SafeSvgDiv } from './SafeMarkup'
type MermaidApi = typeof import('mermaid')['default']
@@ -41,6 +44,7 @@ const MERMAID_RENDER_HOST_STYLE = [
'height:0',
'overflow:hidden',
].join(';')
const OPEN_RAW_EDITOR_LABEL = translate('en', 'editor.toolbar.rawOpen')
function renderIdFromReactId(reactId: string): string {
const safeId = reactId.replace(/[^a-zA-Z0-9_-]/g, '')
@@ -127,6 +131,33 @@ function stopMermaidViewportEvent(event: SyntheticEvent): void {
event.stopPropagation()
}
function openRawEditorForMermaidSource(event: SyntheticEvent): void {
event.preventDefault()
event.stopPropagation()
trackEvent('editor_mermaid_raw_edit_requested')
window.dispatchEvent(new CustomEvent(APP_COMMAND_EVENT_NAME, {
detail: APP_COMMAND_IDS.editToggleRawEditor,
}))
}
function MermaidRawEditorButton() {
return (
<Button
aria-label={OPEN_RAW_EDITOR_LABEL}
className="mermaid-diagram__edit-button"
contentEditable={false}
onClick={openRawEditorForMermaidSource}
onMouseDown={stopMermaidViewportEvent}
size="icon-sm"
title={OPEN_RAW_EDITOR_LABEL}
type="button"
variant="outline"
>
<PencilSimpleLine aria-hidden="true" />
</Button>
)
}
function MermaidLightbox({ svg }: { svg: string }) {
return (
<Dialog>
@@ -186,6 +217,7 @@ export function MermaidDiagram({ diagram, source }: MermaidDiagramProps) {
if (!diagram.trim() || currentState.error) {
return (
<figure className="mermaid-diagram mermaid-diagram--error" data-testid="mermaid-diagram-error">
<MermaidRawEditorButton />
<figcaption>Mermaid diagram unavailable</figcaption>
<MermaidSourceFallback source={source} />
</figure>
@@ -194,6 +226,7 @@ export function MermaidDiagram({ diagram, source }: MermaidDiagramProps) {
return (
<figure className="mermaid-diagram" data-testid="mermaid-diagram">
<MermaidRawEditorButton />
<MermaidLightbox svg={currentState.svg} />
<MermaidSvgViewport
ariaLabel="Mermaid diagram"