refactor: replace dual-layer textarea with Monaco Editor for script body

The transparent-textarea-over-highlighted-overlay approach had
persistent cursor alignment issues. Replace with Monaco Editor
(already used in tree editor Code Mode) using built-in PowerShell
language support and our existing dark theme. Gives proper syntax
highlighting, cursor positioning, line numbers, and editing for free.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-14 19:41:49 -04:00
parent e5899d81c4
commit 50fd7d06da

View File

@@ -1,5 +1,7 @@
import { useRef, useCallback } from 'react' import { useCallback } from 'react'
import { PowerShellHighlighter } from '@/components/scripts/PowerShellHighlighter' import Editor, { type BeforeMount } from '@monaco-editor/react'
import { resolutionFlowTheme, THEME_ID } from '@/components/tree-editor/code-mode/resolutionFlowTheme'
import { Spinner } from '@/components/common/Spinner'
interface Props { interface Props {
value: string value: string
@@ -8,50 +10,43 @@ interface Props {
} }
export function ScriptBodyEditor({ value, onChange, disabled }: Props) { export function ScriptBodyEditor({ value, onChange, disabled }: Props) {
const textareaRef = useRef<HTMLTextAreaElement>(null) const handleBeforeMount: BeforeMount = useCallback((monaco) => {
const overlayRef = useRef<HTMLDivElement>(null) // Register our dark theme if not already defined
monaco.editor.defineTheme(THEME_ID, resolutionFlowTheme)
const handleScroll = useCallback(() => {
if (textareaRef.current && overlayRef.current) {
overlayRef.current.scrollTop = textareaRef.current.scrollTop
overlayRef.current.scrollLeft = textareaRef.current.scrollLeft
}
}, []) }, [])
const handleTab = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Tab') {
e.preventDefault()
const ta = e.currentTarget
const start = ta.selectionStart
const end = ta.selectionEnd
const newValue = value.substring(0, start) + ' ' + value.substring(end)
onChange(newValue)
// Restore cursor position after React re-render
requestAnimationFrame(() => {
ta.selectionStart = ta.selectionEnd = start + 4
})
}
}, [value, onChange])
return ( return (
<div className="relative rounded-xl border border-border overflow-hidden"> <div className="rounded-xl border border-border overflow-hidden">
{/* Highlighted overlay (read-only visual layer) — scroll synced to textarea */} <Editor
<div ref={overlayRef} className="absolute inset-0 pointer-events-none overflow-hidden p-4"> height="300px"
<PowerShellHighlighter script={value || ' '} className="font-label text-sm leading-[21px] whitespace-pre m-0 p-0 border-0" /> language="powershell"
</div> theme={THEME_ID}
{/* Editable textarea (transparent text, visible caret) */}
<textarea
ref={textareaRef}
value={value} value={value}
onChange={e => onChange(e.target.value)} onChange={v => onChange(v ?? '')}
onScroll={handleScroll} beforeMount={handleBeforeMount}
onKeyDown={handleTab} loading={
disabled={disabled} <div className="flex h-[300px] items-center justify-center bg-card">
wrap="off" <Spinner size="sm" className="h-6 w-6 border-t-foreground" />
spellCheck={false} </div>
className="relative z-10 w-full min-h-[300px] resize-y font-label text-sm leading-[21px] bg-transparent text-transparent caret-foreground p-4 whitespace-pre overflow-auto focus:outline-none focus:ring-1 focus:ring-[rgba(6,182,212,0.2)] disabled:cursor-not-allowed disabled:opacity-50" }
placeholder="# Enter your PowerShell script here…&#10;# Use {{ param_name }} for parameter placeholders" options={{
minimap: { enabled: false },
fontSize: 13,
fontFamily: "'JetBrains Mono', monospace",
lineNumbers: 'on',
wordWrap: 'on',
scrollBeyondLastLine: false,
renderLineHighlight: 'line',
tabSize: 4,
insertSpaces: true,
automaticLayout: true,
readOnly: disabled,
padding: { top: 12, bottom: 12 },
scrollbar: {
verticalScrollbarSize: 8,
horizontalScrollbarSize: 8,
},
}}
/> />
</div> </div>
) )