Browse Source

fix(47-02): redesign inline parameter panel with inline labels and solid bg

Solid dark background attached to image preview, labels on same line
as fields, slightly larger text (11px), darker rounded inputs matching
the reference design. Applied consistently across Gemini controls,
LLM controls, and ModelParameters (external provider fields).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
cdb396ddb1
  1. 28
      src/components/nodes/GenerateImageNode.tsx
  2. 10
      src/components/nodes/InlineParameterPanel.tsx
  3. 18
      src/components/nodes/LLMGenerateNode.tsx
  4. 96
      src/components/nodes/ModelParameters.tsx

28
src/components/nodes/GenerateImageNode.tsx

@ -758,16 +758,16 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
onToggle={handleToggleParams} onToggle={handleToggleParams}
nodeId={id} nodeId={id}
> >
{/* Gemini-specific controls - unified styling to match ModelParameters */} {/* Gemini-specific controls */}
{isGeminiProvider && currentModelId && ( {isGeminiProvider && currentModelId && (
<div className="space-y-1.5"> <div className="space-y-1.5">
{/* Model selector */} {/* Model selector */}
<div className="flex flex-col gap-0.5"> <div className="flex items-center gap-2">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Model</label> <label className="text-[11px] text-neutral-400 shrink-0">Model</label>
<select <select
value={currentModelId} value={currentModelId}
onChange={handleModelChange} onChange={handleModelChange}
className="nodrag nopan w-full text-[9px] py-0.5 px-1 bg-neutral-900/50 border border-neutral-700 rounded focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300" className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-neutral-800 border border-neutral-700 rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
> >
{GEMINI_IMAGE_MODELS.map((m) => ( {GEMINI_IMAGE_MODELS.map((m) => (
<option key={m.value} value={m.value}> <option key={m.value} value={m.value}>
@ -778,12 +778,12 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
</div> </div>
{/* Aspect Ratio */} {/* Aspect Ratio */}
<div className="flex flex-col gap-0.5"> <div className="flex items-center gap-2">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Aspect Ratio</label> <label className="text-[11px] text-neutral-400 shrink-0">Aspect Ratio</label>
<select <select
value={nodeData.aspectRatio || "1:1"} value={nodeData.aspectRatio || "1:1"}
onChange={handleAspectRatioChange} onChange={handleAspectRatioChange}
className="nodrag nopan w-full text-[9px] py-0.5 px-1 bg-neutral-900/50 border border-neutral-700 rounded focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300" className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-neutral-800 border border-neutral-700 rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
> >
{aspectRatios.map((ratio) => ( {aspectRatios.map((ratio) => (
<option key={ratio} value={ratio}> <option key={ratio} value={ratio}>
@ -795,12 +795,12 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
{/* Resolution (if supported) */} {/* Resolution (if supported) */}
{supportsResolution && ( {supportsResolution && (
<div className="flex flex-col gap-0.5"> <div className="flex items-center gap-2">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Resolution</label> <label className="text-[11px] text-neutral-400 shrink-0">Resolution</label>
<select <select
value={nodeData.resolution || "2K"} value={nodeData.resolution || "2K"}
onChange={handleResolutionChange} onChange={handleResolutionChange}
className="nodrag nopan w-full text-[9px] py-0.5 px-1 bg-neutral-900/50 border border-neutral-700 rounded focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300" className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-neutral-800 border border-neutral-700 rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
> >
{resolutions.map((res) => ( {resolutions.map((res) => (
<option key={res} value={res}> <option key={res} value={res}>
@ -813,12 +813,12 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
{/* Google Search toggle */} {/* Google Search toggle */}
{(currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2") && ( {(currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2") && (
<label className="flex items-center gap-1.5 text-[9px] text-neutral-300 cursor-pointer"> <label className="flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={nodeData.useGoogleSearch || false} checked={nodeData.useGoogleSearch || false}
onChange={handleGoogleSearchToggle} onChange={handleGoogleSearchToggle}
className="nodrag nopan w-3 h-3 rounded border-neutral-700 bg-neutral-800/50 text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0" className="nodrag nopan w-3 h-3 rounded border-neutral-700 bg-neutral-800 text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/> />
Google Search Google Search
</label> </label>
@ -826,12 +826,12 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
{/* Image Search toggle (NB2 only) */} {/* Image Search toggle (NB2 only) */}
{currentModelId === "nano-banana-2" && ( {currentModelId === "nano-banana-2" && (
<label className="flex items-center gap-1.5 text-[9px] text-neutral-300 cursor-pointer"> <label className="flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={nodeData.useImageSearch || false} checked={nodeData.useImageSearch || false}
onChange={handleImageSearchToggle} onChange={handleImageSearchToggle}
className="nodrag nopan w-3 h-3 rounded border-neutral-700 bg-neutral-800/50 text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0" className="nodrag nopan w-3 h-3 rounded border-neutral-700 bg-neutral-800 text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/> />
Image Search Image Search
</label> </label>

10
src/components/nodes/InlineParameterPanel.tsx

@ -20,12 +20,12 @@ export function InlineParameterPanel({
nodeId, nodeId,
}: InlineParameterPanelProps) { }: InlineParameterPanelProps) {
return ( return (
<div className="w-full"> <div className="w-full bg-[#1a1a1a]">
{/* Chevron toggle button - acts as visual boundary */} {/* Chevron toggle button */}
<button <button
type="button" type="button"
onClick={onToggle} onClick={onToggle}
className="nodrag nopan w-full flex items-center justify-center py-1.5 bg-neutral-800/80 text-neutral-400 hover:text-neutral-200 transition-colors" className="nodrag nopan w-full flex items-center justify-center py-1 text-neutral-500 hover:text-neutral-300 transition-colors"
aria-label={expanded ? "Collapse parameters" : "Expand parameters"} aria-label={expanded ? "Collapse parameters" : "Expand parameters"}
aria-expanded={expanded} aria-expanded={expanded}
aria-controls={`params-${nodeId}`} aria-controls={`params-${nodeId}`}
@ -53,8 +53,8 @@ export function InlineParameterPanel({
opacity: expanded ? 1 : 0, opacity: expanded ? 1 : 0,
}} }}
> >
<div className="nodrag nopan nowheel bg-neutral-900 px-3 pt-2 pb-3 rounded-b-lg"> <div className="nodrag nopan nowheel px-3 pb-3 rounded-b-lg">
<div className="space-y-2 max-w-[280px] mx-auto">{children}</div> <div className="space-y-1.5 max-w-[280px] mx-auto">{children}</div>
</div> </div>
</div> </div>
</div> </div>

18
src/components/nodes/LLMGenerateNode.tsx

@ -244,15 +244,15 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
onToggle={handleToggleParams} onToggle={handleToggleParams}
nodeId={id} nodeId={id}
> >
{/* LLM-specific controls with compact styling */} {/* LLM-specific controls */}
<div className="space-y-1.5"> <div className="space-y-1.5">
{/* Provider */} {/* Provider */}
<div className="flex flex-col gap-0.5"> <div className="flex items-center gap-2">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Provider</label> <label className="text-[11px] text-neutral-400 shrink-0">Provider</label>
<select <select
value={provider} value={provider}
onChange={handleProviderChange} onChange={handleProviderChange}
className="nodrag nopan w-full text-[9px] py-0.5 px-1 bg-neutral-900/50 border border-neutral-700 rounded focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300" className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-neutral-800 border border-neutral-700 rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
> >
{LLM_PROVIDERS.map(p => ( {LLM_PROVIDERS.map(p => (
<option key={p.value} value={p.value}>{p.label}</option> <option key={p.value} value={p.value}>{p.label}</option>
@ -261,12 +261,12 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
</div> </div>
{/* Model */} {/* Model */}
<div className="flex flex-col gap-0.5"> <div className="flex items-center gap-2">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Model</label> <label className="text-[11px] text-neutral-400 shrink-0">Model</label>
<select <select
value={nodeData.model || availableModels[0].value} value={nodeData.model || availableModels[0].value}
onChange={handleModelChange} onChange={handleModelChange}
className="nodrag nopan w-full text-[9px] py-0.5 px-1 bg-neutral-900/50 border border-neutral-700 rounded focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300" className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-neutral-800 border border-neutral-700 rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
> >
{availableModels.map(m => ( {availableModels.map(m => (
<option key={m.value} value={m.value}>{m.label}</option> <option key={m.value} value={m.value}>{m.label}</option>
@ -276,7 +276,7 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
{/* Temperature */} {/* Temperature */}
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide"> <label className="text-[11px] text-neutral-400">
Temperature: {(nodeData.temperature ?? 0.7).toFixed(2)} Temperature: {(nodeData.temperature ?? 0.7).toFixed(2)}
</label> </label>
<input <input
@ -292,7 +292,7 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
{/* Max Tokens */} {/* Max Tokens */}
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide"> <label className="text-[11px] text-neutral-400">
Max Tokens: {(nodeData.maxTokens || 2048).toLocaleString()} Max Tokens: {(nodeData.maxTokens || 2048).toLocaleString()}
</label> </label>
<input <input

96
src/components/nodes/ModelParameters.tsx

@ -228,9 +228,9 @@ function ParameterInput({ param, value, onChange }: ParameterInputProps) {
if (param.enum && param.enum.length > 0) { if (param.enum && param.enum.length > 0) {
// Enum: render as select // Enum: render as select
return ( return (
<div className="flex flex-col gap-0.5"> <div className="flex items-center gap-2">
<label <label
className="text-[9px] text-neutral-400" className="text-[11px] text-neutral-400 shrink-0"
title={param.description || undefined} title={param.description || undefined}
> >
{displayName} {displayName}
@ -251,7 +251,7 @@ function ParameterInput({ param, value, onChange }: ParameterInputProps) {
onChange(val); onChange(val);
} }
}} }}
className="nodrag nopan w-full text-[9px] py-0.5 px-1 border border-neutral-700 rounded bg-neutral-900/50 focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300" className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 border border-neutral-700 rounded-md bg-neutral-800 focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
> >
<option value="">Default</option> <option value="">Default</option>
{param.enum.map((opt) => ( {param.enum.map((opt) => (
@ -271,14 +271,14 @@ function ParameterInput({ param, value, onChange }: ParameterInputProps) {
// Boolean: render as checkbox // Boolean: render as checkbox
return ( return (
<label <label
className="flex items-center gap-1.5 text-[9px] text-neutral-400 cursor-pointer" className="flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer"
title={param.description || undefined} title={param.description || undefined}
> >
<input <input
type="checkbox" type="checkbox"
checked={effectiveValue} checked={effectiveValue}
onChange={(e) => onChange(e.target.checked)} onChange={(e) => onChange(e.target.checked)}
className="nodrag nopan w-2.5 h-2.5 rounded border-neutral-700 bg-neutral-900/50 text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0" className="nodrag nopan w-3 h-3 rounded border-neutral-700 bg-neutral-800 text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/> />
<span>{displayName}</span> <span>{displayName}</span>
</label> </label>
@ -304,45 +304,47 @@ function ParameterInput({ param, value, onChange }: ParameterInputProps) {
return ( return (
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<label <div className="flex items-center gap-2">
className="text-[9px] text-neutral-400 flex items-center gap-1" <label
title={param.description || undefined} className="text-[11px] text-neutral-400 shrink-0 flex items-center gap-1"
> title={param.description || undefined}
{displayName} >
{hasMin && hasMax && ( {displayName}
<span className="text-neutral-500"> {hasMin && hasMax && (
({param.minimum}-{param.maximum}) <span className="text-neutral-500 text-[9px]">
</span> ({param.minimum}-{param.maximum})
)} </span>
</label> )}
<input </label>
type="number" <input
value={localValue} type="number"
min={param.minimum} value={localValue}
max={param.maximum} min={param.minimum}
step={param.type === "integer" ? 1 : 0.1} max={param.maximum}
onFocus={() => { isFocusedRef.current = true; }} step={param.type === "integer" ? 1 : 0.1}
onChange={(e) => { onFocus={() => { isFocusedRef.current = true; }}
setLocalValue(e.target.value); onChange={(e) => {
}} setLocalValue(e.target.value);
onBlur={() => { }}
isFocusedRef.current = false; onBlur={() => {
if (localValue === "") { isFocusedRef.current = false;
onChange(undefined); if (localValue === "") {
} else { onChange(undefined);
const num = param.type === "integer" ? parseInt(localValue, 10) : parseFloat(localValue); } else {
onChange(isNaN(num) ? undefined : num); const num = param.type === "integer" ? parseInt(localValue, 10) : parseFloat(localValue);
} onChange(isNaN(num) ? undefined : num);
}} }
placeholder={param.default !== undefined ? `Default: ${param.default}` : undefined} }}
className={`nodrag nopan w-full text-[9px] py-0.5 px-1 border rounded bg-neutral-900/50 focus:outline-none focus:ring-1 text-neutral-300 placeholder:text-neutral-600 ${ placeholder={param.default !== undefined ? `${param.default}` : undefined}
validationError className={`nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 border rounded-md bg-neutral-800 focus:outline-none focus:ring-1 text-white placeholder:text-neutral-500 ${
? "border-red-500 focus:ring-red-500" validationError
: "border-neutral-700 focus:ring-neutral-600" ? "border-red-500 focus:ring-red-500"
}`} : "border-neutral-700 focus:ring-neutral-600"
/> }`}
/>
</div>
{validationError && ( {validationError && (
<span className="text-[8px] text-red-400">{validationError}</span> <span className="text-[9px] text-red-400">{validationError}</span>
)} )}
</div> </div>
); );
@ -355,9 +357,9 @@ function ParameterInput({ param, value, onChange }: ParameterInputProps) {
// Default: string input — uses local state, syncs to store on blur // Default: string input — uses local state, syncs to store on blur
return ( return (
<div className="flex flex-col gap-0.5"> <div className="flex items-center gap-2">
<label <label
className="text-[9px] text-neutral-400" className="text-[11px] text-neutral-400 shrink-0"
title={param.description || undefined} title={param.description || undefined}
> >
{displayName} {displayName}
@ -373,8 +375,8 @@ function ParameterInput({ param, value, onChange }: ParameterInputProps) {
isFocusedRef.current = false; isFocusedRef.current = false;
onChange(localValue || undefined); onChange(localValue || undefined);
}} }}
placeholder={param.default !== undefined ? `Default: ${param.default}` : undefined} placeholder={param.default !== undefined ? `${param.default}` : undefined}
className="nodrag nopan w-full text-[9px] py-0.5 px-1 border border-neutral-700 rounded bg-neutral-900/50 focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300 placeholder:text-neutral-600" className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 border border-neutral-700 rounded-md bg-neutral-800 focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white placeholder:text-neutral-500"
/> />
</div> </div>
); );

Loading…
Cancel
Save