mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
refactor: replace TOOL_FIELDS with shared Controls components (DRY)
Extract a *Controls subcomponent from all 16 pipeline-compatible tool settings components. Each Controls component holds the UI state and settings controls, accepts an onChange callback, and uses useRef to prevent infinite re-render loops. The standalone *Settings components become thin wrappers that add useToolProcessor, useFileStore, and action buttons. pipeline-step-settings.tsx is rewritten from ~675 lines to ~55 lines: the entire TOOL_FIELDS declarative map and generic renderer are deleted and replaced with direct imports of the Controls components. Pipeline steps now render the exact same UI as standalone tool pages. Special cases: - CropControls: numeric inputs for pipeline (standalone uses canvas) - RotateControls: resetSignal prop for post-processing reset - ColorControls: accepts toolId for tab selection - StripMetadataControls: checkboxes only (no file inspection) - RemoveBgControls: already extracted, unchanged
This commit is contained in:
@@ -1,493 +1,26 @@
|
||||
import { BlurFacesControls } from "./blur-faces-settings";
|
||||
import { BorderControls } from "./border-settings";
|
||||
import { ColorControls } from "./color-settings";
|
||||
import { CompressControls } from "./compress-settings";
|
||||
import { ConvertControls } from "./convert-settings";
|
||||
import { CropControls } from "./crop-settings";
|
||||
import { GifToolsControls } from "./gif-tools-settings";
|
||||
import { RemoveBgControls } from "./remove-bg-settings";
|
||||
import { ReplaceColorControls } from "./replace-color-settings";
|
||||
import { ResizeControls } from "./resize-settings";
|
||||
import { RotateControls } from "./rotate-settings";
|
||||
import { SmartCropControls } from "./smart-crop-settings";
|
||||
import { StripMetadataControls } from "./strip-metadata-settings";
|
||||
import { TextOverlayControls } from "./text-overlay-settings";
|
||||
import { UpscaleControls } from "./upscale-settings";
|
||||
import { WatermarkTextControls } from "./watermark-text-settings";
|
||||
|
||||
type FieldType = "number" | "select" | "boolean" | "text" | "color";
|
||||
|
||||
interface FieldDef {
|
||||
key: string;
|
||||
label: string;
|
||||
type: FieldType;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
placeholder?: string;
|
||||
options?: { value: string; label: string }[];
|
||||
defaultValue?: unknown;
|
||||
showWhen?: (settings: Record<string, unknown>) => boolean;
|
||||
}
|
||||
|
||||
const TOOL_FIELDS: Record<string, FieldDef[]> = {
|
||||
resize: [
|
||||
{ key: "width", label: "Width (px)", type: "number", min: 1, placeholder: "Auto" },
|
||||
{ key: "height", label: "Height (px)", type: "number", min: 1, placeholder: "Auto" },
|
||||
{
|
||||
key: "percentage",
|
||||
label: "Scale (%)",
|
||||
type: "number",
|
||||
min: 1,
|
||||
placeholder: "Use instead of width/height",
|
||||
},
|
||||
{
|
||||
key: "fit",
|
||||
label: "Fit Mode",
|
||||
type: "select",
|
||||
defaultValue: "contain",
|
||||
options: [
|
||||
{ value: "contain", label: "Fit inside" },
|
||||
{ value: "cover", label: "Crop to fit" },
|
||||
{ value: "fill", label: "Stretch" },
|
||||
],
|
||||
},
|
||||
{ key: "withoutEnlargement", label: "Don't enlarge", type: "boolean", defaultValue: false },
|
||||
],
|
||||
|
||||
crop: [
|
||||
{ key: "left", label: "Left offset (px)", type: "number", min: 0, placeholder: "0" },
|
||||
{ key: "top", label: "Top offset (px)", type: "number", min: 0, placeholder: "0" },
|
||||
{ key: "width", label: "Width (px)", type: "number", min: 1, placeholder: "Required" },
|
||||
{ key: "height", label: "Height (px)", type: "number", min: 1, placeholder: "Required" },
|
||||
],
|
||||
|
||||
rotate: [
|
||||
{
|
||||
key: "angle",
|
||||
label: "Angle (degrees)",
|
||||
type: "number",
|
||||
min: -360,
|
||||
max: 360,
|
||||
step: 90,
|
||||
defaultValue: 0,
|
||||
},
|
||||
{ key: "horizontal", label: "Flip horizontal", type: "boolean", defaultValue: false },
|
||||
{ key: "vertical", label: "Flip vertical", type: "boolean", defaultValue: false },
|
||||
],
|
||||
|
||||
convert: [
|
||||
{
|
||||
key: "format",
|
||||
label: "Format",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "jpg", label: "JPEG" },
|
||||
{ value: "png", label: "PNG" },
|
||||
{ value: "webp", label: "WebP" },
|
||||
{ value: "avif", label: "AVIF" },
|
||||
{ value: "tiff", label: "TIFF" },
|
||||
{ value: "gif", label: "GIF" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "quality",
|
||||
label: "Quality (1-100)",
|
||||
type: "number",
|
||||
min: 1,
|
||||
max: 100,
|
||||
placeholder: "Auto",
|
||||
},
|
||||
],
|
||||
|
||||
compress: [
|
||||
{
|
||||
key: "mode",
|
||||
label: "Mode",
|
||||
type: "select",
|
||||
defaultValue: "quality",
|
||||
options: [
|
||||
{ value: "quality", label: "Quality" },
|
||||
{ value: "targetSize", label: "Target size" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "quality",
|
||||
label: "Quality (1-100)",
|
||||
type: "number",
|
||||
min: 1,
|
||||
max: 100,
|
||||
placeholder: "80",
|
||||
showWhen: (s) => s.mode !== "targetSize",
|
||||
},
|
||||
{
|
||||
key: "targetSizeKb",
|
||||
label: "Target size (KB)",
|
||||
type: "number",
|
||||
min: 1,
|
||||
placeholder: "e.g. 200",
|
||||
showWhen: (s) => s.mode === "targetSize",
|
||||
},
|
||||
],
|
||||
|
||||
"strip-metadata": [
|
||||
{ key: "stripAll", label: "Strip all metadata", type: "boolean", defaultValue: true },
|
||||
{
|
||||
key: "stripExif",
|
||||
label: "Strip EXIF",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
showWhen: (s) => !s.stripAll,
|
||||
},
|
||||
{
|
||||
key: "stripGps",
|
||||
label: "Strip GPS",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
showWhen: (s) => !s.stripAll,
|
||||
},
|
||||
{
|
||||
key: "stripIcc",
|
||||
label: "Strip ICC profile",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
showWhen: (s) => !s.stripAll,
|
||||
},
|
||||
{
|
||||
key: "stripXmp",
|
||||
label: "Strip XMP",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
showWhen: (s) => !s.stripAll,
|
||||
},
|
||||
],
|
||||
|
||||
"brightness-contrast": [
|
||||
{
|
||||
key: "brightness",
|
||||
label: "Brightness",
|
||||
type: "number",
|
||||
min: -100,
|
||||
max: 100,
|
||||
defaultValue: 0,
|
||||
},
|
||||
{ key: "contrast", label: "Contrast", type: "number", min: -100, max: 100, defaultValue: 0 },
|
||||
],
|
||||
|
||||
saturation: [
|
||||
{
|
||||
key: "saturation",
|
||||
label: "Saturation",
|
||||
type: "number",
|
||||
min: -100,
|
||||
max: 100,
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
|
||||
"color-channels": [
|
||||
{ key: "red", label: "Red", type: "number", min: 0, max: 200, defaultValue: 100 },
|
||||
{ key: "green", label: "Green", type: "number", min: 0, max: 200, defaultValue: 100 },
|
||||
{ key: "blue", label: "Blue", type: "number", min: 0, max: 200, defaultValue: 100 },
|
||||
],
|
||||
|
||||
"color-effects": [
|
||||
{
|
||||
key: "effect",
|
||||
label: "Effect",
|
||||
type: "select",
|
||||
defaultValue: "none",
|
||||
options: [
|
||||
{ value: "none", label: "None" },
|
||||
{ value: "grayscale", label: "Grayscale" },
|
||||
{ value: "sepia", label: "Sepia" },
|
||||
{ value: "invert", label: "Invert" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
"replace-color": [
|
||||
{ key: "sourceColor", label: "Source color", type: "color", defaultValue: "#FF0000" },
|
||||
{ key: "targetColor", label: "Target color", type: "color", defaultValue: "#00FF00" },
|
||||
{
|
||||
key: "makeTransparent",
|
||||
label: "Make transparent instead",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: "tolerance",
|
||||
label: "Tolerance (0-255)",
|
||||
type: "number",
|
||||
min: 0,
|
||||
max: 255,
|
||||
defaultValue: 30,
|
||||
},
|
||||
],
|
||||
|
||||
"watermark-text": [
|
||||
{ key: "text", label: "Watermark text", type: "text", placeholder: "Your watermark" },
|
||||
{ key: "fontSize", label: "Font size", type: "number", min: 8, max: 200, defaultValue: 48 },
|
||||
{ key: "color", label: "Color", type: "color", defaultValue: "#000000" },
|
||||
{ key: "opacity", label: "Opacity (%)", type: "number", min: 0, max: 100, defaultValue: 50 },
|
||||
{
|
||||
key: "position",
|
||||
label: "Position",
|
||||
type: "select",
|
||||
defaultValue: "center",
|
||||
options: [
|
||||
{ value: "center", label: "Center" },
|
||||
{ value: "top-left", label: "Top left" },
|
||||
{ value: "top-right", label: "Top right" },
|
||||
{ value: "bottom-left", label: "Bottom left" },
|
||||
{ value: "bottom-right", label: "Bottom right" },
|
||||
{ value: "tiled", label: "Tiled" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "rotation",
|
||||
label: "Rotation (degrees)",
|
||||
type: "number",
|
||||
min: -360,
|
||||
max: 360,
|
||||
defaultValue: 0,
|
||||
},
|
||||
],
|
||||
|
||||
"watermark-image": [
|
||||
{
|
||||
key: "position",
|
||||
label: "Position",
|
||||
type: "select",
|
||||
defaultValue: "bottom-right",
|
||||
options: [
|
||||
{ value: "center", label: "Center" },
|
||||
{ value: "top-left", label: "Top left" },
|
||||
{ value: "top-right", label: "Top right" },
|
||||
{ value: "bottom-left", label: "Bottom left" },
|
||||
{ value: "bottom-right", label: "Bottom right" },
|
||||
],
|
||||
},
|
||||
{ key: "opacity", label: "Opacity (%)", type: "number", min: 0, max: 100, defaultValue: 50 },
|
||||
{ key: "scale", label: "Scale (%)", type: "number", min: 1, max: 100, defaultValue: 25 },
|
||||
],
|
||||
|
||||
"text-overlay": [
|
||||
{ key: "text", label: "Text", type: "text", placeholder: "Your text" },
|
||||
{ key: "fontSize", label: "Font size", type: "number", min: 8, max: 200, defaultValue: 48 },
|
||||
{ key: "color", label: "Color", type: "color", defaultValue: "#FFFFFF" },
|
||||
{
|
||||
key: "position",
|
||||
label: "Position",
|
||||
type: "select",
|
||||
defaultValue: "bottom",
|
||||
options: [
|
||||
{ value: "top", label: "Top" },
|
||||
{ value: "center", label: "Center" },
|
||||
{ value: "bottom", label: "Bottom" },
|
||||
],
|
||||
},
|
||||
{ key: "backgroundBox", label: "Background box", type: "boolean", defaultValue: false },
|
||||
{
|
||||
key: "backgroundColor",
|
||||
label: "Box color",
|
||||
type: "color",
|
||||
defaultValue: "#000000",
|
||||
showWhen: (s) => !!s.backgroundBox,
|
||||
},
|
||||
{ key: "shadow", label: "Text shadow", type: "boolean", defaultValue: true },
|
||||
],
|
||||
|
||||
border: [
|
||||
{ key: "borderWidth", label: "Width (px)", type: "number", min: 0, max: 200, defaultValue: 10 },
|
||||
{ key: "borderColor", label: "Color", type: "color", defaultValue: "#000000" },
|
||||
{
|
||||
key: "cornerRadius",
|
||||
label: "Corner radius",
|
||||
type: "number",
|
||||
min: 0,
|
||||
max: 500,
|
||||
defaultValue: 0,
|
||||
},
|
||||
{ key: "padding", label: "Padding (px)", type: "number", min: 0, max: 200, defaultValue: 0 },
|
||||
{ key: "shadowBlur", label: "Shadow blur", type: "number", min: 0, max: 50, defaultValue: 0 },
|
||||
{ key: "shadowColor", label: "Shadow color", type: "color", defaultValue: "#00000080" },
|
||||
],
|
||||
|
||||
split: [
|
||||
{ key: "columns", label: "Columns", type: "number", min: 1, max: 10, defaultValue: 2 },
|
||||
{ key: "rows", label: "Rows", type: "number", min: 1, max: 10, defaultValue: 2 },
|
||||
],
|
||||
|
||||
"blur-faces": [
|
||||
{ key: "blurRadius", label: "Blur radius", type: "number", min: 1, max: 100, defaultValue: 30 },
|
||||
{
|
||||
key: "sensitivity",
|
||||
label: "Sensitivity (0-1)",
|
||||
type: "number",
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
defaultValue: 0.5,
|
||||
},
|
||||
],
|
||||
|
||||
upscale: [
|
||||
{
|
||||
key: "scale",
|
||||
label: "Scale factor",
|
||||
type: "select",
|
||||
defaultValue: "2",
|
||||
options: [
|
||||
{ value: "2", label: "2x" },
|
||||
{ value: "3", label: "3x" },
|
||||
{ value: "4", label: "4x" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
"smart-crop": [
|
||||
{ key: "width", label: "Width (px)", type: "number", min: 1, placeholder: "Required" },
|
||||
{ key: "height", label: "Height (px)", type: "number", min: 1, placeholder: "Required" },
|
||||
],
|
||||
|
||||
vectorize: [
|
||||
{
|
||||
key: "colorMode",
|
||||
label: "Color mode",
|
||||
type: "select",
|
||||
defaultValue: "bw",
|
||||
options: [
|
||||
{ value: "bw", label: "Black & White" },
|
||||
{ value: "color", label: "Color" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "threshold",
|
||||
label: "Threshold (0-255)",
|
||||
type: "number",
|
||||
min: 0,
|
||||
max: 255,
|
||||
defaultValue: 128,
|
||||
},
|
||||
{
|
||||
key: "detail",
|
||||
label: "Detail",
|
||||
type: "select",
|
||||
defaultValue: "medium",
|
||||
options: [
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "high", label: "High" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
"svg-to-raster": [
|
||||
{ key: "width", label: "Width (px)", type: "number", min: 1, max: 8192, defaultValue: 1024 },
|
||||
{ key: "height", label: "Height (px)", type: "number", min: 1, max: 8192, placeholder: "Auto" },
|
||||
{
|
||||
key: "outputFormat",
|
||||
label: "Format",
|
||||
type: "select",
|
||||
defaultValue: "png",
|
||||
options: [
|
||||
{ value: "png", label: "PNG" },
|
||||
{ value: "jpg", label: "JPEG" },
|
||||
{ value: "webp", label: "WebP" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
"gif-tools": [
|
||||
{ key: "width", label: "Width (px)", type: "number", min: 1, max: 4096, placeholder: "Auto" },
|
||||
{ key: "height", label: "Height (px)", type: "number", min: 1, max: 4096, placeholder: "Auto" },
|
||||
{
|
||||
key: "extractFrame",
|
||||
label: "Extract frame #",
|
||||
type: "number",
|
||||
min: 0,
|
||||
placeholder: "All frames",
|
||||
},
|
||||
{ key: "optimize", label: "Optimize", type: "boolean", defaultValue: false },
|
||||
],
|
||||
|
||||
"image-to-pdf": [
|
||||
{
|
||||
key: "pageSize",
|
||||
label: "Page size",
|
||||
type: "select",
|
||||
defaultValue: "A4",
|
||||
options: [
|
||||
{ value: "A4", label: "A4" },
|
||||
{ value: "Letter", label: "Letter" },
|
||||
{ value: "A3", label: "A3" },
|
||||
{ value: "A5", label: "A5" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "orientation",
|
||||
label: "Orientation",
|
||||
type: "select",
|
||||
defaultValue: "portrait",
|
||||
options: [
|
||||
{ value: "portrait", label: "Portrait" },
|
||||
{ value: "landscape", label: "Landscape" },
|
||||
],
|
||||
},
|
||||
{ key: "margin", label: "Margin (mm)", type: "number", min: 0, max: 100, defaultValue: 20 },
|
||||
],
|
||||
|
||||
"bulk-rename": [
|
||||
{
|
||||
key: "pattern",
|
||||
label: "Pattern",
|
||||
type: "text",
|
||||
placeholder: "image-{{index}}",
|
||||
defaultValue: "image-{{index}}",
|
||||
},
|
||||
{ key: "startIndex", label: "Start index", type: "number", min: 0, defaultValue: 1 },
|
||||
],
|
||||
|
||||
ocr: [
|
||||
{
|
||||
key: "engine",
|
||||
label: "Engine",
|
||||
type: "select",
|
||||
defaultValue: "tesseract",
|
||||
options: [
|
||||
{ value: "tesseract", label: "Tesseract" },
|
||||
{ value: "paddleocr", label: "PaddleOCR" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "language",
|
||||
label: "Language",
|
||||
type: "select",
|
||||
defaultValue: "en",
|
||||
options: [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "de", label: "German" },
|
||||
{ value: "fr", label: "French" },
|
||||
{ value: "es", label: "Spanish" },
|
||||
{ value: "zh", label: "Chinese" },
|
||||
{ value: "ja", label: "Japanese" },
|
||||
{ value: "ko", label: "Korean" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
"qr-generate": [
|
||||
{ key: "text", label: "Content", type: "text", placeholder: "URL or text" },
|
||||
{ key: "size", label: "Size (px)", type: "number", min: 100, max: 2000, defaultValue: 400 },
|
||||
{
|
||||
key: "errorCorrection",
|
||||
label: "Error correction",
|
||||
type: "select",
|
||||
defaultValue: "M",
|
||||
options: [
|
||||
{ value: "L", label: "Low (7%)" },
|
||||
{ value: "M", label: "Medium (15%)" },
|
||||
{ value: "Q", label: "Quartile (25%)" },
|
||||
{ value: "H", label: "High (30%)" },
|
||||
],
|
||||
},
|
||||
{ key: "foreground", label: "Foreground", type: "color", defaultValue: "#000000" },
|
||||
{ key: "background", label: "Background", type: "color", defaultValue: "#FFFFFF" },
|
||||
],
|
||||
|
||||
// remove-background uses its own shared controls component (DRY)
|
||||
favicon: [],
|
||||
"color-palette": [],
|
||||
"barcode-read": [],
|
||||
info: [],
|
||||
"erase-object": [],
|
||||
};
|
||||
const COLOR_TOOL_IDS = new Set([
|
||||
"brightness-contrast",
|
||||
"saturation",
|
||||
"color-channels",
|
||||
"color-effects",
|
||||
]);
|
||||
|
||||
interface PipelineStepSettingsProps {
|
||||
toolId: string;
|
||||
@@ -496,179 +29,27 @@ interface PipelineStepSettingsProps {
|
||||
}
|
||||
|
||||
export function PipelineStepSettings({ toolId, settings, onChange }: PipelineStepSettingsProps) {
|
||||
// Tools with their own shared controls component (DRY - same UI as standalone page)
|
||||
if (toolId === "remove-background") {
|
||||
if (toolId === "resize") return <ResizeControls onChange={onChange} />;
|
||||
if (toolId === "crop") return <CropControls onChange={onChange} />;
|
||||
if (toolId === "rotate") return <RotateControls onChange={onChange} />;
|
||||
if (toolId === "convert") return <ConvertControls onChange={onChange} />;
|
||||
if (toolId === "compress") return <CompressControls onChange={onChange} />;
|
||||
if (toolId === "strip-metadata") return <StripMetadataControls onChange={onChange} />;
|
||||
if (toolId === "border") return <BorderControls onChange={onChange} />;
|
||||
if (toolId === "watermark-text") return <WatermarkTextControls onChange={onChange} />;
|
||||
if (toolId === "text-overlay") return <TextOverlayControls onChange={onChange} />;
|
||||
if (toolId === "replace-color") return <ReplaceColorControls onChange={onChange} />;
|
||||
if (toolId === "smart-crop") return <SmartCropControls onChange={onChange} />;
|
||||
if (toolId === "gif-tools") return <GifToolsControls onChange={onChange} />;
|
||||
if (toolId === "upscale") return <UpscaleControls onChange={onChange} />;
|
||||
if (toolId === "blur-faces") return <BlurFacesControls onChange={onChange} />;
|
||||
if (toolId === "remove-background")
|
||||
return <RemoveBgControls settings={settings} onChange={onChange} />;
|
||||
}
|
||||
|
||||
const fields = TOOL_FIELDS[toolId];
|
||||
|
||||
if (!fields || fields.length === 0) {
|
||||
return (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
No configurable settings. Defaults will be used.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const updateField = (key: string, value: unknown) => {
|
||||
const next = { ...settings };
|
||||
if (value === undefined || value === "") {
|
||||
delete next[key];
|
||||
} else {
|
||||
next[key] = value;
|
||||
}
|
||||
onChange(next);
|
||||
};
|
||||
|
||||
const visibleFields = fields.filter((f) => !f.showWhen || f.showWhen(settings));
|
||||
if (COLOR_TOOL_IDS.has(toolId)) return <ColorControls toolId={toolId} onChange={onChange} />;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{visibleFields.map((field) => {
|
||||
const raw = settings[field.key];
|
||||
const value = raw !== undefined ? raw : field.defaultValue;
|
||||
|
||||
switch (field.type) {
|
||||
case "number":
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<label htmlFor={`pipeline-${field.key}`} className="text-xs text-muted-foreground">
|
||||
{field.label}
|
||||
</label>
|
||||
<input
|
||||
id={`pipeline-${field.key}`}
|
||||
type="number"
|
||||
value={value != null && value !== "" ? Number(value) : ""}
|
||||
onChange={(e) =>
|
||||
updateField(
|
||||
field.key,
|
||||
e.target.value === "" ? undefined : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
placeholder={field.placeholder}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "text":
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<label htmlFor={`pipeline-${field.key}`} className="text-xs text-muted-foreground">
|
||||
{field.label}
|
||||
</label>
|
||||
<input
|
||||
id={`pipeline-${field.key}`}
|
||||
type="text"
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => updateField(field.key, e.target.value || undefined)}
|
||||
placeholder={field.placeholder}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "select": {
|
||||
const opts = field.options ?? [];
|
||||
// Use button group for <= 4 options, dropdown for more
|
||||
if (opts.length <= 4) {
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<p className="text-xs text-muted-foreground">{field.label}</p>
|
||||
<div className="flex gap-1 mt-0.5">
|
||||
{opts.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => updateField(field.key, opt.value)}
|
||||
className={`flex-1 text-xs py-1.5 rounded transition-colors ${
|
||||
String(value) === opt.value
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<label htmlFor={`pipeline-${field.key}`} className="text-xs text-muted-foreground">
|
||||
{field.label}
|
||||
</label>
|
||||
<select
|
||||
id={`pipeline-${field.key}`}
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => updateField(field.key, e.target.value)}
|
||||
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
{!value && <option value="">Select...</option>}
|
||||
{opts.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case "boolean":
|
||||
return (
|
||||
<label
|
||||
key={field.key}
|
||||
className="flex items-center gap-2 text-sm text-foreground cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(value)}
|
||||
onChange={(e) => updateField(field.key, e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
{field.label}
|
||||
</label>
|
||||
);
|
||||
|
||||
case "color":
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<label
|
||||
htmlFor={`pipeline-${field.key}-text`}
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
{field.label}
|
||||
</label>
|
||||
<div className="flex gap-2 mt-0.5">
|
||||
<input
|
||||
id={`pipeline-${field.key}-picker`}
|
||||
type="color"
|
||||
value={String(value || "#000000").slice(0, 7)}
|
||||
onChange={(e) => updateField(field.key, e.target.value)}
|
||||
className="h-8 w-8 rounded border border-border cursor-pointer bg-background"
|
||||
/>
|
||||
<input
|
||||
id={`pipeline-${field.key}-text`}
|
||||
type="text"
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => updateField(field.key, e.target.value || undefined)}
|
||||
placeholder="#000000"
|
||||
className="flex-1 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
No configurable settings. Defaults will be used.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user