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,51 +0,0 @@
import { useCallback } from 'react'
/**
* Ref shape for @uiw/react-codemirror (view/state not in package types).
* Use with: ref={editorRef} and type editorRef as React.MutableRefObject<CodeMirrorEditorRef | null>.
*/
export type CodeMirrorEditorRef = {
view: {
state: {
doc: { length: number; toString(): string }
selection: { main: { from: number } }
}
dispatch: (arg: { changes: { from: number; to: number; insert: string } }) => void
}
}
export type InsertPosition = 'prepend' | 'append' | 'cursor'
/**
* Returns a stable insertAt(insertText, mode) that inserts text into the CodeMirror
* editor at the given position (prepend, append, or cursor), then calls onChange
* with the new content. Falls back to string concatenation when the editor ref
* is not yet mounted.
*/
export function useCodeMirrorInsert(
editorRef: React.RefObject<CodeMirrorEditorRef | null>,
currentContent: string,
onChange: (value: string) => void
): (insertText: string, mode: InsertPosition) => void {
return useCallback(
(insertText: string, mode: InsertPosition) => {
const ref = editorRef.current
if (ref?.view) {
const view = ref.view
const doc = view.state.doc
const len = doc.length
const from =
mode === 'prepend' ? 0 : mode === 'append' ? len : view.state.selection.main.from
view.dispatch({ changes: { from, to: from, insert: insertText } })
onChange(view.state.doc.toString())
return
}
if (mode === 'prepend') {
onChange(insertText + currentContent)
} else {
onChange(currentContent + insertText)
}
},
[currentContent, onChange]
)
}

View File

@@ -21,8 +21,7 @@ export function useResizeHeight(
const unobserve = observeResize(el, (size) => {
if (size.height > 0) setHeight(size.height)
})
const initial = el.getBoundingClientRect().height
if (initial > 0) setHeight(initial)
// Rely on ResizeObserver for initial size to avoid forced synchronous layout (getBoundingClientRect).
return unobserve
}, deps ?? [])

View File

@@ -0,0 +1,52 @@
import { useCallback, useRef } from 'react'
export type InsertPosition = 'prepend' | 'append' | 'cursor'
/**
* Returns a stable insertAt(insertText, mode) that inserts text into the simple code editor
* (textarea identified by textareaId) at the given position, then calls onChange with the new content.
* Cursor is restored after the next render via a scheduled effect.
*/
export function useSimpleEditorInsert(
textareaId: string,
currentContent: string,
onChange: (value: string) => void
): (insertText: string, mode: InsertPosition) => void {
const pendingCursorRef = useRef<number | null>(null)
const insertAt = useCallback(
(insertText: string, mode: InsertPosition) => {
const ta = document.getElementById(textareaId) as HTMLTextAreaElement | null
let start: number
let end: number
if (ta) {
start = mode === 'cursor' ? ta.selectionStart : mode === 'prepend' ? 0 : ta.value.length
end = mode === 'cursor' ? ta.selectionEnd : start
} else {
start = mode === 'prepend' ? 0 : currentContent.length
end = start
}
const newValue =
currentContent.slice(0, start) + insertText + currentContent.slice(end)
const nextCursor = start + insertText.length
pendingCursorRef.current = nextCursor
onChange(newValue)
// Restore cursor after React re-renders
if (ta) {
requestAnimationFrame(() => {
const el = document.getElementById(textareaId) as HTMLTextAreaElement | null
if (el && pendingCursorRef.current !== null) {
el.focus()
el.setSelectionRange(pendingCursorRef.current, pendingCursorRef.current)
pendingCursorRef.current = null
}
})
} else {
pendingCursorRef.current = null
}
},
[textareaId, currentContent, onChange]
)
return insertAt
}