default column layout and add field sizing

This commit is contained in:
Josh Hawkins 2026-02-12 11:50:09 -06:00
parent 3042c36168
commit d3924e2cff
28 changed files with 467 additions and 84 deletions

View File

@ -147,6 +147,35 @@ const applyUiSchemaPathOverrides = (
return updated;
};
const applyLayoutGridFieldDefaults = (uiSchema: UiSchema): UiSchema => {
const applyDefaults = (node: unknown): unknown => {
if (Array.isArray(node)) {
return node.map((item) => applyDefaults(item));
}
if (typeof node !== "object" || node === null) {
return node;
}
const nextNode: Record<string, unknown> = {};
Object.entries(node).forEach(([key, value]) => {
nextNode[key] = applyDefaults(value);
});
if (
Array.isArray(nextNode["ui:layoutGrid"]) &&
nextNode["ui:field"] === undefined
) {
nextNode["ui:field"] = "LayoutGridField";
}
return nextNode;
};
return applyDefaults(uiSchema) as UiSchema;
};
export interface ConfigFormProps {
/** JSON Schema for the form */
schema: RJSFSchema;
@ -245,7 +274,9 @@ export function ConfigForm({
transformedSchema,
pathOverrides,
);
const merged = mergeUiSchema(expandedUiSchema, baseUiSchema);
const merged = applyLayoutGridFieldDefaults(
mergeUiSchema(expandedUiSchema, baseUiSchema),
);
// Add field groups
if (fieldGroups) {
@ -311,7 +342,7 @@ export function ConfigForm({
);
return (
<div className={cn("config-form", className)}>
<div className={cn("config-form w-full max-w-5xl", className)}>
<Form
schema={transformedSchema}
uiSchema={finalUiSchema}

View File

@ -32,6 +32,12 @@ const auth: SectionConfigOverrides = {
reset_admin_password: {
"ui:widget": "switch",
},
native_oauth_url: {
"ui:options": { size: "lg" },
},
failed_login_rate_limit: {
"ui:options": { size: "md" },
},
},
},
};

View File

@ -6,6 +6,11 @@ const database: SectionConfigOverrides = {
restartRequired: [],
fieldOrder: ["path"],
advancedFields: [],
uiSchema: {
path: {
"ui:options": { size: "md" },
},
},
},
};

View File

@ -3,6 +3,19 @@ import type { SectionConfigOverrides } from "./types";
const detect: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/camera_specific",
fieldOrder: [
"enabled",
"width",
"height",
"fps",
"min_initialized",
"max_disappeared",
"annotation_offset",
"stationary",
"interval",
"threshold",
"max_frames",
],
restartRequired: [
"width",
"height",
@ -12,7 +25,7 @@ const detect: SectionConfigOverrides = {
"stationary",
],
fieldGroups: {
resolution: ["width", "height", "fps"],
resolution: ["enabled", "width", "height", "fps"],
tracking: ["min_initialized", "max_disappeared"],
},
hiddenFields: ["enabled_in_config"],
@ -22,46 +35,6 @@ const detect: SectionConfigOverrides = {
"annotation_offset",
"stationary",
],
uiSchema: {
"ui:field": "LayoutGridField",
"ui:layoutGrid": [
{
"ui:row": [{ enabled: { "ui:col": "col-span-12 lg:col-span-4" } }],
},
{
"ui:row": [
{ width: { "ui:col": "col-span-12 md:col-span-4" } },
{ height: { "ui:col": "col-span-12 md:col-span-4" } },
{ fps: { "ui:col": "col-span-12 md:col-span-4" } },
],
},
{
"ui:row": [
{ min_initialized: { "ui:col": "col-span-12 md:col-span-3" } },
{ max_disappeared: { "ui:col": "col-span-12 md:col-span-3" } },
],
},
{
"ui:row": [
{ annotation_offset: { "ui:col": "col-span-12 md:col-span-3" } },
],
},
{
"ui:row": [{ stationary: { "ui:col": "col-span-12" } }],
},
],
stationary: {
"ui:field": "LayoutGridField",
"ui:layoutGrid": [
{
"ui:row": [
{ interval: { "ui:col": "col-span-12 md:col-span-3" } },
{ threshold: { "ui:col": "col-span-12 md:col-span-3" } },
],
},
],
},
},
},
};

View File

@ -6,6 +6,11 @@ const environmentVars: SectionConfigOverrides = {
restartRequired: [],
fieldOrder: [],
advancedFields: [],
uiSchema: {
additionalProperties: {
"ui:options": { size: "lg" },
},
},
},
};

View File

@ -58,6 +58,9 @@ const ffmpeg: SectionConfigOverrides = {
"gpu",
],
uiSchema: {
path: {
"ui:options": { size: "md" },
},
global_args: arrayAsTextWidget,
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
input_args: ffmpegArgsWidget("input_args"),
@ -71,6 +74,9 @@ const ffmpeg: SectionConfigOverrides = {
},
inputs: {
items: {
path: {
"ui:options": { size: "full" },
},
global_args: arrayAsTextWidget,
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
input_args: ffmpegArgsWidget("input_args"),
@ -104,6 +110,9 @@ const ffmpeg: SectionConfigOverrides = {
"gpu",
],
uiSchema: {
path: {
"ui:options": { size: "md" },
},
global_args: arrayAsTextWidget,
hwaccel_args: ffmpegArgsWidget("hwaccel_args"),
input_args: ffmpegArgsWidget("input_args"),

View File

@ -14,6 +14,27 @@ const genai: SectionConfigOverrides = {
],
advancedFields: ["base_url", "provider_options", "runtime_options"],
hiddenFields: ["genai.enabled_in_config"],
uiSchema: {
api_key: {
"ui:options": { size: "md" },
},
base_url: {
"ui:options": { size: "lg" },
},
model: {
"ui:options": { size: "md" },
},
provider_options: {
additionalProperties: {
"ui:options": { size: "lg" },
},
},
runtime_options: {
additionalProperties: {
"ui:options": { size: "lg" },
},
},
},
},
};

View File

@ -41,6 +41,9 @@ const lpr: SectionConfigOverrides = {
"replace_rules",
],
uiSchema: {
format: {
"ui:options": { size: "md" },
},
replace_rules: {
"ui:field": "ReplaceRulesField",
"ui:options": {

View File

@ -28,6 +28,14 @@ const model: SectionConfigOverrides = {
"non_logo_attributes",
"plus",
],
uiSchema: {
path: {
"ui:options": { size: "md" },
},
labelmap_path: {
"ui:options": { size: "md" },
},
},
},
};

View File

@ -47,7 +47,26 @@ const mqtt: SectionConfigOverrides = {
"tls_insecure",
],
liveValidate: true,
uiSchema: {},
uiSchema: {
host: {
"ui:options": { size: "sm" },
},
topic_prefix: {
"ui:options": { size: "md" },
},
client_id: {
"ui:options": { size: "sm" },
},
tls_ca_certs: {
"ui:options": { size: "md" },
},
tls_client_cert: {
"ui:options": { size: "md" },
},
tls_client_key: {
"ui:options": { size: "md" },
},
},
},
};

View File

@ -14,11 +14,13 @@ const networking: SectionConfigOverrides = {
"listen.internal": {
"ui:options": {
suppressMultiSchema: true,
size: "sm",
},
},
"listen.external": {
"ui:options": {
suppressMultiSchema: true,
size: "sm",
},
},
},

View File

@ -59,6 +59,16 @@ const objects: SectionConfigOverrides = {
},
prompt: {
"ui:widget": "textarea",
"ui:options": {
size: "full",
},
},
object_prompts: {
additionalProperties: {
"ui:options": {
size: "full",
},
},
},
required_zones: {
"ui:widget": "zoneNames",

View File

@ -20,6 +20,9 @@ const onvif: SectionConfigOverrides = {
advancedFields: ["tls_insecure", "ignore_time_mismatch"],
overrideFields: [],
uiSchema: {
host: {
"ui:options": { size: "sm" },
},
autotracking: {
required_zones: {
"ui:widget": "zoneNames",

View File

@ -14,6 +14,12 @@ const proxy: SectionConfigOverrides = {
advancedFields: ["header_map", "auth_secret", "separator"],
liveValidate: true,
uiSchema: {
logout_url: {
"ui:options": { size: "lg" },
},
auth_secret: {
"ui:options": { size: "md" },
},
header_map: {
"ui:after": { render: "ProxyRoleMap" },
},

View File

@ -20,6 +20,13 @@ const record: SectionConfigOverrides = {
},
hiddenFields: ["enabled_in_config", "sync_recordings"],
advancedFields: ["expire_interval", "preview", "export"],
uiSchema: {
export: {
hwaccel_args: {
"ui:options": { size: "lg" },
},
},
},
},
};

View File

@ -29,9 +29,15 @@ const review: SectionConfigOverrides = {
genai: {
additional_concerns: {
"ui:widget": "textarea",
"ui:options": {
size: "full",
},
},
activity_context_prompt: {
"ui:widget": "textarea",
"ui:options": {
size: "full",
},
},
},
},

View File

@ -6,6 +6,14 @@ const tls: SectionConfigOverrides = {
restartRequired: [],
fieldOrder: ["enabled", "cert", "key"],
advancedFields: [],
uiSchema: {
cert: {
"ui:options": { size: "md" },
},
key: {
"ui:options": { size: "md" },
},
},
},
};

View File

@ -128,7 +128,7 @@ export function AdvancedCollapsible({
{label}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-2">
<CollapsibleContent className="mt-2 space-y-4 rounded-lg border border-border/60 bg-muted/20 p-4">
{children}
</CollapsibleContent>
</Collapsible>

View File

@ -114,6 +114,13 @@ interface PropertyElement {
content: React.ReactElement;
}
function isObjectLikeElement(item: PropertyElement) {
const fieldSchema = item.content.props?.schema as
| { type?: string | string[] }
| undefined;
return fieldSchema?.type === "object";
}
// Custom ObjectFieldTemplate wrapper that applies grid layout
function GridLayoutObjectFieldTemplate(
props: ObjectFieldTemplateProps,
@ -361,9 +368,16 @@ function GridLayoutObjectFieldTemplate(
}
return (
<div key={rowIndex} className="space-y-4">
<div
key={rowIndex}
className={cn(
"space-y-4",
rowGroupKey &&
"rounded-lg border border-border/70 bg-card/30 p-4",
)}
>
{showGroupLabel && (
<div className="text-md font-medium text-primary">
<div className="border-b border-border/60 pb-2 text-sm font-semibold text-primary-variant">
{getGroupLabel(rowGroupKey)}
</div>
)}
@ -404,9 +418,12 @@ function GridLayoutObjectFieldTemplate(
}
leftoverSections.push(
<div key={groupKey} className="space-y-6">
<div
key={groupKey}
className="space-y-4 rounded-lg border border-border/70 bg-card/30 p-4"
>
{showGroupLabel && (
<div className="text-md font-medium text-primary">
<div className="border-b border-border/60 pb-2 text-sm font-semibold text-primary-variant">
{getGroupLabel(groupKey)}
</div>
)}
@ -429,7 +446,16 @@ function GridLayoutObjectFieldTemplate(
)}
>
{ungroupedLeftovers.map((item) => (
<div key={item.name}>{item.content}</div>
<div
key={item.name}
className={cn(
groupedLeftovers.size > 0 &&
!isObjectLikeElement(item) &&
"px-4",
)}
>
{item.content}
</div>
))}
</div>,
);

View File

@ -1,6 +1,7 @@
// Base Input Template - default input wrapper
import type { WidgetProps } from "@rjsf/utils";
import { Input } from "@/components/ui/input";
import { getSizedFieldClassName } from "../utils";
export function BaseInputTemplate(props: WidgetProps) {
const {
@ -14,9 +15,11 @@ export function BaseInputTemplate(props: WidgetProps) {
onFocus,
placeholder,
schema,
options,
} = props;
const inputType = type || "text";
const fieldClassName = getSizedFieldClassName(options, "xs");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
@ -32,6 +35,7 @@ export function BaseInputTemplate(props: WidgetProps) {
<Input
id={id}
type={inputType}
className={fieldClassName}
value={value ?? ""}
disabled={disabled || readonly}
placeholder={placeholder || ""}

View File

@ -101,7 +101,6 @@ export function FieldTemplate(props: FieldTemplateProps) {
const suppressDescription = uiOptionsFromSchema.suppressDescription === true;
// Determine field characteristics
const isAdvanced = uiOptionsFromSchema.advanced === true;
const isBoolean =
schema.type === "boolean" ||
(Array.isArray(schema.type) && schema.type.includes("boolean"));
@ -111,11 +110,33 @@ export function FieldTemplate(props: FieldTemplateProps) {
const suppressMultiSchema =
(uiSchema?.["ui:options"] as UiSchema["ui:options"] | undefined)
?.suppressMultiSchema === true;
const schemaTypes = Array.isArray(schema.type)
? schema.type
: schema.type
? [schema.type]
: [];
const nonNullSchemaTypes = schemaTypes.filter((type) => type !== "null");
const isScalarValueField =
nonNullSchemaTypes.length === 1 &&
["string", "number", "integer"].includes(nonNullSchemaTypes[0]);
// Only suppress labels/descriptions if this is a multi-schema field (anyOf/oneOf) with suppressMultiSchema flag
// This prevents duplicate labels while still showing the inner field's label
const isMultiSchemaWrapper =
(schema.anyOf || schema.oneOf) && (suppressMultiSchema || isNullableUnion);
const useSplitBooleanLayout =
uiOptionsFromSchema.splitLayout !== false &&
isBoolean &&
!isMultiSchemaWrapper &&
!isObjectField &&
!isAdditionalProperty;
const useSplitLayout =
uiOptionsFromSchema.splitLayout !== false &&
isScalarValueField &&
!isBoolean &&
!isMultiSchemaWrapper &&
!isObjectField &&
!isAdditionalProperty;
// Get translation path for this field
const pathSegments = fieldPathId.path.filter(
@ -379,17 +400,11 @@ export function FieldTemplate(props: FieldTemplateProps) {
>
<div className="flex flex-col space-y-6">
{beforeContent}
<div
className={cn(
"space-y-1",
isAdvanced && "border-l-2 border-muted pl-4",
isBoolean && "flex items-center justify-between gap-4",
)}
data-field-id={translationPath}
>
<div className={cn("space-y-1")} data-field-id={translationPath}>
{displayLabel &&
finalLabel &&
!isBoolean &&
!useSplitLayout &&
!isMultiSchemaWrapper &&
!isObjectField &&
!isAdditionalProperty && (
@ -409,29 +424,180 @@ export function FieldTemplate(props: FieldTemplateProps) {
)}
{isBoolean ? (
<div className="flex w-full items-center justify-between gap-4">
<div className="space-y-0.5">
{displayLabel && finalLabel && (
<Label
htmlFor={id}
className={cn(
"text-sm font-medium",
isModified && "text-danger",
useSplitBooleanLayout ? (
<>
<div className="space-y-1.5 md:hidden">
<div className="flex items-center justify-between gap-4">
{displayLabel && finalLabel && (
<Label
htmlFor={id}
className={cn(
"text-sm font-medium",
isModified && "text-danger",
)}
>
{finalLabel}
{required && (
<span className="ml-1 text-destructive">*</span>
)}
</Label>
)}
>
{finalLabel}
{required && (
<span className="ml-1 text-destructive">*</span>
<div className="flex items-center gap-2">{children}</div>
</div>
{finalDescription && shouldShowDescription && (
<p className="text-xs text-muted-foreground">
{finalDescription}
</p>
)}
{fieldDocsUrl && shouldShowDescription && (
<div className="flex items-center text-xs text-primary-variant">
<Link
to={fieldDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
)}
</div>
<div className="hidden md:grid md:grid-cols-[minmax(14rem,22rem)_minmax(0,1fr)] md:items-start md:gap-x-6">
<div className="space-y-0.5">
{displayLabel && finalLabel && (
<Label
htmlFor={id}
className={cn(
"text-sm font-medium",
isModified && "text-danger",
)}
>
{finalLabel}
{required && (
<span className="ml-1 text-destructive">*</span>
)}
</Label>
)}
</Label>
)}
{finalDescription && shouldShowDescription && (
<p className="text-xs text-muted-foreground">
{finalDescription}
</p>
)}
{fieldDocsUrl && shouldShowDescription && (
<div className="flex items-center text-xs text-primary-variant">
<Link
to={fieldDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
)}
</div>
<div className="w-full max-w-2xl">
<div className="flex items-center gap-2">{children}</div>
</div>
</div>
</>
) : (
<div className="flex w-full items-center justify-between gap-4">
<div className="space-y-0.5">
{displayLabel && finalLabel && (
<Label
htmlFor={id}
className={cn(
"text-sm font-medium",
isModified && "text-danger",
)}
>
{finalLabel}
{required && (
<span className="ml-1 text-destructive">*</span>
)}
</Label>
)}
{finalDescription && shouldShowDescription && (
<p className="text-xs text-muted-foreground">
{finalDescription}
</p>
)}
{fieldDocsUrl && shouldShowDescription && (
<div className="flex items-center text-xs text-primary-variant">
<Link
to={fieldDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
)}
</div>
<div className="flex items-center gap-2">{children}</div>
</div>
)
) : useSplitLayout ? (
<div className="space-y-3 md:grid md:grid-cols-[minmax(14rem,22rem)_minmax(0,1fr)] md:items-start md:gap-x-6 md:space-y-0">
<div className="space-y-1.5">
{displayLabel &&
finalLabel &&
!isMultiSchemaWrapper &&
!isObjectField &&
!isAdditionalProperty && (
<Label
htmlFor={id}
className={cn(
"text-sm font-medium",
isModified && "text-danger",
errors &&
errors.props?.errors?.length > 0 &&
"text-destructive",
)}
>
{finalLabel}
{required && (
<span className="ml-1 text-destructive">*</span>
)}
</Label>
)}
{finalDescription && shouldShowDescription && (
<p className="text-xs text-muted-foreground">
<p className="hidden text-xs text-muted-foreground md:block">
{finalDescription}
</p>
)}
{fieldDocsUrl && shouldShowDescription && (
<div className="flex items-center text-xs text-primary-variant">
<div className="hidden items-center text-xs text-primary-variant md:flex">
<Link
to={fieldDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
)}
</div>
<div className="w-full max-w-2xl space-y-1">
{children}
{finalDescription && shouldShowDescription && (
<p className="text-xs text-muted-foreground md:hidden">
{finalDescription}
</p>
)}
{fieldDocsUrl && shouldShowDescription && (
<div className="flex items-center text-xs text-primary-variant md:hidden">
<Link
to={fieldDocsUrl}
target="_blank"
@ -444,7 +610,6 @@ export function FieldTemplate(props: FieldTemplateProps) {
</div>
)}
</div>
<div className="flex items-center gap-2">{children}</div>
</div>
) : (
<>

View File

@ -323,12 +323,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
}>;
const ungrouped = items.filter((item) => !grouped.has(item.name));
const isObjectLikeField = (item: (typeof properties)[number]) => {
const fieldSchema = item.content.props.schema as
| { type?: string | string[] }
| undefined;
return fieldSchema?.type === "object";
};
return (
<div className="space-y-6">
{groups.map((group) => (
<div key={group.key} className="space-y-6">
<div className="text-md font-medium text-primary">
<div
key={group.key}
className="space-y-4 rounded-lg border border-border/70 bg-card/30 p-4"
>
<div className="text-md border-b border-border/60 pb-4 font-semibold text-primary-variant">
{group.label}
</div>
<div className="space-y-6">
@ -342,7 +351,14 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
{ungrouped.length > 0 && (
<div className={cn("space-y-6", groups.length > 0 && "pt-2")}>
{ungrouped.map((element) => (
<div key={element.name}>{element.content}</div>
<div
key={element.name}
className={cn(
groups.length > 0 && !isObjectLikeField(element) && "px-4",
)}
>
{element.content}
</div>
))}
</div>
)}

View File

@ -0,0 +1,37 @@
import { cn } from "@/lib/utils";
const FIELD_SIZE_CLASS_MAP = {
xs: "max-w-xs",
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-2xl",
full: "max-w-full",
} as const;
export type FieldSizeOption = keyof typeof FIELD_SIZE_CLASS_MAP;
type FieldSizingOptions = {
size?: FieldSizeOption;
maxWidthClassName?: string;
className?: string;
};
export function getSizedFieldClassName(
options: unknown,
defaultSize: FieldSizeOption = "lg",
) {
const sizingOptions =
typeof options === "object" && options !== null
? (options as FieldSizingOptions)
: undefined;
const sizeClass =
FIELD_SIZE_CLASS_MAP[sizingOptions?.size ?? defaultSize] ??
FIELD_SIZE_CLASS_MAP[defaultSize];
return cn(
"w-full",
sizingOptions?.maxWidthClassName ?? sizeClass,
sizingOptions?.className,
);
}

View File

@ -15,3 +15,4 @@ export {
normalizeFieldValue,
isSubtreeModified,
} from "./overrides";
export { getSizedFieldClassName } from "./fieldSizing";

View File

@ -4,6 +4,8 @@ import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { LuEye, LuEyeOff } from "react-icons/lu";
import { cn } from "@/lib/utils";
import { getSizedFieldClassName } from "../utils";
export function PasswordWidget(props: WidgetProps) {
const {
@ -16,12 +18,14 @@ export function PasswordWidget(props: WidgetProps) {
onFocus,
placeholder,
schema,
options,
} = props;
const [showPassword, setShowPassword] = useState(false);
const fieldClassName = getSizedFieldClassName(options, "sm");
return (
<div className="relative">
<div className={cn("relative", fieldClassName)}>
<Input
id={id}
type={showPassword ? "text" : "password"}
@ -34,7 +38,7 @@ export function PasswordWidget(props: WidgetProps) {
onBlur={(e) => onBlur(id, e.target.value)}
onFocus={(e) => onFocus(id, e.target.value)}
aria-label={schema.title}
className="pr-10"
className="w-full pr-10"
/>
<Button
type="button"

View File

@ -7,6 +7,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getSizedFieldClassName } from "../utils";
export function SelectWidget(props: WidgetProps) {
const {
@ -21,6 +22,7 @@ export function SelectWidget(props: WidgetProps) {
} = props;
const { enumOptions = [] } = options;
const fieldClassName = getSizedFieldClassName(options, "sm");
return (
<Select
@ -34,7 +36,7 @@ export function SelectWidget(props: WidgetProps) {
}}
disabled={disabled || readonly}
>
<SelectTrigger id={id} className="w-full">
<SelectTrigger id={id} className={fieldClassName}>
<SelectValue placeholder={placeholder || schema.title || "Select..."} />
</SelectTrigger>
<SelectContent>

View File

@ -1,6 +1,8 @@
// Text Widget - maps to shadcn/ui Input
import type { WidgetProps } from "@rjsf/utils";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { getSizedFieldClassName } from "../utils";
export function TextWidget(props: WidgetProps) {
const {
@ -19,11 +21,12 @@ export function TextWidget(props: WidgetProps) {
const isNullable = Array.isArray(schema.type)
? schema.type.includes("null")
: false;
const fieldClassName = getSizedFieldClassName(options, "xs");
return (
<Input
id={id}
className="text-md"
className={cn("text-md", fieldClassName)}
type="text"
value={value ?? ""}
disabled={disabled || readonly}

View File

@ -1,6 +1,8 @@
// Textarea Widget - maps to shadcn/ui Textarea
import type { WidgetProps } from "@rjsf/utils";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { getSizedFieldClassName } from "../utils";
export function TextareaWidget(props: WidgetProps) {
const {
@ -19,11 +21,12 @@ export function TextareaWidget(props: WidgetProps) {
const isNullable = Array.isArray(schema.type)
? schema.type.includes("null")
: false;
const fieldClassName = getSizedFieldClassName(options, "md");
return (
<Textarea
id={id}
className="text-md"
className={cn("text-md", fieldClassName)}
value={value ?? ""}
disabled={disabled || readonly}
placeholder={placeholder || (options.placeholder as string) || ""}