feat: 增强代码节点编辑器与放大阅读体验
- tinyflow-ui: 新增 CodeScriptEditor(CodeMirror 6)并支持语法高亮、上下文补全、自动括号与 _result 高亮 - tinyflow-ui: 代码节点接入引擎能力列表与节点说明提示,统一 JS/Python 编辑体验 - tinyflow-ui: 增加放大编辑模式,支持居中弹层、ESC 与点击外部关闭 - app/workflow: 对接 supportedCodeEngines 能力并透传 codeEngine provider
This commit is contained in:
@@ -0,0 +1,900 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from 'svelte';
|
||||
import {Compartment, EditorState, type Transaction} from '@codemirror/state';
|
||||
import {
|
||||
autocompletion,
|
||||
closeBrackets,
|
||||
closeBracketsKeymap,
|
||||
completionKeymap,
|
||||
pickedCompletion
|
||||
} from '@codemirror/autocomplete';
|
||||
import {defaultKeymap, history, historyKeymap, indentWithTab} from '@codemirror/commands';
|
||||
import {javascript} from '@codemirror/lang-javascript';
|
||||
import {defaultHighlightStyle, indentOnInput, indentUnit, syntaxHighlighting} from '@codemirror/language';
|
||||
import {python} from '@codemirror/lang-python';
|
||||
import {
|
||||
Decoration,
|
||||
type DecorationSet,
|
||||
EditorView,
|
||||
keymap,
|
||||
placeholder as cmPlaceholder,
|
||||
ViewPlugin,
|
||||
type ViewUpdate
|
||||
} from '@codemirror/view';
|
||||
import {Button, FloatingTrigger} from '../base';
|
||||
import type {Parameter} from '#types';
|
||||
import {flattenParameterCandidates} from '../utils/paramToken';
|
||||
import {
|
||||
createBusinessCompletionSource,
|
||||
type CodeEngine,
|
||||
normalizeCodeEngine,
|
||||
shouldAutoAppendCallParens
|
||||
} from '../utils/codeCompletion';
|
||||
|
||||
const {
|
||||
value = '',
|
||||
mode = 'textarea',
|
||||
engine = 'js',
|
||||
parameters = [],
|
||||
rows = 10,
|
||||
placeholder = '',
|
||||
disabled = false,
|
||||
class: className = '',
|
||||
style = '',
|
||||
...rest
|
||||
}: {
|
||||
value?: string;
|
||||
mode?: 'input' | 'textarea';
|
||||
engine?: string;
|
||||
parameters?: Parameter[];
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
style?: string;
|
||||
[key: string]: any;
|
||||
} = $props();
|
||||
|
||||
const TOKEN_PATTERN = /\{\{\s*([^{}]+?)\s*}}/g;
|
||||
const RESULT_PATTERN = /\b_result\b/g;
|
||||
const languageCompartment = new Compartment();
|
||||
const indentCompartment = new Compartment();
|
||||
const completionCompartment = new Compartment();
|
||||
const completionSourceCompartment = new Compartment();
|
||||
const decorationCompartment = new Compartment();
|
||||
const editableCompartment = new Compartment();
|
||||
const placeholderCompartment = new Compartment();
|
||||
|
||||
let containerEl = $state<HTMLDivElement | null>(null);
|
||||
let editorShellEl = $state<HTMLDivElement | null>(null);
|
||||
let editorView = $state<EditorView | null>(null);
|
||||
let triggerObject: any;
|
||||
let applyingExternalValue = $state(false);
|
||||
let expanded = $state(false);
|
||||
let shellOriginalParent = $state<HTMLElement | null>(null);
|
||||
let shellOriginalNextSibling = $state<ChildNode | null>(null);
|
||||
|
||||
const isSingleLine = $derived(mode === 'input');
|
||||
const normalizedEngine = $derived(normalizeCodeEngine(engine));
|
||||
const minRows = $derived(isSingleLine ? 1 : Math.max(3, rows || 3));
|
||||
const minHeight = $derived(`${minRows * 24}px`);
|
||||
const editorMinHeight = $derived(expanded ? '100%' : minHeight);
|
||||
const editorStyle = $derived(`${style};--code-editor-min-height:${editorMinHeight};`);
|
||||
const paramCandidates = $derived(flattenParameterCandidates(parameters));
|
||||
const hasParams = $derived(paramCandidates.length > 0);
|
||||
const paramResolvedMap = $derived.by(() => {
|
||||
const map = new Map<string, boolean>();
|
||||
for (const candidate of paramCandidates) {
|
||||
map.set(candidate.name, candidate.resolved);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
function emitInput(nextValue: string) {
|
||||
rest.oninput?.({
|
||||
target: {
|
||||
value: nextValue
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function emitChange(nextValue: string) {
|
||||
rest.onchange?.({
|
||||
target: {
|
||||
value: nextValue
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createLanguageExtension(targetEngine: CodeEngine) {
|
||||
if (targetEngine === 'python') {
|
||||
return python();
|
||||
}
|
||||
return javascript();
|
||||
}
|
||||
|
||||
function getIndentUnitForEngine(targetEngine: CodeEngine) {
|
||||
if (targetEngine === 'python') {
|
||||
return ' ';
|
||||
}
|
||||
return ' ';
|
||||
}
|
||||
|
||||
function createBusinessCompletionExtension(targetEngine: CodeEngine, candidates: typeof paramCandidates) {
|
||||
return EditorState.languageData.of((_state, _pos, _side) => {
|
||||
return [
|
||||
{
|
||||
autocomplete: createBusinessCompletionSource({
|
||||
engine: targetEngine,
|
||||
paramCandidates: candidates
|
||||
})
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function handlePickedCompletionParens(transactions: readonly Transaction[], currentState: EditorState) {
|
||||
for (const tr of transactions) {
|
||||
const picked = tr.annotation(pickedCompletion);
|
||||
if (!picked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cursor = currentState.selection.main.head;
|
||||
const nextChar = currentState.sliceDoc(cursor, cursor + 1);
|
||||
if (!shouldAutoAppendCallParens(picked, nextChar)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
const head = editorView.state.selection.main.head;
|
||||
const afterHead = editorView.state.sliceDoc(head, head + 1);
|
||||
if (afterHead === '(') {
|
||||
return;
|
||||
}
|
||||
editorView.dispatch({
|
||||
changes: {
|
||||
from: head,
|
||||
to: head,
|
||||
insert: '()'
|
||||
},
|
||||
selection: {
|
||||
anchor: head + 1
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createHighlightDecorations(resolvedMap: Map<string, boolean>) {
|
||||
const resultMark = Decoration.mark({class: 'cm-special-result'});
|
||||
const validTokenMark = Decoration.mark({class: 'cm-template-token-valid'});
|
||||
const unresolvedTokenMark = Decoration.mark({class: 'cm-template-token-unresolved'});
|
||||
const invalidTokenMark = Decoration.mark({class: 'cm-template-token-invalid'});
|
||||
|
||||
function buildDecorations(source: string): DecorationSet {
|
||||
const ranges: Array<{from: number; to: number; value: Decoration}> = [];
|
||||
|
||||
RESULT_PATTERN.lastIndex = 0;
|
||||
let resultMatch: RegExpExecArray | null = RESULT_PATTERN.exec(source);
|
||||
while (resultMatch) {
|
||||
ranges.push({
|
||||
from: resultMatch.index,
|
||||
to: resultMatch.index + resultMatch[0].length,
|
||||
value: resultMark
|
||||
});
|
||||
resultMatch = RESULT_PATTERN.exec(source);
|
||||
}
|
||||
|
||||
TOKEN_PATTERN.lastIndex = 0;
|
||||
let tokenMatch: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
|
||||
while (tokenMatch) {
|
||||
const tokenKey = (tokenMatch[1] || '').trim();
|
||||
const resolved = resolvedMap.get(tokenKey);
|
||||
const decoration =
|
||||
resolved === undefined ? invalidTokenMark : resolved ? validTokenMark : unresolvedTokenMark;
|
||||
ranges.push({
|
||||
from: tokenMatch.index,
|
||||
to: tokenMatch.index + tokenMatch[0].length,
|
||||
value: decoration
|
||||
});
|
||||
tokenMatch = TOKEN_PATTERN.exec(source);
|
||||
}
|
||||
|
||||
return Decoration.set(ranges, true);
|
||||
}
|
||||
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = buildDecorations(view.state.doc.toString());
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = buildDecorations(update.state.doc.toString());
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (value) => value.decorations
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createEditorTheme() {
|
||||
return EditorView.theme({
|
||||
'&': {
|
||||
border: '1px solid #dcdfe6',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: '#fff',
|
||||
fontSize: '13px'
|
||||
},
|
||||
'&.cm-focused': {
|
||||
outline: 'none',
|
||||
borderColor: '#4c82f7',
|
||||
boxShadow: '0 0 0 1px rgba(76, 130, 247, 0.25)'
|
||||
},
|
||||
'&.cm-editor.cm-disabled': {
|
||||
backgroundColor: '#f5f7fa',
|
||||
opacity: 0.85
|
||||
},
|
||||
'.cm-scroller': {
|
||||
minHeight: 'var(--code-editor-min-height)',
|
||||
lineHeight: '1.6',
|
||||
fontFamily:
|
||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace"
|
||||
},
|
||||
'.cm-content': {
|
||||
padding: '8px 38px 8px 10px',
|
||||
caretColor: '#1f2937'
|
||||
},
|
||||
'.cm-gutters': {
|
||||
border: 'none',
|
||||
background: 'transparent'
|
||||
},
|
||||
'.cm-activeLine': {
|
||||
background: 'rgba(76, 130, 247, 0.06)'
|
||||
},
|
||||
'.cm-placeholder': {
|
||||
color: '#c0c4cc'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function insertTextAtSelection(text: string) {
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
const selection = editorView.state.selection.main;
|
||||
const from = selection.from;
|
||||
const to = selection.to;
|
||||
editorView.dispatch({
|
||||
changes: {
|
||||
from,
|
||||
to,
|
||||
insert: text
|
||||
},
|
||||
selection: {
|
||||
anchor: from + text.length
|
||||
}
|
||||
});
|
||||
editorView.focus();
|
||||
}
|
||||
|
||||
function insertParam(paramName: string) {
|
||||
insertTextAtSelection(`{{${paramName}}}`);
|
||||
emitChange(editorView?.state.doc.toString() || '');
|
||||
triggerObject?.hide?.();
|
||||
}
|
||||
|
||||
function toggleExpanded() {
|
||||
expanded = !expanded;
|
||||
}
|
||||
|
||||
function closeExpanded() {
|
||||
expanded = false;
|
||||
}
|
||||
|
||||
function moveExpandedShellToBody() {
|
||||
if (typeof document === 'undefined' || !editorShellEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editorShellEl.parentElement === document.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
shellOriginalParent = editorShellEl.parentElement;
|
||||
shellOriginalNextSibling = editorShellEl.nextSibling;
|
||||
document.body.appendChild(editorShellEl);
|
||||
}
|
||||
|
||||
function restoreExpandedShell() {
|
||||
if (!editorShellEl || !shellOriginalParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editorShellEl.parentElement !== document.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shellOriginalNextSibling && shellOriginalNextSibling.parentNode === shellOriginalParent) {
|
||||
shellOriginalParent.insertBefore(editorShellEl, shellOriginalNextSibling);
|
||||
} else {
|
||||
shellOriginalParent.appendChild(editorShellEl);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!containerEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorView = new EditorView({
|
||||
parent: containerEl,
|
||||
state: EditorState.create({
|
||||
doc: (value || '') as string,
|
||||
extensions: [
|
||||
createEditorTheme(),
|
||||
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
|
||||
history(),
|
||||
indentOnInput(),
|
||||
closeBrackets(),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of((update) => {
|
||||
handlePickedCompletionParens(update.transactions, update.state);
|
||||
|
||||
if (!update.docChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = update.state.doc.toString();
|
||||
if (!applyingExternalValue) {
|
||||
emitInput(nextValue);
|
||||
}
|
||||
}),
|
||||
EditorView.contentAttributes.of({spellcheck: 'false'}),
|
||||
EditorView.domEventHandlers({
|
||||
focus: (event) => {
|
||||
rest.onfocus?.(event);
|
||||
return false;
|
||||
},
|
||||
blur: (event) => {
|
||||
emitChange(editorView?.state.doc.toString() || '');
|
||||
rest.onblur?.(event);
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
keymap.of([
|
||||
indentWithTab,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
...closeBracketsKeymap,
|
||||
...completionKeymap
|
||||
]),
|
||||
languageCompartment.of(createLanguageExtension(normalizedEngine)),
|
||||
indentCompartment.of(indentUnit.of(getIndentUnitForEngine(normalizedEngine))),
|
||||
completionSourceCompartment.of(createBusinessCompletionExtension(normalizedEngine, paramCandidates)),
|
||||
completionCompartment.of(
|
||||
autocompletion({
|
||||
activateOnTyping: true
|
||||
})
|
||||
),
|
||||
decorationCompartment.of(createHighlightDecorations(paramResolvedMap)),
|
||||
editableCompartment.of([EditorView.editable.of(!disabled), EditorState.readOnly.of(disabled)]),
|
||||
placeholderCompartment.of(placeholder ? cmPlaceholder(placeholder) : [])
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
return () => {
|
||||
editorView?.destroy();
|
||||
editorView = null;
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
editorView.dispatch({
|
||||
effects: languageCompartment.reconfigure(createLanguageExtension(normalizedEngine))
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
editorView.dispatch({
|
||||
effects: indentCompartment.reconfigure(indentUnit.of(getIndentUnitForEngine(normalizedEngine)))
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
editorView.dispatch({
|
||||
effects: completionSourceCompartment.reconfigure(
|
||||
createBusinessCompletionExtension(normalizedEngine, paramCandidates)
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
editorView.dispatch({
|
||||
effects: decorationCompartment.reconfigure(createHighlightDecorations(paramResolvedMap))
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
editorView.dispatch({
|
||||
effects: editableCompartment.reconfigure([EditorView.editable.of(!disabled), EditorState.readOnly.of(disabled)])
|
||||
});
|
||||
|
||||
const editorDom = editorView.dom;
|
||||
if (disabled) {
|
||||
editorDom.classList.add('cm-disabled');
|
||||
} else {
|
||||
editorDom.classList.remove('cm-disabled');
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
editorView.dispatch({
|
||||
effects: placeholderCompartment.reconfigure(placeholder ? cmPlaceholder(placeholder) : [])
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!editorView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = (value || '') as string;
|
||||
const currentValue = editorView.state.doc.toString();
|
||||
if (nextValue === currentValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyingExternalValue = true;
|
||||
editorView.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: nextValue
|
||||
}
|
||||
});
|
||||
applyingExternalValue = false;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!expanded || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const onKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeExpanded();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKeydown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeydown);
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const className = 'code-script-expanded-open';
|
||||
if (expanded) {
|
||||
document.body.classList.add(className);
|
||||
} else {
|
||||
document.body.classList.remove(className);
|
||||
}
|
||||
return () => {
|
||||
document.body.classList.remove(className);
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (typeof document === 'undefined' || !editorShellEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
moveExpandedShellToBody();
|
||||
} else {
|
||||
restoreExpandedShell();
|
||||
}
|
||||
|
||||
return () => {
|
||||
restoreExpandedShell();
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div class="code-script-editor-shell {expanded ? 'expanded-host' : ''}" bind:this={editorShellEl}>
|
||||
{#if expanded}
|
||||
<button type="button" class="code-script-overlay" aria-label="关闭放大代码编辑器" onclick={closeExpanded}></button>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="code-script-editor nopan nodrag {className} {expanded ? 'expanded' : ''}"
|
||||
style={editorStyle}
|
||||
>
|
||||
<div class="code-script-editor-inner" bind:this={containerEl}></div>
|
||||
|
||||
<div class="code-script-action">
|
||||
<div class="code-script-action-buttons">
|
||||
<Button
|
||||
class="code-script-button {expanded ? 'expanded-cancel' : ''}"
|
||||
onclick={toggleExpanded}
|
||||
title={expanded ? '取消放大' : '放大编辑区域'}
|
||||
aria-label={expanded ? '取消放大' : '放大编辑区域'}
|
||||
>
|
||||
{#if expanded}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M21 3V9H19V6.414L15.707 9.707L14.293 8.293L17.586 5H15V3H21ZM3 21V15H5V17.586L8.293 14.293L9.707 15.707L6.414 19H9V21H3ZM9.707 8.293L8.293 9.707L5 6.414V9H3V3H9V5H6.414L9.707 8.293ZM14.293 15.707L15.707 14.293L19 17.586V15H21V21H15V19H17.586L14.293 15.707Z"></path>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M15 3H21V9H19V5H15V3ZM3 9V3H9V5H5V9H3ZM15 21V19H19V15H21V21H15ZM3 15H5V19H9V21H3V15Z"></path>
|
||||
</svg>
|
||||
{/if}
|
||||
</Button>
|
||||
<FloatingTrigger placement="bottom-end" bind:this={triggerObject}>
|
||||
<Button
|
||||
class="code-script-button"
|
||||
disabled={!hasParams || disabled}
|
||||
title={hasParams ? '插入参数模板' : '请先在输入参数中定义参数'}
|
||||
>
|
||||
<span>{"{x}"}</span>
|
||||
</Button>
|
||||
{#snippet floating()}
|
||||
<div class="code-script-panel nowheel">
|
||||
{#if hasParams}
|
||||
{#each paramCandidates as candidate}
|
||||
<button
|
||||
class="code-script-item {candidate.resolved ? '' : 'unresolved'}"
|
||||
title={candidate.resolved ? candidate.name : `${candidate.name}(未映射)`}
|
||||
onclick={() => {
|
||||
insertParam(candidate.name);
|
||||
}}
|
||||
>
|
||||
<span>{candidate.name}</span>
|
||||
{#if !candidate.resolved}
|
||||
<span class="code-script-item-warn" title="该参数未映射,运行时可能为空">!</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="code-script-empty">请先在输入参数中定义参数</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</FloatingTrigger>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
.code-script-editor-shell {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.code-script-editor-shell.expanded-host {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.code-script-editor {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.code-script-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
pointer-events: auto;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: rgba(15, 23, 42, 0.28);
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-editor),
|
||||
.code-script-editor :global(.cm-scroller),
|
||||
.code-script-editor :global(.cm-content),
|
||||
.code-script-editor :global(.cm-line) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.code-script-editor.expanded {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 700px;
|
||||
max-width: calc(100vw - 360px);
|
||||
min-width: 520px;
|
||||
height: min(80vh, calc(100vh - 180px));
|
||||
max-height: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.code-script-editor.expanded .code-script-editor-inner {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.code-script-editor.expanded :global(.cm-editor) {
|
||||
height: 100%;
|
||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
|
||||
.code-script-editor.expanded :global(.cm-scroller) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.code-script-editor-inner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.code-script-action {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.code-script-action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:global(.code-script-button) {
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0 4px;
|
||||
font-size: 10px;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:global(.code-script-button svg) {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
:global(.code-script-button.expanded-cancel) {
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
padding: 0 4px;
|
||||
font-size: 10px;
|
||||
border-color: #d3defd;
|
||||
color: #1d4ed8;
|
||||
background: #f5f8ff;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.code-script-editor.expanded {
|
||||
width: calc(100vw - 120px);
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
height: calc(100vh - 96px);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.code-script-button:hover) {
|
||||
background: #f0f3ff;
|
||||
color: #2563eb;
|
||||
border-color: #d3defd;
|
||||
}
|
||||
|
||||
.code-script-panel {
|
||||
min-width: 220px;
|
||||
max-width: 340px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.code-script-item {
|
||||
border: none;
|
||||
background: #fff;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.code-script-item:hover {
|
||||
background: #f5f7ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.code-script-item.unresolved {
|
||||
background: #fff8e6;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.code-script-item-warn {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #d97706;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.code-script-empty {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-special-result) {
|
||||
color: #1d4ed8;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-template-token-valid) {
|
||||
color: #1e4ed8;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-template-token-unresolved) {
|
||||
color: #92400e;
|
||||
background: rgba(217, 119, 6, 0.16);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-template-token-invalid) {
|
||||
color: #b42318;
|
||||
background: rgba(217, 45, 32, 0.14);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete) {
|
||||
border: 1px solid #e6eaf2;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
|
||||
padding: 6px;
|
||||
min-width: 260px;
|
||||
max-width: 360px;
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete > ul) {
|
||||
font-family:
|
||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace";
|
||||
font-size: 13px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete > ul > li) {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]) {
|
||||
background: #eef4ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete > ul > li:hover) {
|
||||
background: #f6f8fc;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete .cm-completionIcon) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
font-size: 12px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected] .cm-completionIcon) {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete .cm-completionLabel) {
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete .cm-completionDetail) {
|
||||
margin-left: auto;
|
||||
color: #94a3b8;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete .cm-completionMatchedText) {
|
||||
text-decoration: none;
|
||||
color: #2563eb;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete > ul::-webkit-scrollbar) {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete > ul::-webkit-scrollbar-thumb) {
|
||||
background: #d6dce8;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.code-script-editor :global(.cm-tooltip.cm-tooltip-autocomplete > ul::-webkit-scrollbar-track) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:global(body.code-script-expanded-open) {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -20,6 +20,7 @@
|
||||
allowSettingOfCondition = true,
|
||||
showSourceHandle = true,
|
||||
showTargetHandle = true,
|
||||
titleHelp = '',
|
||||
onCollapse
|
||||
}: {
|
||||
data: NodeProps['data'],
|
||||
@@ -34,6 +35,7 @@
|
||||
allowSettingOfCondition?: boolean,
|
||||
showSourceHandle?: boolean,
|
||||
showTargetHandle?: boolean,
|
||||
titleHelp?: string,
|
||||
onCollapse?: (key: string) => void,
|
||||
} = $props();
|
||||
|
||||
@@ -45,6 +47,7 @@
|
||||
key: 'key',
|
||||
icon,
|
||||
title: data.title as string,
|
||||
titleHelp,
|
||||
description: data.description as string,
|
||||
content: children
|
||||
}];
|
||||
@@ -296,4 +299,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user