Browse Source

feat: make PromptConstructorNode full-bleed

Textarea fills the entire node with no visible border. Available
variables display as a fixed semi-transparent footer pinned at the
bottom. Unresolved variables warning overlays at the top. Autocomplete
dropdown remains functional. Handles use z-index to stay visible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
b390442e5b
  1. 113
      src/components/nodes/PromptConstructorNode.tsx

113
src/components/nodes/PromptConstructorNode.tsx

@ -180,6 +180,7 @@ export function PromptConstructorNode({ id, data, selected }: NodeProps<PromptCo
<BaseNode
id={id}
selected={selected}
fullBleed
>
{/* Text input handle */}
<Handle
@ -187,76 +188,76 @@ export function PromptConstructorNode({ id, data, selected }: NodeProps<PromptCo
position={Position.Left}
id="text"
data-handletype="text"
style={{ zIndex: 10 }}
/>
<div className="relative flex flex-col gap-2 flex-1">
{/* Warning badge for unresolved variables */}
{unresolvedVars.length > 0 && (
<div className="px-2 py-1 bg-amber-900/30 border border-amber-700/50 rounded text-[10px] text-amber-400">
<span className="font-semibold">Unresolved:</span> {unresolvedVars.map(v => `@${v}`).join(', ')}
</div>
)}
{/* Template textarea with autocomplete */}
<div className="relative flex-1 flex flex-col">
<textarea
ref={textareaRef}
value={localTemplate}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder="Type @ to insert variables..."
className="nodrag nopan nowheel w-full flex-1 min-h-[70px] p-2 text-xs leading-relaxed text-neutral-100 border border-neutral-700 rounded bg-neutral-900/50 resize-none focus:outline-none focus:ring-1 focus:ring-neutral-600 focus:border-neutral-600 placeholder:text-neutral-500"
title={resolvedPreview ? `Preview: ${resolvedPreview}` : undefined}
/>
{/* Autocomplete dropdown */}
{showAutocomplete && filteredAutocompleteVars.length > 0 && (
<div
className="absolute z-10 bg-neutral-800 border border-neutral-600 rounded shadow-xl max-h-40 overflow-y-auto"
style={{
top: autocompletePosition.top,
left: autocompletePosition.left,
}}
>
{filteredAutocompleteVars.map((variable, index) => (
<button
key={variable.nodeId}
onMouseDown={(e) => {
e.preventDefault();
handleAutocompleteSelect(variable.name);
}}
className={`w-full px-3 py-2 text-left text-[11px] flex flex-col gap-0.5 transition-colors ${
index === selectedAutocompleteIndex
? "bg-neutral-700 text-neutral-100"
: "text-neutral-300 hover:bg-neutral-700"
}`}
>
<div className="font-medium text-blue-400">@{variable.name}</div>
<div className="text-neutral-500 truncate max-w-[200px]">
{variable.value || "(empty)"}
</div>
</button>
))}
</div>
)}
{/* Warning badge for unresolved variables - overlay at top */}
{unresolvedVars.length > 0 && (
<div className="absolute top-2 left-2 right-2 z-20 px-2 py-1 bg-amber-900/80 backdrop-blur-sm border border-amber-700/50 rounded text-[10px] text-amber-400 pointer-events-none">
<span className="font-semibold">Unresolved:</span> {unresolvedVars.map(v => `@${v}`).join(', ')}
</div>
)}
{/* Available variables info */}
{availableVariables.length > 0 && (
<div className="text-[10px] text-neutral-500 px-2">
Available: {availableVariables.map(v => `@${v.name}`).join(', ')}
{/* Template textarea with autocomplete */}
<div className="relative w-full h-full">
<textarea
ref={textareaRef}
value={localTemplate}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder="Type @ to insert variables..."
className={`nodrag nopan nowheel w-full h-full p-3 text-xs leading-relaxed text-neutral-100 bg-neutral-800 rounded-lg resize-none focus:outline-none placeholder:text-neutral-500 ${availableVariables.length > 0 ? "pb-7" : ""}`}
title={resolvedPreview ? `Preview: ${resolvedPreview}` : undefined}
/>
{/* Autocomplete dropdown */}
{showAutocomplete && filteredAutocompleteVars.length > 0 && (
<div
className="absolute z-20 bg-neutral-800 border border-neutral-600 rounded shadow-xl max-h-40 overflow-y-auto"
style={{
top: autocompletePosition.top,
left: autocompletePosition.left,
}}
>
{filteredAutocompleteVars.map((variable, index) => (
<button
key={variable.nodeId}
onMouseDown={(e) => {
e.preventDefault();
handleAutocompleteSelect(variable.name);
}}
className={`w-full px-3 py-2 text-left text-[11px] flex flex-col gap-0.5 transition-colors ${
index === selectedAutocompleteIndex
? "bg-neutral-700 text-neutral-100"
: "text-neutral-300 hover:bg-neutral-700"
}`}
>
<div className="font-medium text-blue-400">@{variable.name}</div>
<div className="text-neutral-500 truncate max-w-[200px]">
{variable.value || "(empty)"}
</div>
</button>
))}
</div>
)}
</div>
{/* Available variables - fixed footer pinned at bottom */}
{availableVariables.length > 0 && (
<div className="absolute bottom-0 left-0 right-0 z-10 px-3 py-1.5 bg-neutral-900/80 backdrop-blur-sm rounded-b-lg text-[10px] text-neutral-500 pointer-events-none">
Available: {availableVariables.map(v => `@${v.name}`).join(', ')}
</div>
)}
{/* Text output handle */}
<Handle
type="source"
position={Position.Right}
id="text"
data-handletype="text"
style={{ zIndex: 10 }}
/>
</BaseNode>

Loading…
Cancel
Save