feat: replace codemirror and image viewr, opt for simplier approach

This commit is contained in:
2026-03-12 23:07:51 +01:00
parent b71d32da5e
commit 4dca9c6478
19 changed files with 464 additions and 709 deletions

View File

@@ -1,19 +1,14 @@
import React, { useCallback, useContext, useMemo, useRef } from 'react'
import { autocompletion } from '@codemirror/autocomplete'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { markdown } from '@codemirror/lang-markdown'
import React, { useCallback, useContext, useId, useMemo } from 'react'
import {
AbstractNodeProps,
createAbstractNodeComponent,
getConnectedNodesByType,
useAbstractNode,
} from '@/lib/graph/abstractNode'
import { useCodeMirrorInsert, type CodeMirrorEditorRef } from '@/hooks/useCodeMirrorInsert'
import { CodeEditor } from '@/components/editor/CodeEditor'
import { useSimpleEditorInsert } from '@/hooks/useSimpleEditorInsert'
import { useResizeHeight } from '@/hooks/useResizeHeight'
import { nunjucksCompletionSource } from '@/lib/nunjucksAutocomplete'
import { plantumlLanguage } from '@/lib/plantumlLanguage'
import { useTheme } from '@/lib/themeContext'
import { type HighlightLanguage } from '@/lib/syntaxHighlight'
import {
getConfigTypes,
getConfigContent,
@@ -59,14 +54,13 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
const configTypeId = getConfigTypeId(data ?? {})
const configType = getConfigType(configTypeId)
const content = getConfigContent(data ?? {})
const { theme } = useTheme()
const { nodes, sourceIds, updateData } = useAbstractNode<ConfigNodeData>(id, data ?? {})
const editorRef = useRef<CodeMirrorEditorRef | null>(null)
const editorId = useId()
const onChange = useCallback(
(val: string) => updateData({ content: val, configType: configTypeId }),
[updateData, configTypeId]
)
const insertAt = useCodeMirrorInsert(editorRef, content, onChange)
const insertAt = useSimpleEditorInsert(editorId, content, onChange)
const connectedConfigNodes = useMemo(
() => getConnectedNodesByType(nodes, sourceIds, 'config'),
@@ -146,21 +140,8 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
[connectedConfigNodes],
)
const dataIds = useMemo(() => connectedDataNodes.map((n: any) => n.id), [connectedDataNodes])
const extensions = useMemo(() => {
const lang =
configTypeId === 'wireframe'
? javascript()
: configType.language === 'plantuml'
? plantumlLanguage.extension
: markdown()
return [
lang,
autocompletion({
override: [nunjucksCompletionSource(variableIds, configTitles, functionIds, dataIds)],
activateOnTyping: true,
}),
]
}, [configTypeId, configType.language, variableIds, functionIds, configTitles, dataIds])
const highlightLang: HighlightLanguage =
configTypeId === 'wireframe' ? 'javascript' : configType.language === 'plantuml' ? 'plantuml' : 'markdown'
const [editorHeight, editorContainerRef] = useResizeHeight(180)
const insertBlocksContent = useMemo(() => {
@@ -326,16 +307,17 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
/>
</div>
<div ref={editorContainerRef as React.RefObject<HTMLDivElement>} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input">
<CodeMirror
ref={editorRef}
<div ref={editorContainerRef as React.RefObject<HTMLDivElement>} className="min-h-0 flex-1 w-full nodrag nopan overflow-auto border-t border-input" style={{ minHeight: editorHeight }}>
<CodeEditor
textareaId={editorId}
value={content}
height={`${editorHeight}px`}
theme={theme}
extensions={extensions}
onChange={onChange}
basicSetup={{ lineNumbers: true, foldGutter: false }}
className="text-sm [&_.cm-editor]:outline-none [&_.cm-editor]:cursor-text [&_.cm-gutters]:border-0"
onValueChange={onChange}
language={highlightLang}
minHeight={editorHeight}
style={{ fontSize: 14 }}
textareaClassName="text-sm outline-none border-0 resize-none nodrag nopan"
preClassName="text-sm nodrag nopan"
className="min-h-0 w-full"
/>
</div>
</BaseNodeContent>

View File

@@ -1,15 +1,13 @@
import React, { useCallback, useContext, useMemo, useRef } from 'react'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import React, { useCallback, useContext, useId, useMemo } from 'react'
import {
AbstractNodeProps,
createAbstractNodeComponent,
getConnectedNodesByType,
useAbstractNode,
} from '@/lib/graph/abstractNode'
import { useCodeMirrorInsert, type CodeMirrorEditorRef } from '@/hooks/useCodeMirrorInsert'
import { CodeEditor } from '@/components/editor/CodeEditor'
import { useSimpleEditorInsert } from '@/hooks/useSimpleEditorInsert'
import { useResizeHeight } from '@/hooks/useResizeHeight'
import { useTheme } from '@/lib/themeContext'
import {
BaseNode,
BaseNodeContent,
@@ -30,19 +28,20 @@ export type FunctionNodeData = { body?: string }
type Props = AbstractNodeProps<FunctionNodeData>
const LANGUAGE: HighlightLanguage = 'javascript'
function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
const flowUIContext = useContext(FlowUIContext)
const setFullscreenNodeId = flowUIContext?.setFullscreenNodeId
const supportsFullscreen = getNodeType('function')?.supportsFullscreen
const bodyValue = data?.body ?? ''
const { theme } = useTheme()
const { nodes, sourceIds, updateData } = useAbstractNode<FunctionNodeData>(id, data ?? {})
const editorRef = useRef<CodeMirrorEditorRef | null>(null)
const editorId = useId()
const onChange = useCallback(
(val: string) => updateData({ body: val }),
[updateData]
)
const insertAt = useCodeMirrorInsert(editorRef, bodyValue, onChange)
const insertAt = useSimpleEditorInsert(editorId, bodyValue, onChange)
const connectedVariableNodes = useMemo(
() => getConnectedNodesByType(nodes, sourceIds, 'variable'),
@@ -68,7 +67,6 @@ function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
[insertAt]
)
const extensions = useMemo(() => [javascript()], [])
const [editorHeight, editorContainerRef] = useResizeHeight(120)
const dimensions =
width != null && height != null && width > 0 && height > 0
@@ -120,16 +118,17 @@ function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
}
/>
</div>
<div ref={editorContainerRef as React.RefObject<HTMLDivElement>} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input">
<CodeMirror
ref={editorRef}
<div ref={editorContainerRef as React.RefObject<HTMLDivElement>} className="min-h-0 flex-1 w-full nodrag nopan overflow-auto border-t border-input" style={{ minHeight: editorHeight }}>
<CodeEditor
textareaId={editorId}
value={bodyValue}
height={`${editorHeight}px`}
theme={theme}
extensions={extensions}
onChange={onChange}
basicSetup={{ lineNumbers: true, foldGutter: false }}
className="text-xs [&_.cm-editor]:outline-none [&_.cm-editor]:cursor-text [&_.cm-gutters]:border-0"
onValueChange={onChange}
language="javascript"
minHeight={editorHeight}
style={{ fontSize: 12 }}
textareaClassName="text-xs outline-none border-0 resize-none nodrag nopan"
preClassName="text-xs nodrag nopan"
className="min-h-0 w-full"
/>
</div>
</BaseNodeContent>

View File

@@ -1,6 +1,4 @@
import React from 'react'
import { ZoomIn, ZoomOut, RotateCcw } from 'lucide-react'
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'
import type { RenderingNodeState } from '../useRenderingNodeState'
export type ImageOutputViewProps = {
@@ -13,8 +11,6 @@ export type ImageOutputViewProps = {
export function ImageOutputView({
state,
selected,
viewportFocused,
onViewportFocus,
onViewportBlur,
}: ImageOutputViewProps) {
@@ -25,59 +21,10 @@ export function ImageOutputView({
onFocus={onViewportFocus}
onBlur={onViewportBlur}
>
<TransformWrapper
initialScale={1}
initialPositionX={0}
initialPositionY={0}
minScale={0.2}
maxScale={4}
centerOnInit={false}
panning={{ disabled: !selected && !viewportFocused }}
wheel={{ disabled: !selected && !viewportFocused }}
doubleClick={{ disabled: !selected && !viewportFocused }}
>
{({ zoomIn, zoomOut, resetTransform }) => (
<>
<div className="react-flow__controls absolute bottom-2 left-2 z-10 nodrag nopan">
<button
type="button"
onClick={() => zoomIn()}
className="react-flow__controls-button"
title="Zoom in"
>
<ZoomIn className="size-3 max-w-[12px] max-h-[12px]" />
</button>
<button
type="button"
onClick={() => zoomOut()}
className="react-flow__controls-button"
title="Zoom out"
>
<ZoomOut className="size-3 max-w-[12px] max-h-[12px]" />
</button>
<button
type="button"
onClick={() => resetTransform()}
className="react-flow__controls-button"
title="Reset view (fit all)"
>
<RotateCcw className="size-3 max-w-[12px] max-h-[12px]" />
</button>
</div>
<div className="absolute inset-0 nodrag nopan overflow-hidden [&_.react-transform-component]:!w-full [&_.react-transform-component]:!h-full [&_.react-transform-wrapper]:!w-full [&_.react-transform-wrapper]:!h-full">
<TransformComponent
wrapperClass="!w-full !h-full"
contentClass="nodrag nopan !w-full !h-full !block !min-h-0"
>
<div
className="rendering-diagram absolute inset-0 w-full h-full min-w-0 min-h-0 nodrag nopan"
dangerouslySetInnerHTML={{ __html: state.displayContent ?? '' }}
/>
</TransformComponent>
</div>
</>
)}
</TransformWrapper>
<div
className="absolute inset-0 nodrag nopan overflow-auto"
dangerouslySetInnerHTML={{ __html: state.displayContent ?? '' }}
/>
</div>
)
}

View File

@@ -1,10 +1,8 @@
import React, { useMemo } from 'react'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { markdown } from '@codemirror/lang-markdown'
import { Copy } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { plantumlLanguage } from '@/lib/plantumlLanguage'
import { CodeEditor } from '@/components/editor/CodeEditor'
import { type HighlightLanguage } from '@/lib/syntaxHighlight'
import type { RenderingNodeState } from '../useRenderingNodeState'
import { toast } from 'sonner'
@@ -15,21 +13,19 @@ export type RawOutputViewProps = {
theme: 'light' | 'dark'
}
export function RawOutputView({ state, height, containerRef, theme }: RawOutputViewProps) {
const extensions = useMemo(() => {
const lang =
state.rawLanguage === 'wireframe'
? javascript()
: state.rawLanguage === 'plantuml'
? plantumlLanguage.extension
: markdown()
return [lang]
}, [state.rawLanguage])
function languageForRaw(rawLanguage: string): HighlightLanguage {
if (rawLanguage === 'wireframe') return 'javascript'
if (rawLanguage === 'plantuml') return 'plantuml'
return 'markdown'
}
export function RawOutputView({ state, height, containerRef }: RawOutputViewProps) {
const language = useMemo(() => languageForRaw(state.rawLanguage), [state.rawLanguage])
return (
<div
ref={containerRef}
className="relative min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input"
className="relative min-h-0 flex-1 w-full nodrag nopan overflow-auto border-t border-input"
>
<Button
type="button"
@@ -50,15 +46,16 @@ export function RawOutputView({ state, height, containerRef, theme }: RawOutputV
>
<Copy className="size-3.5" />
</Button>
<CodeMirror
<CodeEditor
value={state.rawDisplayContent}
height={`${height}px`}
theme={theme}
extensions={extensions}
onValueChange={() => {}}
language={language}
readOnly
editable={false}
basicSetup={{ lineNumbers: true, foldGutter: false }}
className="text-xs [&_.cm-editor]:outline-none [&_.cm-editor]:cursor-text [&_.cm-gutters]:border-0 [&_.cm-scroller]:min-h-0"
minHeight={height}
style={{ fontSize: 12 }}
textareaClassName="text-xs outline-none border-0 resize-none nodrag nopan"
preClassName="text-xs nodrag nopan min-h-0"
className="min-h-0 w-full"
/>
</div>
)