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}
nodeId={id}
>
{/* Gemini-specific controls - unified styling to match ModelParameters */}
{/* Gemini-specific controls */}
{isGeminiProvider && currentModelId && (
<div className="space-y-1.5">
{/* Model selector */}
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Model</label>
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Model</label>
<select
value={currentModelId}
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) => (
<option key={m.value} value={m.value}>
@ -778,12 +778,12 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
</div>
{/* Aspect Ratio */}
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Aspect Ratio</label>
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Aspect Ratio</label>
<select
value={nodeData.aspectRatio || "1:1"}
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) => (
<option key={ratio} value={ratio}>
@ -795,12 +795,12 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
{/* Resolution (if supported) */}
{supportsResolution && (
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Resolution</label>
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Resolution</label>
<select
value={nodeData.resolution || "2K"}
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) => (
<option key={res} value={res}>
@ -813,12 +813,12 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
{/* Google Search toggle */}
{(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
type="checkbox"
checked={nodeData.useGoogleSearch || false}
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
</label>
@ -826,12 +826,12 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
{/* Image Search toggle (NB2 only) */}
{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
type="checkbox"
checked={nodeData.useImageSearch || false}
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
</label>

10
src/components/nodes/InlineParameterPanel.tsx

@ -20,12 +20,12 @@ export function InlineParameterPanel({
nodeId,
}: InlineParameterPanelProps) {
return (
<div className="w-full">
{/* Chevron toggle button - acts as visual boundary */}
<div className="w-full bg-[#1a1a1a]">
{/* Chevron toggle button */}
<button
type="button"
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-expanded={expanded}
aria-controls={`params-${nodeId}`}
@ -53,8 +53,8 @@ export function InlineParameterPanel({
opacity: expanded ? 1 : 0,
}}
>
<div className="nodrag nopan nowheel bg-neutral-900 px-3 pt-2 pb-3 rounded-b-lg">
<div className="space-y-2 max-w-[280px] mx-auto">{children}</div>
<div className="nodrag nopan nowheel px-3 pb-3 rounded-b-lg">
<div className="space-y-1.5 max-w-[280px] mx-auto">{children}</div>
</div>
</div>
</div>

18
src/components/nodes/LLMGenerateNode.tsx

@ -244,15 +244,15 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
onToggle={handleToggleParams}
nodeId={id}
>
{/* LLM-specific controls with compact styling */}
{/* LLM-specific controls */}
<div className="space-y-1.5">
{/* Provider */}
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Provider</label>
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Provider</label>
<select
value={provider}
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 => (
<option key={p.value} value={p.value}>{p.label}</option>
@ -261,12 +261,12 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
</div>
{/* Model */}
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Model</label>
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Model</label>
<select
value={nodeData.model || availableModels[0].value}
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 => (
<option key={m.value} value={m.value}>{m.label}</option>
@ -276,7 +276,7 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
{/* Temperature */}
<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)}
</label>
<input
@ -292,7 +292,7 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
{/* Max Tokens */}
<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()}
</label>
<input

96
src/components/nodes/ModelParameters.tsx

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

Loading…
Cancel
Save