{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "auto-form-core", "title": "Protoform AutoForm core", "description": "Engine-neutral shadcn-compatible AutoForm renderer shared by native form-library adapters.", "dependencies": [ "@base-ui/react", "@buf/googleapis_googleapis.bufbuild_es", "@bufbuild/cel", "@bufbuild/protobuf", "@bufbuild/protovalidate", "@connectrpc/connect", "@standard-schema/spec", "@tanstack/react-router", "class-variance-authority", "clsx", "cmdk", "date-fns", "lucide-react", "motion", "prismjs", "react-day-picker", "react-simple-code-editor", "sonner", "tailwind-merge", "zod" ], "registryDependencies": [ "@protoform/protoform-core", "@protoform/protobuf-provider" ], "files": [ { "path": "registry/base-nova/protoform/components/alert/index.tsx", "content": "import { cva, type VariantProps } from 'class-variance-authority';\nimport { InfoIcon } from 'lucide-react';\nimport React from 'react';\n\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nconst alertVariants = cva(\n 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',\n {\n variants: {\n variant: {\n info: 'bg-card text-card-foreground',\n destructive:\n '!border-destructive/20 bg-destructive/10 text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>href]:text-current [&>svg]:text-current',\n warning:\n '!border-blue-200 bg-blue-50 text-blue-800 *:data-[slot=alert-description]:text-blue-800 [&>href]:text-current [&>svg]:text-current',\n success:\n '!border-green-200 dark:!border-green-800/40 bg-green-50 text-green-800 *:data-[slot=alert-description]:text-green-800 dark:bg-green-950/30 dark:text-green-300 dark:*:data-[slot=alert-description]:text-green-300 [&>href]:text-current [&>svg]:text-current',\n },\n },\n defaultVariants: {\n variant: 'info',\n },\n }\n);\n\nconst Alert = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<'div'> & VariantProps & SharedProps & { icon?: React.ReactNode }\n>(({ className, variant, testId, icon = , children, ...props }, ref) => (\n \n {icon}\n {children}\n \n));\n\nAlert.displayName = 'Alert';\n\nconst AlertTitle = React.forwardRef & SharedProps>(\n ({ className, testId, ...props }, ref) => (\n \n )\n);\n\nAlertTitle.displayName = 'AlertTitle';\n\nconst AlertDescription = React.forwardRef & SharedProps>(\n ({ className, testId, ...props }, ref) => (\n \n )\n);\n\nAlertDescription.displayName = 'AlertDescription';\n\nexport { Alert, AlertDescription, AlertTitle };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/configuration.ts", "content": "import type { ParsedField } from './core-types';\nimport type { DataProviderRegistry } from './data-providers';\nimport { AutoFormFieldComponentRegistry, defaultRegistry } from './fields';\nimport { readDataProviderId } from './fields/shared';\nimport { getFieldUiConfig, resolveRenderFieldType } from './helpers';\nimport { buildFieldMatchContext, type FieldTypeRegistry } from './registry';\nimport {\n mergeFieldOverrides,\n protoConversionOptionsFromFieldConfig,\n resolveSchema,\n} from './schema';\nimport type { AutoFormSchemaInput, FieldConfigMap } from './types';\n\nexport type AutoFormConfigurationDiagnosticCode =\n | 'invalid-configuration-path'\n | 'missing-data-provider'\n | 'missing-renderer'\n | 'unsupported-configuration';\n\nexport interface AutoFormConfigurationDiagnostic {\n code: AutoFormConfigurationDiagnosticCode;\n message: string;\n path: string;\n severity: 'error';\n}\n\nexport interface InspectAutoFormConfigurationInput<\n T extends Record = Record,\n TCustomFieldType extends string = never,\n> {\n schema: AutoFormSchemaInput;\n fieldConfig?: FieldConfigMap;\n fieldRegistry?: FieldTypeRegistry;\n dataProviders?: DataProviderRegistry;\n}\n\nconst STRUCTURAL_FIELD_TYPES = new Set(['array', 'map', 'object', 'oneof']);\n\nfunction collectFields(\n fields: ParsedField[],\n path: readonly string[] = []\n): Array<{ field: ParsedField; path: string }> {\n return fields.flatMap((field) => {\n const fieldPath = [...path, field.key];\n return [\n { field, path: fieldPath.join('.') },\n ...collectFields(field.schema ?? [], fieldPath),\n ];\n });\n}\n\nfunction resolvedRenderer(\n field: ParsedField,\n registry: FieldTypeRegistry\n): string {\n const configured = getFieldUiConfig(field).control;\n if (configured) {\n return configured;\n }\n return (\n registry.resolve(field, buildFieldMatchContext(field))?.name ??\n resolveRenderFieldType(field)\n );\n}\n\nfunction hasRenderer(\n field: ParsedField,\n renderer: string,\n registry: FieldTypeRegistry\n): boolean {\n if (STRUCTURAL_FIELD_TYPES.has(field.type) && renderer === field.type) {\n return true;\n }\n if (\n renderer !== 'fallback' &&\n Object.hasOwn(AutoFormFieldComponentRegistry, renderer)\n ) {\n return true;\n }\n return registry.list().some((definition) => definition.name === renderer);\n}\n\nfunction fieldDiagnostics(\n field: ParsedField,\n path: string,\n registry: FieldTypeRegistry,\n dataProviders: DataProviderRegistry | undefined\n): AutoFormConfigurationDiagnostic[] {\n const diagnostics: AutoFormConfigurationDiagnostic[] = [];\n const renderer = resolvedRenderer(field, registry);\n if (!hasRenderer(field, renderer, registry)) {\n diagnostics.push({\n code: 'missing-renderer',\n message: `Renderer \"${renderer}\" is not registered.`,\n path,\n severity: 'error',\n });\n }\n\n const dataProviderId = readDataProviderId(field);\n if (dataProviderId !== undefined) {\n if (!dataProviders?.[dataProviderId]) {\n diagnostics.push({\n code: 'missing-data-provider',\n message: `Data provider \"${dataProviderId}\" is not registered.`,\n path,\n severity: 'error',\n });\n }\n if (field.type !== 'string' && field.type !== 'number') {\n diagnostics.push({\n code: 'unsupported-configuration',\n message: 'Data providers are supported only on scalar string or number fields.',\n path,\n severity: 'error',\n });\n }\n }\n\n if (\n field.fieldConfig?.emptyRepeatedStringPolicy &&\n !(field.type === 'array' && field.schema?.[0]?.type === 'string')\n ) {\n diagnostics.push({\n code: 'unsupported-configuration',\n message:\n 'emptyRepeatedStringPolicy is supported only on repeated string fields.',\n path,\n severity: 'error',\n });\n }\n\n return diagnostics;\n}\n\nexport function inspectAutoFormConfiguration<\n T extends Record = Record,\n TCustomFieldType extends string = never,\n>({\n schema,\n fieldConfig,\n fieldRegistry,\n dataProviders,\n}: InspectAutoFormConfigurationInput<\n T,\n TCustomFieldType\n>): AutoFormConfigurationDiagnostic[] {\n const resolvedSchema = resolveSchema(\n schema,\n protoConversionOptionsFromFieldConfig(fieldConfig)\n );\n const fields = mergeFieldOverrides(resolvedSchema.parsedSchema.fields, fieldConfig);\n const flattenedFields = collectFields(fields);\n const validPaths = new Set(flattenedFields.map((entry) => entry.path));\n const activeRegistry = fieldRegistry ?? defaultRegistry;\n const diagnostics: AutoFormConfigurationDiagnostic[] = [];\n\n for (const path of Object.keys(fieldConfig ?? {})) {\n if (!validPaths.has(path)) {\n diagnostics.push({\n code: 'invalid-configuration-path',\n message: `Field configuration path \"${path}\" does not exist in the schema.`,\n path,\n severity: 'error',\n });\n }\n }\n\n for (const { field, path } of flattenedFields) {\n diagnostics.push(\n ...fieldDiagnostics(field, path, activeRegistry, dataProviders)\n );\n }\n\n return diagnostics.sort(\n (left, right) =>\n left.path.localeCompare(right.path) || left.code.localeCompare(right.code)\n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/context.tsx", "content": "import React from 'react';\n\nimport type { AutoFormFieldComponents, AutoFormUIComponents, ParsedField } from './core-types';\nimport type { DataProviderRegistry } from './data-providers';\nimport { getPathInObject } from './field-utils';\nimport type { FieldTypeRegistry } from './registry';\nimport type { AutoFormUiRule } from './types';\nimport type { DeprecatedFieldPolicy } from './types';\n\nexport type AutoFormContextValue = {\n uiComponents: AutoFormUIComponents;\n formComponents: AutoFormFieldComponents;\n formValues: Record;\n evaluateRules: (rules: AutoFormUiRule[] | undefined, fieldValue?: unknown) => boolean;\n getFieldUiConfig: (field: ParsedField) => Record;\n testIdPrefix: string;\n fieldRegistry?: FieldTypeRegistry;\n dataProviders?: DataProviderRegistry;\n deprecatedFields: DeprecatedFieldPolicy;\n};\n\nexport const AutoFormContext = React.createContext(null);\n\nexport function useAutoForm(): AutoFormContextValue {\n const context = React.useContext(AutoFormContext);\n if (!context) {\n throw new Error('useAutoForm must be used inside an AutoForm component.');\n }\n return context;\n}\n\nexport function useAutoFormField(path: string[]) {\n const { formValues, evaluateRules, getFieldUiConfig: getUiConfig, testIdPrefix } = useAutoForm();\n const fieldValue = getPathInObject(formValues, path);\n\n return {\n fieldValue,\n evaluateRules,\n getFieldUiConfig: getUiConfig,\n testIdPrefix,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Legacy aliases – kept during migration, will be removed once all consumers\n// switch to useAutoForm().\n// ---------------------------------------------------------------------------\n\nexport type InternalAutoFormRenderContextValue = Pick;\nexport type InternalAutoFormRuntimeContextValue = Pick<\n AutoFormContextValue,\n | 'deprecatedFields'\n | 'formValues'\n | 'evaluateRules'\n | 'fieldRegistry'\n | 'getFieldUiConfig'\n | 'testIdPrefix'\n>;\n\nexport const AutoFormRenderContext = AutoFormContext;\nexport const AutoFormRuntimeContext = AutoFormContext;\n\nexport function useAutoFormRenderContext(): InternalAutoFormRenderContextValue {\n return useAutoForm();\n}\n\nexport function useAutoFormRuntimeContext(): InternalAutoFormRuntimeContextValue {\n return useAutoForm();\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/core-types.ts", "content": "import type React from 'react';\nimport type { ReactNode } from 'react';\n\n// Re-export schema contract types from shared lib so existing consumers\n// can continue importing from './core-types' without changes.\nexport type {\n FieldConfig,\n FieldWrapperProps,\n ParsedField,\n ParsedSchema,\n Renderable,\n SchemaProvider,\n SchemaValidation,\n SchemaValidationContext,\n SchemaValidationError,\n UiRenderable,\n} from '../../lib/form-types';\n\nimport type { FieldWrapperProps, ParsedField, UiRenderable } from '../../lib/form-types';\nexport { getFieldHints } from '../../lib/form-types';\n\n// ---------------------------------------------------------------------------\n// UI component contracts — AutoForm-specific wrapper and field props.\n// ---------------------------------------------------------------------------\n\nexport type ObjectWrapperProps = {\n label: UiRenderable;\n children: ReactNode;\n field: ParsedField;\n hasError?: boolean;\n};\n\nexport type ArrayWrapperProps = {\n label: UiRenderable;\n children: ReactNode;\n field: ParsedField;\n onAddItem: () => void;\n};\n\nexport type ArrayElementWrapperProps = {\n children: ReactNode;\n onRemove: () => void;\n index: number;\n};\n\nexport type AutoFormUIComponents = {\n Form: React.ComponentType>;\n FieldWrapper: React.ComponentType;\n ErrorMessage: React.ComponentType<{ error: string }>;\n SubmitButton: React.ComponentType<{ children: ReactNode; disabled?: boolean; testId?: string }>;\n ObjectWrapper: React.ComponentType;\n ArrayWrapper: React.ComponentType;\n ArrayElementWrapper: React.ComponentType;\n};\n\nexport type AutoFormFieldProps = {\n label: UiRenderable;\n field: ParsedField;\n value: any;\n error?: string;\n id: string;\n path: string[];\n inputProps: Record;\n};\n\nexport type AutoFormFieldComponents = {\n fallback: React.ComponentType;\n} & Partial>>;\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/data-providers.ts", "content": "/**\n * Data-provider registry for AutoForm dropdowns.\n *\n * AutoForm is RPC-agnostic: the registry only knows about option lists.\n * Each provider is a React hook that returns `{ options, isLoading?, error? }`.\n * Whether the options are a static array or backed by an RPC is a concern\n * of the hosting app's wiring layer, not AutoForm.\n *\n * The registry is keyed by a string id that mirrors a proto `DataProviderId`\n * enum. The hosting app registers one implementation per id; a CI test\n * enumerates the proto descriptors and asserts completeness.\n */\n\nimport type React from 'react';\n\nexport interface DataProviderOption {\n /** Optional helper line shown beneath the label. */\n description?: string;\n /** Optional group heading so related options cluster visually. */\n group?: string;\n /**\n * Optional glyph rendered before the label. Most useful when a dropdown\n * combines options from multiple sources and a visual mark helps users\n * distinguish them. Lists from one source should usually omit a repeated\n * icon because it adds noise without new information.\n */\n icon?: React.ReactNode;\n /** Display label shown in the dropdown. */\n label: string;\n /** Wire value stored in form state. */\n value: string;\n}\n\nexport interface DataProviderResult {\n /** Non-null when the provider failed to load. */\n error?: unknown;\n /** True while an async source is loading. Static providers may omit this. */\n isLoading?: boolean;\n options: DataProviderOption[];\n}\n\n/**\n * A data provider is a React hook. Implementations may call `useQuery`,\n * return a memoised constant array, or anything in between — AutoForm\n * never inspects the internals.\n */\nexport type DataProvider = () => DataProviderResult;\n\nexport type DataProviderRegistry = Record;\n\n/**\n * Resolve a data provider by id. Returns `undefined` when the id is not\n * registered; the caller is responsible for rendering a graceful fallback\n * (and, in dev, logging a warning).\n */\nexport function resolveDataProvider(\n registry: DataProviderRegistry | undefined,\n id: string | undefined\n): DataProvider | undefined {\n if (!(registry && id)) {\n return;\n }\n return registry[id];\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/field-utils.ts", "content": "import type { ParsedField } from '../../lib/form-types';\n\n// Common protocol / tech acronyms the default title-casing would\n// otherwise emit as mixed-case (\"Api Key\", \"Tls\", \"Aws Region\", etc.).\n// The replacement is the canonical form the industry uses — gRPC is\n// intentionally lower-case `g`, everything else is fully upper.\n// Matched as whole words (case-insensitive) after the initial\n// camelCase/snake_case split in `beautifyLabel`.\nconst ACRONYMS: Record = {\n api: 'API',\n aws: 'AWS',\n dsn: 'DSN',\n gcp: 'GCP',\n grpc: 'gRPC',\n http: 'HTTP',\n https: 'HTTPS',\n id: 'ID',\n json: 'JSON',\n jwt: 'JWT',\n mcp: 'MCP',\n oauth: 'OAuth',\n sasl: 'SASL',\n sdk: 'SDK',\n sns: 'SNS',\n sql: 'SQL',\n sqs: 'SQS',\n ssl: 'SSL',\n tls: 'TLS',\n tts: 'TTS',\n url: 'URL',\n uri: 'URI',\n uuid: 'UUID',\n vpc: 'VPC',\n yaml: 'YAML',\n};\n\nfunction applyAcronyms(label: string): string {\n return label.replace(/\\b[A-Za-z]+\\b/g, (word) => ACRONYMS[word.toLowerCase()] ?? word);\n}\n\nfunction beautifyLabel(label: string): string {\n if (!label) {\n return '';\n }\n let output = label.replace(/([A-Z])/g, ' $1');\n output = output.charAt(0).toUpperCase() + output.slice(1);\n if (!Number.isNaN(Number(output))) {\n return '';\n }\n if (output === '*') {\n return '';\n }\n return applyAcronyms(output);\n}\n\nexport function getLabel(field: ParsedField): string {\n return (field.fieldConfig?.label as string) || (field.description as string) || beautifyLabel(field.key);\n}\n\nexport function sortFieldsByOrder(\n fields: ParsedField[] | undefined\n): ParsedField[] {\n if (!fields) {\n return [];\n }\n return fields\n .map((field) => (field.schema ? { ...field, schema: sortFieldsByOrder(field.schema) } : field))\n .sort((a, b) => (a.fieldConfig?.order ?? 0) - (b.fieldConfig?.order ?? 0));\n}\n\nexport function getPathInObject(obj: Record, path: string[]): any {\n let current: unknown = obj;\n for (const key of path) {\n if (current === undefined || current === null) {\n return;\n }\n current = (current as Record)[key];\n }\n return current;\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/field-wrapper.tsx", "content": "'use client';\n\nimport { AlertCircle, ChevronDown, CircleHelp, ExternalLink, PlusIcon, TrashIcon } from 'lucide-react';\nimport React from 'react';\nimport { cn, type SharedProps } from '../../lib/utils';\nimport { Alert, AlertDescription, AlertTitle } from '../alert';\nimport { Button } from '../button';\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../collapsible';\nimport { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from '../field';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';\nimport { Heading, Text } from '../typography';\nimport { useAutoFormRuntimeContext } from './context';\nimport type { ArrayElementWrapperProps, ArrayWrapperProps, FieldWrapperProps, ObjectWrapperProps } from './core-types';\nimport { formSpacing } from './form-spacing';\nimport { getFieldDescriptionText, getFieldDocsUrl, getFieldHelpText, getFieldUiConfig } from './helpers';\nimport { FormDepthProvider, headingLevelForDepth, useFormDepth } from './layout-context';\nimport { getAutoFormFieldTestId } from './test-ids';\n\nconst REGEX_ERROR_PATTERN = /regex pattern\\s*`([^`]+)`/;\n\nexport const Form = React.forwardRef & SharedProps>(\n ({ children, testId, ...props }, ref) => (\n
\n {children}\n
\n )\n);\nForm.displayName = 'Form';\n\nexport const ArrayElementWrapper: React.FC<\n ArrayElementWrapperProps & {\n testId?: string;\n removeButtonTestId?: string;\n }\n> = ({ children, onRemove, removeButtonTestId, testId }) => (\n \n \n \n \n
{children}
\n \n);\n\nexport const ArrayWrapper: React.FC<\n ArrayWrapperProps & {\n addButtonTestId?: string;\n testId?: string;\n }\n> = ({ label, children, onAddItem, addButtonTestId, testId }) => (\n
\n {children}\n \n
\n);\n\nexport const ErrorMessage: React.FC<{ error: string }> = ({ error }) => (\n \n \n AutoForm error\n {error}\n \n);\n\nfunction augmentError(\n error: FieldWrapperProps['error'],\n field: FieldWrapperProps['field']\n): FieldWrapperProps['error'] {\n if (!(typeof error === 'string' && REGEX_ERROR_PATTERN.test(error))) {\n return error;\n }\n const uiConfig = getFieldUiConfig(field);\n if (uiConfig.example) {\n return `${error}\\nExample: ${uiConfig.example}`;\n }\n return error;\n}\n\nexport const FieldWrapper: React.FC = ({ label, children, id, field, error: rawError }) => {\n const depth = useFormDepth();\n const { testIdPrefix } = useAutoFormRuntimeContext();\n const isCompact = Boolean((field.fieldConfig?.customData as Record | undefined)?.compactRow);\n const tooltipText = isCompact ? '' : getFieldHelpText(field);\n const helpText = isCompact ? undefined : getFieldDescriptionText(field);\n const docsUrl = isCompact ? undefined : getFieldDocsUrl(field);\n const error = augmentError(rawError, field);\n const isDisabled = Boolean(field.fieldConfig?.inputProps?.disabled);\n const hasVisibleLabel = !(typeof label === 'string' && label.trim().length === 0);\n const fallbackLabel =\n typeof field.fieldConfig?.label === 'string' && field.fieldConfig.label.trim().length > 0\n ? field.fieldConfig.label\n : field.key;\n const helpLabel = typeof label === 'string' && label.trim().length > 0 ? label : fallbackLabel;\n const fieldTestId = getAutoFormFieldTestId(testIdPrefix, id);\n const isSplit = depth === 0 && !isCompact;\n\n // Match the non-AutoForm usage pattern in managed-create-form.tsx:\n // `` with label / control / description / error as *direct*\n // siblings, so the Field component's native `gap-3` drives the\n // label → input → description → error rhythm. The previous\n // `` nesting produced a cramped\n // 8px label/input gap and misaligned the internal rhythm from every\n // manually-constructed form in the app — users could spot the\n // AutoForm at a glance from the tighter stack alone.\n return (\n \n {isCompact ? null : (\n
\n \n \n {hasVisibleLabel ? label : fallbackLabel}\n \n {field.required ? (\n \n *\n \n ) : null}\n \n {tooltipText ? (\n \n \n \n \n \n \n \n {tooltipText}\n \n \n ) : null}\n
\n )}\n \n {children}\n {error ? (\n {error}\n ) : (helpText || docsUrl) && !isCompact ? (\n \n {helpText ? {helpText} : null}\n {docsUrl ? (\n <>\n {helpText ? ' ' : null}\n \n Learn more\n \n \n \n ) : null}\n \n ) : null}\n \n
\n );\n};\n\nexport const ObjectWrapper: React.FC = ({\n label,\n children,\n field,\n testId,\n hasError,\n}) => {\n const depth = useFormDepth();\n const headingLevel = headingLevelForDepth(depth);\n const helpText = getFieldDescriptionText(field);\n const hasVisibleLabel = !(typeof label === 'string' && label.trim().length === 0);\n const customData = (field.fieldConfig?.customData ?? {}) as Record;\n const isCollapsible = Boolean(customData.collapsible);\n // Divider under a section header. Defaults to true for parity with the\n // historical ObjectWrapper behavior. Consumers can opt out by setting\n // `customData.showDivider = false` — same escape hatch as FormSection's\n // `divider` prop so both entry points agree on when a rule renders.\n const showDivider = customData.showDivider !== false && hasVisibleLabel;\n const isSplit = depth === 0 && hasVisibleLabel && !isCollapsible;\n const [isOpen, setIsOpen] = React.useState(false);\n\n // Auto-expand when section has validation errors\n React.useEffect(() => {\n if (hasError && !isOpen) {\n setIsOpen(true);\n }\n }, [hasError, isOpen]);\n\n if (isCollapsible && hasVisibleLabel) {\n return (\n \n
\n \n \n
\n
\n \n {label}\n \n {field.required ? (\n \n *\n \n ) : null}\n
\n {helpText ? (\n \n {helpText}\n \n ) : null}\n
\n \n \n
\n \n \n
{children}
\n
\n
\n
\n
\n );\n }\n\n return (\n \n {hasVisibleLabel ? (\n \n
\n \n {label}\n \n {field.required ? (\n \n *\n \n ) : null}\n
\n {helpText ? (\n \n {helpText}\n \n ) : null}\n \n ) : null}\n \n
{children}
\n
\n \n );\n};\n\nexport const SubmitButton: React.FC<{ children: React.ReactNode; disabled?: boolean; testId?: string }> = ({\n children,\n disabled,\n testId,\n}) => (\n \n);\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/boolean.tsx", "content": "'use client';\n\nimport { Checkbox } from '../../checkbox';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../select';\nimport { Switch } from '../../switch';\nimport { Toggle } from '../../toggle';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { CONSENT_FIELD_PATTERN } from '../helpers';\nimport { getProtoFieldCustomData } from '../proto';\nimport type { FieldTypeDefinition } from '../registry';\nimport { getControlLabel, useFieldTestIds } from './shared';\n\n// ---------------------------------------------------------------------------\n// BooleanFieldComponent — tri-state select (true / false / unset)\n// ---------------------------------------------------------------------------\n\nfunction BooleanFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const fieldLabel = getControlLabel(label, field);\n\n return (\n {\n if (value === null) {\n inputProps.onValueChange(undefined);\n return;\n }\n inputProps.onValueChange(value === 'true');\n }}\n value={inputProps.value === undefined ? null : String(Boolean(inputProps.value))}\n >\n \n \n \n \n \n Not set\n \n \n True\n \n \n False\n \n \n \n );\n}\n\n// ---------------------------------------------------------------------------\n// CheckboxFieldComponent\n// ---------------------------------------------------------------------------\n\nfunction CheckboxFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n\n return (\n
\n inputProps.onValueChange(Boolean(checked))}\n testId={testIds.control}\n />\n
\n );\n}\n\n// ---------------------------------------------------------------------------\n// SwitchFieldComponent\n// ---------------------------------------------------------------------------\n\nfunction SwitchFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n\n return (\n
\n inputProps.onValueChange(Boolean(checked))}\n testId={testIds.control}\n />\n
\n );\n}\n\n// ---------------------------------------------------------------------------\n// ToggleFieldComponent — not registered by default (available as override)\n// ---------------------------------------------------------------------------\n\nfunction ToggleFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n\n return (\n
\n inputProps.onValueChange(Boolean(checked))}\n testId={testIds.controlPart('switch')}\n />\n \n {inputProps.value ? 'On' : 'Off'}\n \n
\n );\n}\n\nexport { BooleanFieldComponent, CheckboxFieldComponent, SwitchFieldComponent, ToggleFieldComponent };\n\nexport const booleanFieldDefinition: FieldTypeDefinition = {\n name: 'boolean',\n priority: 15,\n match: (field) => {\n if (field.type !== 'boolean') {\n return false;\n }\n const protoData = getProtoFieldCustomData(field);\n return Boolean(protoData?.supportsUnset) && !field.required;\n },\n component: BooleanFieldComponent,\n};\n\nexport const checkboxFieldDefinition: FieldTypeDefinition = {\n name: 'checkbox',\n priority: 14,\n match: (field, context) => field.type === 'boolean' && CONSENT_FIELD_PATTERN.test(context.identity),\n component: CheckboxFieldComponent,\n};\n\nexport const switchFieldDefinition: FieldTypeDefinition = {\n name: 'switch',\n priority: 10,\n match: (field) => field.type === 'boolean',\n component: SwitchFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/bytes.tsx", "content": "'use client';\n\nimport { Textarea } from '../../textarea';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { useFieldTestIds } from './shared';\n\nfunction BytesFieldComponent({ error, field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n\n return (\n inputProps.onValueChange(event.target.value)}\n placeholder={getFieldUiConfig(field).placeholder || 'Base64 payload'}\n resize=\"vertical\"\n testId={testIds.control}\n value={(inputProps.value as string | undefined) ?? ''}\n />\n );\n}\n\nexport { BytesFieldComponent };\n\nexport const bytesFieldDefinition: FieldTypeDefinition = {\n name: 'bytes',\n priority: 10,\n match: (field) => field.type === 'bytes',\n component: BytesFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/choicebox.tsx", "content": "'use client';\n\nimport {\n Choicebox,\n ChoiceboxItem,\n ChoiceboxItemContent,\n ChoiceboxItemHeader,\n ChoiceboxItemIndicator,\n ChoiceboxItemTitle,\n} from '../../choicebox';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { getControlLabel, getFlatOptions, hasNumericOptions, renderOptionLabel, useFieldTestIds } from './shared';\n\nfunction ChoiceboxFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const numericOptions = hasNumericOptions(field);\n const value = inputProps.value === undefined || inputProps.value === null ? '' : String(inputProps.value);\n const options = getFlatOptions(field);\n\n return (\n inputProps.onValueChange(numericOptions ? Number(nextValue) : nextValue)}\n testId={testIds.control}\n value={value}\n >\n {options.map((option) => (\n \n \n {renderOptionLabel(option)}\n \n \n \n \n \n ))}\n \n );\n}\n\nexport { ChoiceboxFieldComponent };\n\nexport const choiceboxFieldDefinition: FieldTypeDefinition = {\n name: 'choicebox',\n priority: 25,\n match: (field) => {\n if (field.type !== 'select') {\n return false;\n }\n return getFieldUiConfig(field).control === 'choicebox';\n },\n component: ChoiceboxFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/combobox.tsx", "content": "'use client';\n\nimport { Combobox } from '../../combobox';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { getFlatOptions, getGroupedOptions, hasNumericOptions, useFieldTestIds } from './shared';\n\nfunction ComboboxFieldComponent({ field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const numericOptions = hasNumericOptions(field);\n const optionGroups = getGroupedOptions(field);\n const options = optionGroups?.length\n ? optionGroups.flatMap((group) =>\n group.options.map((option) => ({\n group: String(group.label ?? ''),\n groupTestId: testIds.group(String(group.label ?? option.value)),\n testId: testIds.option(option.value),\n value: option.value,\n label: `${group.label ? `${group.label} · ` : ''}${String(option.label ?? option.value)}`,\n }))\n )\n : getFlatOptions(field).map((option) => ({\n value: option.value,\n label: String(option.label ?? option.value),\n testId: testIds.option(option.value),\n }));\n\n return (\n inputProps.onValueChange(numericOptions ? Number(value) : value)}\n options={options}\n placeholder={getFieldUiConfig(field).placeholder || 'Search options'}\n value={inputProps.value === undefined || inputProps.value === null ? '' : String(inputProps.value)}\n />\n );\n}\n\nexport { ComboboxFieldComponent };\n\nexport const comboboxFieldDefinition: FieldTypeDefinition = {\n name: 'combobox',\n priority: 18,\n match: (field) => {\n if (field.type !== 'select') {\n return false;\n }\n const optionCount = field.options?.length ?? 0;\n return optionCount > 8;\n },\n component: ComboboxFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/currency.tsx", "content": "'use client';\n\nimport { DollarSignIcon } from 'lucide-react';\nimport { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from '../../input-group';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { CURRENCY_FIELD_PATTERN, getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { useFieldTestIds } from './shared';\n\nfunction CurrencyFieldComponent({ error, field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n\n return (\n \n \n \n \n \n \n inputProps.onValueChange(event.target.value)}\n placeholder={getFieldUiConfig(field).placeholder}\n testId={testIds.controlPart('input')}\n value={(inputProps.value as string | number | undefined)?.toString() ?? ''}\n />\n \n );\n}\n\nexport { CurrencyFieldComponent };\n\nexport const currencyFieldDefinition: FieldTypeDefinition = {\n name: 'currency',\n priority: 18,\n match: (field, context) => field.type === 'string' && CURRENCY_FIELD_PATTERN.test(context.identity),\n component: CurrencyFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/date.tsx", "content": "'use client';\n\nimport { format } from 'date-fns';\nimport { CalendarIcon, Clock3Icon } from 'lucide-react';\nimport { Calendar } from '../../calendar';\nimport { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText } from '../../input-group';\nimport { Popover, PopoverContent, PopoverTrigger } from '../../popover';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport {\n buildTimestampValue,\n getControlLabel,\n normalizeDateValue,\n normalizeTimeValue,\n parseCalendarDate,\n resolveControlTestId,\n useFieldTestIds,\n} from './shared';\n\n// ---------------------------------------------------------------------------\n// DateFieldComponent\n// ---------------------------------------------------------------------------\n\nfunction DateFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const controlTestId = resolveControlTestId(inputProps, testIds.control);\n const value = normalizeDateValue(inputProps.value);\n const selectedDate = parseCalendarDate(value);\n\n return (\n \n \n inputProps.onValueChange(event.target.value)}\n placeholder={getFieldUiConfig(field).placeholder || 'YYYY-MM-DD'}\n testId={`${controlTestId}-input`}\n value={value}\n />\n \n \n \n \n \n \n \n \n \n {\n inputProps.onValueChange(date ? format(date, 'yyyy-MM-dd') : '');\n }}\n selected={selectedDate}\n />\n \n \n );\n}\n\n// ---------------------------------------------------------------------------\n// TimestampFieldComponent\n// ---------------------------------------------------------------------------\n\nfunction TimestampFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const dateValue = normalizeDateValue(inputProps.value);\n const timeValue = normalizeTimeValue(inputProps.value);\n\n return (\n
\n inputProps.onValueChange(buildTimestampValue(nextDate, timeValue)),\n }}\n label={label}\n path={[]}\n value={dateValue}\n />\n \n \n \n \n \n \n inputProps.onValueChange(buildTimestampValue(dateValue, event.target.value))}\n placeholder=\"HH:mm\"\n testId={testIds.controlPart('time-input')}\n value={timeValue}\n />\n \n
\n );\n}\n\nexport { DateFieldComponent, TimestampFieldComponent };\n\nexport const dateFieldDefinition: FieldTypeDefinition = {\n name: 'date',\n priority: 10,\n match: (field) => field.type === 'date',\n component: DateFieldComponent,\n};\n\nexport const timestampFieldDefinition: FieldTypeDefinition = {\n name: 'timestamp',\n priority: 10,\n match: (field) => field.type === 'timestamp',\n component: TimestampFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/duration.tsx", "content": "'use client';\n\nimport { Input } from '../../input';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { useFieldTestIds } from './shared';\n\nfunction DurationFieldComponent({ error, field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n\n return (\n inputProps.onValueChange(event.target.value)}\n placeholder={getFieldUiConfig(field).placeholder || '300s'}\n testId={testIds.control}\n value={(inputProps.value as string | undefined) ?? ''}\n />\n );\n}\n\nexport { DurationFieldComponent };\n\nexport const durationFieldDefinition: FieldTypeDefinition = {\n name: 'duration',\n priority: 10,\n match: (field) => field.type === 'duration',\n component: DurationFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/email.tsx", "content": "'use client';\n\nimport { MailIcon } from 'lucide-react';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { EMAIL_FIELD_PATTERN, getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { StringLikeInput, useFieldTestIds } from './shared';\n\nfunction EmailFieldComponent(props: AutoFormFieldProps) {\n const testIds = useFieldTestIds(props.id);\n\n return (\n }\n id={props.id}\n inputProps={props.inputProps}\n placeholder={getFieldUiConfig(props.field).placeholder}\n testId={testIds.control}\n type=\"email\"\n />\n );\n}\n\nexport { EmailFieldComponent };\n\nexport const emailFieldDefinition: FieldTypeDefinition = {\n name: 'email',\n priority: 20,\n match: (field, context) =>\n field.type === 'string' && (context.inputType === 'email' || EMAIL_FIELD_PATTERN.test(context.identity)),\n component: EmailFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/fallback.tsx", "content": "'use client';\n\nimport { useAutoFormRenderContext } from '../context';\nimport type { AutoFormFieldProps } from '../core-types';\n\nfunction MissingFieldComponent({ field }: AutoFormFieldProps) {\n const { uiComponents } = useAutoFormRenderContext();\n\n return (\n \n );\n}\n\nexport { MissingFieldComponent };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/field-mask.tsx", "content": "'use client';\n\nimport { Textarea } from '../../textarea';\nimport { Text } from '../../typography';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { FIELD_MASK_PATH_SPLIT_PATTERN, getFieldUiConfig } from '../helpers';\nimport { getProtoFieldCustomData } from '../proto';\nimport type { FieldTypeDefinition } from '../registry';\nimport { useFieldTestIds } from './shared';\n\nfunction FieldMaskFieldComponent({ error, field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const allowedPaths = getProtoFieldCustomData(field)?.allowedPaths;\n const textValue = Array.isArray(inputProps.value) ? inputProps.value.join('\\n') : '';\n\n return (\n
\n {\n const paths = event.target.value\n .split(FIELD_MASK_PATH_SPLIT_PATTERN)\n .map((entry) => entry.trim())\n .filter(Boolean);\n inputProps.onValueChange(paths);\n }}\n placeholder={getFieldUiConfig(field).placeholder || 'profile\\nnotifications.email'}\n resize=\"vertical\"\n testId={testIds.control}\n value={textValue}\n />\n {allowedPaths?.length ? (\n \n Allowed paths: {allowedPaths.join(', ')}\n \n ) : null}\n
\n );\n}\n\nexport { FieldMaskFieldComponent };\n\nexport const fieldMaskFieldDefinition: FieldTypeDefinition = {\n name: 'fieldMask',\n priority: 10,\n match: (field) => field.type === 'fieldMask',\n component: FieldMaskFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/index.ts", "content": "import type { AutoFormFieldComponents } from '../core-types';\nimport { FieldTypeRegistry } from '../registry';\nimport type { FieldTypes } from '../types';\nimport {\n booleanFieldDefinition,\n checkboxFieldDefinition,\n switchFieldDefinition,\n ToggleFieldComponent,\n} from './boolean';\nimport { bytesFieldDefinition } from './bytes';\nimport { choiceboxFieldDefinition } from './choicebox';\nimport { comboboxFieldDefinition } from './combobox';\nimport { currencyFieldDefinition } from './currency';\nimport { dateFieldDefinition, timestampFieldDefinition } from './date';\nimport { durationFieldDefinition } from './duration';\nimport { emailFieldDefinition } from './email';\nimport { MissingFieldComponent } from './fallback';\nimport { fieldMaskFieldDefinition } from './field-mask';\nimport { int64FieldDefinition } from './int64';\nimport { jsonFieldDefinition } from './json';\nimport { keyValueFieldDefinition } from './key-value';\nimport { dataProviderMultiselectFieldDefinition, multiselectFieldDefinition } from './multiselect';\nimport { numberFieldDefinition } from './number';\nimport { passwordFieldDefinition } from './password';\nimport { radioFieldDefinition } from './radio';\nimport { dataProviderSelectFieldDefinition, selectFieldDefinition } from './select';\nimport { sliderFieldDefinition } from './slider';\nimport { stringFieldDefinition } from './string';\nimport { textareaFieldDefinition } from './textarea';\nimport { toggleGroupFieldDefinition } from './toggle-group';\nimport { urlFieldDefinition } from './url';\n\n// ---------------------------------------------------------------------------\n// Default registry with all built-in field types\n// ---------------------------------------------------------------------------\n\nexport const defaultRegistry = new FieldTypeRegistry();\n\ndefaultRegistry\n // Data-provider-annotated fields win over every default matcher — the\n // annotation is an explicit instruction from proto, overriding the\n // string/email/etc. fallbacks.\n .register(dataProviderSelectFieldDefinition)\n // String-family (higher priority first so they match before the generic string)\n .register(passwordFieldDefinition)\n .register(emailFieldDefinition)\n .register(urlFieldDefinition)\n .register(currencyFieldDefinition)\n .register(textareaFieldDefinition)\n .register(stringFieldDefinition)\n\n // Number-family\n .register(sliderFieldDefinition)\n .register(numberFieldDefinition)\n\n // Int64\n .register(int64FieldDefinition)\n\n // Boolean-family\n .register(booleanFieldDefinition)\n .register(checkboxFieldDefinition)\n .register(switchFieldDefinition)\n\n // Date-family\n .register(dateFieldDefinition)\n .register(timestampFieldDefinition)\n\n // Select-family\n .register(choiceboxFieldDefinition)\n .register(toggleGroupFieldDefinition)\n .register(comboboxFieldDefinition)\n .register(radioFieldDefinition)\n .register(selectFieldDefinition)\n\n // Array / map\n .register(dataProviderMultiselectFieldDefinition)\n .register(multiselectFieldDefinition)\n .register(keyValueFieldDefinition)\n\n // Protobuf-specific\n .register(bytesFieldDefinition)\n .register(durationFieldDefinition)\n .register(fieldMaskFieldDefinition)\n .register(jsonFieldDefinition);\n\n// ---------------------------------------------------------------------------\n// Legacy map-based registry for backwards compatibility during migration\n// ---------------------------------------------------------------------------\n\nexport const AutoFormFieldComponentRegistry = {\n string: stringFieldDefinition.component,\n textarea: textareaFieldDefinition.component,\n password: passwordFieldDefinition.component,\n email: emailFieldDefinition.component,\n url: urlFieldDefinition.component,\n currency: currencyFieldDefinition.component,\n number: numberFieldDefinition.component,\n slider: sliderFieldDefinition.component,\n int64: int64FieldDefinition.component,\n boolean: booleanFieldDefinition.component,\n checkbox: checkboxFieldDefinition.component,\n switch: switchFieldDefinition.component,\n toggle: ToggleFieldComponent,\n date: dateFieldDefinition.component,\n timestamp: timestampFieldDefinition.component,\n select: selectFieldDefinition.component,\n // Share the same component with `select` — the routing rule is\n // different (data-provider annotation vs proto enum), but the\n // component handles both via its internal provider branch.\n dataProviderSelect: dataProviderSelectFieldDefinition.component,\n radio: radioFieldDefinition.component,\n combobox: comboboxFieldDefinition.component,\n choicebox: choiceboxFieldDefinition.component,\n toggleGroup: toggleGroupFieldDefinition.component,\n multiselect: multiselectFieldDefinition.component,\n dataProviderMultiSelect: dataProviderMultiselectFieldDefinition.component,\n bytes: bytesFieldDefinition.component,\n duration: durationFieldDefinition.component,\n fieldMask: fieldMaskFieldDefinition.component,\n json: jsonFieldDefinition.component,\n 'dropzone-json': jsonFieldDefinition.component,\n keyValue: keyValueFieldDefinition.component,\n fallback: MissingFieldComponent,\n} satisfies AutoFormFieldComponents;\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport { MissingFieldComponent } from './fallback';\n\nexport {\n booleanFieldDefinition,\n bytesFieldDefinition,\n checkboxFieldDefinition,\n choiceboxFieldDefinition,\n comboboxFieldDefinition,\n currencyFieldDefinition,\n dataProviderMultiselectFieldDefinition,\n dataProviderSelectFieldDefinition,\n dateFieldDefinition,\n durationFieldDefinition,\n emailFieldDefinition,\n fieldMaskFieldDefinition,\n int64FieldDefinition,\n jsonFieldDefinition,\n keyValueFieldDefinition,\n multiselectFieldDefinition,\n numberFieldDefinition,\n passwordFieldDefinition,\n radioFieldDefinition,\n selectFieldDefinition,\n sliderFieldDefinition,\n stringFieldDefinition,\n switchFieldDefinition,\n textareaFieldDefinition,\n timestampFieldDefinition,\n toggleGroupFieldDefinition,\n urlFieldDefinition,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/int64.tsx", "content": "'use client';\n\nimport { Input } from '../../input';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { useFieldTestIds } from './shared';\n\nfunction Int64FieldComponent({ error, field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n\n return (\n inputProps.onValueChange(event.target.value)}\n placeholder={getFieldUiConfig(field).placeholder}\n testId={testIds.control}\n value={(inputProps.value as string | undefined) ?? ''}\n />\n );\n}\n\nexport { Int64FieldComponent };\n\nexport const int64FieldDefinition: FieldTypeDefinition = {\n name: 'int64',\n priority: 10,\n match: (field) => field.type === 'int64',\n component: Int64FieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/json.tsx", "content": "'use client';\n\nimport { JSONField } from '../../json-field';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getProtoFieldCustomData, getProtoJsonSchema } from '../proto';\nimport type { FieldTypeDefinition } from '../registry';\nimport { useFieldTestIds } from './shared';\n\nfunction JsonFieldComponent({ field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n\n return (\n inputProps.onValueChange(value)}\n schema={getProtoJsonSchema(field) as never}\n showPlaceholder={false}\n testId={testIds.control}\n value={\n ((inputProps.value as unknown) ?? (getProtoFieldCustomData(field)?.jsonKind === 'listValue' ? [] : {})) as never\n }\n />\n );\n}\n\nexport { JsonFieldComponent };\n\nexport const jsonFieldDefinition: FieldTypeDefinition = {\n name: 'json',\n priority: 10,\n match: (field) => field.type === 'json',\n component: JsonFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/key-value.tsx", "content": "'use client';\n\nimport { KeyValueField } from '../../key-value-field';\nimport type { AutoFormFieldProps } from '../core-types';\nimport {\n denormalizeKeyValueEntries,\n getFieldUiConfig,\n normalizeKeyValueEntries,\n resolveRenderFieldType,\n} from '../helpers';\nimport { getProtoFieldCustomData } from '../proto';\nimport type { FieldTypeDefinition } from '../registry';\nimport { useFieldTestIds } from './shared';\n\nfunction KeyValueFieldComponent({ field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const valueField =\n field.type === 'map'\n ? field.schema?.[1]\n : field.schema?.[0]?.schema?.find((candidate) => candidate.key === 'value');\n const valueRenderType = valueField ? resolveRenderFieldType(valueField) : undefined;\n const protoData = getProtoFieldCustomData(field);\n\n const valueFieldProps =\n valueRenderType === 'select' || valueRenderType === 'combobox' || valueRenderType === 'radio'\n ? {\n mode: 'combobox' as const,\n options: (valueField?.options || []).map(([value, optionLabel]) => ({ value, label: optionLabel })),\n placeholder: getFieldUiConfig(valueField ?? field).placeholder || 'Value',\n }\n : {\n placeholder: getFieldUiConfig(valueField ?? field).placeholder || 'Value',\n };\n\n return (\n inputProps.onValueChange(denormalizeKeyValueEntries(entries, field))}\n showAddButton\n testId={testIds.control}\n value={normalizeKeyValueEntries(inputProps.value)}\n valueFieldProps={valueFieldProps}\n />\n );\n}\n\nexport { KeyValueFieldComponent };\n\n/**\n * Helper to check if a field's schema entry is a scalar type suitable for key-value use.\n */\nfunction isKeyValueScalarField(field: { type?: string } | undefined): boolean {\n if (!field) {\n return false;\n }\n return ['string', 'email', 'url', 'password', 'currency', 'number', 'int64', 'select', 'combobox'].includes(\n field.type ?? ''\n );\n}\n\nfunction isSimpleKeyValueLikeObject(\n field: { type?: string; schema?: Array<{ key: string; type?: string }> } | undefined\n): boolean {\n if (!(field?.type === 'object' && field.schema?.length === 2)) {\n return false;\n }\n\n const keyField = field.schema.find((candidate) => candidate.key === 'key');\n const valueField = field.schema.find((candidate) => candidate.key === 'value');\n return Boolean(keyField && valueField && isKeyValueScalarField(keyField) && isKeyValueScalarField(valueField));\n}\n\nexport const keyValueFieldDefinition: FieldTypeDefinition = {\n name: 'keyValue',\n priority: 18,\n match: (field) => {\n // Array with key-value-like object items\n if (field.type === 'array') {\n const itemField = field.schema?.[0];\n return isSimpleKeyValueLikeObject(itemField);\n }\n\n // Map with scalar key + value\n if (field.type === 'map') {\n const keyField = field.schema?.[0];\n const valueField = field.schema?.[1];\n return isKeyValueScalarField(keyField) && isKeyValueScalarField(valueField);\n }\n\n return false;\n },\n component: KeyValueFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/multiselect.tsx", "content": "'use client';\n\nimport { SimpleMultiSelect } from '../../multi-select';\nimport { useAutoForm } from '../context';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { resolveDataProvider } from '../data-providers';\nimport { getFieldUiConfig, NUMERIC_OPTION_PATTERN } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { getGroupedOptions, readDataProviderId, renderOptionLabel, useFieldTestIds } from './shared';\n\nfunction MultiSelectFieldComponent({ field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const itemField = field.schema?.[0];\n const numericOptions = Boolean(itemField?.options?.every(([value]) => NUMERIC_OPTION_PATTERN.test(value)));\n const optionGroups = itemField ? getGroupedOptions(itemField) : undefined;\n const options = optionGroups?.length\n ? optionGroups.map((group) => ({\n heading: group.label,\n testId: testIds.group(String(group.label ?? 'group')),\n children: group.options.map((option) => ({\n label: renderOptionLabel(option),\n selectedTestId: testIds.selected(option.value),\n testId: testIds.option(option.value),\n value: option.value,\n })),\n }))\n : (itemField?.options || []).map(([value, optionLabel]) => ({\n label: optionLabel,\n selectedTestId: testIds.selected(value),\n testId: testIds.option(value),\n value,\n }));\n\n return (\n \n inputProps.onValueChange(numericOptions ? values.map((value) => Number(value)) : values)\n }\n options={options}\n placeholder={getFieldUiConfig(field).placeholder || 'Select one or more options'}\n testId={testIds.field}\n value={Array.isArray(inputProps.value) ? inputProps.value.map((value: unknown) => String(value)) : []}\n width=\"full\"\n />\n );\n}\n\nexport { MultiSelectFieldComponent };\n\nexport const multiselectFieldDefinition: FieldTypeDefinition = {\n name: 'multiselect',\n priority: 20,\n match: (field) => {\n if (field.type !== 'array') {\n return false;\n }\n const itemField = field.schema?.[0];\n return Boolean(itemField?.type === 'select' && itemField.options?.length);\n },\n component: MultiSelectFieldComponent,\n};\n\n// ── Data-provider-backed multi-select ─────────────────────────────────\n// Matches `repeated string` whose item carries a `data_provider`\n// annotation, e.g. OpenAPI `include_methods` / `exclude_methods`. The\n// previous behavior rendered a list of single dropdowns with an \"Add\"\n// button — one row per method. A multi-select collapses that to a single\n// control that holds every picked method as a chip.\n\nfunction DataProviderMultiSelectComponent({ field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const itemField = field.schema?.[0];\n const providerId = readDataProviderId(itemField);\n const { dataProviders } = useAutoForm();\n const provider = resolveDataProvider(dataProviders, providerId);\n const { options: providerOptions = [], isLoading } = provider ? provider() : { options: [] };\n\n const options = providerOptions.map((option) => {\n // `label` is typed as ReactNode on MultiSelectOptionItem, so we can\n // render icon + text + description inline instead of stringifying.\n const labelNode = (\n \n {option.icon ? (\n svg]:h-full [&>svg]:w-full\">\n {option.icon}\n \n ) : null}\n {option.label}\n {option.description ? — {option.description} : null}\n \n );\n return {\n label: labelNode,\n selectedTestId: testIds.selected(option.value),\n testId: testIds.option(option.value),\n value: option.value,\n };\n });\n\n const currentValue = Array.isArray(inputProps.value) ? inputProps.value.map((value: unknown) => String(value)) : [];\n\n return (\n inputProps.onValueChange(values)}\n options={options}\n placeholder={getFieldUiConfig(field).placeholder || (isLoading ? 'Loading…' : 'Select one or more options')}\n testId={testIds.field}\n value={currentValue}\n width=\"full\"\n />\n );\n}\n\nexport const dataProviderMultiselectFieldDefinition: FieldTypeDefinition = {\n name: 'dataProviderMultiSelect',\n // Higher than the default `multiselect` (20) so an annotated item wins\n // over the legacy \"array-of-select-enum\" branch even when both match.\n priority: 120,\n match: (field) => {\n if (field.type !== 'array') {\n return false;\n }\n const itemField = field.schema?.[0];\n if (!itemField || (itemField.type !== 'string' && itemField.type !== 'number')) {\n return false;\n }\n return readDataProviderId(itemField) !== undefined;\n },\n component: DataProviderMultiSelectComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/number.tsx", "content": "'use client';\n\nimport { Input } from '../../input';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { normalizeNumberValue, resolveNumericStep, useFieldTestIds } from './shared';\n\nfunction NumberFieldComponent({ error, field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const stepValue = resolveNumericStep(inputProps, normalizeNumberValue(inputProps.value));\n\n return (\n {\n const nextValue = event.target.value;\n inputProps.onValueChange(nextValue === '' ? undefined : Number(nextValue));\n }}\n placeholder={getFieldUiConfig(field).placeholder}\n step={stepValue}\n testId={testIds.control}\n type=\"number\"\n value={inputProps.value ?? ''}\n />\n );\n}\n\nexport { NumberFieldComponent };\n\nexport const numberFieldDefinition: FieldTypeDefinition = {\n name: 'number',\n priority: 10,\n match: (field) => {\n if (field.type !== 'number') {\n return false;\n }\n const min = Number(field.fieldConfig?.inputProps?.min);\n const max = Number(field.fieldConfig?.inputProps?.max);\n return !(Number.isFinite(min) && Number.isFinite(max));\n },\n component: NumberFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/password.tsx", "content": "'use client';\n\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig, SECRET_FIELD_PATTERN } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { StringLikeInput, useFieldTestIds } from './shared';\n\nfunction PasswordFieldComponent(props: AutoFormFieldProps) {\n const testIds = useFieldTestIds(props.id);\n\n return (\n \n );\n}\n\nexport { PasswordFieldComponent };\n\nexport const passwordFieldDefinition: FieldTypeDefinition = {\n name: 'password',\n priority: 20,\n match: (field, context) =>\n field.type === 'string' && (SECRET_FIELD_PATTERN.test(context.identity) || context.inputType === 'password'),\n component: PasswordFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/radio.tsx", "content": "'use client';\n\nimport { RadioGroup, RadioGroupItem } from '../../radio-group';\nimport { Text } from '../../typography';\nimport type { AutoFormFieldProps } from '../core-types';\nimport type { FieldTypeDefinition } from '../registry';\nimport {\n getControlLabel,\n getFlatOptions,\n getGroupedOptions,\n hasNumericOptions,\n renderOptionLabel,\n useFieldTestIds,\n} from './shared';\n\nfunction RadioFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const numericOptions = hasNumericOptions(field);\n const value = inputProps.value === undefined || inputProps.value === null ? '' : String(inputProps.value);\n const optionGroups = getGroupedOptions(field);\n const flatOptions = getFlatOptions(field);\n\n return (\n inputProps.onValueChange(numericOptions ? Number(nextValue) : nextValue)}\n testId={testIds.control}\n value={value}\n >\n {(optionGroups?.length ? optionGroups : [{ label: undefined, options: flatOptions }]).map((group, groupIndex) => (\n \n {group.label ? (\n \n {group.label}\n \n ) : null}\n
\n {group.options.map((option) => (\n \n {renderOptionLabel(option)}\n \n ))}\n
\n \n ))}\n \n );\n}\n\nexport { RadioFieldComponent };\n\nexport const radioFieldDefinition: FieldTypeDefinition = {\n name: 'radio',\n priority: 15,\n match: (field) => {\n if (field.type !== 'select') {\n return false;\n }\n const optionCount = field.options?.length ?? 0;\n return optionCount > 0 && optionCount <= 3;\n },\n component: RadioFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/select.tsx", "content": "'use client';\n\nimport { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '../../select';\nimport { useAutoForm } from '../context';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { type DataProviderOption, resolveDataProvider } from '../data-providers';\nimport type { FieldTypeDefinition } from '../registry';\nimport {\n getControlLabel,\n getFlatOptions,\n getGroupedOptions,\n hasNumericOptions,\n readDataProviderId,\n renderOptionLabel,\n useFieldTestIds,\n} from './shared';\n\nfunction SelectFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const { dataProviders } = useAutoForm();\n const providerId = readDataProviderId(field);\n const provider = resolveDataProvider(dataProviders, providerId);\n const numericOptions = hasNumericOptions(field);\n const currentValue = inputProps.value === undefined || inputProps.value === null ? null : String(inputProps.value);\n const fieldLabel = getControlLabel(label, field);\n const optionGroups = getGroupedOptions(field);\n const flatOptions = getFlatOptions(field);\n\n if (provider) {\n return (\n \n );\n }\n\n if (providerId) {\n // Annotated but no implementation registered — loud dev warning, graceful fallback.\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[AutoForm] Field \"${field.key}\" is annotated with data_provider=\"${providerId}\" ` +\n 'but no provider is registered. Check the AutoForm dataProviders map.'\n );\n }\n }\n\n return (\n ({\n label: renderOptionLabel(option),\n value: option.value,\n })),\n ]}\n onValueChange={(value) => {\n if (value === null) {\n inputProps.onValueChange(undefined);\n return;\n }\n inputProps.onValueChange(numericOptions ? Number(value) : value);\n }}\n value={currentValue}\n >\n \n \n \n \n {field.required ? null : (\n \n Not set\n \n )}\n {optionGroups?.length\n ? optionGroups.map((group, groupIndex) => (\n \n {group.label ? {group.label} : null}\n {group.options.map((option) => (\n \n {renderOptionLabel(option)}\n \n ))}\n \n ))\n : flatOptions.map((option) => (\n \n {renderOptionLabel(option)}\n \n ))}\n \n \n );\n}\n\nfunction SelectFieldFromProvider({\n currentValue,\n error,\n field,\n fieldLabel,\n id,\n inputProps,\n provider,\n testIds,\n}: {\n currentValue: string | null;\n error: AutoFormFieldProps['error'];\n field: AutoFormFieldProps['field'];\n fieldLabel: string;\n id: string;\n inputProps: AutoFormFieldProps['inputProps'];\n provider: () => { options: DataProviderOption[]; isLoading?: boolean; error?: unknown };\n testIds: ReturnType;\n}) {\n const { options, isLoading, error: providerError } = provider();\n\n if (providerError) {\n // `SelectTrigger` / `SelectValue` are Radix primitives that need a\n // `Select.Root` context. Wrap them in a disabled `\n \n \n \n \n );\n }\n\n const grouped = options.reduce>((acc, option) => {\n const key = option.group ?? '';\n acc[key] = acc[key] ? [...acc[key], option] : [option];\n return acc;\n }, {});\n const hasGroups = Object.keys(grouped).some((k) => k !== '');\n\n return (\n ({\n label: ,\n value: option.value,\n })),\n ]}\n onValueChange={(value) => {\n if (value === null) {\n inputProps.onValueChange(undefined);\n return;\n }\n inputProps.onValueChange(value);\n }}\n value={currentValue}\n >\n \n \n \n \n {field.required ? null : (\n \n Not set\n \n )}\n {options.length === 0 && !isLoading ? (\n \n No options available\n \n ) : null}\n {hasGroups\n ? Object.entries(grouped).map(([groupLabel, groupOptions]) => (\n \n {groupLabel ? {groupLabel} : null}\n {groupOptions.map((option) => (\n \n \n \n ))}\n \n ))\n : options.map((option) => (\n \n \n \n ))}\n \n \n );\n}\n\nfunction ProviderOptionLabel({ option }: { option: DataProviderOption }) {\n const labelWithIcon = option.icon ? (\n \n svg]:h-full [&>svg]:w-full\">\n {option.icon}\n \n {option.label}\n \n ) : (\n {option.label}\n );\n\n if (!option.description) {\n return labelWithIcon;\n }\n return (\n \n {labelWithIcon}\n {option.description}\n \n );\n}\n\nexport { SelectFieldComponent };\n\nexport const selectFieldDefinition: FieldTypeDefinition = {\n name: 'select',\n priority: 12,\n match: (field) => {\n if (field.type !== 'select') {\n return false;\n }\n const optionCount = field.options?.length ?? 0;\n return optionCount > 3 && optionCount <= 8;\n },\n component: SelectFieldComponent,\n};\n\n/**\n * Second routing rule for the same `SelectFieldComponent`. Matches any\n * field annotated with `data_provider`, regardless of its underlying\n * proto type — a string field with `data_provider = AWS_REGIONS`\n * becomes a select populated from the hosting app's data-provider\n * registry. High priority so the annotation wins over the default\n * `string` / `password` / `email` matchers.\n *\n * The same component handles both rules; the routing split exists only\n * because static proto-enum selects and annotation-driven selects\n * match under different conditions.\n */\nexport const dataProviderSelectFieldDefinition: FieldTypeDefinition = {\n name: 'dataProviderSelect',\n priority: 120,\n match: (field) => {\n // Arrays / maps / objects keep their native renderers even when the\n // parent field is annotated with a data provider. Support for\n // array-of-strings multi-select from a data provider is a follow-up.\n if (field.type !== 'string' && field.type !== 'number') {\n return false;\n }\n return readDataProviderId(field) !== undefined;\n },\n component: SelectFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/shared.tsx", "content": "'use client';\n\nimport { format, isValid, parse } from 'date-fns';\nimport React from 'react';\n\nimport { Input } from '../../input';\nimport { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from '../../input-group';\nimport { useAutoFormRuntimeContext } from '../context';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig, NUMERIC_OPTION_PATTERN } from '../helpers';\nimport { getAutoFormChoiceTestId, getAutoFormFieldTestId } from '../test-ids';\nimport type { AutoFormOptionGroup, AutoFormOptionItem } from '../types';\n\n// ---------------------------------------------------------------------------\n// Label utilities\n// ---------------------------------------------------------------------------\n\nexport function getControlLabel(label: AutoFormFieldProps['label'], field: AutoFormFieldProps['field']): string {\n return typeof label === 'string' || typeof label === 'number'\n ? String(label)\n : String(field.fieldConfig?.label ?? field.key);\n}\n\n// ---------------------------------------------------------------------------\n// Test-id utilities\n// ---------------------------------------------------------------------------\n\nexport function useFieldTestIds(id: string) {\n const { testIdPrefix } = useAutoFormRuntimeContext();\n\n return React.useMemo(\n () => ({\n control: getAutoFormFieldTestId(testIdPrefix, id, 'control'),\n controlPart: (part: string) => getAutoFormFieldTestId(testIdPrefix, id, `control-${part}`),\n field: getAutoFormFieldTestId(testIdPrefix, id),\n group: (group: string | number) => getAutoFormChoiceTestId(testIdPrefix, id, 'group', group),\n option: (value: string | number) => getAutoFormChoiceTestId(testIdPrefix, id, 'option', value),\n selected: (value: string | number) => getAutoFormChoiceTestId(testIdPrefix, id, 'selected', value),\n }),\n [id, testIdPrefix]\n );\n}\n\nexport function resolveControlTestId(inputProps: AutoFormFieldProps['inputProps'], fallback: string): string {\n return typeof (inputProps as { testId?: unknown }).testId === 'string'\n ? ((inputProps as { testId: string }).testId ?? fallback)\n : fallback;\n}\n\n// ---------------------------------------------------------------------------\n// Date utilities\n// ---------------------------------------------------------------------------\n\nexport function normalizeDateValue(value: unknown): string {\n if (typeof value === 'string') {\n if (/([+-]\\d{2}:\\d{2}|Z)$/.test(value)) {\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? '' : format(parsed, 'yyyy-MM-dd');\n }\n\n return value.includes('T') ? (value.split('T')[0] ?? '') : value;\n }\n\n if (value instanceof Date && isValid(value)) {\n return format(value, 'yyyy-MM-dd');\n }\n\n return '';\n}\n\nexport function normalizeTimeValue(value: unknown): string {\n if (typeof value === 'string') {\n if (/([+-]\\d{2}:\\d{2}|Z)$/.test(value)) {\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? '' : format(parsed, 'HH:mm');\n }\n\n return value.includes('T') ? (value.split('T')[1]?.slice(0, 5) ?? '') : '';\n }\n\n if (value instanceof Date && isValid(value)) {\n return format(value, 'HH:mm');\n }\n\n return '';\n}\n\nexport function parseCalendarDate(value: string): Date | undefined {\n if (!value) {\n return;\n }\n\n const parsed = parse(value, 'yyyy-MM-dd', new Date());\n return isValid(parsed) ? parsed : undefined;\n}\n\nexport function buildTimestampValue(datePart: string, timePart: string): string {\n if (!datePart) {\n return '';\n }\n\n return `${datePart}T${timePart || '00:00'}`;\n}\n\n// ---------------------------------------------------------------------------\n// Number utilities\n// ---------------------------------------------------------------------------\n\nexport function parseNumericProp(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value : undefined;\n }\n\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n\n return;\n}\n\nexport function resolveNumericStep(inputProps: AutoFormFieldProps['inputProps'], fallbackValue?: number): number {\n const explicitStep = parseNumericProp(inputProps.step);\n if (explicitStep !== undefined && explicitStep > 0) {\n return explicitStep;\n }\n\n const values = [parseNumericProp(inputProps.min), parseNumericProp(inputProps.max), fallbackValue].filter(\n (value): value is number => value !== undefined\n );\n return values.some((value) => !Number.isInteger(value)) ? 0.01 : 1;\n}\n\nexport function normalizeNumberValue(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value : undefined;\n }\n\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n\n return;\n}\n\n// ---------------------------------------------------------------------------\n// Option utilities\n// ---------------------------------------------------------------------------\n\nexport function getGroupedOptions(field: AutoFormFieldProps['field']): AutoFormOptionGroup[] | undefined {\n return getFieldUiConfig(field).optionGroups;\n}\n\nexport function getFlatOptions(field: AutoFormFieldProps['field']): AutoFormOptionItem[] {\n const uiConfig = getFieldUiConfig(field);\n const labelOverrides = uiConfig.optionLabels;\n const optionGroups = getGroupedOptions(field);\n\n if (optionGroups?.length) {\n const options = optionGroups.flatMap((group) => group.options);\n if (labelOverrides) {\n return options.map((opt) => ({ ...opt, label: labelOverrides[opt.value] ?? opt.label }));\n }\n return options;\n }\n\n return (field.options || []).map(([value, label]) => ({\n value,\n label: labelOverrides?.[value] ?? label,\n }));\n}\n\nexport function renderOptionLabel(option: AutoFormOptionItem | { label?: React.ReactNode; value: string }) {\n return (\n \n {'icon' in option && option.icon ? {option.icon} : null}\n {option.label ?? option.value}\n \n );\n}\n\nexport function hasNumericOptions(field: AutoFormFieldProps['field']): boolean {\n const options = getFlatOptions(field);\n return options.length > 0 && options.every((option) => NUMERIC_OPTION_PATTERN.test(option.value));\n}\n\n// ---------------------------------------------------------------------------\n// Shared input component\n// ---------------------------------------------------------------------------\n\nexport function StringLikeInput({\n error,\n icon,\n id,\n inputProps,\n placeholder,\n testId,\n type = 'text',\n}: {\n error?: string;\n icon?: React.ReactNode;\n id: string;\n inputProps: AutoFormFieldProps['inputProps'];\n placeholder?: string;\n testId: string;\n type?: React.ComponentProps['type'];\n}) {\n if (icon) {\n return (\n \n \n {icon}\n \n inputProps.onValueChange(event.target.value)}\n placeholder={placeholder}\n ref={inputProps.ref}\n testId={`${testId}-input`}\n type={type}\n value={(inputProps.value as string | undefined) ?? ''}\n />\n \n );\n }\n\n return (\n inputProps.onValueChange(event.target.value)}\n placeholder={placeholder}\n ref={inputProps.ref}\n testId={testId}\n type={type}\n value={(inputProps.value as string | undefined) ?? ''}\n />\n );\n}\n\n/**\n * Extract the registered data-provider id from a field's customData.\n * Accepts both the flat `customData.dataProvider` shape and the proto-derived\n * `customData.ui.dataProvider` shape so authors can use either.\n */\nexport function readDataProviderId(field: AutoFormFieldProps['field'] | undefined): string | undefined {\n const customData = field?.fieldConfig?.customData;\n if (!(customData && typeof customData === 'object')) {\n return;\n }\n const bag = customData as { dataProvider?: unknown; ui?: { dataProvider?: unknown } };\n if (typeof bag.dataProvider === 'string') {\n return bag.dataProvider;\n }\n if (bag.ui && typeof bag.ui === 'object' && typeof bag.ui.dataProvider === 'string') {\n return bag.ui.dataProvider;\n }\n return;\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/slider.tsx", "content": "'use client';\n\nimport React from 'react';\nimport { Input } from '../../input';\nimport { Slider } from '../../slider';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { normalizeNumberValue, parseNumericProp, resolveNumericStep, useFieldTestIds } from './shared';\n\n/**\n * Slider widget — renders the track alongside a companion numeric\n * input. The track is for quick coarse adjustments; the input\n * handles precise entry (including tiny step sizes beyond a drag's\n * resolution) and keeps the value readable when the track is tiny\n * on narrow columns. Both controls bind to the same form value.\n */\nfunction SliderFieldComponent({ error, field, id, inputProps, label: fieldLabel }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const min = parseNumericProp(inputProps.min) ?? 0;\n const max = parseNumericProp(inputProps.max) ?? 100;\n const value = normalizeNumberValue(inputProps.value) ?? min;\n const step = resolveNumericStep(inputProps, value);\n const clamped = Math.min(max, Math.max(min, value));\n const label = typeof fieldLabel === 'string' || typeof fieldLabel === 'number' ? String(fieldLabel) : id;\n\n // Seed the form state with the slider's minimum on mount when the\n // field is undefined/null. Without this, the track renders at `min`\n // (thanks to the `?? min` fallback above) but the companion number\n // input shows as empty and the form value stays undefined — users\n // see a \"blank\" input even though the slider is clearly at zero.\n // Seeding once aligns both controls visually and makes 0 (or the\n // proto-declared min) an explicit starting value in the payload.\n const hasSeededRef = React.useRef(false);\n React.useEffect(() => {\n if (hasSeededRef.current) {\n return;\n }\n if (inputProps.value === undefined || inputProps.value === null) {\n hasSeededRef.current = true;\n inputProps.onValueChange(min, {\n shouldDirty: false,\n shouldTouch: false,\n shouldValidate: false,\n });\n }\n }, [inputProps, min]);\n\n return (\n
\n inputProps.onValueChange(nextValues[0] ?? min)}\n step={step}\n testId={testIds.controlPart('slider')}\n value={[clamped]}\n />\n {\n const nextValue = event.target.value;\n inputProps.onValueChange(nextValue === '' ? min : Number(nextValue));\n }}\n placeholder={getFieldUiConfig(field).placeholder}\n step={step}\n testId={testIds.controlPart('input')}\n type=\"number\"\n value={inputProps.value ?? clamped}\n />\n
\n );\n}\n\nexport { SliderFieldComponent };\n\nexport const sliderFieldDefinition: FieldTypeDefinition = {\n name: 'slider',\n priority: 15,\n /**\n * Slider is an opt-in widget — it renders only when the proto field\n * carries `field_ui.control = CONTROL_TYPE_SLIDER` (surfaced as\n * `customData.ui.control === 'slider'`). A numeric field with\n * `min`/`max` alone does NOT auto-promote to slider, because that\n * collides with the plain number renderer and produces the\n * \"slider + standalone number input\" double-render seen on\n * Max In Flight fields before this change. Proto drives the choice.\n */\n match: (field) => {\n if (field.type !== 'number') {\n return false;\n }\n const customData = field.fieldConfig?.customData;\n if (!(customData && typeof customData === 'object')) {\n return false;\n }\n const bag = customData as { control?: unknown; ui?: { control?: unknown } };\n if (bag.control === 'slider') {\n return true;\n }\n if (bag.ui && typeof bag.ui === 'object' && bag.ui.control === 'slider') {\n return true;\n }\n return false;\n },\n component: SliderFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/string.tsx", "content": "'use client';\n\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { StringLikeInput, useFieldTestIds } from './shared';\n\nfunction StringFieldComponent(props: AutoFormFieldProps) {\n const testIds = useFieldTestIds(props.id);\n\n return (\n \n );\n}\n\nexport { StringFieldComponent };\n\nexport const stringFieldDefinition: FieldTypeDefinition = {\n name: 'string',\n priority: 10,\n match: (field) => field.type === 'string',\n component: StringFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/textarea.tsx", "content": "'use client';\n\nimport { Textarea } from '../../textarea';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig, LONG_TEXT_FIELD_PATTERN } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { useFieldTestIds } from './shared';\n\nfunction TextareaFieldComponent({ error, field, id, inputProps }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n\n return (\n inputProps.onValueChange(event.target.value)}\n placeholder={getFieldUiConfig(field).placeholder}\n resize=\"vertical\"\n testId={testIds.control}\n value={(inputProps.value as string | undefined) ?? ''}\n />\n );\n}\n\nexport { TextareaFieldComponent };\n\nexport const textareaFieldDefinition: FieldTypeDefinition = {\n name: 'textarea',\n priority: 15,\n match: (field, context) =>\n field.type === 'string' &&\n (context.inputType === 'textarea' || context.maxLength > 120 || LONG_TEXT_FIELD_PATTERN.test(context.identity)),\n component: TextareaFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/toggle-group.tsx", "content": "'use client';\n\nimport { ToggleGroup, ToggleGroupItem } from '../../toggle-group';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { getControlLabel, getFlatOptions, hasNumericOptions, renderOptionLabel, useFieldTestIds } from './shared';\n\nfunction ToggleGroupFieldComponent({ error, field, id, inputProps, label }: AutoFormFieldProps) {\n const testIds = useFieldTestIds(id);\n const numericOptions = hasNumericOptions(field);\n const value = inputProps.value === undefined || inputProps.value === null ? '' : String(inputProps.value);\n const options = getFlatOptions(field);\n\n return (\n inputProps.onValueChange(numericOptions ? Number(nextValue) : nextValue)}\n testId={testIds.control}\n type=\"single\" // Multi-select not supported yet — use multiselect field type instead\n value={value}\n variant=\"outline\"\n >\n {options.map((option) => (\n \n {renderOptionLabel(option)}\n \n ))}\n \n );\n}\n\nexport { ToggleGroupFieldComponent };\n\nexport const toggleGroupFieldDefinition: FieldTypeDefinition = {\n name: 'toggleGroup',\n priority: 25,\n match: (field) => {\n if (field.type !== 'select') {\n return false;\n }\n return getFieldUiConfig(field).control === 'toggleGroup';\n },\n component: ToggleGroupFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/fields/url.tsx", "content": "'use client';\n\nimport { Link2Icon } from 'lucide-react';\nimport type { AutoFormFieldProps } from '../core-types';\nimport { getFieldUiConfig, URL_FIELD_PATTERN } from '../helpers';\nimport type { FieldTypeDefinition } from '../registry';\nimport { StringLikeInput, useFieldTestIds } from './shared';\n\nfunction UrlFieldComponent(props: AutoFormFieldProps) {\n const testIds = useFieldTestIds(props.id);\n\n return (\n }\n id={props.id}\n inputProps={props.inputProps}\n placeholder={getFieldUiConfig(props.field).placeholder || 'https://'}\n testId={testIds.control}\n type=\"url\"\n />\n );\n}\n\nexport { UrlFieldComponent };\n\nexport const urlFieldDefinition: FieldTypeDefinition = {\n name: 'url',\n priority: 20,\n match: (field, context) =>\n field.type === 'string' && (context.inputType === 'url' || URL_FIELD_PATTERN.test(context.identity)),\n component: UrlFieldComponent,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/form-spacing.ts", "content": "/**\n * Layout spacing tokens for AutoForm primitives.\n * Single source of truth for every vertical rhythm the form emits.\n * Names kept generic so the module ports cleanly to shadcn upstream.\n */\nexport const formSpacing = {\n /** Top-level form children: sections, root fields, submit slot. */\n form: 'space-y-8',\n /** Sibling fields inside a section or nested field-list. */\n field: 'space-y-6',\n /** Inside a single field: label → control → help/error. */\n labelStack: 'space-y-2',\n /** Inside a section header: title → description. */\n sectionHeader: 'space-y-1',\n /** Divider under a section heading when shown. */\n sectionDivider: 'pb-4 border-b border-border/60',\n /** Separator between array items (applied to every item except the first). */\n arrayItemSeparator: 'pt-4 border-t border-border/60',\n /** Gap between sibling rows inside an array/map body (before separator). */\n collectionRow: 'space-y-4',\n /** Gap between a oneof selector and the field it reveals. */\n oneofStack: 'space-y-4',\n} as const;\n\nexport type FormSpacingToken = keyof typeof formSpacing;\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/helpers.ts", "content": "import { getFieldHints } from '../../lib/core';\n\nimport type { ParsedField, SchemaValidation } from './core-types';\nimport { getLabel, getPathInObject, sortFieldsByOrder } from './field-utils';\nimport type { AutoFormOptionGroup, AutoFormOptionItem, AutoFormUiRule, FieldTypes } from './types';\n\nexport const NUMERIC_OPTION_PATTERN = /^-?\\d+$/;\nexport const FIELD_MASK_PATH_SPLIT_PATTERN = /[\\n,]/;\n\nexport const SECRET_FIELD_PATTERN = /(password|secret|token|api[_-]?key|private[_-]?key|credential)/i;\nexport const CONSENT_FIELD_PATTERN = /(accept|agree|consent|terms|policy|opt[-_ ]?in)/i;\nexport const URL_FIELD_PATTERN = /(url|uri|website|homepage|link)/i;\nexport const EMAIL_FIELD_PATTERN = /(email|e-mail)/i;\nexport const CURRENCY_FIELD_PATTERN = /(amount|price|cost|balance|budget|revenue|salary|subtotal|total)/i;\nexport const LONG_TEXT_FIELD_PATTERN = /(bio|description|details|notes?|summary|message|comment)/i;\n\nexport function isRecord(value: unknown): value is Record {\n return Boolean(value && typeof value === 'object' && !Array.isArray(value));\n}\n\nfunction toUiRules(value: unknown): AutoFormUiRule[] | undefined {\n if (!Array.isArray(value)) {\n return;\n }\n\n const rules: AutoFormUiRule[] = [];\n\n for (const rule of value) {\n if (!isRecord(rule) || typeof rule.expression !== 'string') {\n continue;\n }\n\n rules.push({\n id: typeof rule.id === 'string' ? rule.id : undefined,\n expression: rule.expression,\n message: typeof rule.message === 'string' ? rule.message : undefined,\n });\n }\n\n return rules.length > 0 ? rules : undefined;\n}\n\nfunction toOptionItem(value: unknown): AutoFormOptionItem | undefined {\n if (!isRecord(value) || typeof value.value !== 'string') {\n return;\n }\n\n return {\n value: value.value,\n label: typeof value.label === 'string' || typeof value.label === 'number' ? String(value.label) : undefined,\n icon: value.icon as AutoFormOptionItem['icon'],\n };\n}\n\nfunction toOptionGroups(value: unknown): AutoFormOptionGroup[] | undefined {\n if (!Array.isArray(value)) {\n return;\n }\n\n const groups: AutoFormOptionGroup[] = [];\n\n for (const candidate of value) {\n if (!(isRecord(candidate) && Array.isArray(candidate.options))) {\n continue;\n }\n\n const options = candidate.options\n .map(toOptionItem)\n .filter((option): option is AutoFormOptionItem => Boolean(option));\n if (options.length === 0) {\n continue;\n }\n\n groups.push({\n label:\n typeof candidate.label === 'string' || typeof candidate.label === 'number'\n ? String(candidate.label)\n : undefined,\n options,\n });\n }\n\n return groups.length > 0 ? groups : undefined;\n}\n\n/**\n * Derive the field-type name from the annotation set so the AutoForm\n * resolver picks the right widget even when a proto `control` is also\n * set. Order:\n * 1. `dataProvider` wins — the field is an opinionated dropdown.\n * 2. `dropzone` + `control === 'json'` wins — the field is a JSON\n * editor with drag-and-drop.\n * 3. Otherwise, `undefined` so the existing control/type path resolves.\n *\n * Reads from all three UI sources (proto, nested customData.ui, direct\n * customData) so either side can trigger the override.\n */\ninterface AnnotatedUi {\n control?: unknown;\n dataProvider?: unknown;\n dropzone?: unknown;\n}\n\n/**\n * `deriveAnnotatedControl` must NOT run for complex field types\n * (array / map / object). If it did, a `repeated string` field\n * annotated with `data_provider` — e.g. `OpenAPI.filter.include_methods`\n * — would short-circuit to `dataProviderSelect` on the array field\n * itself, bypassing `ArrayFieldRenderer`'s Add-button UI entirely.\n * The annotation still flows through to the array's schema[0] item\n * template (same proto descriptor), so each added item picks up\n * `dataProviderSelect` on its own.\n */\nfunction isComplexFieldType(type: unknown): boolean {\n return type === 'array' || type === 'map' || type === 'object';\n}\n\nfunction deriveAnnotatedControl(\n fieldType: unknown,\n protoUi: AnnotatedUi | undefined,\n nested: AnnotatedUi | undefined,\n direct: AnnotatedUi | undefined\n): FieldTypes | undefined {\n if (isComplexFieldType(fieldType)) {\n return;\n }\n\n const dataProvider =\n (typeof direct?.dataProvider === 'string' && direct.dataProvider) ||\n (typeof nested?.dataProvider === 'string' && nested.dataProvider) ||\n (typeof protoUi?.dataProvider === 'string' && protoUi.dataProvider) ||\n undefined;\n if (dataProvider) {\n return 'dataProviderSelect' as FieldTypes;\n }\n\n const dropzone = direct?.dropzone === true || nested?.dropzone === true || protoUi?.dropzone === true;\n const control = direct?.control ?? nested?.control ?? protoUi?.control;\n if (dropzone && control === 'json') {\n return 'dropzone-json' as FieldTypes;\n }\n\n return;\n}\n\nexport function getFieldUiConfig(field: ParsedField): {\n control?: FieldTypes;\n placeholder?: string;\n example?: string;\n help?: string;\n description?: string;\n visibleWhen?: AutoFormUiRule[];\n disabledWhen?: AutoFormUiRule[];\n summaryLabel?: string;\n optionGroups?: AutoFormOptionGroup[];\n optionLabels?: Record;\n} {\n const protoUi = getFieldHints(field);\n const customData = isRecord(field.fieldConfig?.customData) ? field.fieldConfig.customData : undefined;\n const nestedUi = customData && isRecord(customData.ui) ? customData.ui : undefined;\n\n const direct = customData\n ? {\n control: typeof customData.control === 'string' ? (customData.control as FieldTypes) : undefined,\n placeholder: typeof customData.placeholder === 'string' ? customData.placeholder : undefined,\n example: typeof customData.example === 'string' ? customData.example : undefined,\n help: typeof customData.help === 'string' ? customData.help : undefined,\n description: typeof customData.description === 'string' ? customData.description : undefined,\n visibleWhen: toUiRules(customData.visibleWhen),\n disabledWhen: toUiRules(customData.disabledWhen),\n summaryLabel: typeof customData.summaryLabel === 'string' ? customData.summaryLabel : undefined,\n optionGroups: toOptionGroups(customData.optionGroups),\n optionLabels: isRecord(customData.optionLabels)\n ? (customData.optionLabels as Record)\n : undefined,\n }\n : undefined;\n\n const nested = nestedUi\n ? {\n control: typeof nestedUi.control === 'string' ? (nestedUi.control as FieldTypes) : undefined,\n placeholder: typeof nestedUi.placeholder === 'string' ? nestedUi.placeholder : undefined,\n example: typeof nestedUi.example === 'string' ? nestedUi.example : undefined,\n help: typeof nestedUi.help === 'string' ? nestedUi.help : undefined,\n description: typeof nestedUi.description === 'string' ? nestedUi.description : undefined,\n visibleWhen: toUiRules(nestedUi.visibleWhen),\n disabledWhen: toUiRules(nestedUi.disabledWhen),\n summaryLabel: typeof nestedUi.summaryLabel === 'string' ? nestedUi.summaryLabel : undefined,\n optionGroups: toOptionGroups(nestedUi.optionGroups),\n optionLabels: isRecord(nestedUi.optionLabels) ? (nestedUi.optionLabels as Record) : undefined,\n }\n : undefined;\n\n // Proto-level widget annotations (data_provider, dropzone) override the\n // plain control. A string field annotated with `data_provider` should\n // render as `dataProviderSelect` regardless of any `CONTROL_TYPE_TEXT`\n // default; a JSON field annotated with `dropzone: true` should render\n // as `dropzone-json` rather than the default JSON editor. Keeping this\n // override inside `getFieldUiConfig` means the existing `resolveFieldType`\n // short-circuit (which prefers explicit control over the registry) stays\n // intact while still selecting the right widget.\n const annotatedControl = deriveAnnotatedControl(field.type, protoUi, nested, direct);\n\n return {\n ...(protoUi ?? {}),\n ...(nested ?? {}),\n ...(direct ?? {}),\n control:\n (typeof field.fieldConfig?.fieldType === 'string'\n ? (field.fieldConfig.fieldType as FieldTypes)\n : undefined) ||\n annotatedControl ||\n direct?.control ||\n nested?.control ||\n (protoUi?.control as FieldTypes | undefined),\n placeholder:\n (typeof field.fieldConfig?.inputProps?.placeholder === 'string'\n ? (field.fieldConfig.inputProps.placeholder as string)\n : undefined) ||\n direct?.placeholder ||\n nested?.placeholder ||\n protoUi?.placeholder,\n example: direct?.example || nested?.example || protoUi?.example,\n help: direct?.help || nested?.help || protoUi?.help,\n description: direct?.description || nested?.description || protoUi?.description,\n visibleWhen: direct?.visibleWhen || nested?.visibleWhen || toUiRules(protoUi?.visibleWhen),\n disabledWhen: direct?.disabledWhen || nested?.disabledWhen || toUiRules(protoUi?.disabledWhen),\n summaryLabel: direct?.summaryLabel || nested?.summaryLabel || protoUi?.summaryLabel,\n optionGroups: direct?.optionGroups || nested?.optionGroups,\n optionLabels: direct?.optionLabels || nested?.optionLabels,\n };\n}\n\nexport function getRootErrorMessage(rootError: unknown): string | undefined {\n if (!rootError) {\n return;\n }\n\n if (typeof rootError === 'string') {\n return rootError;\n }\n\n if (typeof rootError === 'object') {\n const errorRecord = rootError as Record;\n if (typeof errorRecord.message === 'string') {\n return errorRecord.message;\n }\n\n return Object.values(errorRecord)\n .map((value) => getRootErrorMessage(value))\n .filter((message): message is string => Boolean(message))\n .join('\\n');\n }\n\n return;\n}\n\nexport function getFieldErrorMessage(errors: unknown, path: string[]): string | undefined {\n const nestedError = getPathInObject(errors as Record, path);\n const message = nestedError?.message;\n return typeof message === 'string' ? message : undefined;\n}\n\nexport function createEmptyFieldValue(field: ParsedField | undefined): unknown {\n if (!field) {\n return;\n }\n\n const protoData = getFieldHints(field);\n\n switch (field.type) {\n case 'string':\n case 'bytes':\n case 'duration':\n case 'int64':\n case 'timestamp':\n return '';\n case 'number':\n return;\n case 'boolean':\n return protoData?.supportsUnset && !field.required ? undefined : false;\n case 'select':\n if (field.required && field.options?.length) {\n const firstOptionValue = field.options[0]?.[0];\n return firstOptionValue ? Number(firstOptionValue) || firstOptionValue : undefined;\n }\n return undefined;\n case 'fieldMask':\n return [];\n case 'json':\n switch (protoData?.jsonKind) {\n case 'listValue':\n return [];\n case 'any':\n return { typeUrl: '', valueBase64: '' };\n default:\n return {};\n }\n case 'array':\n return [];\n case 'map':\n return [];\n case 'oneof':\n return { case: undefined, value: undefined };\n case 'object':\n return {};\n case 'date':\n return '';\n default:\n return;\n }\n}\n\nfunction isKeyValueScalarField(field: ParsedField | undefined): boolean {\n if (!field) {\n return false;\n }\n\n const renderType = resolveRenderFieldType(field);\n return ['string', 'email', 'url', 'password', 'currency', 'number', 'int64', 'select', 'combobox'].includes(\n renderType\n );\n}\n\nfunction isSimpleKeyValueLikeObject(field: ParsedField | undefined): boolean {\n if (!(field?.type === 'object' && field.schema?.length === 2)) {\n return false;\n }\n\n const keyField = field.schema.find((candidate) => candidate.key === 'key');\n const valueField = field.schema.find((candidate) => candidate.key === 'value');\n return Boolean(keyField && valueField && isKeyValueScalarField(keyField) && isKeyValueScalarField(valueField));\n}\n\nexport function resolveRenderFieldType(\n field: ParsedField\n): FieldTypes {\n const uiConfig = getFieldUiConfig(field);\n if (uiConfig.control) {\n return uiConfig.control;\n }\n\n const label = String(field.fieldConfig?.label ?? getLabel(field));\n const identity = `${field.key} ${label}`.toLowerCase();\n const inputType = String(field.fieldConfig?.inputProps?.type ?? getFieldHints(field)?.inputType ?? '');\n const maxLength = Number(field.fieldConfig?.inputProps?.maxLength ?? 0);\n\n if (field.type === 'boolean') {\n const protoData = getFieldHints(field);\n if (protoData?.supportsUnset && !field.required) {\n return 'boolean';\n }\n if (CONSENT_FIELD_PATTERN.test(identity)) {\n return 'checkbox';\n }\n return 'switch';\n }\n\n if (field.type === 'select') {\n const optionCount = field.options?.length ?? 0;\n if (optionCount > 8) {\n return 'combobox';\n }\n if (optionCount > 0 && optionCount <= 3) {\n return 'radio';\n }\n return 'select';\n }\n\n if (field.type === 'string') {\n if (SECRET_FIELD_PATTERN.test(identity) || inputType === 'password') {\n return 'password';\n }\n if (inputType === 'email' || EMAIL_FIELD_PATTERN.test(identity)) {\n return 'email';\n }\n if (inputType === 'url' || URL_FIELD_PATTERN.test(identity)) {\n return 'url';\n }\n if (CURRENCY_FIELD_PATTERN.test(identity)) {\n return 'currency';\n }\n if (inputType === 'textarea' || maxLength > 120 || LONG_TEXT_FIELD_PATTERN.test(identity)) {\n return 'textarea';\n }\n }\n\n if (field.type === 'number') {\n const min = Number(field.fieldConfig?.inputProps?.min);\n const max = Number(field.fieldConfig?.inputProps?.max);\n\n // NOTE: `sliderFieldDefinition.match` no longer auto-promotes number\n // fields with min/max to the slider widget — slider is opt-in via the\n // proto `control = CONTROL_TYPE_SLIDER` annotation (see\n // `fields/slider.tsx`). This fallback resolver still returns 'slider'\n // for bounded numbers because `buildFallbackHelp` below reads it to\n // suppress a redundant range hint; the actual rendered widget is\n // decided by the registry + `getFieldUiConfig.control` override, not\n // this function. If you're trying to \"fix\" a number field that's\n // rendering as plain when you expected a slider, add the proto\n // annotation — don't change this branch.\n if (Number.isFinite(min) && Number.isFinite(max)) {\n return 'slider';\n }\n }\n\n if (field.type === 'array') {\n const itemField = field.schema?.[0];\n if (itemField?.type === 'select' && itemField.options?.length) {\n return 'multiselect';\n }\n if (isSimpleKeyValueLikeObject(itemField)) {\n return 'keyValue';\n }\n }\n\n if (field.type === 'map') {\n const keyField = field.schema?.[0];\n const valueField = field.schema?.[1];\n if (isKeyValueScalarField(keyField) && isKeyValueScalarField(valueField)) {\n return 'keyValue';\n }\n }\n\n return field.type as FieldTypes;\n}\n\nfunction buildRangeHint(field: ParsedField): string | undefined {\n const min = field.fieldConfig?.inputProps?.min;\n const max = field.fieldConfig?.inputProps?.max;\n\n // Number.isFinite() matches resolveRenderFieldType's check — NaN/Infinity\n // passing as `number` would otherwise produce nonsense like\n // \"Accepted range: NaN to Infinity\".\n if (Number.isFinite(min) && Number.isFinite(max)) {\n return `Accepted range: ${min} to ${max}.`;\n }\n if (Number.isFinite(min)) {\n return `Accepted minimum: ${min}.`;\n }\n if (Number.isFinite(max)) {\n return `Accepted maximum: ${max}.`;\n }\n\n return;\n}\n\nfunction buildFallbackHelp(field: ParsedField): string {\n const renderType = resolveRenderFieldType(field);\n const hints = [\n field.fieldConfig?.inputProps?.pattern ? 'Follow the expected format for this value.' : undefined,\n renderType === 'multiselect' ? 'Choose one or more options.' : undefined,\n renderType === 'radio' || renderType === 'select' || renderType === 'combobox'\n ? 'Choose one of the available options.'\n : undefined,\n renderType === 'slider' ? undefined : buildRangeHint(field),\n renderType === 'keyValue' ? 'Add one or more key-value pairs.' : undefined,\n renderType === 'json' ? 'Provide valid JSON for this field.' : undefined,\n field.type === 'duration' ? 'Use protobuf duration syntax like 300s.' : undefined,\n field.type === 'fieldMask' ? 'Enter one field path per line, or separate them with commas.' : undefined,\n ].filter((hint): hint is string => Boolean(hint));\n\n return hints[0] ?? '';\n}\n\n/**\n * Tooltip text (shown when hovering the info icon).\n * Pulls from proto `help` annotation. Only shown when it adds information\n * beyond what's already visible in the inline description.\n */\nexport function getFieldHelpText(field: ParsedField): string {\n const uiConfig = getFieldUiConfig(field);\n const descriptionText = getFieldDescriptionText(field);\n\n // Build the tooltip from help + example\n const parts = [uiConfig.help, uiConfig.example ? `Example: ${uiConfig.example}` : undefined].filter(\n (value): value is string => Boolean(value)\n );\n\n const tooltip = [...new Set(parts)].join(' ');\n\n // If tooltip would be identical to the inline description, suppress it\n // so the info icon doesn't appear redundantly.\n if (tooltip && tooltip === descriptionText) {\n return '';\n }\n\n return tooltip;\n}\n\n/**\n * Upstream docs URL annotated on the field. Surfaces as a \"Learn more\"\n * anchor in the field's description slot — so model catalogs, region\n * lists, and API parameter references always point at the vendor's\n * live list rather than a static snapshot maintained inside this repo.\n */\nexport function getFieldDocsUrl(field: ParsedField): string | undefined {\n const customData = field.fieldConfig?.customData;\n if (!(customData && typeof customData === 'object')) {\n return;\n }\n const bag = customData as { docsUrl?: unknown; ui?: { docsUrl?: unknown } };\n if (typeof bag.docsUrl === 'string' && bag.docsUrl) {\n return bag.docsUrl;\n }\n if (bag.ui && typeof bag.ui === 'object' && typeof bag.ui.docsUrl === 'string' && bag.ui.docsUrl) {\n return bag.ui.docsUrl;\n }\n return;\n}\n\n/**\n * Inline description text (shown directly below the input field).\n * Prefers proto `description` annotation (concise one-liner), falls back to\n * `help`, then to auto-generated fallback hints.\n */\nexport function getFieldDescriptionText(field: ParsedField): string | undefined {\n const uiConfig = getFieldUiConfig(field);\n\n // 1. Prefer explicit proto `description` annotation\n if (uiConfig.description) {\n return uiConfig.description;\n }\n\n // 2. Fall back to fieldConfig.description (set programmatically)\n const configDescription =\n typeof field.fieldConfig?.description === 'string' ? field.fieldConfig.description : undefined;\n if (configDescription) {\n return configDescription;\n }\n\n // 3. Fall back to example. `help` remains exclusive to the tooltip so\n // the question-mark affordance does not disappear when no description is set.\n if (uiConfig.example) {\n return `Example: ${uiConfig.example}`;\n }\n\n // 4. Fall back to auto-generated hints\n const fallback = buildFallbackHelp(field);\n return fallback.length > 0 ? fallback : undefined;\n}\n\nfunction hasSimpleRequiredCount(field: ParsedField): boolean {\n const hints = getFieldHints(field);\n return Boolean((hints?.minItems ?? 0) > 0 || (hints?.minPairs ?? 0) > 0);\n}\n\n/**\n * Default classification: required fields are \"simple\", optional fields are \"advanced\".\n * Can be overridden via explicit `advanced` metadata in field config or the `classifyField` prop.\n */\nexport function defaultClassifyField(field: ParsedField): 'simple' | 'advanced' {\n const customData = isRecord(field.fieldConfig?.customData) ? field.fieldConfig.customData : undefined;\n if (customData?.advanced === true) {\n return 'advanced';\n }\n if (customData?.advanced === false) {\n return 'simple';\n }\n return field.required || hasSimpleRequiredCount(field) ? 'simple' : 'advanced';\n}\n\nexport function deriveSimpleFields(\n fields: ParsedField[] | undefined,\n classifyField: (field: ParsedField) => 'simple' | 'advanced' = defaultClassifyField\n): ParsedField[] {\n if (!fields) {\n return [];\n }\n\n return sortFieldsByOrder(\n fields.flatMap((field) => {\n const simpleChildren = deriveSimpleFields(field.schema, classifyField);\n const hasRequiredDescendants = simpleChildren.length > 0;\n const classification = classifyField(field);\n const isSimple = classification === 'simple';\n\n if (field.type === 'object') {\n if (isSimple && !hasRequiredDescendants && field.schema?.length) {\n return [{ ...field, schema: field.schema }];\n }\n if (isSimple || hasRequiredDescendants) {\n return [{ ...field, schema: hasRequiredDescendants ? simpleChildren : field.schema }];\n }\n return [];\n }\n\n if (field.type === 'oneof') {\n if (isSimple || hasRequiredDescendants) {\n return [{ ...field, schema: field.schema }];\n }\n return [];\n }\n\n if (field.type === 'array' || field.type === 'map') {\n if (isSimple || hasRequiredDescendants) {\n return [{ ...field, schema: hasRequiredDescendants ? simpleChildren : field.schema }];\n }\n return [];\n }\n\n return isSimple ? [field] : [];\n })\n );\n}\n\nexport function collectLeafFieldPaths(fields: ParsedField[], path: string[] = []): string[] {\n return fields.flatMap((field) => {\n const nextPath = [...path, field.key];\n const renderType = resolveRenderFieldType(field);\n\n if (renderType === 'object' || renderType === 'array' || renderType === 'map' || renderType === 'oneof') {\n return field.schema?.length ? collectLeafFieldPaths(field.schema, nextPath) : [nextPath.join('.')];\n }\n\n return [nextPath.join('.')];\n });\n}\n\nexport function filterFieldsByPaths(fields: ParsedField[], paths: string[], currentPath: string[] = []): ParsedField[] {\n if (paths.length === 0) {\n return [];\n }\n\n return sortFieldsByOrder(\n fields.flatMap((field) => {\n const nextPath = [...currentPath, field.key];\n const fullPath = nextPath.join('.');\n const matchesDirectly = paths.some((path) => path === fullPath);\n const hasDescendantMatch = paths.some((path) => path.startsWith(`${fullPath}.`));\n\n if (!(matchesDirectly || hasDescendantMatch)) {\n return [];\n }\n\n if (!field.schema?.length || matchesDirectly) {\n return [\n {\n ...field,\n schema: matchesDirectly ? field.schema : filterFieldsByPaths(field.schema ?? [], paths, nextPath),\n },\n ];\n }\n\n return [{ ...field, schema: filterFieldsByPaths(field.schema, paths, nextPath) }];\n })\n );\n}\n\nexport function projectValuesToFields(values: Record, fields: ParsedField[]): Record {\n const projected: Record = {};\n\n for (const field of fields) {\n const value = values[field.key];\n if (value === undefined) {\n continue;\n }\n\n if (field.type === 'object' && isRecord(value) && field.schema?.length) {\n projected[field.key] = projectValuesToFields(value, field.schema);\n continue;\n }\n\n projected[field.key] = value;\n }\n\n return projected;\n}\n\nexport function isMeaningfulValue(value: unknown): boolean {\n if (value === undefined || value === null) {\n return false;\n }\n if (typeof value === 'string') {\n return value.trim().length > 0;\n }\n if (Array.isArray(value)) {\n return value.length > 0;\n }\n if (isRecord(value)) {\n return Object.values(value).some((entry) => isMeaningfulValue(entry));\n }\n return true;\n}\n\nexport function stringifySummaryValue(value: unknown): string {\n if (value === undefined || value === null) {\n return '—';\n }\n if (typeof value === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint') {\n return String(value);\n }\n if (Array.isArray(value)) {\n return `${value.length} item${value.length === 1 ? '' : 's'}`;\n }\n if (isRecord(value)) {\n return `${Object.keys(value).length} field${Object.keys(value).length === 1 ? '' : 's'}`;\n }\n return String(value);\n}\n\nexport function flattenSummaryEntries(\n payload: unknown,\n prefix = ''\n): Array<{ key: string; value: unknown; isComplex: boolean }> {\n if (!isRecord(payload)) {\n return [];\n }\n\n return Object.entries(payload).flatMap(([key, value]) => {\n const nextKey = prefix ? `${prefix}.${key}` : key;\n if (!isMeaningfulValue(value)) {\n return [];\n }\n\n if (isRecord(value)) {\n const nested = flattenSummaryEntries(value, nextKey);\n return nested.length > 0 ? nested : [{ key: nextKey, value, isComplex: true }];\n }\n\n if (Array.isArray(value) && value.some((entry) => isRecord(entry) || Array.isArray(entry))) {\n return [{ key: nextKey, value, isComplex: true }];\n }\n\n return [{ key: nextKey, value, isComplex: Array.isArray(value) }];\n });\n}\n\nexport function isValidationSuccess(\n validation: SchemaValidation\n): validation is Extract {\n return validation.success;\n}\n\nexport function normalizeKeyValueEntries(value: unknown): Array<{ key: string; value: string }> {\n if (!Array.isArray(value)) {\n return [];\n }\n\n return value.map((entry) => {\n if (!isRecord(entry)) {\n return { key: '', value: '' };\n }\n\n return {\n key: entry.key === undefined || entry.key === null ? '' : String(entry.key),\n value: entry.value === undefined || entry.value === null ? '' : String(entry.value),\n };\n });\n}\n\nexport function denormalizeKeyValueEntries(\n entries: Array<{ key: string; value: string }>,\n field: ParsedField\n): Array<{ key: unknown; value: unknown }> {\n const valueField =\n field.type === 'map'\n ? field.schema?.[1]\n : field.schema?.[0]?.schema?.find((candidate) => candidate.key === 'value');\n\n return entries.map((entry) => ({\n key: entry.key,\n value: normalizeCollectionScalar(entry.value, valueField),\n }));\n}\n\nfunction normalizeCollectionScalar(value: string, field: ParsedField | undefined): unknown {\n if (!field) {\n return value;\n }\n\n const renderType = resolveRenderFieldType(field);\n if (renderType === 'number') {\n return value === '' ? undefined : Number(value);\n }\n if (renderType === 'int64') {\n return value;\n }\n if (renderType === 'select' && field.options?.every(([optionValue]) => NUMERIC_OPTION_PATTERN.test(optionValue))) {\n return value === '' ? undefined : Number(value);\n }\n return value;\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/auto-form-core.tsx", "content": "'use client';\n\nimport {\n createUpdateMask,\n formValuesToProto,\n preserveProtoMessageSource,\n} from '../../lib/protobuf-provider';\nimport React from 'react';\n\nimport { Alert, AlertDescription, AlertTitle } from '@/registry/base-nova/protoform/components/alert';\nimport { TooltipProvider } from '@/registry/base-nova/protoform/components/tooltip';\nimport { Heading, Text } from '@/registry/base-nova/protoform/components/typography';\n\nimport type { SchemaValidation } from './core-types';\nimport type { AutoFormEngine } from './engine';\nimport {\n ArrayElementWrapper,\n ArrayWrapper,\n ErrorMessage,\n FieldWrapper,\n Form,\n ObjectWrapper,\n SubmitButton,\n} from './field-wrapper';\nimport { AutoFormFieldComponentRegistry } from './fields';\nimport { deriveSimpleFields } from './helpers';\nimport { AutoFormModeShell } from './mode-shell';\nimport {\n getProtoMessageUiConfig,\n isProtoMessageDescriptor,\n isProtoProvider,\n PROTO_FORM_ROOT_ERROR_KEY,\n resolveProtoSourceMessage,\n} from './proto';\nimport { AutoFormFields } from './renderers';\nimport { AutoFormRuntimeProvider } from './runtime-provider';\nimport {\n mergeFieldOverrides,\n normalizeProtoInitialValues,\n protoConversionOptionsFromFieldConfig,\n resolveSchema,\n} from './schema';\nimport { AutoFormStepPanel, fieldsForStep, initialStepIndex, validateSteps } from './stepper';\nimport { buildAutoFormTestId, resolveAutoFormTestIdPrefix } from './test-ids';\nimport type {\n AutoFormMode,\n AutoFormProps,\n AutoFormRevalidationMode,\n AutoFormStepperConfig,\n AutoFormSubmitContext,\n AutoFormValidationMode,\n ResolvedSchema,\n} from './types';\nimport { normalizeModes, resolveInitialMode } from './utils/modes';\n\nconst noopOnSubmit = async () => undefined;\n\nconst ShadcnUIComponents = {\n Form,\n FieldWrapper,\n ErrorMessage,\n SubmitButton,\n ObjectWrapper,\n ArrayWrapper,\n ArrayElementWrapper,\n};\n\nexport const ShadcnAutoFormFieldComponents = AutoFormFieldComponentRegistry;\n\nexport type AutoFormEngineRender = (props: {\n children: (engine: AutoFormEngine) => React.ReactNode;\n defaultValues: Record;\n validateSchema: (\n values: Record,\n signal: AbortSignal\n ) => Promise;\n values?: Record;\n}) => React.ReactNode;\n\nexport type AutoFormCoreProps<\n T extends Record,\n TNativeForm,\n TCustomFieldType extends string = never,\n> = Omit<\n AutoFormProps,\n 'formOptions' | 'resolver'\n> & {\n renderEngine: AutoFormEngineRender;\n};\n\nfunction renderModeContent({\n fields,\n testIdPrefix,\n withSubmit,\n children,\n SubmitButtonComponent,\n stepper,\n currentStepIndex,\n onStepBack,\n onStepContinue,\n isAdvancing,\n isSubmitting,\n}: {\n fields: ReturnType;\n testIdPrefix: string;\n withSubmit: boolean;\n children: React.ReactNode;\n SubmitButtonComponent: React.ComponentType<{\n children: React.ReactNode;\n disabled?: boolean;\n testId?: string;\n }>;\n stepper?: AutoFormStepperConfig;\n currentStepIndex: number;\n onStepBack: () => void;\n onStepContinue: (fields: ReturnType) => void | Promise;\n isAdvancing: boolean;\n isSubmitting: boolean;\n}) {\n if (stepper) {\n const step = stepper.steps[currentStepIndex];\n if (!step) {\n return null;\n }\n const stepFields = fieldsForStep(fields, stepper.steps, step.id);\n return (\n onStepContinue(stepFields)}\n orientation={stepper.orientation ?? 'horizontal'}\n step={step}\n steps={stepper.steps}\n submit={\n withSubmit ? (\n \n {isSubmitting ? 'Submitting…' : 'Submit'}\n \n ) : null\n }\n >\n {children}\n \n );\n }\n\n return (\n <>\n {children}\n {withSubmit ? (\n \n \n {isSubmitting ? 'Submitting…' : 'Submit'}\n \n \n ) : null}\n \n );\n}\n\ntype AutoFormContentProps<\n T extends Record,\n TNativeForm,\n TCustomFieldType extends string,\n> = Omit<\n AutoFormCoreProps,\n 'defaultValues' | 'renderEngine' | 'schema' | 'values'\n> & {\n engine: AutoFormEngine;\n resolvedSchema: ResolvedSchema;\n};\n\nfunction AutoFormContent<\n T extends Record,\n TNativeForm,\n TCustomFieldType extends string,\n>({\n engine,\n resolvedSchema,\n testId,\n onSubmit = noopOnSubmit,\n children,\n uiComponents,\n formComponents,\n withSubmit = false,\n onFormInit,\n formProps = {},\n fieldConfig: fieldConfigOverrides,\n modes,\n defaultMode,\n showSummary = false,\n renderSummary,\n fieldRegistry,\n dataProviders,\n deprecatedFields = 'show',\n classifyField,\n payloadSchema,\n payloadBuilder,\n payloadParser,\n onFieldChange,\n renderRootHeader,\n rootHeader = 'auto',\n stepper,\n validationMode = 'submit',\n revalidationMode = 'change',\n}: AutoFormContentProps) {\n const testIdPrefix = resolveAutoFormTestIdPrefix(testId);\n const submitController = React.useRef(undefined);\n const validationController = React.useRef(undefined);\n const initializedForm = React.useRef(undefined);\n const advancedFields = mergeFieldOverrides(resolvedSchema.parsedSchema.fields, fieldConfigOverrides);\n const simpleFields = deriveSimpleFields(advancedFields, classifyField);\n const protoMessageUi = resolvedSchema.protoDesc ? getProtoMessageUiConfig(resolvedSchema.protoDesc) : undefined;\n const rootHeaderMetadata = {\n description: protoMessageUi?.description,\n title: protoMessageUi?.title,\n };\n const mergedUiComponents = { ...ShadcnUIComponents, ...uiComponents };\n const mergedFormComponents = { ...ShadcnAutoFormFieldComponents, ...formComponents };\n const conversionOptions = protoConversionOptionsFromFieldConfig(fieldConfigOverrides);\n const availableModes = normalizeModes(modes);\n const preferredMode = resolveInitialMode(availableModes, defaultMode);\n const [mode, setMode] = React.useState(preferredMode);\n const [currentStepIndex, setCurrentStepIndex] = React.useState(() =>\n stepper ? initialStepIndex(stepper.steps, stepper.defaultStep) : 0\n );\n const previousStepIndex = React.useRef(currentStepIndex);\n const [isAdvancing, setIsAdvancing] = React.useState(false);\n const previousDefaultMode = React.useRef(defaultMode);\n const previousLifecycleValues = React.useRef(engine.values);\n const hasSubmitted = React.useRef(false);\n\n if (stepper) {\n validateSteps(stepper.steps);\n }\n\n React.useEffect(() => {\n if (initializedForm.current !== engine.nativeForm) {\n initializedForm.current = engine.nativeForm;\n onFormInit?.(engine.nativeForm as TNativeForm);\n }\n }, [engine.nativeForm, onFormInit]);\n\n React.useEffect(function abortAsyncWorkOnUnmount() {\n return () => {\n submitController.current?.abort();\n validationController.current?.abort();\n };\n }, []);\n\n React.useEffect(() => {\n if (!availableModes.includes(mode)) {\n setMode(preferredMode);\n previousDefaultMode.current = defaultMode;\n return;\n }\n\n if (previousDefaultMode.current !== defaultMode) {\n previousDefaultMode.current = defaultMode;\n if (defaultMode && availableModes.includes(defaultMode)) {\n setMode(defaultMode);\n }\n }\n }, [availableModes, defaultMode, mode, preferredMode]);\n\n async function validateWithProvider(\n submittedValues: Record,\n signal: AbortSignal\n ): Promise {\n try {\n return await Promise.resolve(resolvedSchema.provider.validateSchema(submittedValues, { signal }));\n } catch (error) {\n return {\n success: false,\n errors: [\n {\n path: [],\n message: error instanceof Error ? error.message : 'Failed to validate form values.',\n },\n ],\n };\n }\n }\n\n function activeValidationMode(): AutoFormValidationMode | AutoFormRevalidationMode {\n return hasSubmitted.current ? revalidationMode : validationMode;\n }\n\n async function runLifecycleValidation(valuesToValidate: Record) {\n if (engine.validatesSchema) {\n return;\n }\n const controller = beginValidation();\n const result = await validateWithProvider(valuesToValidate, controller.signal);\n if (controller.signal.aborted) {\n return;\n }\n engine.setValidationErrors(result.success ? [] : result.errors);\n }\n\n React.useEffect(() => {\n if (previousLifecycleValues.current === engine.values) {\n return;\n }\n previousLifecycleValues.current = engine.values;\n if (activeValidationMode() === 'change') {\n void runLifecycleValidation(engine.values);\n }\n }, [engine.values, revalidationMode, validationMode]);\n\n function routeToFirstStepError() {\n if (!stepper) {\n return;\n }\n const targetStepIndex = stepper.steps.findIndex((step) =>\n fieldsForStep(advancedFields, stepper.steps, step.id).some((field) =>\n engine.getFieldInvalid(field.key)\n )\n );\n if (targetStepIndex < 0) {\n return;\n }\n if (targetStepIndex !== currentStepIndex) {\n setCurrentStepIndex(targetStepIndex);\n return;\n }\n const firstErrorField = fieldsForStep(\n advancedFields,\n stepper.steps,\n stepper.steps[targetStepIndex]?.id ?? ''\n ).find((field) => engine.getFieldInvalid(field.key));\n if (firstErrorField) {\n engine.focus(firstErrorField.key);\n }\n }\n\n function beginSubmit() {\n submitController.current?.abort();\n validationController.current?.abort();\n const controller = new AbortController();\n submitController.current = controller;\n return controller;\n }\n\n function beginValidation() {\n validationController.current?.abort();\n const controller = new AbortController();\n validationController.current = controller;\n return controller;\n }\n\n function getSubmitContext(signal: AbortSignal): AutoFormSubmitContext {\n return {\n form: engine,\n signal,\n updateMask: resolvedSchema.protoDesc\n ? createUpdateMask(\n resolvedSchema.protoDesc,\n engine.dirtyFields,\n engine.getValues(),\n engine.defaultValues\n )\n : undefined,\n };\n }\n\n async function submitValidatedValues(values: T, controller: AbortController) {\n try {\n const context = getSubmitContext(controller.signal);\n await engine.runNativeSubmit?.();\n if (controller.signal.aborted) {\n return;\n }\n const validatedProtoMessage = resolvedSchema.protoDesc\n ? resolveProtoSourceMessage(resolvedSchema.protoDesc, values)\n : undefined;\n const submittedValues = resolvedSchema.protoDesc\n ? validatedProtoMessage\n ? preserveProtoMessageSource(\n resolvedSchema.protoDesc,\n validatedProtoMessage,\n resolvedSchema.protoSource as never\n )\n : formValuesToProto(\n resolvedSchema.protoDesc,\n values,\n resolvedSchema.protoSource as never,\n conversionOptions\n )\n : values;\n await onSubmit(submittedValues as T, engine.nativeForm as TNativeForm, context);\n if (!controller.signal.aborted) {\n routeToFirstStepError();\n }\n } catch (error) {\n if (!controller.signal.aborted) {\n engine.setRootError(error instanceof Error ? error.message : 'Submission failed.');\n }\n }\n }\n\n async function handleSubmit(submittedValues: Record) {\n hasSubmitted.current = true;\n const controller = beginSubmit();\n engine.clearErrors(['root', PROTO_FORM_ROOT_ERROR_KEY]);\n\n if (engine.validatesSchema) {\n await submitValidatedValues(submittedValues as T, controller);\n return;\n }\n\n const validationResult = await validateWithProvider(submittedValues, controller.signal);\n if (controller.signal.aborted) {\n return;\n }\n if (!validationResult.success) {\n engine.setValidationErrors(validationResult.errors);\n routeToFirstStepError();\n return;\n }\n await submitValidatedValues(validationResult.data as T, controller);\n }\n\n function handleStepBack() {\n validationController.current?.abort();\n setIsAdvancing(false);\n setCurrentStepIndex((index) => Math.max(0, index - 1));\n }\n\n async function handleStepContinue(stepFields: ReturnType) {\n if (!stepper || isAdvancing) {\n return;\n }\n setIsAdvancing(true);\n const controller = beginValidation();\n const fieldNames = stepFields.map((field) => field.key);\n engine.clearErrors([...fieldNames, 'root', PROTO_FORM_ROOT_ERROR_KEY]);\n\n try {\n const nativeValid = await engine.trigger(engine.validatesSchema ? undefined : fieldNames);\n if (controller.signal.aborted) {\n return;\n }\n\n if (engine.validatesSchema) {\n if (nativeValid) {\n setCurrentStepIndex((index) => Math.min(stepper.steps.length - 1, index + 1));\n } else {\n const firstInvalid = fieldNames.find(engine.getFieldInvalid);\n if (firstInvalid) {\n engine.focus(firstInvalid);\n }\n }\n return;\n }\n\n const validationResult = await validateWithProvider(engine.getValues(), controller.signal);\n if (controller.signal.aborted) {\n return;\n }\n const currentFields = new Set(fieldNames);\n const currentErrors = validationResult.success\n ? []\n : validationResult.errors.filter((error) => {\n const root = error.path[0];\n return error.path.length === 0 || (typeof root === 'string' && currentFields.has(root));\n });\n if (currentErrors.length > 0) {\n engine.setValidationErrors(currentErrors);\n const firstFieldError = currentErrors.find((error) => error.path.length > 0);\n if (firstFieldError) {\n engine.focus(firstFieldError.path.join('.'));\n }\n return;\n }\n if (nativeValid) {\n setCurrentStepIndex((index) => Math.min(stepper.steps.length - 1, index + 1));\n }\n } finally {\n if (validationController.current === controller && !controller.signal.aborted) {\n setIsAdvancing(false);\n }\n }\n }\n\n React.useEffect(() => {\n if (!stepper) {\n return;\n }\n const fieldErrorKeys = Object.keys(engine.errors);\n if (fieldErrorKeys.length === 0) {\n return;\n }\n const targetStepIndex = stepper.steps.findIndex((step) => {\n const stepFieldKeys = new Set(\n fieldsForStep(advancedFields, stepper.steps, step.id).map((field) => field.key)\n );\n return fieldErrorKeys.some((key) => stepFieldKeys.has(key));\n });\n if (targetStepIndex >= 0 && targetStepIndex !== currentStepIndex) {\n setCurrentStepIndex(targetStepIndex);\n }\n }, [advancedFields, currentStepIndex, engine.errors, stepper]);\n\n React.useEffect(() => {\n if (!stepper || previousStepIndex.current === currentStepIndex) {\n return;\n }\n previousStepIndex.current = currentStepIndex;\n const step = stepper.steps[currentStepIndex];\n const firstField = step ? fieldsForStep(advancedFields, stepper.steps, step.id)[0] : undefined;\n if (firstField) {\n engine.focus(firstField.key);\n }\n }, [advancedFields, currentStepIndex, engine, stepper]);\n\n function renderFormForMode(targetMode: Exclude) {\n return renderModeContent({\n fields: targetMode === 'simple' ? simpleFields : advancedFields,\n testIdPrefix,\n withSubmit,\n children,\n SubmitButtonComponent: mergedUiComponents.SubmitButton as React.ComponentType<{\n children: React.ReactNode;\n disabled?: boolean;\n testId?: string;\n }>,\n isAdvancing,\n isSubmitting: engine.isSubmitting,\n stepper,\n currentStepIndex,\n onStepBack: handleStepBack,\n onStepContinue: handleStepContinue,\n });\n }\n\n const formOnBlurCapture =\n typeof Reflect.get(formProps, 'onBlurCapture') === 'function'\n ? (Reflect.get(formProps, 'onBlurCapture') as React.FocusEventHandler)\n : undefined;\n\n return (\n \n \n advancedFields={advancedFields}\n conversionOptions={conversionOptions}\n dataProviders={dataProviders}\n deprecatedFields={deprecatedFields}\n fieldRegistry={fieldRegistry}\n formComponents={mergedFormComponents}\n mode={mode}\n onFieldChange={onFieldChange}\n payloadBuilder={payloadBuilder}\n payloadParser={payloadParser}\n payloadSchema={payloadSchema}\n renderContent={(bag) => (\n {\n formOnBlurCapture?.(event);\n if (activeValidationMode() === 'blur') {\n void runLifecycleValidation(engine.getValues());\n }\n }}\n onSubmit={engine.handleSubmit(handleSubmit)}\n testId={testIdPrefix}\n >\n {engine.rootError ? (\n \n Form validation failed\n {engine.rootError}\n \n ) : null}\n\n {rootHeader === 'hidden' ? null : renderRootHeader ? (\n renderRootHeader(rootHeaderMetadata)\n ) : protoMessageUi?.title || protoMessageUi?.description ? (\n \n {protoMessageUi.title ? {protoMessageUi.title} : null}\n {protoMessageUi.description ? (\n \n {protoMessageUi.description}\n \n ) : null}\n \n ) : null}\n\n \n \n )}\n resolvedSchema={resolvedSchema}\n simpleFields={simpleFields}\n testIdPrefix={testIdPrefix}\n uiComponents={mergedUiComponents}\n >\n {null}\n \n \n );\n}\n\nfunction AutoFormCoreInner<\n T extends Record,\n TNativeForm,\n TCustomFieldType extends string,\n>({\n schema,\n defaultValues,\n values,\n renderEngine,\n ...props\n}: AutoFormCoreProps) {\n const conversionOptions = protoConversionOptionsFromFieldConfig(props.fieldConfig);\n const protoDescriptor = isProtoMessageDescriptor(schema)\n ? schema\n : isProtoProvider(schema)\n ? schema.getMessageDescriptor()\n : undefined;\n const protoSource = protoDescriptor\n ? resolveProtoSourceMessage(protoDescriptor, values, defaultValues)\n : undefined;\n const resolvedSchema = resolveSchema(schema, conversionOptions, protoSource);\n const providerDefaults = resolvedSchema.provider.getDefaultValues();\n const initialDefaultValues =\n resolvedSchema.isProto && resolvedSchema.protoDesc\n ? {\n ...providerDefaults,\n ...(normalizeProtoInitialValues(resolvedSchema.protoDesc, defaultValues) ?? {}),\n }\n : { ...providerDefaults, ...(defaultValues ?? {}) };\n const controlledValues = values\n ? resolvedSchema.isProto && resolvedSchema.protoDesc\n ? normalizeProtoInitialValues(resolvedSchema.protoDesc, values)\n : values\n : undefined;\n\n return renderEngine({\n defaultValues: initialDefaultValues,\n validateSchema: async (submittedValues, signal) => {\n try {\n return await Promise.resolve(\n resolvedSchema.provider.validateSchema(submittedValues, { signal })\n );\n } catch (error) {\n return {\n success: false,\n errors: [\n {\n path: [],\n message:\n error instanceof Error\n ? error.message\n : 'Failed to validate form values.',\n },\n ],\n };\n }\n },\n values: controlledValues,\n children: (engine) => (\n \n {...props}\n engine={engine}\n resolvedSchema={resolvedSchema}\n />\n ),\n });\n}\n\ntype AutoFormErrorBoundaryState = { error: Error | null };\n\nclass AutoFormErrorBoundary extends React.Component<{ children: React.ReactNode }, AutoFormErrorBoundaryState> {\n constructor(props: { children: React.ReactNode }) {\n super(props);\n this.state = { error: null };\n }\n\n static getDerivedStateFromError(error: Error): AutoFormErrorBoundaryState {\n return { error };\n }\n\n override render() {\n if (this.state.error) {\n return (\n \n AutoForm failed to render\n {this.state.error.message}\n \n );\n }\n return this.props.children;\n }\n}\n\nexport function AutoFormCore<\n T extends Record,\n TNativeForm,\n TCustomFieldType extends string = never,\n>(\n props: AutoFormCoreProps\n) {\n const schemaRef = React.useRef(props.schema);\n const [schemaKey, setSchemaKey] = React.useState(0);\n if (schemaRef.current !== props.schema) {\n schemaRef.current = props.schema;\n setSchemaKey((key) => key + 1);\n }\n return (\n \n \n \n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/engine.tsx", "content": "'use client';\n\nimport React from 'react';\n\nimport type { SchemaValidationError } from './core-types';\n\nexport type AutoFormFieldController = {\n errors: string[];\n name: string;\n onBlur: () => void;\n onChange: (\n value: unknown,\n options?: { shouldDirty?: boolean; shouldTouch?: boolean; shouldValidate?: boolean }\n ) => void;\n ref: (element: HTMLElement | null) => void;\n value: unknown;\n};\n\nexport type AutoFormArrayController = {\n append: (value: unknown) => void;\n items: Array<{ key: string; value: unknown }>;\n remove: (index: number) => void;\n};\n\nexport type AutoFormEngineHandle = {\n clearErrors: (paths?: string[]) => void;\n focus: (path: string) => void;\n getValues: () => Record;\n /** Establish the current values as the new clean baseline. */\n markClean: () => void;\n reset: (values: Record, options?: { keepDefaultValues?: boolean }) => void;\n setValue: (\n path: string,\n value: unknown,\n options?: { shouldDirty?: boolean; shouldTouch?: boolean; shouldValidate?: boolean }\n ) => void;\n};\n\nexport type AutoFormEngine = AutoFormEngineHandle & {\n ArrayController: React.ComponentType<{\n children: (controller: AutoFormArrayController) => React.ReactNode;\n name: string;\n }>;\n FieldController: React.ComponentType<{\n children: (controller: AutoFormFieldController) => React.ReactNode;\n name: string;\n }>;\n defaultValues: Record | undefined;\n dirtyFields: Record;\n errors: Record;\n getFieldInvalid: (path: string) => boolean;\n handleSubmit: (\n onValid: (values: Record) => void | Promise\n ) => React.FormEventHandler;\n isSubmitting: boolean;\n isDirty: boolean;\n nativeForm: unknown;\n rootError: string | undefined;\n runNativeSubmit?: () => void | Promise;\n setRootError: (message: string) => void;\n setValidationErrors: (errors: SchemaValidationError[]) => void;\n trigger: (paths?: string[]) => Promise;\n /** Native validation already returns the schema's transformed output. */\n validatesSchema: boolean;\n values: Record;\n};\n\nconst AutoFormEngineContext = React.createContext(null);\n\nexport function AutoFormEngineProvider({\n children,\n engine,\n}: {\n children: React.ReactNode;\n engine: AutoFormEngine;\n}) {\n return {children};\n}\n\nexport function useAutoFormEngine(): AutoFormEngine {\n const engine = React.useContext(AutoFormEngineContext);\n if (!engine) {\n throw new Error('AutoForm engine controls must be used inside an AutoForm engine provider.');\n }\n return engine;\n}\n\nexport function errorMessage(value: unknown): string | undefined {\n if (typeof value === 'string') {\n return value;\n }\n if (value instanceof Error) {\n return value.message;\n }\n if (value && typeof value === 'object') {\n const message = Reflect.get(value, 'message');\n if (typeof message === 'string') {\n return message;\n }\n }\n return;\n}\n\nexport function errorMessages(values: unknown[]): string[] {\n return values.map(errorMessage).filter((message): message is string => Boolean(message));\n}\n\nexport function useDirtyStateNotification(\n isDirty: boolean,\n onDirtyChange: ((isDirty: boolean) => void) | undefined\n) {\n const callbackRef = React.useRef(onDirtyChange);\n const lastNotificationRef = React.useRef(undefined);\n callbackRef.current = onDirtyChange;\n\n function notifyDirtyChange(nextIsDirty: boolean) {\n if (lastNotificationRef.current === nextIsDirty) {\n return;\n }\n lastNotificationRef.current = nextIsDirty;\n callbackRef.current?.(nextIsDirty);\n }\n\n React.useEffect(() => {\n notifyDirtyChange(isDirty);\n }, [isDirty]);\n\n return notifyDirtyChange;\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/layout-context.tsx", "content": "import React from 'react';\n\nconst FormDepthContext = React.createContext(0);\n\nexport function useFormDepth(): number {\n return React.useContext(FormDepthContext);\n}\n\nexport function FormDepthProvider({ depth, children }: { depth: number; children: React.ReactNode }) {\n return {children};\n}\n\n/**\n * Map a nesting depth (0 = root section, 1 = first-nested, ...) to an\n * HTML heading level. Root sections render as h2 because h1 is reserved\n * for the page heading. The clamp at 5 matches the `Heading` component's\n * styled variants (h1..h5). Depth is always ≥ 0 because the context\n * initialises at 0 and is only incremented, so no lower-bound guard is\n * needed.\n */\nexport function headingLevelForDepth(depth: number): 2 | 3 | 4 | 5 {\n const level = 2 + depth;\n if (level > 5) {\n return 5;\n }\n return level as 2 | 3 | 4 | 5;\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/layout.tsx", "content": "'use client';\n\nimport React from 'react';\nimport { cn } from '../../lib/utils';\nimport { Button } from '../button';\nimport { FieldLabel } from '../field';\nimport { Heading, Text } from '../typography';\nimport { formSpacing } from './form-spacing';\nimport { FormDepthProvider, headingLevelForDepth, useFormDepth } from './layout-context';\n\nexport interface FormLayoutProps extends Omit, 'children'> {\n children?: React.ReactNode;\n ref?: React.Ref;\n testId?: string;\n}\n\nexport function FormLayout({ children, className, testId, ref, ...formProps }: FormLayoutProps) {\n return (\n
\n {children}\n
\n );\n}\n\nexport interface FormSectionProps {\n children?: React.ReactNode;\n className?: string;\n description?: React.ReactNode;\n /** Override divider visibility. Defaults to true when a title is present. */\n divider?: boolean;\n required?: boolean;\n testId?: string;\n title?: React.ReactNode;\n}\n\nexport function FormSection({ title, description, divider, required, testId, className, children }: FormSectionProps) {\n const depth = useFormDepth();\n const hasHeader = Boolean(title) || Boolean(description);\n const showDivider = hasHeader && (divider ?? Boolean(title));\n const level = headingLevelForDepth(depth);\n\n return (\n
\n {hasHeader ? (\n
\n {title ? (\n
\n \n {title}\n \n {required ? (\n \n *\n \n ) : null}\n
\n ) : null}\n {description ? (\n \n {description}\n \n ) : null}\n
\n ) : null}\n \n
{children}
\n
\n
\n );\n}\n\nexport interface FormFieldProps {\n children: React.ReactNode;\n className?: string;\n error?: React.ReactNode;\n helpText?: React.ReactNode;\n htmlFor?: string;\n label?: React.ReactNode;\n required?: boolean;\n testId?: string;\n}\n\nexport function FormField({ label, helpText, error, required, htmlFor, testId, className, children }: FormFieldProps) {\n return (\n
\n {label ? (\n \n \n {label}\n \n {required ? (\n \n *\n \n ) : null}\n \n ) : null}\n {children}\n {error ? (\n \n {error}\n \n ) : helpText ? (\n \n {helpText}\n \n ) : null}\n
\n );\n}\n\nexport interface FormSubmitProps extends React.ComponentProps {\n children?: React.ReactNode;\n ref?: React.Ref;\n}\n\nexport function FormSubmit({ children = 'Submit', type = 'submit', ref, ...props }: FormSubmitProps) {\n return (\n \n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/mode-shell.tsx", "content": "'use client';\n\nimport { MotionConfig } from 'motion/react';\nimport React from 'react';\nimport { Alert, AlertDescription, AlertTitle } from '../alert';\nimport { Button } from '../button';\nimport { CopyButton } from '../copy-button';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '../tabs';\nimport { Textarea } from '../textarea';\nimport { Heading, Text } from '../typography';\nimport { formSpacing } from './form-spacing';\nimport { buildAutoFormTestId } from './test-ids';\nimport type { AutoFormMode, AutoFormSummaryContext } from './types';\nimport { safeStringify } from './utils/serialization';\n\nfunction JsonBlock({ description, jsonText, title }: { title: string; description: string; jsonText: string }) {\n return (\n
\n
\n
\n {title}\n \n {description}\n \n
\n
\n
\n
\n \n Payload JSON\n \n \n Copy JSON\n \n
\n
\n
{jsonText}
\n
\n
\n
\n );\n}\n\nfunction JsonEditorPanel({\n bestEffort,\n editorError,\n jsonText,\n onFormat,\n onJsonTextChange,\n onReset,\n testIdPrefix,\n}: {\n bestEffort: boolean;\n editorError?: string;\n jsonText: string;\n onFormat: () => void;\n onJsonTextChange: (value: string) => void;\n onReset: () => void;\n testIdPrefix: string;\n}) {\n return (\n
\n
\n
\n Payload JSON\n \n Edit the payload directly, then switch back to the form whenever you want.\n \n
\n
\n \n Copy JSON\n \n \n \n
\n
\n {bestEffort ? (\n \n Best-effort preview\n \n Some values are still invalid, so this payload may not be ready to submit just yet.\n \n \n ) : null}\n {editorError ? (\n \n Invalid JSON\n {editorError}\n \n ) : null}\n onJsonTextChange(event.target.value)}\n resize=\"vertical\"\n testId={buildAutoFormTestId(testIdPrefix, 'json-editor')}\n value={jsonText}\n />\n
\n );\n}\n\nfunction FormPanel({\n children,\n context,\n payload,\n renderSummary,\n testIdPrefix,\n}: {\n children: React.ReactNode;\n context: AutoFormSummaryContext;\n payload: unknown;\n renderSummary?: (payload: unknown, context: AutoFormSummaryContext) => React.ReactNode;\n testIdPrefix: string;\n}) {\n const payloadText = safeStringify(payload);\n\n return (\n
\n {/*\n The form column's top-level rhythm is driven by `formSpacing.form`\n so every root sibling (fields, sections, Submit) is separated by\n the same token the `
` primitive applies internally. Keeping\n the two entry points on the same token means Submit always sits\n one `form` step below the last section regardless of whether the\n preceding child is a leaf field or a nested group.\n */}\n
{children}
\n \n {renderSummary ? (\n renderSummary(payload, context)\n ) : (\n \n )}\n \n
\n );\n}\n\nexport function AutoFormModeShell({\n bestEffort,\n jsonEditorError,\n jsonText,\n mode,\n modes,\n onFormatJson,\n onJsonTextChange,\n onModeChange,\n onResetJson,\n payload,\n renderSummary,\n summaryContext,\n renderFormMode,\n showSummary,\n testIdPrefix,\n}: {\n bestEffort: boolean;\n jsonEditorError?: string;\n jsonText: string;\n mode: AutoFormMode;\n modes: AutoFormMode[];\n onFormatJson: () => void;\n onJsonTextChange: (value: string) => void;\n onModeChange: (mode: AutoFormMode) => void;\n onResetJson: () => void;\n payload: unknown;\n renderSummary?: (payload: unknown, context: AutoFormSummaryContext) => React.ReactNode;\n summaryContext: AutoFormSummaryContext;\n renderFormMode: (mode: Exclude) => React.ReactNode;\n showSummary: boolean;\n testIdPrefix: string;\n}) {\n function renderModeBody(targetMode: AutoFormMode) {\n if (targetMode === 'json') {\n return (\n \n );\n }\n\n const formContent = renderFormMode(targetMode);\n if (!showSummary) {\n return formContent;\n }\n\n return (\n \n {formContent}\n \n );\n }\n\n if (modes.length <= 1) {\n return <>{renderModeBody(mode)};\n }\n\n return (\n \n {\n if (value !== null) {\n onModeChange(value as AutoFormMode);\n }\n }}\n testId={buildAutoFormTestId(testIdPrefix, 'tabs')}\n value={mode}\n >\n \n {modes.map((tabMode) => (\n \n {tabMode === 'json' ? 'JSON' : tabMode === 'simple' ? 'Simple' : 'Advanced'}\n \n ))}\n \n {modes.map((tabMode) => (\n \n {renderModeBody(tabMode)}\n \n ))}\n \n \n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/proto/conversion.ts", "content": "import { isMessage, type DescMessage, type MessageShape } from '@bufbuild/protobuf';\nimport { getFieldHints } from '../../../lib/core';\n\nimport type { ParsedField } from '../../../lib/form-types';\nimport {\n type ProtoMapFormEntry,\n protoFormValuesToPayload,\n protoPayloadToFormValues,\n protoToFormValues,\n} from '../../../lib/protobuf-provider';\n\nexport { protoFormValuesToPayload, protoPayloadToFormValues, protoToFormValues };\n\nexport function getProtoJsonSchema(field: ParsedField): Record {\n const hints = getFieldHints(field);\n\n switch (hints?.jsonKind) {\n case 'listValue':\n return { type: 'array' };\n case 'any':\n return {\n type: 'object',\n properties: {\n typeUrl: { type: 'string', title: 'Type URL' },\n valueBase64: { type: 'string', title: 'Base64 Payload' },\n },\n };\n default:\n return { type: 'object' };\n }\n}\n\nexport function isProtoMapEntries(value: unknown): value is ProtoMapFormEntry[] {\n return Array.isArray(value);\n}\n\nfunction isProtoMessageShape(value: unknown): boolean {\n return Boolean(value && typeof value === 'object' && '$typeName' in (value as Record));\n}\n\nexport function resolveProtoSourceMessage(\n desc: Desc,\n ...candidates: unknown[]\n): MessageShape | undefined {\n return candidates.find((candidate): candidate is MessageShape => isMessage(candidate, desc));\n}\n\nexport function normalizeProtoInitialValues(\n desc: DescMessage,\n values?: Partial>\n): Record | undefined {\n if (!values) {\n return;\n }\n\n if (isProtoMessageShape(values)) {\n return protoToFormValues(desc, values as never);\n }\n\n return values as Record;\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/proto/index.ts", "content": "// Proto bridge — re-exports from protobuf-provider and local conversion utilities.\n// All proto-related imports within auto-form go through this single entry point.\n\nexport type { ProtoFieldRenderType, ProtoUiRule } from '../../../lib/protobuf-provider';\nexport {\n getProtoFieldCustomData,\n getProtoMessageUiConfig,\n isProtoMessageDescriptor,\n isProtoProvider,\n PROTO_FORM_ROOT_ERROR_KEY,\n ProtoProvider,\n} from '../../../lib/protobuf-provider';\nexport {\n getProtoJsonSchema,\n isProtoMapEntries,\n normalizeProtoInitialValues,\n protoFormValuesToPayload,\n protoPayloadToFormValues,\n protoToFormValues,\n resolveProtoSourceMessage,\n} from './conversion';\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/proto/schema.ts", "content": "export {\n getProtoMessageUiConfig,\n isProtoMessageDescriptor,\n isProtoProvider,\n ProtoProvider,\n} from '../../../lib/protobuf-provider';\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/registry.ts", "content": "import type React from 'react';\n\nimport type { AutoFormFieldProps, ParsedField } from './core-types';\nimport { getLabel } from './field-utils';\nimport { getProtoFieldCustomData } from './proto';\n\nexport type FieldTypeDefinition = {\n name: TName;\n match: (field: ParsedField, context: FieldMatchContext) => boolean;\n priority: number;\n component: React.ComponentType;\n};\n\nexport type FieldMatchContext = {\n identity: string; // `${field.key} ${label}`.toLowerCase()\n inputType: string;\n maxLength: number;\n};\n\nexport class FieldTypeRegistry {\n private definitions: FieldTypeDefinition[] = [];\n\n register(\n definition: FieldTypeDefinition\n ): FieldTypeRegistry {\n this.definitions.push(definition);\n this.definitions.sort((a, b) => b.priority - a.priority);\n return this as FieldTypeRegistry;\n }\n\n resolve(field: ParsedField, context: FieldMatchContext): FieldTypeDefinition | undefined {\n return this.definitions.find((def) => def.match(field, context)) as\n | FieldTypeDefinition\n | undefined;\n }\n\n list(): readonly FieldTypeDefinition[] {\n return this.definitions as unknown as readonly FieldTypeDefinition[];\n }\n\n clone(): FieldTypeRegistry {\n const registry = new FieldTypeRegistry();\n for (const def of this.definitions) {\n registry.register(def);\n }\n return registry;\n }\n}\n\nexport function buildFieldMatchContext(field: ParsedField): FieldMatchContext {\n const label = String(field.fieldConfig?.label ?? getLabel(field));\n const identity = `${field.key} ${label}`.toLowerCase();\n const inputType = String(field.fieldConfig?.inputProps?.type ?? getProtoFieldCustomData(field)?.inputType ?? '');\n const maxLength = Number(field.fieldConfig?.inputProps?.maxLength ?? 0);\n return { identity, inputType, maxLength };\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/renderers/array.tsx", "content": "'use client';\n\nimport { TrashIcon } from 'lucide-react';\nimport React from 'react';\nimport { Button } from '../../button';\nimport { useAutoFormRenderContext, useAutoFormRuntimeContext } from '../context';\nimport type { ParsedField } from '../core-types';\nimport { type AutoFormArrayController, useAutoFormEngine } from '../engine';\nimport { formSpacing } from '../form-spacing';\nimport { createEmptyFieldValue, getFieldErrorMessage } from '../helpers';\nimport { FormDepthProvider, useFormDepth } from '../layout-context';\nimport { getAutoFormCollectionRemoveTestId, getAutoFormCollectionRowTestId, getAutoFormFieldTestId } from '../test-ids';\nimport { AutoFormFieldRenderer } from './index';\nimport { cloneFieldForCompactRow, getRenderedLabel, isComplexCollectionField, useFieldPresentation } from './shared';\n\nconst COMPACT_ROW_GRID = 'grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3';\n\nfunction RequiredArraySeeder({\n controller,\n disabled,\n field,\n itemField,\n}: {\n controller: AutoFormArrayController;\n disabled: boolean;\n field: ParsedField;\n itemField: ParsedField | undefined;\n}) {\n const hasSeededRef = React.useRef(false);\n React.useEffect(() => {\n if (hasSeededRef.current || disabled || !field.required || controller.items.length > 0 || !itemField) {\n return;\n }\n hasSeededRef.current = true;\n controller.append(createEmptyFieldValue(itemField));\n }, [controller, disabled, field.required, itemField]);\n return null;\n}\n\nfunction getCollectionItemLabel(collectionLabel: string, index: number): string {\n const singularLabel = collectionLabel.endsWith('ies')\n ? `${collectionLabel.slice(0, -3)}y`\n : collectionLabel.endsWith('s')\n ? collectionLabel.slice(0, -1)\n : collectionLabel;\n\n return `${singularLabel} ${index + 1}`;\n}\n\nexport function ArrayFieldRenderer({\n field,\n path,\n inheritedDisabled = false,\n}: {\n field: ParsedField;\n path: string[];\n inheritedDisabled?: boolean;\n}) {\n const { uiComponents } = useAutoFormRenderContext();\n const { ArrayController, errors } = useAutoFormEngine();\n const { testIdPrefix } = useAutoFormRuntimeContext();\n const fullPath = path.join('.');\n const itemField = field.schema?.[0];\n const error = getFieldErrorMessage(errors, path);\n const label = getRenderedLabel(field);\n const { isDisabled, isVisible, renderField } = useFieldPresentation(field, path, inheritedDisabled);\n const FieldWrapperComponent = field.fieldConfig?.fieldWrapper || uiComponents.FieldWrapper;\n const ArrayWrapperComponent = uiComponents.ArrayWrapper as React.ComponentType<\n React.ComponentProps & {\n addButtonTestId?: string;\n testId?: string;\n }\n >;\n const ArrayElementWrapperComponent = uiComponents.ArrayElementWrapper as React.ComponentType<\n React.ComponentProps & {\n removeButtonTestId?: string;\n testId?: string;\n }\n >;\n const compactItemField = itemField ? cloneFieldForCompactRow(itemField) : undefined;\n const useCompactRows = itemField ? !isComplexCollectionField(itemField) : false;\n const depth = useFormDepth();\n\n if (!isVisible) {\n return null;\n }\n\n return (\n \n \n {(controller) => (\n <>\n \n {\n if (!isDisabled) {\n controller.append(createEmptyFieldValue(itemField));\n }\n }}\n testId={getAutoFormFieldTestId(testIdPrefix, fullPath, 'items')}\n >\n {controller.items.map((item, index) => {\n const rowTestId = getAutoFormCollectionRowTestId(testIdPrefix, fullPath, index);\n const removeButtonTestId = getAutoFormCollectionRemoveTestId(testIdPrefix, fullPath, index);\n const removeItem = () => {\n if (!isDisabled) {\n controller.remove(index);\n }\n };\n\n if (compactItemField && useCompactRows) {\n return (\n 0 ? `${COMPACT_ROW_GRID} ${formSpacing.arrayItemSeparator}` : COMPACT_ROW_GRID\n }\n data-testid={rowTestId}\n key={item.key}\n >\n \n \n \n \n \n );\n }\n\n const renderedItemField = itemField\n ? {\n ...itemField,\n fieldConfig: {\n ...(itemField.fieldConfig ?? {}),\n label: getCollectionItemLabel(label, index),\n },\n }\n : undefined;\n\n return (\n \n {renderedItemField ? (\n \n \n \n ) : null}\n \n );\n })}\n \n \n )}\n \n \n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/renderers/controlled.tsx", "content": "'use client';\n\nimport React from 'react';\nimport { useAutoFormRenderContext, useAutoFormRuntimeContext } from '../context';\nimport type { AutoFormFieldProps, ParsedField } from '../core-types';\nimport { useAutoFormEngine } from '../engine';\nimport { getAutoFormFieldTestId } from '../test-ids';\nimport { getRenderedLabel, useFieldPresentation } from './shared';\n\nexport function ControlledFieldRenderer({\n field,\n path,\n renderType,\n inheritedDisabled = false,\n}: {\n field: ParsedField;\n path: string[];\n renderType?: string;\n inheritedDisabled?: boolean;\n}) {\n const { formComponents, uiComponents } = useAutoFormRenderContext();\n const { FieldController } = useAutoFormEngine();\n const fullPath = path.join('.');\n const label = getRenderedLabel(field);\n const {\n isVisible,\n renderField,\n renderType: inferredRenderType,\n } = useFieldPresentation(field, path, inheritedDisabled);\n const { fieldRegistry, testIdPrefix } = useAutoFormRuntimeContext();\n\n if (!isVisible) {\n return null;\n }\n\n const resolvedRenderType = renderType ?? inferredRenderType;\n const FieldWrapperComponent = field.fieldConfig?.fieldWrapper || uiComponents.FieldWrapper;\n const registeredComponent =\n formComponents[resolvedRenderType] ??\n fieldRegistry?.list().find((definition) => definition.name === resolvedRenderType)?.component;\n const FieldComponent = (registeredComponent ?? formComponents.fallback) as React.ComponentType;\n const componentField = registeredComponent\n ? renderField\n : {\n ...renderField,\n type: resolvedRenderType,\n };\n\n return (\n \n {(controller) => {\n const error = controller.errors.join('\\n') || undefined;\n return (\n \n \n \n );\n }}\n \n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/renderers/index.tsx", "content": "'use client';\n\nimport React from 'react';\nimport { useAutoForm } from '../context';\nimport type { ParsedField } from '../core-types';\nimport { defaultRegistry } from '../fields';\nimport { getFieldUiConfig, resolveRenderFieldType } from '../helpers';\nimport { buildFieldMatchContext, type FieldTypeRegistry } from '../registry';\nimport type { AutoFormSlotProps } from '../slot';\nimport { ArrayFieldRenderer } from './array';\nimport { ControlledFieldRenderer } from './controlled';\nimport { MapFieldRenderer } from './map';\nimport { ObjectFieldRenderer } from './object';\nimport { OneofFieldRenderer } from './oneof';\nimport { isFieldHidden } from './shared';\n\nfunction resolveFieldType(field: ParsedField, registry: FieldTypeRegistry): string {\n const explicitControl = getFieldUiConfig(field).control;\n if (explicitControl) {\n return explicitControl;\n }\n\n const matchContext = buildFieldMatchContext(field);\n const resolved = registry.resolve(field, matchContext);\n return resolved?.name ?? resolveRenderFieldType(field);\n}\n\nexport function AutoFormFieldRenderer({\n field,\n path,\n inheritedDisabled = false,\n registry,\n}: {\n field: ParsedField;\n path: string[];\n inheritedDisabled?: boolean;\n registry?: FieldTypeRegistry;\n}) {\n const activeRegistry = registry ?? defaultRegistry;\n const renderType = resolveFieldType(field, activeRegistry);\n\n if ((field.type === 'array' || field.type === 'map' || field.type === 'object') && renderType !== field.type) {\n return (\n \n );\n }\n\n switch (field.type) {\n case 'object':\n return ;\n case 'array':\n return ;\n case 'map':\n return ;\n case 'oneof':\n return ;\n default:\n return (\n \n );\n }\n}\n\ntype SlotEntry = {\n before?: string;\n after?: string;\n content: React.ReactNode;\n};\n\nfunction extractSlots(children: React.ReactNode): { slots: SlotEntry[]; other: React.ReactNode[] } {\n const slots: SlotEntry[] = [];\n const other: React.ReactNode[] = [];\n\n React.Children.forEach(children, (child) => {\n if (React.isValidElement(child) && (child.type as { displayName?: string }).displayName === 'AutoFormSlot') {\n const props = child.props as AutoFormSlotProps;\n slots.push({\n before: props.before,\n after: props.after,\n content: props.children,\n });\n } else if (child !== null && child !== undefined) {\n other.push(child);\n }\n });\n\n return { slots, other };\n}\n\nexport function AutoFormFields({ fields, children }: { fields: ParsedField[]; children?: React.ReactNode }) {\n const { deprecatedFields, fieldRegistry } = useAutoForm();\n const { slots, other } = React.useMemo(() => extractSlots(children), [children]);\n\n // Build slot maps for O(1) lookup\n const beforeSlots = React.useMemo(() => {\n const map = new Map();\n for (const slot of slots) {\n if (slot.before) {\n const existing = map.get(slot.before) ?? [];\n existing.push(slot.content);\n map.set(slot.before, existing);\n }\n }\n return map;\n }, [slots]);\n\n const afterSlots = React.useMemo(() => {\n const map = new Map();\n for (const slot of slots) {\n if (slot.after) {\n const existing = map.get(slot.after) ?? [];\n existing.push(slot.content);\n map.set(slot.after, existing);\n }\n }\n return map;\n }, [slots]);\n\n // Slots without before/after render at the top\n const topSlots = slots.filter((s) => !(s.before || s.after)).map((s) => s.content);\n\n return (\n
\n {topSlots.map((content, i) => (\n
\n {content}\n
\n ))}\n {other.length > 0\n ? other.map((content, i) => (\n
\n {content}\n
\n ))\n : null}\n {fields.filter((field) => !isFieldHidden(field, deprecatedFields)).map((field) => (\n
\n {beforeSlots.get(field.key)?.map((content, i) => (\n {content}\n ))}\n \n {afterSlots.get(field.key)?.map((content, i) => (\n {content}\n ))}\n
\n ))}\n
\n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/renderers/map.tsx", "content": "'use client';\n\nimport { TrashIcon } from 'lucide-react';\nimport React from 'react';\nimport { Button } from '../../button';\nimport { useAutoFormRenderContext, useAutoFormRuntimeContext } from '../context';\nimport type { ParsedField } from '../core-types';\nimport { useAutoFormEngine } from '../engine';\nimport { formSpacing } from '../form-spacing';\nimport { createEmptyFieldValue, getFieldErrorMessage } from '../helpers';\nimport { getAutoFormCollectionRemoveTestId, getAutoFormCollectionRowTestId, getAutoFormFieldTestId } from '../test-ids';\nimport { AutoFormFieldRenderer } from './index';\nimport { cloneFieldForCompactRow, getRenderedLabel, isComplexCollectionField, useFieldPresentation } from './shared';\n\nconst COMPACT_PAIR_GRID = 'grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] items-center gap-3';\nconst KEY_REMOVE_GRID = 'grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3';\n\nexport function MapFieldRenderer({\n field,\n path,\n inheritedDisabled = false,\n}: {\n field: ParsedField;\n path: string[];\n inheritedDisabled?: boolean;\n}) {\n const { uiComponents } = useAutoFormRenderContext();\n const { ArrayController, errors } = useAutoFormEngine();\n const { testIdPrefix } = useAutoFormRuntimeContext();\n const fullPath = path.join('.');\n const keyField = field.schema?.[0];\n const valueField = field.schema?.[1];\n const error = getFieldErrorMessage(errors, path);\n const label = getRenderedLabel(field);\n const { isDisabled, isVisible, renderField } = useFieldPresentation(field, path, inheritedDisabled);\n const FieldWrapperComponent = field.fieldConfig?.fieldWrapper || uiComponents.FieldWrapper;\n const ArrayWrapperComponent = uiComponents.ArrayWrapper as React.ComponentType<\n React.ComponentProps & {\n addButtonTestId?: string;\n testId?: string;\n }\n >;\n const compactKeyField = keyField ? cloneFieldForCompactRow(keyField) : undefined;\n const compactValueField = valueField ? cloneFieldForCompactRow(valueField) : undefined;\n const valueIsComplex = isComplexCollectionField(valueField);\n\n if (!isVisible) {\n return null;\n }\n\n return (\n \n \n {(controller) => (\n {\n if (!isDisabled) {\n controller.append({ key: '', value: createEmptyFieldValue(valueField) });\n }\n }}\n testId={getAutoFormFieldTestId(testIdPrefix, fullPath, 'items')}\n >\n {controller.items.map((item, index) => {\n const rowTestId = getAutoFormCollectionRowTestId(testIdPrefix, fullPath, index);\n const removeButtonTestId = getAutoFormCollectionRemoveTestId(testIdPrefix, fullPath, index);\n const removeEntry = () => {\n if (!isDisabled) {\n controller.remove(index);\n }\n };\n\n if (compactKeyField && compactValueField && !valueIsComplex) {\n return (\n 0 ? `${COMPACT_PAIR_GRID} ${formSpacing.arrayItemSeparator}` : COMPACT_PAIR_GRID}\n data-testid={rowTestId}\n key={item.key}\n >\n \n \n \n \n \n \n );\n }\n\n return (\n 0\n ? `${formSpacing.collectionRow} ${formSpacing.arrayItemSeparator}`\n : formSpacing.collectionRow\n }\n data-testid={rowTestId}\n key={item.key}\n >\n
\n {compactKeyField ? (\n \n ) : null}\n \n \n \n
\n {valueField ? (\n \n ) : null}\n \n );\n })}\n \n )}\n
\n
\n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/renderers/object.tsx", "content": "'use client';\n\nimport { Text } from '../../typography';\nimport { useAutoFormRenderContext, useAutoFormRuntimeContext } from '../context';\nimport type { ParsedField } from '../core-types';\nimport { useAutoFormEngine } from '../engine';\nimport { getPathInObject } from '../field-utils';\nimport { getFieldErrorMessage } from '../helpers';\nimport { getAutoFormFieldTestId } from '../test-ids';\nimport { AutoFormFieldRenderer } from './index';\nimport { getRenderedLabel, useFieldPresentation } from './shared';\n\nexport function ObjectFieldRenderer({\n field,\n path,\n inheritedDisabled = false,\n}: {\n field: ParsedField;\n path: string[];\n inheritedDisabled?: boolean;\n}) {\n const { uiComponents } = useAutoFormRenderContext();\n const { errors } = useAutoFormEngine();\n const fullPath = path.join('.');\n const error = getFieldErrorMessage(errors, path);\n const label = getRenderedLabel(field);\n const { isVisible, renderField } = useFieldPresentation(field, path, inheritedDisabled);\n const { testIdPrefix } = useAutoFormRuntimeContext();\n\n // Check for errors on the object itself or any descendant field.\n // This ensures collapsible sections auto-expand when a child has an error.\n const errorAtPath = getPathInObject(errors as Record, path);\n const hasDescendantError = errorAtPath !== undefined && errorAtPath !== null && typeof errorAtPath === 'object';\n const hasError = Boolean(error) || hasDescendantError;\n\n if (!isVisible) {\n return null;\n }\n\n const ObjectWrapperComponent = uiComponents.ObjectWrapper;\n\n // Render the error inline as a sibling of the section only when present.\n // The previous version reserved a `min-h-5` slot unconditionally, which\n // added ~20px of whitespace under every nested object and drifted the\n // rhythm away from manually-composed Field-based forms. A naked\n // `
` + optional error line lets the parent's `formSpacing.form`\n // token drive the gap between siblings without any extra padding.\n return (\n <>\n \n {(renderField.schema ?? []).map((subField) => (\n \n ))}\n \n {error ? (\n \n {error}\n \n ) : null}\n \n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/renderers/oneof.tsx", "content": "'use client';\n\nimport React from 'react';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../select';\nimport { useAutoFormRenderContext, useAutoFormRuntimeContext } from '../context';\nimport type { ParsedField } from '../core-types';\nimport { useAutoFormEngine } from '../engine';\nimport { getPathInObject } from '../field-utils';\nimport { getLabel } from '../field-utils';\nimport { formSpacing } from '../form-spacing';\nimport { createEmptyFieldValue, getFieldErrorMessage, getFieldUiConfig } from '../helpers';\nimport { FormDepthProvider, useFormDepth } from '../layout-context';\nimport { getAutoFormFieldTestId } from '../test-ids';\nimport { AutoFormFieldRenderer } from './index';\nimport { getRenderedLabel, isDeprecatedField, isFieldHidden, useFieldPresentation } from './shared';\n\nexport function OneofFieldRenderer({\n field,\n path,\n inheritedDisabled = false,\n}: {\n field: ParsedField;\n path: string[];\n inheritedDisabled?: boolean;\n}) {\n const { uiComponents } = useAutoFormRenderContext();\n const { deprecatedFields, evaluateRules } = useAutoFormRuntimeContext();\n const form = useAutoFormEngine();\n const fullPath = path.join('.');\n const oneofValue = (getPathInObject(form.values, path) as { case?: string; value?: unknown } | undefined) ?? {\n case: undefined,\n value: undefined,\n };\n const error = getFieldErrorMessage(form.errors, path);\n const label = getRenderedLabel(field);\n const { isDisabled, isVisible, renderField } = useFieldPresentation(field, path, inheritedDisabled);\n const FieldWrapperComponent = field.fieldConfig?.fieldWrapper || uiComponents.FieldWrapper;\n const { testIdPrefix } = useAutoFormRuntimeContext();\n const controlTestId = getAutoFormFieldTestId(testIdPrefix, fullPath, 'control');\n const depth = useFormDepth();\n\n const ruleVisibleFields = (field.schema ?? []).filter((candidate) => {\n const candidateUi = getFieldUiConfig(candidate);\n const candidateValue = candidate.key === oneofValue.case ? oneofValue.value : undefined;\n return evaluateRules(candidateUi.visibleWhen, candidateValue);\n });\n const availableFields = ruleVisibleFields.filter(\n (candidate) => !isFieldHidden(candidate, deprecatedFields)\n );\n\n const selectedField = availableFields.find((candidate) => candidate.key === oneofValue.case);\n const selectedSchemaField = (field.schema ?? []).find(\n (candidate) => candidate.key === oneofValue.case\n );\n const selectedDeprecatedDisabled =\n deprecatedFields === 'disable' &&\n selectedSchemaField !== undefined &&\n isDeprecatedField(selectedSchemaField);\n const oneofDisabled = isDisabled || selectedDeprecatedDisabled;\n const selectedValueLabel = selectedField\n ? getLabel(selectedField)\n : oneofValue.case\n ? 'Unavailable selection'\n : field.required\n ? undefined\n : 'Not set';\n\n React.useEffect(() => {\n if (!oneofValue.case) {\n return;\n }\n\n const stillVisibleByRule = ruleVisibleFields.some(\n (candidate) => candidate.key === oneofValue.case\n );\n if (!stillVisibleByRule) {\n form.setValue(\n fullPath,\n { case: undefined, value: undefined },\n { shouldDirty: true, shouldTouch: true, shouldValidate: true }\n );\n }\n }, [form, fullPath, oneofValue.case, ruleVisibleFields]);\n\n if (!isVisible) {\n return null;\n }\n\n return (\n \n
\n ({\n label: getLabel(candidate),\n value: candidate.key,\n })),\n ]}\n onValueChange={(value) => {\n if (oneofDisabled) {\n return;\n }\n if (value === null) {\n form.setValue(\n fullPath,\n { case: undefined, value: undefined },\n { shouldDirty: true, shouldValidate: true }\n );\n return;\n }\n const nextField = availableFields.find((candidate) => candidate.key === value);\n form.clearErrors([`${fullPath}.value`]);\n form.setValue(\n fullPath,\n {\n case: value,\n value: oneofValue.case === value ? oneofValue.value : createEmptyFieldValue(nextField),\n },\n { shouldDirty: true, shouldTouch: true, shouldValidate: true }\n );\n }}\n value={oneofValue.case ?? null}\n >\n \n {selectedValueLabel}\n \n \n {field.required ? null : (\n \n Not set\n \n )}\n {availableFields.map((candidate) => (\n \n {getLabel(candidate)}\n \n ))}\n \n \n {selectedField ? (\n selectedField.type === 'object' && (!selectedField.schema || selectedField.schema.length === 0) ? (\n
\n

\n {getLabel(selectedField)} selected. No additional configuration needed.\n

\n
\n ) : (\n // Oneof values render conceptually one level deeper than the\n // selector itself. Bumping depth here keeps headings consulted\n // by ObjectWrapper consistent with siblings reached via\n // plain nested-object paths.\n \n \n \n )\n ) : null}\n
\n
\n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/renderers/shared.ts", "content": "import React from 'react';\n\nimport { useAutoFormRuntimeContext } from '../context';\nimport type { ParsedField } from '../core-types';\nimport { getFieldHints } from '../core-types';\nimport { getLabel, getPathInObject } from '../field-utils';\nimport { getFieldUiConfig, resolveRenderFieldType } from '../helpers';\nimport type { DeprecatedFieldPolicy } from '../types';\n\nexport function isDeprecatedField(field: ParsedField): boolean {\n return getFieldHints(field)?.deprecated === true;\n}\n\nexport function isFieldHidden(field: ParsedField, policy: DeprecatedFieldPolicy): boolean {\n const customData = (field.fieldConfig?.customData ?? {}) as Record;\n return Boolean(customData.hidden) || (policy === 'hide' && isDeprecatedField(field));\n}\n\nexport function cloneFieldWithDisabled(field: ParsedField, disabled: boolean): ParsedField {\n if (!disabled) {\n return field;\n }\n\n return {\n ...field,\n fieldConfig: {\n ...(field.fieldConfig ?? {}),\n inputProps: {\n ...(field.fieldConfig?.inputProps ?? {}),\n disabled: true,\n },\n },\n };\n}\n\nexport function useFieldPresentation(field: ParsedField, path: string[], inheritedDisabled = false) {\n const { deprecatedFields, formValues, evaluateRules } = useAutoFormRuntimeContext();\n const fieldValue = getPathInObject(formValues, path);\n const uiConfig = getFieldUiConfig(field);\n const customData = (field.fieldConfig?.customData ?? {}) as Record;\n const isHidden = isFieldHidden(field, deprecatedFields);\n const isImmutable = Boolean(customData.immutable);\n const isVisible = !isHidden && evaluateRules(uiConfig.visibleWhen, fieldValue);\n const isDisabledByRule = uiConfig.disabledWhen?.length ? evaluateRules(uiConfig.disabledWhen, fieldValue) : false;\n const isDisabled =\n inheritedDisabled ||\n isDisabledByRule ||\n isImmutable ||\n (deprecatedFields === 'disable' && isDeprecatedField(field));\n const renderField = React.useMemo(() => cloneFieldWithDisabled(field, isDisabled), [field, isDisabled]);\n\n return {\n fieldValue,\n isDisabled,\n isVisible,\n renderField,\n renderType: resolveRenderFieldType(field),\n };\n}\n\nexport function cloneFieldForCompactRow(field: ParsedField): ParsedField {\n const label = String(getLabel(field));\n\n const existingCustomData = (field.fieldConfig?.customData ?? {}) as Record;\n const existingUi = (existingCustomData.ui ?? {}) as Record;\n\n return {\n ...field,\n fieldConfig: {\n ...(field.fieldConfig ?? {}),\n label: '',\n description: '',\n customData: {\n ...existingCustomData,\n compactRow: true,\n ui: {\n ...existingUi,\n help: '',\n example: '',\n },\n },\n inputProps: {\n ...(field.fieldConfig?.inputProps ?? {}),\n placeholder:\n (field.fieldConfig?.inputProps?.placeholder as string | undefined) ||\n (field.type === 'select' || field.type === 'boolean' ? undefined : label),\n },\n },\n };\n}\n\nexport function getRenderedLabel(field: ParsedField): string {\n if (typeof field.fieldConfig?.label === 'string') {\n return field.fieldConfig.label;\n }\n\n return String(getLabel(field));\n}\n\nexport function isComplexCollectionField(field: ParsedField | undefined): boolean {\n if (!field) {\n return false;\n }\n\n const renderType = resolveRenderFieldType(field);\n return ['object', 'array', 'map', 'oneof', 'json'].includes(renderType);\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/runtime-provider.tsx", "content": "'use client';\n\nimport type { DescMessage } from '@bufbuild/protobuf';\nimport React from 'react';\nimport type { ProtoConversionOptions } from '../../lib/protobuf-provider';\n\nimport { AutoFormContext, type AutoFormContextValue } from './context';\nimport type {\n AutoFormFieldComponents,\n AutoFormUIComponents,\n ParsedField,\n ParsedSchema,\n SchemaProvider,\n} from './core-types';\nimport type { DataProviderRegistry } from './data-providers';\nimport { type AutoFormEngine, useAutoFormEngine } from './engine';\nimport { getFieldUiConfig, isRecord, isValidationSuccess } from './helpers';\nimport { protoFormValuesToPayload, protoPayloadToFormValues } from './proto';\nimport type { FieldTypeRegistry } from './registry';\nimport type {\n AutoFormMode,\n AutoFormPayloadBuilderContext,\n AutoFormSummaryContext,\n AutoFormUiRule,\n DeprecatedFieldPolicy,\n} from './types';\nimport { evaluateUiRules } from './ui-rules';\nimport { isPromiseLike, safeStringify } from './utils/serialization';\n\ntype PayloadBag = {\n payloadState: { bestEffort: boolean; payload: unknown };\n jsonEditorText: string;\n jsonEditorError: string | undefined;\n payloadText: string;\n summaryContext: AutoFormSummaryContext;\n handleJsonTextChange: (value: string) => void;\n handleResetJson: () => void;\n handleFormatJson: () => void;\n};\n\ntype AutoFormRuntimeProviderProps = {\n children: React.ReactNode;\n uiComponents: AutoFormUIComponents;\n formComponents: AutoFormFieldComponents;\n testIdPrefix: string;\n fieldRegistry?: FieldTypeRegistry;\n conversionOptions?: ProtoConversionOptions;\n dataProviders?: DataProviderRegistry;\n deprecatedFields: DeprecatedFieldPolicy;\n resolvedSchema: {\n provider: SchemaProvider>;\n parsedSchema: ParsedSchema;\n isProto: boolean;\n protoDesc?: DescMessage;\n resolver?: unknown;\n };\n mode: AutoFormMode;\n simpleFields: ParsedField[];\n advancedFields: ParsedField[];\n payloadBuilder?: (\n values: Record,\n context: AutoFormPayloadBuilderContext\n ) => unknown;\n payloadParser?: (\n payload: unknown,\n context: AutoFormPayloadBuilderContext\n ) => Record | undefined | Promise | undefined>;\n payloadSchema?: {\n safeParse: (data: unknown) => { success: boolean; error?: { issues: Array<{ path: unknown[]; message: string }> } };\n };\n renderContent: (bag: PayloadBag) => React.ReactNode;\n onFieldChange?: (\n fieldPath: string,\n value: unknown,\n form: TNativeForm\n ) => void | Promise;\n};\n\n// ---------------------------------------------------------------------------\n// AutoFormPayloadController — leaf component that owns payload/JSON state.\n// Uses useDeferredValue so expensive payload computation (SchemaProvider\n// validation, proto conversion, payloadBuilder) doesn't block typing on large forms.\n// ---------------------------------------------------------------------------\n\nfunction AutoFormPayloadController({\n watchedValues,\n methods,\n resolvedSchema,\n mode,\n simpleFields,\n advancedFields,\n payloadBuilder,\n payloadParser,\n payloadSchema,\n renderContent,\n conversionOptions,\n}: {\n watchedValues: Record;\n methods: AutoFormEngine;\n resolvedSchema: AutoFormRuntimeProviderProps['resolvedSchema'];\n mode: AutoFormMode;\n simpleFields: ParsedField[];\n advancedFields: ParsedField[];\n payloadBuilder: AutoFormRuntimeProviderProps['payloadBuilder'];\n payloadParser: AutoFormRuntimeProviderProps['payloadParser'];\n payloadSchema: AutoFormRuntimeProviderProps['payloadSchema'];\n conversionOptions: AutoFormRuntimeProviderProps['conversionOptions'];\n renderContent: AutoFormRuntimeProviderProps['renderContent'];\n}) {\n const deferredValues = React.useDeferredValue(watchedValues);\n const payloadValidationController = React.useMemo(\n () => new AbortController(),\n [deferredValues, resolvedSchema.provider]\n );\n\n React.useEffect(\n function abortPayloadValidation() {\n return () => payloadValidationController.abort();\n },\n [payloadValidationController]\n );\n\n const payloadContextBase = React.useMemo(\n () => ({\n form: methods.nativeForm as TNativeForm,\n autoForm: methods,\n schema: resolvedSchema.parsedSchema,\n isProto: resolvedSchema.isProto,\n protoDesc: resolvedSchema.protoDesc,\n mode,\n simpleFields,\n advancedFields,\n }),\n [\n advancedFields,\n methods,\n mode,\n resolvedSchema.isProto,\n resolvedSchema.parsedSchema,\n resolvedSchema.protoDesc,\n simpleFields,\n ]\n );\n\n const payloadState = React.useMemo(() => {\n let validationSuccess = false;\n let validatedData: unknown;\n let bestEffort = false;\n\n try {\n const validationResult = resolvedSchema.provider.validateSchema(deferredValues as never, {\n signal: payloadValidationController.signal,\n });\n if (isPromiseLike(validationResult)) {\n // Payload preview is best-effort; the engine's awaited validation path\n // owns user-visible errors. Observe rejection here to avoid leaking it.\n void Promise.resolve(validationResult).catch(() => undefined);\n bestEffort = true;\n } else if (isValidationSuccess(validationResult)) {\n validationSuccess = true;\n validatedData = validationResult.data;\n } else {\n bestEffort = true;\n }\n } catch {\n bestEffort = true;\n }\n\n let payload: unknown;\n\n if (payloadBuilder) {\n try {\n payload = payloadBuilder(\n deferredValues,\n payloadContextBase as AutoFormPayloadBuilderContext\n );\n } catch (error) {\n console.warn('[AutoForm] payloadBuilder threw:', error);\n bestEffort = true;\n }\n }\n\n if (payload === undefined) {\n if (resolvedSchema.isProto && resolvedSchema.protoDesc) {\n payload = protoFormValuesToPayload(\n resolvedSchema.protoDesc,\n deferredValues,\n conversionOptions\n );\n bestEffort ||= !validationSuccess;\n } else if (validationSuccess) {\n payload = validatedData;\n } else {\n payload = deferredValues;\n bestEffort = true;\n }\n }\n\n if (payloadSchema && payload !== undefined) {\n const validation = payloadSchema.safeParse(payload);\n if (!validation.success && validation.error) {\n console.warn(\n '[AutoForm] payloadSchema validation failed:',\n validation.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join(', ')\n );\n }\n }\n\n return { bestEffort, payload };\n }, [\n deferredValues,\n conversionOptions,\n payloadBuilder,\n payloadContextBase,\n payloadSchema,\n payloadValidationController.signal,\n resolvedSchema.isProto,\n resolvedSchema.protoDesc,\n resolvedSchema.provider,\n ]);\n\n const payloadText = React.useMemo(() => safeStringify(payloadState.payload), [payloadState.payload]);\n const [jsonEditorText, setJsonEditorText] = React.useState(payloadText);\n const [jsonEditorError, setJsonEditorError] = React.useState();\n\n React.useEffect(() => {\n if (!jsonEditorError) {\n setJsonEditorText(payloadText);\n }\n }, [jsonEditorError, payloadText]);\n\n const applySeqRef = React.useRef(0);\n\n const applyPayloadToForm = React.useCallback(\n async (incoming: unknown) => {\n const seq = ++applySeqRef.current;\n try {\n let nextValues: Record | undefined;\n\n if (payloadParser) {\n const parsed = payloadParser(\n incoming,\n payloadContextBase as AutoFormPayloadBuilderContext\n );\n nextValues = isPromiseLike(parsed) ? await parsed : parsed;\n } else if (resolvedSchema.isProto && resolvedSchema.protoDesc) {\n nextValues = protoPayloadToFormValues(resolvedSchema.protoDesc, incoming);\n } else if (isRecord(incoming)) {\n nextValues = incoming;\n }\n\n if (applySeqRef.current !== seq) {\n return;\n }\n\n if (!nextValues) {\n setJsonEditorError('AutoForm could not map this JSON payload back into the form.');\n return;\n }\n\n methods.reset(nextValues, { keepDefaultValues: true });\n setJsonEditorError(undefined);\n } catch (error) {\n if (applySeqRef.current !== seq) {\n return;\n }\n setJsonEditorError(error instanceof Error ? error.message : 'AutoForm could not apply this payload.');\n }\n },\n [methods, payloadContextBase, payloadParser, resolvedSchema.isProto, resolvedSchema.protoDesc]\n );\n\n const handleJsonTextChange = React.useCallback(\n (value: string) => {\n setJsonEditorText(value);\n try {\n const parsed = JSON.parse(value);\n setJsonEditorError(undefined);\n void applyPayloadToForm(parsed);\n } catch (error) {\n setJsonEditorError(error instanceof Error ? error.message : 'Invalid JSON');\n }\n },\n [applyPayloadToForm]\n );\n\n const handleResetJson = React.useCallback(() => {\n setJsonEditorError(undefined);\n setJsonEditorText(payloadText);\n }, [payloadText]);\n\n const handleFormatJson = React.useCallback(() => {\n try {\n const parsed = JSON.parse(jsonEditorText);\n const formatted = JSON.stringify(parsed, null, 2);\n setJsonEditorText(formatted);\n setJsonEditorError(undefined);\n void applyPayloadToForm(parsed);\n } catch (error) {\n setJsonEditorError(error instanceof Error ? error.message : 'Invalid JSON');\n }\n }, [applyPayloadToForm, jsonEditorText]);\n\n const summaryContext = React.useMemo>(\n () => ({\n ...(payloadContextBase as AutoFormPayloadBuilderContext),\n payload: payloadState.payload,\n bestEffort: payloadState.bestEffort,\n }),\n [payloadContextBase, payloadState.bestEffort, payloadState.payload]\n );\n\n return (\n <>\n {renderContent({\n payloadState,\n jsonEditorText,\n jsonEditorError,\n payloadText,\n summaryContext,\n handleJsonTextChange,\n handleResetJson,\n handleFormatJson,\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// AutoFormRuntimeProvider — provides the AutoFormContext with live form values.\n// Payload computation is delegated to the AutoFormPayloadController child.\n// ---------------------------------------------------------------------------\n\nexport function AutoFormRuntimeProvider({\n children: _children,\n uiComponents,\n formComponents,\n testIdPrefix,\n fieldRegistry,\n conversionOptions,\n dataProviders,\n deprecatedFields,\n resolvedSchema,\n mode,\n simpleFields,\n advancedFields,\n payloadBuilder,\n payloadParser,\n payloadSchema,\n renderContent,\n onFieldChange,\n}: AutoFormRuntimeProviderProps) {\n const methods = useAutoFormEngine();\n const watchedValues = methods.values;\n\n const prevValuesRef = React.useRef>(watchedValues);\n\n React.useEffect(() => {\n // Note: only fires for root-level field keys. Nested changes (e.g. address.city)\n // fire as onFieldChange(\"address\", ...) when the parent object reference changes.\n if (!onFieldChange) return;\n const prev = prevValuesRef.current;\n for (const key of Object.keys(watchedValues)) {\n if (watchedValues[key] !== prev[key]) {\n void onFieldChange(key, watchedValues[key], methods.nativeForm as TNativeForm);\n }\n }\n prevValuesRef.current = { ...watchedValues };\n }, [watchedValues, onFieldChange, methods]);\n\n const contextValue = React.useMemo(\n () => ({\n uiComponents,\n formComponents,\n formValues: watchedValues,\n evaluateRules: (rules: AutoFormUiRule[] | undefined, fieldValue?: unknown) =>\n evaluateUiRules(rules, { form: watchedValues, thisValue: fieldValue }),\n getFieldUiConfig,\n testIdPrefix,\n fieldRegistry,\n dataProviders,\n deprecatedFields,\n }),\n [dataProviders, deprecatedFields, fieldRegistry, formComponents, uiComponents, testIdPrefix, watchedValues]\n );\n\n return (\n \n \n advancedFields={advancedFields}\n conversionOptions={conversionOptions}\n methods={methods}\n mode={mode}\n payloadBuilder={payloadBuilder}\n payloadParser={payloadParser}\n payloadSchema={payloadSchema}\n renderContent={renderContent}\n resolvedSchema={resolvedSchema}\n simpleFields={simpleFields}\n watchedValues={watchedValues}\n />\n \n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/schema.ts", "content": "import type { ParsedField, SchemaProvider } from './core-types';\nimport type { Message } from '@bufbuild/protobuf';\nimport type { ProtoConversionOptions } from '../../lib/protobuf-provider';\nimport { sortFieldsByOrder } from './field-utils';\nimport { isProtoMessageDescriptor, isProtoProvider, ProtoProvider } from './proto';\nimport type {\n AutoFormSchemaInput,\n FieldConfigMap,\n FieldTypes,\n RenderFieldConfig,\n ResolvedSchema,\n} from './types';\n\nfunction isSchemaProvider(value: unknown): value is SchemaProvider> {\n return Boolean(\n value &&\n typeof value === 'object' &&\n 'parseSchema' in value &&\n typeof (value as SchemaProvider>).parseSchema === 'function' &&\n 'validateSchema' in value &&\n typeof (value as SchemaProvider>).validateSchema === 'function' &&\n 'getDefaultValues' in value &&\n typeof (value as SchemaProvider>).getDefaultValues === 'function'\n );\n}\n\nexport { normalizeProtoInitialValues } from './proto';\n\nexport function resolveSchema>(\n schemaInput: AutoFormSchemaInput,\n conversionOptions: ProtoConversionOptions = {},\n protoSource?: Message\n): ResolvedSchema {\n if (isSchemaProvider(schemaInput)) {\n const provider = schemaInput as SchemaProvider>;\n const parsedSchema = provider.parseSchema();\n\n if (isProtoProvider(provider)) {\n const protoDesc = provider.getMessageDescriptor();\n return {\n provider,\n parsedSchema,\n isProto: true,\n protoDesc,\n protoSource,\n };\n }\n\n return {\n provider,\n parsedSchema,\n isProto: false,\n };\n }\n\n if (isProtoMessageDescriptor(schemaInput)) {\n const provider = new ProtoProvider(schemaInput, conversionOptions);\n return {\n provider,\n parsedSchema: provider.parseSchema(),\n isProto: true,\n protoDesc: schemaInput,\n protoSource,\n };\n }\n\n throw new Error('Unsupported AutoForm schema input. Pass a SchemaProvider or a Buf message descriptor.');\n}\n\nexport function protoConversionOptionsFromFieldConfig(\n fieldConfig: FieldConfigMap | undefined\n): ProtoConversionOptions {\n const emptyRepeatedStringPolicies = Object.fromEntries(\n Object.entries(fieldConfig ?? {}).flatMap(([path, config]) =>\n config.emptyRepeatedStringPolicy\n ? [[path, config.emptyRepeatedStringPolicy] as const]\n : []\n )\n );\n return Object.keys(emptyRepeatedStringPolicies).length > 0\n ? { emptyRepeatedStringPolicies }\n : {};\n}\n\nexport function mergeFieldOverrides(\n fields: ParsedField[] | undefined,\n overrides: FieldConfigMap | undefined,\n path: string[] = []\n): ParsedField>[] {\n if (!fields) {\n return [];\n }\n\n return sortFieldsByOrder(\n fields.map((field) => {\n const fieldPath = [...path, field.key].join('.');\n const override = overrides?.[fieldPath];\n const existingConfig = field.fieldConfig as RenderFieldConfig | undefined;\n const mergedFieldConfig: RenderFieldConfig | undefined = override\n ? ({\n ...(existingConfig ?? {}),\n ...override,\n inputProps: {\n ...(existingConfig?.inputProps ?? {}),\n ...(override.inputProps ?? {}),\n },\n customData: {\n ...(existingConfig?.customData ?? {}),\n ...(override.customData ?? {}),\n },\n } as RenderFieldConfig)\n : existingConfig;\n\n const nextSchema = field.schema?.length\n ? mergeFieldOverrides(field.schema, overrides, [...path, field.key])\n : field.schema;\n\n return {\n ...field,\n fieldConfig: mergedFieldConfig,\n schema: nextSchema,\n } as ParsedField>;\n })\n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/slot.tsx", "content": "'use client';\n\nimport type React from 'react';\n\ntype AutoFormSlotProps = {\n /** Render this slot before the field with this key */\n before?: string;\n /** Render this slot after the field with this key */\n after?: string;\n children: React.ReactNode;\n};\n\nfunction AutoFormSlot({ children }: AutoFormSlotProps) {\n // AutoFormSlot is a marker component — its props are read by AutoFormFields\n // to determine placement. It never renders itself directly.\n return <>{children};\n}\n\n// Sentinel to identify AutoFormSlot elements in children\nAutoFormSlot.displayName = 'AutoFormSlot';\n\nexport { AutoFormSlot, type AutoFormSlotProps };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/stepper.tsx", "content": "'use client';\n\nimport { Check } from 'lucide-react';\nimport React from 'react';\n\nimport { Button } from '@/registry/base-nova/protoform/components/button';\nimport { Heading, Text } from '@/registry/base-nova/protoform/components/typography';\n\nimport type { ParsedField } from './core-types';\nimport { FormDepthProvider, useFormDepth } from './layout-context';\nimport type { AutoFormStep, AutoFormStepperOrientation } from './types';\n\nfunction fieldStep(field: ParsedField, firstStepId: string, stepIds: Set): string {\n const configuredStep = field.hints?.step;\n return configuredStep && stepIds.has(configuredStep) ? configuredStep : firstStepId;\n}\n\nexport function fieldsForStep(fields: ParsedField[], steps: AutoFormStep[], stepId: string): ParsedField[] {\n const firstStepId = steps[0]?.id;\n if (!firstStepId) {\n return fields;\n }\n const stepIds = new Set(steps.map((step) => step.id));\n return fields.filter((field) => fieldStep(field, firstStepId, stepIds) === stepId);\n}\n\nexport function initialStepIndex(steps: AutoFormStep[], defaultStep: string | undefined): number {\n const index = defaultStep ? steps.findIndex((step) => step.id === defaultStep) : 0;\n return index >= 0 ? index : 0;\n}\n\nexport function validateSteps(steps: AutoFormStep[]): void {\n if (steps.length < 2) {\n throw new Error('AutoForm stepper requires at least two steps.');\n }\n const ids = new Set();\n for (const step of steps) {\n if (!step.id.trim()) {\n throw new Error('AutoForm step ids must not be empty.');\n }\n if (ids.has(step.id)) {\n throw new Error(`AutoForm step ids must be unique. Duplicate: ${step.id}`);\n }\n ids.add(step.id);\n }\n}\n\nfunction StepMarker({\n index,\n isComplete,\n isCurrent,\n}: {\n index: number;\n isComplete: boolean;\n isCurrent: boolean;\n}) {\n return (\n \n {isComplete ? : index + 1}\n \n );\n}\n\nexport function AutoFormStepIndicator({\n steps,\n currentIndex,\n orientation,\n}: {\n steps: AutoFormStep[];\n currentIndex: number;\n orientation: AutoFormStepperOrientation;\n}) {\n return (\n \n );\n}\n\nexport function AutoFormStepPanel({\n children,\n currentIndex,\n onBack,\n onContinue,\n isAdvancing,\n orientation,\n step,\n steps,\n submit,\n}: {\n children: React.ReactNode;\n currentIndex: number;\n onBack: () => void;\n onContinue: () => void | Promise;\n isAdvancing: boolean;\n orientation: AutoFormStepperOrientation;\n step: AutoFormStep;\n steps: AutoFormStep[];\n submit: React.ReactNode;\n}) {\n const isLastStep = currentIndex === steps.length - 1;\n const depth = useFormDepth();\n\n return (\n
\n \n \n \n
\n \n {step.title}\n \n {step.description ? (\n \n {step.description}\n \n ) : null}\n
\n \n
{children}
\n
\n
\n \n \n {currentIndex > 0 ? (\n \n ) : (\n \n )}\n {isLastStep ? (\n submit\n ) : (\n \n )}\n \n \n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/test-ids.ts", "content": "const DEFAULT_AUTOFORM_TEST_ID_PREFIX = 'autoform';\n\nfunction normalizeSegment(value: string | number | null | undefined): string {\n if (value === undefined || value === null) {\n return '';\n }\n\n return String(value)\n .trim()\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .toLowerCase()\n .replace(/[\\s._[\\]]+/g, '-')\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\nexport function resolveAutoFormTestIdPrefix(testId?: string): string {\n return normalizeSegment(testId) || DEFAULT_AUTOFORM_TEST_ID_PREFIX;\n}\n\nexport function buildAutoFormTestId(prefix: string, ...segments: Array): string {\n const normalizedSegments = segments.map(normalizeSegment).filter(Boolean);\n return [resolveAutoFormTestIdPrefix(prefix), ...normalizedSegments].join('-');\n}\n\nexport function getAutoFormFieldTestId(prefix: string, path: string | string[], slot?: string | number): string {\n const normalizedPath = Array.isArray(path) ? path.join('.') : path;\n return buildAutoFormTestId(prefix, 'field', normalizedPath, slot);\n}\n\nexport function getAutoFormCollectionRowTestId(prefix: string, path: string | string[], index: number): string {\n return getAutoFormFieldTestId(prefix, path, `row-${index}`);\n}\n\nexport function getAutoFormCollectionRemoveTestId(prefix: string, path: string | string[], index: number): string {\n return getAutoFormFieldTestId(prefix, path, `remove-${index}`);\n}\n\nexport function getAutoFormChoiceTestId(\n prefix: string,\n path: string | string[],\n kind: 'option' | 'group' | 'selected',\n value: string | number\n): string {\n return getAutoFormFieldTestId(prefix, path, `${kind}-${value}`);\n}\n\nexport { DEFAULT_AUTOFORM_TEST_ID_PREFIX };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/types.ts", "content": "import type { DescMessage, Message } from '@bufbuild/protobuf';\nimport type { FieldMask } from '@bufbuild/protobuf/wkt';\nimport type { ReactNode } from 'react';\n\nimport type {\n AutoFormFieldComponents,\n AutoFormUIComponents,\n FieldConfig,\n ParsedField,\n ParsedSchema,\n SchemaProvider,\n} from './core-types';\nimport type { AutoFormEngineHandle } from './engine';\nimport type { ProtoFieldRenderType, ProtoUiRule } from './proto';\nimport type { FieldTypeRegistry } from './registry';\n\nexport type AutoFormMode = 'simple' | 'advanced' | 'json';\nexport type AutoFormValidationMode = 'submit' | 'blur' | 'change';\nexport type AutoFormRevalidationMode = Exclude;\nexport type AutoFormStepperOrientation = 'horizontal' | 'vertical';\nexport type AutoFormRootHeaderMode = 'auto' | 'hidden';\nexport type DeprecatedFieldPolicy = 'show' | 'disable' | 'hide';\n\nexport type AutoFormRootHeaderMetadata = {\n description?: string;\n title?: string;\n};\n\nexport type AutoFormStep = {\n description?: ReactNode;\n id: string;\n title: string;\n};\n\nexport type AutoFormStepperConfig = {\n defaultStep?: string;\n orientation?: AutoFormStepperOrientation;\n steps: AutoFormStep[];\n};\n\nexport type AutoFormOptionItem = {\n value: string;\n label?: ReactNode;\n icon?: ReactNode;\n};\n\nexport type AutoFormOptionGroup = {\n label?: ReactNode;\n options: AutoFormOptionItem[];\n};\n\nexport type BuiltInFieldType = ProtoFieldRenderType | 'dataProviderMultiSelect' | 'date' | 'slider';\nexport type FieldTypes = BuiltInFieldType | TCustom;\n\nexport type RenderFieldConfig = FieldConfig<\n FieldTypes,\n Record\n>;\nexport type FieldConfigMap = Record<\n string,\n RenderFieldConfig\n>;\nexport type AutoFormSchemaInput> = SchemaProvider | DescMessage;\n\nexport type AutoFormUiRule = ProtoUiRule;\n\nexport type ResolvedSchema = {\n provider: SchemaProvider>;\n parsedSchema: ParsedSchema;\n isProto: boolean;\n protoDesc?: DescMessage;\n protoSource?: Message;\n};\n\nexport type AutoFormPayloadBuilderContext = {\n form: TNativeForm;\n autoForm: AutoFormEngineHandle;\n schema: ParsedSchema;\n isProto: boolean;\n protoDesc?: DescMessage;\n mode: AutoFormMode;\n simpleFields: ParsedField[];\n advancedFields: ParsedField[];\n};\n\nexport type AutoFormSummaryContext =\n AutoFormPayloadBuilderContext & {\n payload: unknown;\n bestEffort: boolean;\n };\n\nexport type AutoFormSubmitContext = {\n /** Aborted when a newer submit supersedes this attempt or the form unmounts. */\n signal: AbortSignal;\n /** Engine-neutral operations for callbacks shared across form engines. */\n form: AutoFormEngineHandle;\n /** Dirty, writable protobuf paths since the form was initialized or reset. */\n updateMask?: FieldMask;\n};\n\nexport type AutoFormProps<\n T extends Record = Record,\n TNativeForm = unknown,\n TFormOptions = unknown,\n TResolver = unknown,\n TCustomFieldType extends string = never,\n> = {\n schema: AutoFormSchemaInput;\n /**\n * Called when a root-level field value changes. Note: nested changes (e.g.\n * address.city) fire as onFieldChange('address', {...}) when the parent\n * object reference changes — the callback receives the root-level key, not\n * the dotted sub-path.\n */\n onFieldChange?: (\n fieldPath: string,\n value: unknown,\n form: TNativeForm\n ) => void | Promise;\n testId?: string;\n onSubmit?: (\n values: T,\n form: TNativeForm,\n context: AutoFormSubmitContext\n ) => void | Promise;\n defaultValues?: Partial | Partial>;\n values?: Partial | Partial>;\n children?: React.ReactNode;\n uiComponents?: Partial;\n formComponents?: Partial>>;\n withSubmit?: boolean;\n onFormInit?: (form: TNativeForm) => void;\n /** Reports distinct engine-neutral dirty-state transitions, including the initial clean state. */\n onDirtyChange?: (isDirty: boolean) => void;\n /** Controls whether schema-provided root metadata is shown. */\n rootHeader?: AutoFormRootHeaderMode;\n /** Replaces the default root header while preserving resolved schema metadata. */\n renderRootHeader?: (metadata: AutoFormRootHeaderMetadata) => ReactNode;\n /** Presentation policy for fields marked deprecated by the schema. */\n deprecatedFields?: DeprecatedFieldPolicy;\n formProps?: React.ComponentProps<'form'> | Record;\n fieldConfig?: FieldConfigMap;\n formOptions?: TFormOptions;\n resolver?: TResolver;\n modes?: AutoFormMode[];\n defaultMode?: AutoFormMode;\n /** Shared validation lifecycle across supported AutoForm engines. */\n validationMode?: AutoFormValidationMode;\n /** Lifecycle used after the first submit attempt. */\n revalidationMode?: AutoFormRevalidationMode;\n /** Opt-in linear flow. Field membership comes from schema-agnostic `hints.step` metadata. */\n stepper?: AutoFormStepperConfig;\n showSummary?: boolean;\n renderSummary?: (payload: unknown, context: AutoFormSummaryContext) => React.ReactNode;\n fieldRegistry?: FieldTypeRegistry>;\n /**\n * Named data-source implementations consumed by dropdown-style controls\n * annotated with `field_ui.data_provider`. The keys mirror the proto\n * `DataProviderId` enum (snake-cased or exact string). Values are React\n * hooks returning `{ options, isLoading?, error? }` — AutoForm never\n * inspects internals, so providers can be static arrays or RPC-backed.\n *\n * A CI test (see `__tests__/data-providers.test.ts`) enumerates proto\n * descriptors and asserts every referenced id is registered here.\n */\n dataProviders?: import('./data-providers').DataProviderRegistry;\n classifyField?: (\n field: ParsedField>\n ) => 'simple' | 'advanced';\n payloadSchema?: {\n safeParse: (data: unknown) => { success: boolean; error?: { issues: Array<{ path: unknown[]; message: string }> } };\n };\n payloadBuilder?: (\n values: Record,\n context: AutoFormPayloadBuilderContext\n ) => unknown;\n payloadParser?: (\n payload: unknown,\n context: AutoFormPayloadBuilderContext\n ) => Record | undefined | Promise | undefined>;\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/cel-runtime.ts", "content": "import {\n CelScalar,\n type CelError,\n celEnv,\n celError,\n celFunc,\n isCelError,\n parse,\n plan,\n} from '@bufbuild/cel';\n\ntype CelExpr = NonNullable['expr']>;\n\nconst CONDITIONAL_FUNCTION = '_?_:_';\nconst INDEX_FUNCTION = '_[_]';\nconst COST_FUNCTION = 'protoform.consume_cost';\nconst UNKNOWN_ERROR_PREFIX = 'protoform unknown attribute: ';\n\nexport const DEFAULT_CEL_MAX_COST = 10_000;\n\nexport interface CompileCelExpressionOptions {\n maxCost?: number;\n unknownAttributes?: readonly string[];\n}\n\nexport type CelEvaluation =\n | { cost: number; kind: 'value'; value: unknown }\n | { attributes: readonly string[]; cost: number; kind: 'unknown' }\n | { cost: number; error: CelError; kind: 'error' }\n | { cost: number; kind: 'cost-exceeded'; limit: number };\n\nexport type CompiledCelExpression = (\n bindings?: Record\n) => CelEvaluation;\n\ninterface EvaluationBudget {\n cost: number;\n limit: number;\n}\n\nclass CelCostLimitError extends Error {\n constructor(\n readonly cost: number,\n readonly limit: number\n ) {\n super(`CEL evaluation cost ${cost} exceeds limit ${limit}.`);\n this.name = 'CelCostLimitError';\n }\n}\n\nfunction childExpressions(expr: CelExpr): CelExpr[] {\n switch (expr.exprKind.case) {\n case 'callExpr':\n return expr.exprKind.value.target\n ? [expr.exprKind.value.target, ...expr.exprKind.value.args]\n : expr.exprKind.value.args;\n case 'comprehensionExpr': {\n const comprehension = expr.exprKind.value;\n return [\n comprehension.iterRange,\n comprehension.accuInit,\n comprehension.loopCondition,\n comprehension.loopStep,\n comprehension.result,\n ].filter((child): child is CelExpr => child !== undefined);\n }\n case 'listExpr':\n return expr.exprKind.value.elements;\n case 'selectExpr':\n return expr.exprKind.value.operand ? [expr.exprKind.value.operand] : [];\n case 'structExpr':\n return expr.exprKind.value.entries.flatMap((entry) => {\n const children: CelExpr[] = [];\n if (entry.keyKind.case === 'mapKey') {\n children.push(entry.keyKind.value);\n }\n if (entry.value) {\n children.push(entry.value);\n }\n return children;\n });\n case 'constExpr':\n case 'identExpr':\n case undefined:\n return [];\n default: {\n const exhaustive: never = expr.exprKind;\n return exhaustive;\n }\n }\n}\n\nfunction maximumExpressionId(expr: CelExpr): bigint {\n return childExpressions(expr).reduce(\n (maximum, child) => {\n const childMaximum = maximumExpressionId(child);\n return childMaximum > maximum ? childMaximum : maximum;\n },\n expr.id\n );\n}\n\nfunction constantPathSegment(expr: CelExpr): string | undefined {\n if (expr.exprKind.case !== 'constExpr') {\n return undefined;\n }\n\n const constant = expr.exprKind.value.constantKind;\n switch (constant.case) {\n case 'stringValue':\n return /^[A-Za-z_][A-Za-z0-9_]*$/.test(constant.value)\n ? `.${constant.value}`\n : `[${JSON.stringify(constant.value)}]`;\n case 'int64Value':\n return `[${constant.value}]`;\n case 'uint64Value':\n return `[${constant.value}]`;\n case 'boolValue':\n case 'bytesValue':\n case 'doubleValue':\n case 'durationValue':\n case 'nullValue':\n case 'timestampValue':\n case undefined:\n return undefined;\n default: {\n const exhaustive: never = constant;\n return exhaustive;\n }\n }\n}\n\nfunction attributePath(expr: CelExpr): string | undefined {\n switch (expr.exprKind.case) {\n case 'identExpr':\n return expr.exprKind.value.name;\n case 'selectExpr': {\n const operand = expr.exprKind.value.operand;\n const parent = operand ? attributePath(operand) : undefined;\n return parent ? `${parent}.${expr.exprKind.value.field}` : undefined;\n }\n case 'callExpr': {\n const call = expr.exprKind.value;\n if (call.function !== INDEX_FUNCTION || call.args.length !== 2) {\n return undefined;\n }\n const parent = call.args[0] ? attributePath(call.args[0]) : undefined;\n const segment = call.args[1]\n ? constantPathSegment(call.args[1])\n : undefined;\n return parent && segment ? `${parent}${segment}` : undefined;\n }\n case 'comprehensionExpr':\n case 'constExpr':\n case 'listExpr':\n case 'structExpr':\n case undefined:\n return undefined;\n default: {\n const exhaustive: never = expr.exprKind;\n return exhaustive;\n }\n }\n}\n\nfunction replaceWithIdentifier(expr: CelExpr, name: string): void {\n expr.exprKind = {\n case: 'identExpr',\n value: {\n $typeName: 'cel.expr.Expr.Ident',\n name,\n },\n };\n}\n\nfunction replaceUnknownAttributes(\n expr: CelExpr,\n unknownAttributes: ReadonlySet,\n unknownBindings: Map\n): void {\n const path = attributePath(expr);\n if (path && unknownAttributes.has(path)) {\n let name = [...unknownBindings].find(([, value]) => value === path)?.[0];\n if (!name) {\n name = `_protoform_unknown_${unknownBindings.size}`;\n unknownBindings.set(name, path);\n }\n replaceWithIdentifier(expr, name);\n return;\n }\n\n for (const child of childExpressions(expr)) {\n replaceUnknownAttributes(child, unknownAttributes, unknownBindings);\n }\n}\n\nfunction createCostCall(template: CelExpr, id: bigint): CelExpr {\n const expr = structuredClone(template);\n expr.id = id;\n expr.exprKind = {\n case: 'callExpr',\n value: {\n $typeName: 'cel.expr.Expr.Call',\n args: [],\n function: COST_FUNCTION,\n },\n };\n return expr;\n}\n\nfunction createBooleanConstant(\n template: CelExpr,\n id: bigint,\n value: boolean\n): CelExpr {\n const expr = structuredClone(template);\n expr.id = id;\n expr.exprKind = {\n case: 'constExpr',\n value: {\n $typeName: 'cel.expr.Constant',\n constantKind: { case: 'boolValue', value },\n },\n };\n return expr;\n}\n\nfunction instrumentCost(\n expr: CelExpr,\n nextExpressionId: () => bigint\n): void {\n for (const child of childExpressions(expr)) {\n instrumentCost(child, nextExpressionId);\n }\n\n const original = structuredClone(expr);\n expr.id = nextExpressionId();\n expr.exprKind = {\n case: 'callExpr',\n value: {\n $typeName: 'cel.expr.Expr.Call',\n args: [\n createCostCall(original, nextExpressionId()),\n original,\n createBooleanConstant(original, nextExpressionId(), false),\n ],\n function: CONDITIONAL_FUNCTION,\n },\n };\n}\n\nfunction collectUnknownAttributes(\n value: unknown,\n attributes: Set,\n visited: Set\n): void {\n if (Array.isArray(value)) {\n for (const item of value) {\n collectUnknownAttributes(item, attributes, visited);\n }\n return;\n }\n if (!isCelError(value) || visited.has(value)) {\n return;\n }\n\n visited.add(value);\n if (value.message.startsWith(UNKNOWN_ERROR_PREFIX)) {\n attributes.add(value.message.slice(UNKNOWN_ERROR_PREFIX.length));\n }\n collectUnknownAttributes(value.cause, attributes, visited);\n}\n\nfunction findCostLimitError(\n value: unknown,\n visited = new Set()\n): CelCostLimitError | undefined {\n if (value instanceof CelCostLimitError) {\n return value;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n const found = findCostLimitError(item, visited);\n if (found) {\n return found;\n }\n }\n return undefined;\n }\n if (!isCelError(value) || visited.has(value)) {\n return undefined;\n }\n\n visited.add(value);\n return findCostLimitError(value.cause, visited);\n}\n\nfunction validateMaxCost(value: number): number {\n if (!(Number.isSafeInteger(value) && value > 0)) {\n throw new RangeError('CEL maxCost must be a positive safe integer.');\n }\n return value;\n}\n\n/** Compile a reusable CEL evaluator with partial unknowns and a per-run step budget. */\nexport function compileCelExpression(\n expression: string,\n options: CompileCelExpressionOptions = {}\n): CompiledCelExpression {\n const maxCost = validateMaxCost(options.maxCost ?? DEFAULT_CEL_MAX_COST);\n const parsed = parse(expression);\n if (!parsed.expr) {\n throw new Error('CEL parser returned an empty expression.');\n }\n\n const expr = structuredClone(parsed.expr);\n const unknownBindings = new Map();\n replaceUnknownAttributes(\n expr,\n new Set(options.unknownAttributes ?? []),\n unknownBindings\n );\n\n let expressionId = maximumExpressionId(expr) + 1n;\n const nextExpressionId = () => {\n const next = expressionId;\n expressionId += 1n;\n return next;\n };\n instrumentCost(expr, nextExpressionId);\n\n let activeBudget: EvaluationBudget | undefined;\n const consumeCost = celFunc(\n COST_FUNCTION,\n [],\n CelScalar.BOOL,\n (): boolean => {\n if (!activeBudget) {\n throw new Error('CEL cost meter used outside an evaluation.');\n }\n activeBudget.cost += 1;\n if (activeBudget.cost > activeBudget.limit) {\n throw new CelCostLimitError(activeBudget.cost, activeBudget.limit);\n }\n return true;\n }\n );\n const evaluate = plan(celEnv({ funcs: [consumeCost] }), expr);\n\n return (bindings = {}) => {\n const budget: EvaluationBudget = { cost: 0, limit: maxCost };\n activeBudget = budget;\n try {\n const activation: Record = { ...bindings };\n for (const [name, path] of unknownBindings) {\n activation[name] = celError(`${UNKNOWN_ERROR_PREFIX}${path}`);\n }\n\n const result = evaluate(activation as never);\n if (!isCelError(result)) {\n return { cost: budget.cost, kind: 'value', value: result };\n }\n\n const costError = findCostLimitError(result);\n if (costError) {\n return {\n cost: costError.cost,\n kind: 'cost-exceeded',\n limit: costError.limit,\n };\n }\n\n const attributes = new Set();\n collectUnknownAttributes(result, attributes, new Set());\n if (attributes.size > 0) {\n return {\n attributes: [...attributes].sort(),\n cost: budget.cost,\n kind: 'unknown',\n };\n }\n\n return { cost: budget.cost, error: result, kind: 'error' };\n } catch (error) {\n return { cost: budget.cost, error: celError(error), kind: 'error' };\n } finally {\n activeBudget = undefined;\n }\n };\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/ui-rules.ts", "content": "import { compileCelExpression } from './cel-runtime';\nimport type { AutoFormUiRule } from './types';\n\nconst CEL_CACHE_MAX_SIZE = 256;\nconst compiledRuleCache = new Map<\n string,\n ReturnType\n>();\n\nfunction getCompiledRule(expression: string) {\n const cached = compiledRuleCache.get(expression);\n if (cached) {\n return cached;\n }\n\n if (compiledRuleCache.size >= CEL_CACHE_MAX_SIZE) {\n const firstKey = compiledRuleCache.keys().next().value;\n if (firstKey !== undefined) {\n compiledRuleCache.delete(firstKey);\n }\n }\n\n const compiled = compileCelExpression(expression);\n compiledRuleCache.set(expression, compiled);\n return compiled;\n}\n\nexport function evaluateUiRules(\n rules: AutoFormUiRule[] | undefined,\n context: {\n form: Record;\n thisValue: unknown;\n }\n): boolean {\n if (!rules?.length) {\n return true;\n }\n\n return rules.every((rule) => {\n try {\n const result = getCompiledRule(rule.expression)({\n form: context.form,\n this: context.thisValue,\n });\n return result.kind === 'value' && result.value === true;\n } catch {\n return false;\n }\n });\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/utils/modes.ts", "content": "import type { AutoFormMode } from '../types';\n\nexport function normalizeModes(modes: AutoFormMode[] | undefined): AutoFormMode[] {\n const resolved = (modes?.length ? modes : ['advanced']).filter(\n (mode): mode is AutoFormMode => mode === 'simple' || mode === 'advanced' || mode === 'json'\n );\n return resolved.length ? Array.from(new Set(resolved)) : ['advanced'];\n}\n\nexport function resolveInitialMode(availableModes: AutoFormMode[], defaultMode?: AutoFormMode): AutoFormMode {\n if (defaultMode && availableModes.includes(defaultMode)) {\n return defaultMode;\n }\n\n if (availableModes.includes('advanced')) {\n return 'advanced';\n }\n\n return availableModes[0] ?? 'advanced';\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/utils/serialization.ts", "content": "export function isPromiseLike(value: T | Promise): value is Promise {\n return Boolean(value && typeof value === 'object' && 'then' in (value as Record));\n}\n\nexport function safeStringify(value: unknown): string {\n try {\n return JSON.stringify(\n value,\n (_key, currentValue) => {\n if (typeof currentValue === 'bigint') {\n return currentValue.toString();\n }\n if (currentValue instanceof Date) {\n return currentValue.toISOString();\n }\n return currentValue;\n },\n 2\n );\n } catch {\n return '/* serialization error */';\n }\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/badge/index.tsx", "content": "import { cva, type VariantProps } from 'class-variance-authority';\nimport React from 'react';\n\nimport { Slot } from '@/registry/base-nova/protoform/lib/base-ui-compat';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nconst badgeVariants = cva(\n 'inline-flex max-w-full shrink-0 items-center justify-center overflow-hidden truncate text-ellipsis whitespace-nowrap rounded-md border font-medium transition-[color,box-shadow] selection:bg-selected selection:text-selected-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none',\n {\n variants: {\n variant: {\n // === NEUTRAL (Grey - semantic tokens) ===\n neutral:\n 'border-transparent bg-background-inverse-subtle text-inverse [a&]:hover:bg-background-inverse-subtle-hover',\n 'neutral-inverted': 'border-transparent bg-surface-subtle [a&]:hover:bg-background-subtle-hover',\n 'neutral-outline': '!border-outline-inverse border [a&]:hover:bg-background-subtle-hover',\n\n // === SIMPLE (Light grey - semantic tokens) ===\n simple: 'text-secondary [a&]:hover:bg-background-subtle-hover',\n 'simple-inverted': 'text-secondary [a&]:hover:bg-background-subtle-hover',\n 'simple-outline': '!border-outline-inverse border text-secondary [a&]:hover:bg-background-subtle-hover',\n\n // === INFO (Blue - semantic tokens) ===\n info: 'border-transparent bg-surface-informative text-inverse [a&]:hover:bg-surface-informative-hover',\n 'info-inverted':\n 'border-transparent bg-background-informative-subtle text-info [a&]:hover:bg-background-informative-subtle-hover',\n 'info-outline':\n 'border-outline-informative bg-transparent text-info [a&]:hover:bg-background-informative-subtle',\n\n // === ACCENT (Brand Red - uses theme brand tokens) ===\n accent: 'border-transparent bg-brand text-inverse [a&]:hover:bg-surface-brand-hover',\n 'accent-inverted': 'border-transparent bg-background-brand-subtle text-brand [a&]:hover:bg-brand-alpha-default',\n 'accent-outline': 'border-outline-brand bg-transparent text-brand [a&]:hover:bg-brand-alpha-subtle',\n\n // === SUCCESS (Green - semantic tokens) ===\n success: 'border-transparent bg-surface-success text-inverse [a&]:hover:bg-surface-success-hover',\n 'success-inverted':\n 'border-transparent bg-background-success-subtle text-success [a&]:hover:bg-background-success-subtle-hover',\n 'success-outline': 'border-outline-success bg-transparent text-success [a&]:hover:bg-background-success-subtle',\n\n // === WARNING (Yellow/Orange - semantic tokens) ===\n warning: 'border-transparent bg-surface-warning text-inverse [a&]:hover:bg-surface-warning-hover',\n 'warning-inverted': 'border-transparent bg-background-warning-subtle text-warning [a&]:hover:bg-warning-subtle',\n 'warning-outline': 'border-outline-warning bg-transparent text-warning [a&]:hover:bg-background-warning-subtle',\n\n // === DISABLED (Muted - semantic tokens) ===\n disabled: 'cursor-not-allowed border-transparent bg-background-disabled text-disabled',\n 'disabled-inverted': 'cursor-not-allowed border-transparent bg-surface-subtle text-disabled',\n 'disabled-outline': 'cursor-not-allowed border-border-strong bg-transparent text-disabled',\n\n // === DESTRUCTIVE/ERROR (Red - semantic tokens) ===\n destructive:\n 'border-transparent bg-surface-error text-inverse focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-surface-error-hover',\n 'destructive-inverted':\n 'border-transparent bg-background-error-subtle text-destructive [a&]:hover:bg-destructive-subtle',\n 'destructive-outline':\n 'border-outline-error bg-transparent text-destructive [a&]:hover:bg-background-error-subtle',\n\n // === SECONDARY (Dark Blue) ===\n secondary: 'border-transparent bg-secondary text-inverse [a&]:hover:bg-secondary/90',\n 'secondary-inverted': 'border-transparent bg-secondary/10 text-secondary [a&]:hover:bg-secondary/20',\n 'secondary-outline': 'border-secondary text-secondary [a&]:hover:bg-secondary/10',\n\n // === PRIMARY (Indigo) ===\n primary: 'border-transparent bg-primary text-inverse [a&]:hover:bg-primary/90',\n 'primary-inverted': 'border-transparent bg-primary/10 text-primary [a&]:hover:bg-primary/20',\n 'primary-outline': 'border-primary text-primary [a&]:hover:bg-primary/10',\n\n // === OUTLINE (generic) ===\n outline: 'border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',\n },\n size: {\n // Small: 20px height (from Figma)\n sm: 'h-5 gap-1 px-1.5 py-0 text-[11px] has-[>svg]:px-1 [&_svg]:size-3',\n // Medium: 24px height (from Figma)\n md: 'h-6 gap-1 px-2 py-0 text-xs has-[>svg]:px-1.5 [&_svg]:size-3.5',\n // Large: 32px height (from Figma)\n lg: 'h-8 gap-1.5 px-3 py-0 text-sm has-[>svg]:px-2 [&_svg]:size-4',\n },\n },\n defaultVariants: {\n variant: 'neutral',\n size: 'md',\n },\n }\n);\n\nexport type BadgeVariant = VariantProps['variant'];\nexport type BadgeSize = VariantProps['size'];\n\nfunction Badge({\n className,\n variant,\n asChild = false,\n testId,\n icon,\n children,\n size,\n ref,\n ...props\n}: React.ComponentProps<'span'> &\n SharedProps & {\n asChild?: boolean;\n icon?: React.ReactNode;\n variant?: BadgeVariant;\n size?: BadgeSize;\n }) {\n const Comp = asChild ? Slot : 'span';\n\n // When asChild is used with Slot, we can only pass ONE child element\n // to satisfy React.Children.only(). In asChild mode, users must include\n // icons inside children instead of using the icon prop.\n const renderContent = () => {\n if (asChild) {\n return children;\n }\n\n // Normal badge mode - can have icon + children\n if (icon && children) {\n return (\n <>\n {icon}\n {children}\n \n );\n }\n\n if (icon) {\n return icon;\n }\n\n if (children) {\n return {children};\n }\n\n return null;\n };\n\n return (\n \n {renderContent()}\n \n );\n}\n\nexport { Badge, badgeVariants };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/button/index.tsx", "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport React, { type ElementType } from 'react';\n\nimport { useGroup } from '@/registry/base-nova/protoform/components/group';\nimport { Spinner } from '@/registry/base-nova/protoform/components/spinner';\nimport { Slot } from '@/registry/base-nova/protoform/lib/base-ui-compat';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nconst buttonVariants = cva(\n [\n 'group/button inline-flex shrink-0 items-center justify-center',\n 'whitespace-nowrap rounded-lg border border-transparent bg-clip-padding text-sm font-medium',\n 'transition-all outline-none select-none',\n 'cursor-pointer',\n 'disabled:pointer-events-none disabled:cursor-not-allowed',\n 'disabled:opacity-50',\n 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50',\n 'aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20',\n 'dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',\n 'selection:bg-selected selection:text-selected-foreground',\n 'active:not-aria-[haspopup]:translate-y-px',\n '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\"size-\"])]:size-4',\n ],\n {\n variants: {\n variant: {\n primary: [\n 'bg-primary text-primary-foreground shadow-xs',\n 'hover:bg-primary/90',\n 'active:bg-primary/80',\n undefined,\n ],\n secondary: [\n 'bg-surface-primary text-inverse shadow-xs',\n 'hover:bg-surface-primary-hover',\n 'active:bg-surface-primary-pressed',\n 'disabled:bg-background-disabled disabled:text-disabled',\n ],\n accent: [\n 'bg-brand text-inverse shadow-xs',\n 'hover:bg-surface-brand-hover',\n 'active:bg-surface-brand-pressed',\n 'disabled:bg-background-disabled disabled:text-disabled',\n ],\n destructive: [\n 'bg-destructive text-inverse shadow-xs',\n 'hover:bg-surface-error-hover',\n 'active:bg-surface-error-pressed',\n 'focus-visible:ring-destructive',\n 'disabled:bg-background-disabled disabled:text-disabled',\n ],\n inverse: [\n 'bg-surface-inverse text-secondary shadow-xs',\n 'hover:bg-surface-inverse-hover',\n 'active:bg-surface-inverse-pressed',\n 'disabled:bg-surface-inverse-disabled disabled:text-disabled',\n ],\n outline: [\n 'border border-input bg-background text-foreground shadow-xs',\n 'hover:bg-muted hover:text-foreground',\n 'active:bg-muted/80',\n ],\n 'secondary-outline': [\n '!border-outline-inverse border text-secondary shadow-xs',\n 'hover:border-outline-hover hover:bg-secondary-alpha-subtle',\n 'active:border-outline-pressed active:bg-secondary-alpha-default',\n 'disabled:border-outline-inverse-disabled disabled:text-disabled',\n ],\n 'accent-outline': [\n '!border-brand border bg-transparent text-brand shadow-xs',\n 'hover:border-outline-brand-hover hover:bg-brand-alpha-subtle',\n 'active:border-outline-brand-pressed active:bg-brand-alpha-default',\n 'disabled:border-border disabled:text-disabled',\n ],\n 'destructive-outline': [\n '!border-destructive border bg-transparent text-destructive shadow-xs',\n 'hover:border-outline-error-hover hover:bg-destructive-alpha-subtle',\n 'active:border-outline-error-pressed active:bg-destructive-alpha-default',\n 'focus-visible:ring-destructive',\n 'disabled:border-border disabled:text-disabled',\n ],\n 'inverse-outline': [\n '!border-inverse-primary border bg-transparent text-inverse-primary shadow-xs',\n 'hover:border-transparent hover:bg-light-alpha-strong',\n 'active:border-transparent active:bg-light-alpha-stronger',\n 'disabled:border-inverse-disabled disabled:text-inverse-disabled',\n ],\n ghost: [\n 'bg-transparent text-foreground',\n 'hover:bg-muted hover:text-foreground',\n 'active:bg-muted/80',\n ],\n 'secondary-ghost': [\n 'bg-transparent text-secondary',\n 'hover:bg-surface-secondary-subtle',\n 'active:bg-surface-secondary-subtle-hover',\n 'disabled:text-disabled',\n ],\n 'accent-ghost': [\n 'bg-transparent text-brand',\n 'hover:bg-surface-brand-subtle hover:text-brand',\n 'active:bg-surface-brand-subtle-hover',\n 'disabled:text-disabled',\n ],\n 'destructive-ghost': [\n 'bg-transparent text-destructive',\n 'hover:bg-background-error-subtle hover:text-destructive',\n 'active:bg-destructive-subtle',\n 'focus-visible:ring-destructive',\n 'disabled:text-disabled',\n ],\n 'inverse-ghost': [\n 'bg-transparent text-inverse-primary',\n 'hover:bg-light-alpha-strong',\n 'active:bg-light-alpha-stronger',\n 'disabled:text-inverse-disabled',\n ],\n // Link variant\n link: [\n 'text-primary underline-offset-4',\n 'hover:text-primary/80 hover:underline',\n 'active:text-primary/60',\n 'disabled:text-disabled disabled:no-underline',\n ],\n // Dashed border variant\n dashed: [\n '!border-primary border-2 border-dashed bg-transparent text-primary',\n 'hover:border-primary/80 hover:bg-primary/5',\n 'active:bg-primary/10',\n 'disabled:border-border disabled:text-disabled',\n ],\n },\n size: {\n xs: 'h-6 gap-1 px-2 text-xs has-[>svg]:px-1.5 [&_svg]:size-3',\n sm: 'h-7 gap-1 px-2.5 text-[0.8rem] has-[>svg]:px-2 [&_svg]:size-3.5',\n md: 'h-8 gap-1.5 px-2.5 has-[>svg]:px-2 [&_svg]:size-4',\n lg: 'h-9 gap-1.5 px-2.5 has-[>svg]:px-2 [&_svg]:size-4',\n icon: 'size-8 [&_svg]:size-4',\n 'icon-xs': 'size-6 [&_svg]:size-3',\n 'icon-sm': 'size-7 [&_svg]:size-3.5',\n 'icon-lg': 'size-9 [&_svg]:size-4',\n },\n },\n defaultVariants: {\n variant: 'primary',\n size: 'md',\n },\n }\n);\n\nexport type ButtonVariants = VariantProps;\n\nexport type ButtonProps = React.ComponentProps<'button'> &\n ButtonVariants & {\n asChild?: boolean;\n as?: ElementType;\n to?: string;\n icon?: React.ReactNode;\n /**\n * Renders a centered spinner overlay while keeping the button at its\n * natural width. Also disables interaction and sets aria-busy.\n */\n isLoading?: boolean;\n // Support anchor element props when as=\"a\"\n href?: string;\n target?: string;\n rel?: string;\n } & SharedProps;\n\nconst Button = React.forwardRef(\n (\n {\n className,\n variant,\n size,\n asChild = false,\n testId,\n as,\n to,\n icon,\n isLoading = false,\n disabled,\n children,\n ...props\n },\n ref\n ) => {\n const Comp = as ?? (asChild ? Slot : 'button');\n const { attached, position } = useGroup();\n\n let positionClasses = 'rounded-lg';\n if (attached && position === 'first') {\n positionClasses = 'rounded-r-none rounded-l-lg border-r-0';\n } else if (attached && position === 'last') {\n positionClasses = 'rounded-r-lg rounded-l-none border-l-0';\n } else if (attached && position === 'middle') {\n positionClasses = 'rounded-none border-r-0 border-l-0';\n }\n\n const isDisabled = disabled || isLoading;\n\n // When asChild is used with Slot, we can only pass ONE child element\n // to satisfy React.Children.only(). In asChild mode, users must include\n // icons inside children instead of using the icon prop.\n const renderContent = () => {\n if (asChild) {\n return children;\n }\n\n // Normal button mode - can have children + icon prop\n const content = icon ? (\n <>\n {children}\n {icon}\n \n ) : (\n children\n );\n\n if (isLoading) {\n return (\n <>\n {content}\n \n \n \n \n );\n }\n\n return content;\n };\n\n return (\n \n {renderContent()}\n \n );\n }\n);\n\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/calendar/index.tsx", "content": "'use client';\n\nimport { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';\nimport React from 'react';\nimport { type DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker';\n\nimport { Button, buttonVariants } from '@/registry/base-nova/protoform/components/button';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nfunction CalendarRoot({\n className,\n rootRef,\n ...props\n}: React.ComponentProps<'div'> & { rootRef?: React.Ref }) {\n return
;\n}\n\nfunction CalendarChevron({\n className,\n orientation,\n ...props\n}: {\n className?: string;\n orientation?: 'left' | 'right' | 'down' | 'up';\n}) {\n if (orientation === 'left') {\n return ;\n }\n\n if (orientation === 'right') {\n return ;\n }\n\n return ;\n}\n\nfunction CalendarWeekNumber({ children, ...props }: React.ComponentProps<'td'>) {\n return (\n \n
{children}
\n \n );\n}\n\nconst CalendarRootWithTestId = React.memo(\n ({ testId, ...props }: React.ComponentProps & { testId?: string }) => (\n \n )\n);\n\nCalendarRootWithTestId.displayName = 'CalendarRootWithTestId';\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = 'label',\n buttonVariant = 'ghost',\n formatters,\n components,\n testId,\n ...props\n}: React.ComponentProps &\n SharedProps & {\n buttonVariant?: React.ComponentProps['variant'];\n }) {\n const defaultClassNames = getDefaultClassNames();\n\n const rootComponent = React.useMemo(\n () =>\n ({ ref, ...rootProps }: React.ComponentProps & { ref?: React.Ref }) => (\n \n ),\n [testId]\n );\n\n return (\n svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className\n )}\n classNames={{\n root: cn('w-fit', defaultClassNames.root),\n months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),\n month: cn('flex w-full flex-col gap-4', defaultClassNames.month),\n nav: cn('absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1', defaultClassNames.nav),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',\n defaultClassNames.button_previous\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',\n defaultClassNames.button_next\n ),\n month_caption: cn(\n 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',\n defaultClassNames.month_caption\n ),\n dropdowns: cn(\n 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm',\n defaultClassNames.dropdowns\n ),\n dropdown_root: cn(\n '!border-input relative rounded-md border shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50',\n defaultClassNames.dropdown_root\n ),\n dropdown: cn('absolute inset-0 opacity-0', defaultClassNames.dropdown),\n caption_label: cn(\n 'select-none font-medium',\n captionLayout === 'label'\n ? 'text-sm'\n : 'flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',\n defaultClassNames.caption_label\n ),\n weekdays: cn('flex', defaultClassNames.weekdays),\n weekday: cn(\n 'flex-1 select-none rounded-md font-normal text-[0.8rem] text-muted-foreground',\n defaultClassNames.weekday\n ),\n week: cn('mt-2 flex w-full', defaultClassNames.week),\n week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header),\n week_number: cn('select-none text-[0.8rem] text-muted-foreground', defaultClassNames.week_number),\n day: cn(\n 'group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md',\n defaultClassNames.day\n ),\n range_start: cn('rounded-l-md bg-accent', defaultClassNames.range_start),\n range_middle: cn('rounded-none', defaultClassNames.range_middle),\n range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),\n today: cn(\n 'rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none',\n defaultClassNames.today\n ),\n outside: cn('text-muted-foreground aria-selected:text-muted-foreground', defaultClassNames.outside),\n disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),\n hidden: cn('invisible', defaultClassNames.hidden),\n ...classNames,\n }}\n components={{\n Root: rootComponent,\n Chevron: CalendarChevron,\n DayButton: CalendarDayButton,\n WeekNumber: CalendarWeekNumber,\n ...components,\n }}\n formatters={{\n formatMonthDropdown: (date) => date.toLocaleString('default', { month: 'short' }),\n ...formatters,\n }}\n showOutsideDays={showOutsideDays}\n {...props}\n />\n );\n}\n\nfunction CalendarDayButton({ className, day, modifiers, ...props }: React.ComponentProps) {\n const defaultClassNames = getDefaultClassNames();\n\n const ref = React.useRef(null);\n React.useEffect(() => {\n if (modifiers.focused) {\n ref.current?.focus();\n }\n }, [modifiers.focused]);\n\n return (\n span]:text-xs [&>span]:opacity-70',\n defaultClassNames.day,\n className\n )}\n data-day={day.date.toLocaleDateString()}\n data-range-end={modifiers.range_end}\n data-range-middle={modifiers.range_middle}\n data-range-start={modifiers.range_start}\n data-selected-single={\n modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle\n }\n ref={ref}\n size=\"icon\"\n variant=\"ghost\"\n {...props}\n />\n );\n}\n\nexport { Calendar, CalendarDayButton };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/card/index.tsx", "content": "import { cva, type VariantProps } from 'class-variance-authority';\nimport { type MotionProps, motion } from 'motion/react';\nimport React from 'react';\n\nimport { Heading, Text } from '@/registry/base-nova/protoform/components/typography';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nconst cardVariants = cva(\n 'flex min-w-0 flex-col rounded-lg border border-border border-solid bg-card text-card-foreground',\n {\n variants: {\n size: {\n sm: 'max-w-sm gap-2 px-6 py-4',\n md: 'max-w-md gap-4 px-8 py-6',\n lg: 'max-w-lg gap-4 px-10 py-8',\n xl: 'max-w-xl gap-6 px-12 py-10',\n full: 'w-full gap-4 px-8 py-6',\n },\n variant: {\n standard: '',\n elevated: 'shadow-elevated',\n outlined: 'border-1',\n ghost: 'border-0 bg-transparent shadow-none dark:bg-transparent',\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'elevated',\n },\n }\n);\n\nexport type CardVariant = VariantProps['variant'];\nexport type CardSize = VariantProps['size'];\n\ntype BaseCardProps = SharedProps & {\n size?: CardSize;\n variant?: CardVariant;\n className?: string;\n};\n\ntype StaticCardProps = BaseCardProps & Omit, keyof BaseCardProps> & { animated?: false };\ntype AnimatedCardProps = BaseCardProps & Omit & { animated: true };\n\nexport type CardProps = StaticCardProps | AnimatedCardProps;\n\nconst Card = React.forwardRef(\n ({ className, size, variant, testId, animated = false, ...props }, ref) => {\n const cardClassName = cn(cardVariants({ size, variant }), className);\n\n if (animated) {\n return (\n )}\n />\n );\n }\n\n return (\n )}\n />\n );\n }\n);\n\nCard.displayName = 'Card';\n\nconst cardHeaderVariants = cva(\n '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',\n {\n variants: {\n spacing: {\n tight: 'gap-1',\n normal: 'gap-1.5',\n loose: 'gap-2',\n },\n padding: {\n none: '',\n sm: 'px-3',\n md: 'px-6',\n lg: 'px-8',\n },\n },\n defaultVariants: {\n spacing: 'normal',\n padding: 'none',\n },\n }\n);\n\ninterface CardHeaderProps extends React.ComponentProps<'div'>, VariantProps, SharedProps {}\n\nconst CardHeader = React.forwardRef(\n ({ className, spacing, padding, testId, ...props }, ref) => (\n \n )\n);\n\nCardHeader.displayName = 'CardHeader';\n\nconst CardTitle = React.forwardRef<\n HTMLHeadingElement,\n React.ComponentProps<'div'> & SharedProps & { level?: 1 | 2 | 3 | 4 }\n>(({ className, level = 4, testId, children, ...props }, ref) => {\n let content: React.ReactNode = null;\n if (children) {\n content = typeof children === 'string' ? {children} : children;\n }\n\n return (\n
\n {content}\n
\n );\n});\n\nCardTitle.displayName = 'CardTitle';\n\nconst CardDescription = React.forwardRef & SharedProps>(\n ({ className, testId, children, ...props }, ref) => {\n let content: React.ReactNode = null;\n if (children) {\n content = typeof children === 'string' ? {children} : children;\n }\n\n return (\n \n {content}\n
\n );\n }\n);\n\nCardDescription.displayName = 'CardDescription';\n\nconst CardAction = React.forwardRef & SharedProps>(\n ({ className, testId, ...props }, ref) => (\n \n )\n);\n\nCardAction.displayName = 'CardAction';\n\nconst cardContentVariants = cva('', {\n variants: {\n padding: {\n none: '',\n sm: 'px-3',\n md: 'px-6',\n lg: 'px-8',\n },\n space: {\n none: '',\n sm: 'space-y-2',\n md: 'space-y-4',\n lg: 'space-y-6',\n },\n },\n defaultVariants: {\n padding: 'none',\n space: 'md',\n },\n});\n\ninterface CardContentProps extends React.ComponentProps<'div'>, VariantProps, SharedProps {}\n\nconst CardContent = React.forwardRef(\n ({ className, padding, space, testId, ...props }, ref) => (\n \n )\n);\n\nCardContent.displayName = 'CardContent';\n\nconst cardFooterVariants = cva('flex items-center [.border-t]:pt-6', {\n variants: {\n direction: {\n row: 'flex-row',\n column: 'flex-col',\n },\n justify: {\n start: 'justify-start',\n center: 'justify-center',\n end: 'justify-end',\n between: 'justify-between',\n around: 'justify-around',\n },\n gap: {\n none: '',\n sm: 'gap-2',\n md: 'gap-4',\n lg: 'gap-6',\n },\n padding: {\n none: '',\n sm: 'px-3',\n md: 'px-6',\n lg: 'px-8',\n },\n },\n defaultVariants: {\n direction: 'row',\n justify: 'between',\n gap: 'sm',\n padding: 'none',\n },\n});\n\ninterface CardFooterProps extends React.ComponentProps<'div'>, VariantProps, SharedProps {}\n\nconst CardFooter = React.forwardRef(\n ({ className, direction, justify, gap, padding, testId, ...props }, ref) => (\n \n )\n);\n\nCardFooter.displayName = 'CardFooter';\n\n// Form-specific layout helpers\nconst cardFormVariants = cva('grid w-full items-center', {\n variants: {\n gap: {\n sm: 'gap-2',\n md: 'gap-4',\n lg: 'gap-6',\n },\n },\n defaultVariants: {\n gap: 'md',\n },\n});\n\ninterface CardFormProps extends React.ComponentProps<'div'>, VariantProps, SharedProps {}\n\nconst CardForm = React.forwardRef(({ className, gap, testId, ...props }, ref) => (\n
\n));\n\nCardForm.displayName = 'CardForm';\n\nconst cardFieldVariants = cva('flex flex-col', {\n variants: {\n spacing: {\n tight: 'space-y-1',\n normal: 'space-y-1.5',\n loose: 'space-y-2',\n },\n },\n defaultVariants: {\n spacing: 'normal',\n },\n});\n\ninterface CardFieldProps extends React.ComponentProps<'div'>, VariantProps, SharedProps {}\n\nconst CardField = React.forwardRef(({ className, spacing, testId, ...props }, ref) => (\n
\n));\n\nCardField.displayName = 'CardField';\n\nexport { Card, CardAction, CardContent, CardDescription, CardField, CardFooter, CardForm, CardHeader, CardTitle };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/checkbox/index.tsx", "content": "'use client';\n\nimport { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { type HTMLMotionProps, motion } from 'motion/react';\nimport React from 'react';\n\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\n// Path-draw animation driven by CSS:\n// - pathLength={1} normalizes the stroke-dash coordinate space to 0..1\n// regardless of actual path length.\n// - Hidden: stroke-dashoffset 1 + opacity 0 → path is shifted off-screen.\n// - Visible (data-visible=\"true\"): stroke-dashoffset 0 + opacity 1, with a\n// 100ms delay so the box-fill transition leads the stroke draw-in slightly.\n// - The browser's native CSS transition handles the tween, so it's immune to\n// React re-render frequency in controlled-mode parents.\nconst pathDrawClassName =\n '[stroke-dasharray:1] [stroke-dashoffset:1] opacity-0 transition-[stroke-dashoffset,opacity] duration-200 ease-out data-[visible=true]:[stroke-dashoffset:0] data-[visible=true]:opacity-100 data-[visible=true]:delay-[100ms]';\n\nconst checkboxVariants = cva(\n 'peer relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[4px] border transition-colors outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',\n {\n variants: {\n variant: {\n primary:\n '!border-input data-[state=checked]:border-primary data-[state=indeterminate]:border-primary data-[state=checked]:bg-primary data-[state=indeterminate]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:text-primary-foreground',\n secondary:\n '!border-input data-[state=checked]:border-secondary data-[state=indeterminate]:border-secondary data-[state=checked]:bg-secondary data-[state=indeterminate]:bg-secondary data-[state=checked]:text-inverse data-[state=indeterminate]:text-inverse',\n outline:\n '!border-input data-[state=checked]:border-foreground data-[state=indeterminate]:border-foreground data-[state=checked]:bg-transparent data-[state=indeterminate]:bg-transparent data-[state=checked]:text-foreground data-[state=indeterminate]:text-foreground',\n },\n },\n defaultVariants: {\n variant: 'primary',\n },\n }\n);\n\n// Radix API: `checked` accepts `boolean | 'indeterminate'`.\n// Base UI API: `checked: boolean` with a separate `indeterminate?: boolean` prop.\n// Preserve the Radix signature externally and translate internally.\ntype CheckboxProps = Omit<\n React.ComponentProps,\n 'checked' | 'defaultChecked' | 'onCheckedChange'\n> &\n HTMLMotionProps<'button'> &\n VariantProps &\n SharedProps & {\n checked?: boolean | 'indeterminate';\n defaultChecked?: boolean | 'indeterminate';\n onCheckedChange?: (checked: boolean | 'indeterminate') => void;\n };\n\nconst Checkbox = React.forwardRef(\n ({ className, onCheckedChange, testId, variant, checked, defaultChecked, indeterminate, ...props }, ref) => {\n // Track state for animation purposes in uncontrolled mode\n const [internalChecked, setInternalChecked] = React.useState(defaultChecked ?? false);\n\n // Determine if component is controlled (checked prop is provided)\n const isControlled = checked !== undefined;\n\n // Use controlled value if provided, otherwise use internal state for uncontrolled mode\n const isChecked = isControlled ? checked : internalChecked;\n\n const handleCheckedChange = React.useCallback(\n (nextChecked: boolean) => {\n // Only update internal state in uncontrolled mode\n if (!isControlled) {\n setInternalChecked(nextChecked);\n }\n // Always call parent callback\n onCheckedChange?.(nextChecked);\n },\n [isControlled, onCheckedChange]\n );\n\n // Translate Radix `checked='indeterminate'` to Base UI `indeterminate` + `checked=false`.\n const isIndeterminate = indeterminate ?? isChecked === 'indeterminate';\n const baseChecked = isChecked === 'indeterminate' ? false : (isChecked as boolean | undefined);\n const baseDefaultChecked =\n (defaultChecked as unknown) === 'indeterminate' ? false : (defaultChecked as boolean | undefined);\n\n const dataState = isIndeterminate ? 'indeterminate' : isChecked ? 'checked' : 'unchecked';\n const showCheckmark = isChecked === true && !isIndeterminate;\n\n const renderRoot = React.useCallback(\n // biome-ignore lint/suspicious/noExplicitAny: Base UI render merges Root attrs for the consumer element\n (rootProps: Record) => (\n \n ) => (\n \n Checkbox\n \n \n \n )}\n />\n \n ),\n [className, dataState, isIndeterminate, showCheckmark, testId, variant]\n );\n\n return (\n )}\n checked={baseChecked}\n defaultChecked={baseDefaultChecked}\n indeterminate={isIndeterminate}\n nativeButton\n onCheckedChange={handleCheckedChange}\n ref={ref as React.Ref}\n render={renderRoot}\n />\n );\n }\n);\n\nCheckbox.displayName = 'Checkbox';\n\nexport { Checkbox, type CheckboxProps, checkboxVariants };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/choicebox/index.tsx", "content": "import { Radio as RadioGroupPrimitive } from '@base-ui/react/radio';\nimport { Circle } from 'lucide-react';\nimport { AnimatePresence, motion, type Transition } from 'motion/react';\nimport { type ComponentProps, forwardRef, type HTMLAttributes } from 'react';\n\nimport { Card, CardContent, CardDescription, CardHeader, type CardProps, CardTitle } from '@/registry/base-nova/protoform/components/card';\nimport { RadioGroup } from '@/registry/base-nova/protoform/components/radio-group';\nimport { renderWithDataState } from '@/registry/base-nova/protoform/lib/base-ui-compat';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nexport type ChoiceboxProps = ComponentProps & SharedProps;\n\nexport const Choicebox = ({ className, testId, ...props }: ChoiceboxProps) => (\n \n);\n\nexport type ChoiceboxItemProps = ComponentProps &\n SharedProps &\n Partial>;\n\nexport const ChoiceboxItem = forwardRef(\n ({ className, children, testId, size, ...props }, ref) => (\n \n \n {children}\n \n \n )\n);\n\nChoiceboxItem.displayName = 'ChoiceboxItem';\n\nexport type ChoiceboxItemHeaderProps = ComponentProps;\n\nexport const ChoiceboxItemHeader = ({ className, ...props }: ComponentProps) => (\n \n);\n\nexport type ChoiceboxItemTitleProps = ComponentProps;\n\nexport const ChoiceboxItemTitle = ({ className, ...props }: ChoiceboxItemTitleProps) => (\n \n);\n\nexport type ChoiceboxItemSubtitleProps = HTMLAttributes;\n\nexport const ChoiceboxItemSubtitle = ({ className, ...props }: ChoiceboxItemSubtitleProps) => (\n \n);\n\nexport type ChoiceboxItemDescriptionProps = ComponentProps;\n\nexport const ChoiceboxItemDescription = ({ className, ...props }: ChoiceboxItemDescriptionProps) => (\n \n);\n\nexport type ChoiceboxItemContentProps = ComponentProps;\n\nexport const ChoiceboxItemContent = ({ className, ...props }: ChoiceboxItemContentProps) => (\n \n);\n\nexport type ChoiceboxItemIndicatorProps = ComponentProps & {\n transition?: Transition;\n};\n\nexport const ChoiceboxItemIndicator = ({\n className,\n transition = { type: 'spring', stiffness: 200, damping: 16 },\n ...props\n}: ChoiceboxItemIndicatorProps) => (\n \n \n \n \n \n \n \n);\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/collapsible/index.tsx", "content": "'use client';\n\nimport { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';\nimport { AnimatePresence, type HTMLMotionProps, motion, type Transition } from 'motion/react';\nimport React from 'react';\n\nimport { asChildToRender, asChildTrigger, narrowOpenChange } from '@/registry/base-nova/protoform/lib/base-ui-compat';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\ntype CollapsibleContextType = {\n isOpen: boolean;\n};\n\nconst CollapsibleContext = React.createContext(undefined);\n\nconst useCollapsible = (): CollapsibleContextType => {\n const context = React.useContext(CollapsibleContext);\n if (!context) {\n throw new Error('useCollapsible must be used within a Collapsible');\n }\n return context;\n};\n\ntype CollapsibleProps = Omit, 'onOpenChange'> &\n SharedProps & {\n asChild?: boolean;\n onOpenChange?: (open: boolean) => void;\n };\n\nfunction Collapsible({ children, testId, asChild, ...props }: CollapsibleProps) {\n const [isOpen, setIsOpen] = React.useState(props?.open ?? props?.defaultOpen ?? false);\n\n React.useEffect(() => {\n if (props?.open !== undefined) {\n setIsOpen(props.open);\n }\n }, [props?.open]);\n\n const handleOpenChange = React.useCallback(\n (open: boolean) => {\n setIsOpen(open);\n props.onOpenChange?.(open);\n },\n // biome-ignore lint/correctness/useExhaustiveDependencies: part of the collapsible implementation\n [props]\n );\n\n return (\n \n \n \n );\n}\n\ntype CollapsibleTriggerProps = React.ComponentProps & {\n asChild?: boolean;\n};\n\nfunction CollapsibleTrigger({ className, ...props }: CollapsibleTriggerProps) {\n return (\n \n );\n}\n\ntype CollapsibleContentProps = React.ComponentProps &\n HTMLMotionProps<'div'> & {\n transition?: Transition;\n };\n\nfunction CollapsibleContent({\n className,\n children,\n transition = { type: 'spring', stiffness: 150, damping: 22 },\n ...props\n}: CollapsibleContentProps) {\n const { isOpen } = useCollapsible();\n\n return (\n \n {isOpen ? (\n \n {children}\n \n }\n {...props}\n />\n ) : null}\n \n );\n}\n\nexport {\n Collapsible,\n CollapsibleContent,\n type CollapsibleContentProps,\n type CollapsibleContextType,\n type CollapsibleProps,\n CollapsibleTrigger,\n type CollapsibleTriggerProps,\n useCollapsible,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/combobox/combobox-utils.ts", "content": "import type { ComboboxOption } from './index';\n\n/** Prefix for the creatable item's cmdk value to distinguish from real options. */\nexport const CREATE_ITEM_PREFIX = '__create__';\n\nexport type GroupedOptions = {\n readonly heading: string;\n readonly testId?: string;\n readonly options: ReadonlyArray;\n};\n\n/** Resolve a controlled value to its display label. */\nexport const resolveLabel = (options: ReadonlyArray, value: string): string => {\n const opt = options.find((o) => o.value === value);\n return opt?.label ?? value;\n};\n\n/** Filter options by a case-insensitive query against label and value. */\nexport const filterOptions = (\n options: ReadonlyArray,\n query: string,\n selectedLabel: string\n): ComboboxOption[] => {\n if (!query) {\n return [...options];\n }\n // If input matches the selected option's label exactly, show all options\n if (query === selectedLabel && selectedLabel) {\n return [...options];\n }\n const lowerQuery = query.toLowerCase();\n return options.filter(\n (option) => option.label.toLowerCase().includes(lowerQuery) || option.value.toLowerCase().includes(lowerQuery)\n );\n};\n\n/** Group options by their `group` field, preserving insertion order. Returns undefined if no groups exist. */\nexport const groupOptions = (options: ReadonlyArray): GroupedOptions[] | undefined => {\n if (!options.some((option) => option.group)) {\n return;\n }\n\n const groups = new Map();\n for (const option of options) {\n const groupKey = option.group || '';\n const existing = groups.get(groupKey);\n if (existing) {\n groups.set(groupKey, { ...existing, options: [...existing.options, option] });\n } else {\n groups.set(groupKey, {\n heading: groupKey,\n options: [option],\n testId: option.groupTestId,\n });\n }\n }\n return Array.from(groups.values());\n};\n\n/** Build the flat list of navigable cmdk values for keyboard navigation. */\nexport const getNavigableValues = (\n filteredOptions: ReadonlyArray,\n canCreate: boolean,\n inputValue: string\n): string[] => {\n const base = filteredOptions.map((opt) => opt.label);\n return canCreate ? [...base, `${CREATE_ITEM_PREFIX}${inputValue}`] : base;\n};\n\n/** Compute the next highlight value with circular wrapping. */\nexport const computeNextHighlight = (\n navigableValues: ReadonlyArray,\n currentHighlight: string,\n direction: 1 | -1\n): string => {\n if (navigableValues.length === 0) {\n return '';\n }\n const currentIndex = navigableValues.findIndex((v) => v.toLowerCase() === currentHighlight.toLowerCase());\n let nextIndex: number;\n if (currentIndex === -1) {\n nextIndex = direction === 1 ? 0 : navigableValues.length - 1;\n } else {\n nextIndex = currentIndex + direction;\n if (nextIndex < 0) {\n nextIndex = navigableValues.length - 1;\n }\n if (nextIndex >= navigableValues.length) {\n nextIndex = 0;\n }\n }\n return navigableValues[nextIndex] ?? currentHighlight;\n};\n\n/** Find the first matching option for a query string (used for auto-highlight on type). */\nexport const findFirstMatch = (options: ReadonlyArray, query: string): string => {\n if (!query) {\n return '';\n }\n const lowerQuery = query.toLowerCase();\n const match = options.find(\n (opt) => opt.label.toLowerCase().includes(lowerQuery) || opt.value.toLowerCase().includes(lowerQuery)\n );\n return match?.label ?? match?.value ?? '';\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/combobox/index.tsx", "content": "'use client';\n\nimport { useCommandState } from 'cmdk';\nimport { Check, ChevronsUpDown, Plus, Search, X } from 'lucide-react';\nimport React, { memo, useCallback, useEffect, useId, useMemo, useReducer, useRef } from 'react';\n\nimport { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/registry/base-nova/protoform/components/command';\nimport { Input, InputEnd, InputStart } from '@/registry/base-nova/protoform/components/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/registry/base-nova/protoform/components/popover';\nimport { Spinner } from '@/registry/base-nova/protoform/components/spinner';\nimport { cn, type PortalContentProps, type PortalRootProps, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nimport {\n CREATE_ITEM_PREFIX,\n computeNextHighlight,\n filterOptions,\n findFirstMatch,\n getNavigableValues,\n groupOptions,\n resolveLabel,\n} from './combobox-utils';\nimport { comboboxReducer, createInitialState } from './use-combobox-reducer';\n\n/**\n * Sentinel value to prevent cmdk from auto-selecting the first item.\n * cmdk auto-selects when value is falsy, so this truthy value that\n * matches no real option prevents any highlight on open.\n */\nconst NO_HIGHLIGHT = '__no_highlight__';\n\nconst preventDefault = (e: { preventDefault: () => void }) => e.preventDefault();\n\n/**\n * Bridge component rendered inside to read cmdk's internal\n * selectedItemId via useCommandState (requires Command context).\n * Reports the DOM id of the highlighted item for aria-activedescendant.\n */\nfunction ActiveDescendantBridge({ onIdChange }: { onIdChange: (id: string | undefined) => void }) {\n const selectedItemId = useCommandState((state) => state.selectedItemId);\n useEffect(() => {\n onIdChange(selectedItemId);\n }, [selectedItemId, onIdChange]);\n return null;\n}\n\nexport type ComboboxOption = {\n value: string;\n label: string;\n group?: string;\n groupTestId?: string;\n testId?: string;\n /** When true, the option is rendered but cannot be selected. */\n disabled?: boolean;\n /** Arbitrary payload passed back to `renderOption` for rich item rendering. */\n data?: unknown;\n};\n\nexport interface ComboboxProps\n extends SharedProps,\n Pick,\n Pick {\n /** @deprecated No longer used. The combobox now uses list-based navigation. Will be removed in next major version. */\n autocomplete?: boolean;\n className?: string;\n /** @default true - Show a clear (X) button when a value is selected */\n clearable?: boolean;\n /** If true, the combobox will allow the user to create a new option */\n creatable?: boolean;\n /** Noun used in the create prompt (e.g. \"option\", \"context\"). @default \"option\" */\n createLabel?: string;\n disabled?: boolean;\n /**\n * Override the default \"No options found.\" empty state. Ignored when\n * `loading` is true.\n */\n emptyState?: React.ReactNode;\n inputTestId?: string;\n /**\n * When true, an inline spinner is rendered inside the popover and the\n * default \"No options found.\" empty state is suppressed. Useful while\n * async options are being fetched.\n */\n loading?: boolean;\n onChange: (value: string) => void;\n onClose?: () => void;\n /** Callback function to create a new option */\n onCreateOption?: (value: string) => void;\n /**\n * Called on every keystroke with the current input text. Use to drive an\n * async/remote search; typically paired with a debounced fetch that\n * updates `options`.\n */\n onInputValueChange?: (value: string) => void;\n onOpen?: () => void;\n options: ComboboxOption[];\n placeholder?: string;\n preventAutoFocusOnOpen?: boolean;\n /**\n * Override the rendering of each option row. Defaults to rendering the\n * option label. The active check icon is still rendered by the component.\n */\n renderOption?: (option: ComboboxOption) => React.ReactNode;\n /** Content for the start slot of the input. Defaults to a search icon. Pass `null` to hide. */\n start?: React.ReactNode | null;\n value?: string;\n}\n\nconst DEFAULT_START = ;\n\nexport const Combobox = memo(\n ({\n options,\n value: controlledValue = '',\n onChange,\n placeholder,\n disabled,\n creatable,\n onCreateOption,\n createLabel = 'option',\n start = DEFAULT_START,\n clearable = true,\n className,\n onOpen,\n onClose,\n container,\n testId,\n defaultOpen = false,\n preventAutoFocusOnOpen = false,\n inputTestId,\n onInputValueChange,\n loading = false,\n emptyState,\n renderOption,\n }: ComboboxProps) => {\n const [state, dispatch] = useReducer(comboboxReducer, { options, controlledValue, defaultOpen }, (init) =>\n createInitialState(init.options, init.controlledValue, init.defaultOpen)\n );\n const { open, inputValue, highlightedValue, activeDescendantId, userHasTyped } = state;\n\n const inputRef = useRef(null);\n const listId = useId();\n const hasStart = start !== null && start !== undefined;\n\n // Derived values (pure computations)\n const controlledLabel = useMemo(() => resolveLabel(options, controlledValue), [controlledValue, options]);\n const filteredOptions = useMemo(\n () => filterOptions(options, inputValue, controlledLabel),\n [options, inputValue, controlledLabel]\n );\n const groupedOptions = useMemo(() => groupOptions(filteredOptions), [filteredOptions]);\n const canCreate =\n !!creatable && inputValue.trim().length > 0 && !options.some((option) => option.value === inputValue);\n const navigableValues = useMemo(\n () => getNavigableValues(filteredOptions, canCreate, inputValue),\n [filteredOptions, canCreate, inputValue]\n );\n const showClearButton = clearable && controlledValue && !disabled;\n\n // ── Effects (genuine side effects only) ───────────────────────────\n\n // Sync inputValue when controlled value changes externally\n useEffect(() => {\n dispatch({ type: 'SYNC_CONTROLLED', controlledLabel });\n }, [controlledLabel]);\n\n // Focus input when popover opens\n useEffect(() => {\n if (!(inputRef.current && open && !preventAutoFocusOnOpen)) {\n return;\n }\n const timer = setTimeout(() => {\n if (inputRef.current) {\n inputRef.current.focus();\n const length = inputRef.current.value.length;\n inputRef.current.setSelectionRange(length, length);\n }\n }, 0);\n return () => clearTimeout(timer);\n }, [open, preventAutoFocusOnOpen]);\n\n // Fire onOpen/onClose callbacks (single-concern: only callbacks)\n const prevOpenRef = useRef(open);\n useEffect(() => {\n if (prevOpenRef.current !== open) {\n if (open) {\n onOpen?.();\n } else {\n onClose?.();\n }\n prevOpenRef.current = open;\n }\n }, [open, onOpen, onClose]);\n\n // ── Handlers ──────────────────────────────────────────────────────\n\n const handleActiveDescendantChange = useCallback(\n (id: string | undefined) => dispatch({ type: 'SET_ACTIVE_DESCENDANT', id }),\n []\n );\n\n const handleHighlightChange = useCallback(\n (value: string) => dispatch({ type: 'NAVIGATE', nextHighlight: value }),\n []\n );\n\n const selectOption = useCallback(\n (option: ComboboxOption) => {\n if (option.disabled) {\n return;\n }\n if (controlledValue === option.value) {\n onChange('');\n dispatch({ type: 'TOGGLE_OFF' });\n } else {\n onChange(option.value);\n dispatch({ type: 'SELECT', label: option.label });\n }\n },\n [onChange, controlledValue]\n );\n\n const handleCreatableSubmit = useCallback(() => {\n onChange(inputValue);\n dispatch({ type: 'CREATE_SUBMIT', inputValue });\n onCreateOption?.(inputValue);\n }, [inputValue, onChange, onCreateOption]);\n\n const handleClear = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation();\n onChange('');\n dispatch({ type: 'CLEAR' });\n inputRef.current?.focus();\n },\n [onChange]\n );\n\n const handlePopoverOpenChange = useCallback(\n (newOpen: boolean) => {\n if (disabled) {\n return;\n }\n dispatch(newOpen ? { type: 'OPEN' } : { type: 'CLOSE' });\n },\n [disabled]\n );\n\n const handleInputChange = useCallback(\n (e: React.ChangeEvent) => {\n const newValue = e.target.value;\n dispatch({ type: 'TYPE', value: newValue, firstMatch: findFirstMatch(options, newValue) });\n onInputValueChange?.(newValue);\n },\n [options, onInputValueChange]\n );\n\n const handleInputClick = useCallback(() => {\n if (!open) {\n dispatch({ type: 'INPUT_CLICK' });\n }\n }, [open]);\n\n const handleBlur = useCallback(() => {\n if (inputValue.trim() === '' && controlledValue && userHasTyped) {\n onChange('');\n dispatch({ type: 'BLUR_CLEAR' });\n } else if (inputValue !== controlledLabel) {\n const matchesOption = options.some((opt) => opt.value === inputValue || opt.label === inputValue);\n if (!creatable || (!matchesOption && inputValue.trim() === '')) {\n dispatch({ type: 'BLUR_REVERT', controlledLabel });\n }\n }\n }, [inputValue, controlledValue, controlledLabel, options, creatable, onChange, userHasTyped]);\n\n // ── Keyboard handlers (decomposed per key) ────────────────────────\n\n const handleArrowKey = useCallback(\n (event: React.KeyboardEvent, direction: 1 | -1) => {\n event.preventDefault();\n if (!open) {\n dispatch({ type: 'ARROW_OPEN' });\n return;\n }\n dispatch({\n type: 'NAVIGATE',\n nextHighlight: computeNextHighlight(navigableValues, highlightedValue, direction),\n });\n },\n [open, navigableValues, highlightedValue]\n );\n\n const handleEnterKey = useCallback(\n (event: React.KeyboardEvent) => {\n if (!open) {\n return; // Let Enter propagate to form when closed\n }\n event.preventDefault();\n event.stopPropagation();\n\n const isCreateHighlighted = highlightedValue.toLowerCase().startsWith(CREATE_ITEM_PREFIX.toLowerCase());\n\n if (isCreateHighlighted && canCreate) {\n handleCreatableSubmit();\n } else {\n const option = filteredOptions.find((o) => o.label.toLowerCase() === highlightedValue.toLowerCase());\n if (option) {\n selectOption(option);\n } else if (inputValue.trim() === '' && controlledValue) {\n onChange('');\n dispatch({ type: 'ENTER_CLEAR' });\n } else if (creatable && canCreate) {\n handleCreatableSubmit();\n } else {\n dispatch({ type: 'ENTER_REVERT', controlledLabel });\n }\n }\n },\n [\n open,\n highlightedValue,\n canCreate,\n handleCreatableSubmit,\n filteredOptions,\n selectOption,\n inputValue,\n controlledValue,\n controlledLabel,\n onChange,\n creatable,\n ]\n );\n\n const handleEscapeKey = useCallback(\n (event: React.KeyboardEvent) => {\n if (open) {\n event.preventDefault();\n dispatch({ type: 'CLOSE' });\n } else if (controlledValue) {\n event.preventDefault();\n event.stopPropagation();\n onChange('');\n dispatch({ type: 'ESCAPE_CLEAR' });\n }\n },\n [open, controlledValue, onChange]\n );\n\n const handleArrowRightKey = useCallback(\n (event: React.KeyboardEvent) => {\n const input = inputRef.current;\n if (input && input.selectionStart === input.value.length && open && highlightedValue) {\n const option = filteredOptions.find((o) => o.label.toLowerCase() === highlightedValue.toLowerCase());\n if (option) {\n event.preventDefault();\n selectOption(option);\n }\n }\n },\n [open, highlightedValue, filteredOptions, selectOption]\n );\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n switch (event.key) {\n case 'ArrowDown':\n return handleArrowKey(event, 1);\n case 'ArrowUp':\n return handleArrowKey(event, -1);\n case 'Enter':\n return handleEnterKey(event);\n case 'Escape':\n return handleEscapeKey(event);\n case 'ArrowRight':\n return handleArrowRightKey(event);\n }\n },\n [handleArrowKey, handleEnterKey, handleEscapeKey, handleArrowRightKey]\n );\n\n // ── Memoized props ────────────────────────────────────────────────\n\n const popoverStyle = useMemo(\n () => ({ width: inputRef.current?.clientWidth }),\n // Recalculate when open changes (ref width may have changed)\n // biome-ignore lint/correctness/useExhaustiveDependencies: inputRef.current?.clientWidth is not reactive\n [open]\n );\n\n const preventAutoFocusHandler = useMemo(\n () => (preventAutoFocusOnOpen ? preventDefault : undefined),\n [preventAutoFocusOnOpen]\n );\n\n // ── Render ────────────────────────────────────────────────────────\n\n return (\n \n \n \n {hasStart ? {start} : null}\n \n {showClearButton ? (\n \n \n \n ) : null}\n \n \n \n \n \n \n \n \n {loading ? (\n \n \n Loading…\n
\n ) : (\n {emptyState ?? 'No options found.'}\n )}\n {(groupedOptions ?? [{ heading: '', options: filteredOptions }]).map((group) => (\n \n {group.options.map((option) => (\n selectOption(option)}\n testId={option.testId}\n value={option.label}\n >\n {renderOption ? renderOption(option) : option.label}\n \n \n ))}\n \n ))}\n {canCreate ? (\n \n \n \n Create \"{inputValue}\"\n \n \n ) : null}\n {creatable && !canCreate ? (\n \n \n \n Type to create a new {createLabel}...\n \n \n ) : null}\n \n \n \n \n );\n }\n);\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/combobox/use-combobox-reducer.ts", "content": "import { resolveLabel } from './combobox-utils';\nimport type { ComboboxOption } from './index';\n\n// ── State ─────────────────────────────────────────────────────────────\n// Readonly at every level — the reducer MUST return a new object.\nexport type ComboboxState = Readonly<{\n open: boolean;\n inputValue: string;\n highlightedValue: string;\n activeDescendantId: string | undefined;\n userHasTyped: boolean;\n}>;\n\n// ── Actions (discriminated union) ─────────────────────────────────────\n// Each variant carries only the payload it needs. TypeScript narrows\n// the type inside each switch case, so `action.value` is only available\n// on variants that declare it.\nexport type ComboboxAction =\n // Popover lifecycle\n | { readonly type: 'OPEN' }\n | { readonly type: 'CLOSE' }\n | { readonly type: 'ARROW_OPEN' }\n\n // User input\n | { readonly type: 'INPUT_CLICK' }\n | { readonly type: 'TYPE'; readonly value: string; readonly firstMatch: string }\n\n // Selection\n | { readonly type: 'SELECT'; readonly label: string }\n | { readonly type: 'TOGGLE_OFF' }\n | { readonly type: 'CLEAR' }\n | { readonly type: 'CREATE_SUBMIT'; readonly inputValue: string }\n\n // Keyboard navigation\n | { readonly type: 'NAVIGATE'; readonly nextHighlight: string }\n\n // Enter key variants (each has distinct state transition)\n | { readonly type: 'ENTER_REVERT'; readonly controlledLabel: string }\n | { readonly type: 'ENTER_CLEAR' }\n\n // Escape\n | { readonly type: 'ESCAPE_CLEAR' }\n\n // Blur\n | { readonly type: 'BLUR_CLEAR' }\n | { readonly type: 'BLUR_REVERT'; readonly controlledLabel: string }\n\n // External sync\n | { readonly type: 'SYNC_CONTROLLED'; readonly controlledLabel: string }\n\n // cmdk bridge\n | { readonly type: 'SET_ACTIVE_DESCENDANT'; readonly id: string | undefined };\n\n// ── Exhaustive check helper ───────────────────────────────────────────\n// If a new action variant is added to the union but not handled in the\n// switch, TypeScript will error: \"Argument of type '...' is not\n// assignable to parameter of type 'never'.\"\nconst assertNever = (action: never): never => {\n throw new Error(`Unhandled combobox action: ${(action as ComboboxAction).type}`);\n};\n\n// ── Reducer (pure function — no React imports, no side effects) ───────\nexport const comboboxReducer = (state: ComboboxState, action: ComboboxAction): ComboboxState => {\n switch (action.type) {\n case 'OPEN':\n return { ...state, open: true, highlightedValue: '', userHasTyped: false };\n case 'CLOSE':\n return { ...state, open: false, highlightedValue: '', activeDescendantId: undefined };\n case 'ARROW_OPEN':\n return { ...state, open: true };\n case 'INPUT_CLICK':\n return { ...state, open: true, inputValue: '', userHasTyped: false };\n case 'TYPE':\n return {\n ...state,\n open: true,\n inputValue: action.value,\n highlightedValue: action.firstMatch,\n userHasTyped: true,\n };\n case 'SELECT':\n return { ...state, open: false, inputValue: action.label };\n case 'TOGGLE_OFF':\n return { ...state, open: false, inputValue: '' };\n case 'CLEAR':\n return { ...state, inputValue: '' };\n case 'CREATE_SUBMIT':\n return { ...state, open: false, inputValue: action.inputValue };\n case 'NAVIGATE':\n return { ...state, highlightedValue: action.nextHighlight };\n case 'ENTER_REVERT':\n return { ...state, open: false, inputValue: action.controlledLabel };\n case 'ENTER_CLEAR':\n return { ...state, open: false, inputValue: '' };\n case 'ESCAPE_CLEAR':\n return { ...state, inputValue: '' };\n case 'BLUR_CLEAR':\n return { ...state, inputValue: '', userHasTyped: false };\n case 'BLUR_REVERT':\n return { ...state, inputValue: action.controlledLabel, userHasTyped: false };\n case 'SYNC_CONTROLLED':\n if (state.inputValue === action.controlledLabel) return state;\n return { ...state, inputValue: action.controlledLabel };\n case 'SET_ACTIVE_DESCENDANT':\n return { ...state, activeDescendantId: action.id };\n default:\n return assertNever(action);\n }\n};\n\n// ── Initial state factory ─────────────────────────────────────────────\nexport const createInitialState = (\n options: ReadonlyArray,\n controlledValue: string,\n defaultOpen: boolean\n): ComboboxState => ({\n open: defaultOpen,\n inputValue: resolveLabel(options, controlledValue),\n highlightedValue: '',\n activeDescendantId: undefined,\n userHasTyped: false,\n});\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/command/index.tsx", "content": "import { cva, type VariantProps } from 'class-variance-authority';\nimport { Command as CommandPrimitive } from 'cmdk';\nimport { ChevronRight, SearchIcon } from 'lucide-react';\nimport React from 'react';\n\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/registry/base-nova/protoform/components/dialog';\nimport { Popover, PopoverAnchor, PopoverContent } from '@/registry/base-nova/protoform/components/popover';\nimport { Text } from '@/registry/base-nova/protoform/components/typography';\nimport { cn, type FixedPositionContentProps, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nconst commandVariants = cva(\n 'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',\n {\n variants: {\n variant: {\n elevated: '!border-input border shadow-md',\n minimal: '',\n dialog: '',\n },\n size: {\n sm: 'min-w-[300px] max-w-sm',\n md: 'min-w-[400px] max-w-lg md:min-w-[450px]',\n lg: 'min-w-[500px] max-w-2xl',\n full: 'w-full',\n },\n },\n defaultVariants: {\n variant: 'elevated',\n size: 'md',\n },\n }\n);\n\ninterface CommandProps\n extends React.ComponentProps,\n VariantProps,\n SharedProps {}\n\nfunction Command({ className, variant, size, testId, ...props }: CommandProps) {\n return (\n \n );\n}\n\nfunction CommandDialog({\n title = 'Command Palette',\n description = 'Search for a command to run...',\n children,\n showOverlay = true,\n container,\n onOpenAutoFocus,\n className,\n ...props\n}: Omit, 'children'> &\n Pick & {\n title?: string;\n description?: string;\n className?: string;\n children?: React.ReactNode;\n }) {\n return (\n \n \n {title}\n {description}\n \n \n \n {children}\n \n \n \n );\n}\n\nfunction CommandInput({\n className,\n testId,\n ...props\n}: React.ComponentProps & SharedProps) {\n return (\n
\n \n \n
\n );\n}\n\nfunction CommandList({ className, ...props }: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction CommandEmpty({ ...props }: React.ComponentProps) {\n return ;\n}\n\nfunction CommandGroup({\n className,\n testId,\n ...props\n}: React.ComponentProps & SharedProps) {\n return (\n \n );\n}\n\nfunction CommandSeparator({ className, ...props }: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction CommandItem({\n className,\n testId,\n ...props\n}: React.ComponentProps & SharedProps) {\n return (\n \n );\n}\n\nfunction CommandShortcut({ className, children, ...props }: React.ComponentProps<'span'>) {\n return (\n \n {children}\n \n );\n}\n\n// ── Command Submenu ───────────────────────────────────────────────────\n\ntype CommandSubContextType = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n};\n\nconst CommandSubContext = React.createContext(undefined);\n\ntype CommandSubProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n children: React.ReactNode;\n};\n\nfunction CommandSub({ open, onOpenChange, children }: CommandSubProps) {\n return (\n \n \n {children}\n \n \n );\n}\n\ninterface CommandSubTriggerProps extends React.ComponentProps {\n inset?: boolean;\n}\n\nfunction CommandSubTrigger({ className, children, inset, ...props }: CommandSubTriggerProps) {\n const ctx = React.useContext(CommandSubContext);\n\n return (\n ctx?.onOpenChange(true)}\n {...props}\n />\n }\n >\n {children}\n \n \n );\n}\n\ntype CommandSubContentProps = {\n className?: string;\n children: React.ReactNode;\n};\n\nfunction CommandSubContent({ className, children }: CommandSubContentProps) {\n return (\n e.preventDefault()}\n side=\"right\"\n sideOffset={4}\n >\n {children}\n \n );\n}\n\n// Simplified interface for backend developers\ninterface SimpleCommandProps extends SharedProps {\n className?: string;\n emptyMessage?: string;\n groups: Array<{\n heading?: string;\n items: Array<{\n icon?: React.ReactNode;\n label: string;\n shortcut?: string;\n disabled?: boolean;\n onSelect?: () => void;\n }>;\n }>;\n placeholder?: string;\n size?: 'sm' | 'md' | 'lg' | 'full';\n}\n\nfunction SimpleCommand({\n placeholder = 'Type a command or search...',\n emptyMessage = 'No results found.',\n groups,\n size = 'md',\n className,\n testId,\n}: SimpleCommandProps) {\n return (\n \n \n \n {emptyMessage}\n {groups.map((group, groupIndex) => (\n \n {groupIndex > 0 && }\n \n {group.items.map((item) => (\n \n {item.icon}\n {item.label}\n {item.shortcut ? {item.shortcut} : null}\n \n ))}\n \n \n ))}\n \n \n );\n}\n\nexport {\n Command,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n CommandShortcut,\n CommandSub,\n CommandSubContent,\n CommandSubTrigger,\n commandVariants,\n SimpleCommand,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/copy-button/index.tsx", "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { CheckIcon, CopyIcon } from 'lucide-react';\nimport { AnimatePresence, type HTMLMotionProps, motion } from 'motion/react';\nimport React from 'react';\n\nimport { cn } from '@/registry/base-nova/protoform/lib/utils';\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n primary: 'bg-secondary text-inverse shadow-xs hover:bg-secondary/80',\n destructive:\n 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40',\n outline:\n '!border-outline-primary border text-primary-inverse shadow-xs hover:border-outline-primary-hover hover:bg-primary-alpha-subtle active:border-outline-primary-pressed active:bg-primary-alpha-subtle-default disabled:border-outline-inverse-disabled disabled:text-disabled',\n secondary: 'bg-primary text-inverse shadow-xs hover:bg-primary/90',\n ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',\n },\n size: {\n md: 'h-9 px-4 py-2 has-[>svg]:px-3',\n sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',\n lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',\n icon: 'size-9',\n },\n },\n defaultVariants: {\n variant: 'primary',\n size: 'md',\n },\n }\n);\n\ntype CopyButtonProps = Omit, 'onCopy' | 'children'> &\n VariantProps & {\n content?: string;\n delay?: number;\n onCopy?: (content: string) => void;\n isCopied?: boolean;\n onCopyChange?: (isCopied: boolean) => void;\n testId?: string;\n children?: React.ReactNode;\n };\n\nfunction CopyButton({\n content,\n className,\n size,\n variant,\n delay = 3000,\n onClick,\n onCopy,\n isCopied,\n onCopyChange,\n testId,\n children,\n ...props\n}: CopyButtonProps) {\n const [localIsCopied, setLocalIsCopied] = React.useState(isCopied ?? false);\n const Icon = localIsCopied ? CheckIcon : CopyIcon;\n\n React.useEffect(() => {\n setLocalIsCopied(isCopied ?? false);\n }, [isCopied]);\n\n const handleIsCopied = React.useCallback(\n (isCopiedState: boolean) => {\n setLocalIsCopied(isCopiedState);\n onCopyChange?.(isCopiedState);\n },\n [onCopyChange]\n );\n\n const handleCopy = React.useCallback(\n (e: React.MouseEvent) => {\n if (isCopied) {\n return;\n }\n if (content) {\n navigator.clipboard\n .writeText(content)\n .then(() => {\n handleIsCopied(true);\n setTimeout(() => handleIsCopied(false), delay);\n onCopy?.(content);\n })\n .catch((error) => {\n // biome-ignore lint/suspicious/noConsole: needed for copy button implementation\n console.error('Error copying command', error);\n });\n }\n onClick?.(e);\n },\n [isCopied, content, delay, onClick, onCopy, handleIsCopied]\n );\n\n return (\n \n \n \n \n \n \n {children}\n \n );\n}\n\nexport { buttonVariants, CopyButton, type CopyButtonProps };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/dialog/index.tsx", "content": "import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { X } from 'lucide-react';\nimport React from 'react';\n\nimport { Button } from '@/registry/base-nova/protoform/components/button';\nimport { usePortalContainer } from '@/registry/base-nova/protoform/hooks/use-portal-container';\nimport {\n asChildTrigger,\n narrowOpenChange,\n renderDescription,\n renderWithDataState,\n warnDeprecatedProp,\n} from '@/registry/base-nova/protoform/lib/base-ui-compat';\nimport { cn, type FixedPositionContentProps, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\ntype DialogRootProps = Omit, 'onOpenChange'> &\n SharedProps & {\n onOpenChange?: (open: boolean) => void;\n };\n\nfunction Dialog({ testId, onOpenChange, ...props }: DialogRootProps) {\n return (\n \n );\n}\n\ntype DialogTriggerProps = React.ComponentProps & {\n asChild?: boolean;\n};\n\nfunction DialogTrigger({ className, ...props }: DialogTriggerProps) {\n return (\n \n );\n}\n\nfunction DialogPortal({ ...props }: React.ComponentProps) {\n return ;\n}\n\ntype DialogCloseProps = React.ComponentProps & {\n asChild?: boolean;\n};\n\nfunction DialogClose({ ...props }: DialogCloseProps) {\n return ;\n}\n\nfunction DialogOverlay({ className, ...props }: React.ComponentProps) {\n return (\n \n );\n}\n\nconst dialogContentVariants = cva(\n 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex max-h-[85vh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-xl border bg-background fill-mode-forwards shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in',\n {\n variants: {\n size: {\n sm: 'sm:max-w-sm',\n md: 'sm:max-w-lg',\n lg: 'sm:max-w-2xl',\n xl: 'sm:max-w-4xl',\n full: 'sm:max-w-[90vw]',\n },\n variant: {\n standard: '',\n centered: 'text-center',\n destructive: 'border-destructive/50',\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'standard',\n },\n }\n);\n\ninterface DialogContentProps\n extends React.ComponentProps,\n VariantProps,\n SharedProps,\n Pick {\n showCloseButton?: boolean;\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n showOverlay = true,\n size,\n variant,\n testId,\n container,\n onOpenAutoFocus,\n ...props\n}: DialogContentProps) {\n warnDeprecatedProp(\n 'DialogContent',\n 'onOpenAutoFocus',\n onOpenAutoFocus,\n 'Use `initialFocus` on Base UI `Dialog.Popup` instead.'\n );\n const portalContainer = usePortalContainer();\n return (\n \n {showOverlay ? : null}\n \n {children}\n {showCloseButton ? (\n \n \n \n }\n />\n ) : null}\n \n \n );\n}\n\nconst dialogHeaderVariants = cva('flex shrink-0 flex-col p-4 [&:has(+[data-slot=dialog-body])]:border-b', {\n variants: {\n align: {\n left: 'text-left',\n center: 'text-center',\n responsive: 'text-center sm:text-left',\n },\n spacing: {\n tight: 'space-y-1',\n normal: 'space-y-1.5',\n loose: 'space-y-2',\n },\n },\n defaultVariants: {\n align: 'responsive',\n spacing: 'normal',\n },\n});\n\ninterface DialogHeaderProps extends React.ComponentProps<'div'>, VariantProps {}\n\nfunction DialogHeader({ className, align, spacing, ...props }: DialogHeaderProps) {\n return (\n
\n );\n}\n\nconst dialogFooterVariants = cva('flex shrink-0 p-4 [[data-slot=dialog-body]+&]:border-t', {\n variants: {\n direction: {\n column: 'flex-col',\n row: 'flex-row items-center',\n responsive: 'flex-col-reverse sm:flex-row sm:items-center',\n },\n justify: {\n start: 'justify-start',\n center: 'justify-center',\n end: 'justify-end sm:justify-end',\n between: 'justify-between',\n },\n gap: {\n sm: 'gap-1',\n md: 'gap-2',\n lg: 'gap-4',\n },\n },\n defaultVariants: {\n direction: 'responsive',\n justify: 'end',\n gap: 'md',\n },\n});\n\ninterface DialogFooterProps extends React.ComponentProps<'div'>, VariantProps {}\n\nfunction DialogFooter({ className, direction, justify, gap, ...props }: DialogFooterProps) {\n return (\n \n );\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction DialogDescription({\n className,\n children,\n asChild,\n ...props\n}: React.ComponentProps & { asChild?: boolean }) {\n // Render as
(not

) so block-level children don't trigger validateDOMNesting.\n return (\n \n );\n}\n\n// min-h-0 lets the body shrink below its natural height so overflow-y-auto scrolls.\nconst dialogBodyVariants = cva('min-h-0 flex-1 overflow-y-auto p-4', {\n variants: {\n spacing: {\n none: '',\n sm: 'space-y-2',\n md: 'space-y-4',\n lg: 'space-y-6',\n },\n },\n defaultVariants: {\n spacing: 'md',\n },\n});\n\ninterface DialogBodyProps extends React.ComponentProps<'div'>, VariantProps {}\n\nfunction DialogBody({ className, spacing, ...props }: DialogBodyProps) {\n return

;\n}\n\nconst dialogFieldVariants = cva('flex flex-col', {\n variants: {\n spacing: {\n tight: 'space-y-1',\n normal: 'space-y-1.5',\n loose: 'space-y-2',\n },\n },\n defaultVariants: {\n spacing: 'normal',\n },\n});\n\ninterface DialogFieldProps extends React.ComponentProps<'div'>, VariantProps {}\n\nfunction DialogField({ className, spacing, ...props }: DialogFieldProps) {\n return
;\n}\n\nexport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogField,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/field/index.tsx", "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { createContext, useContext, useId, useMemo } from 'react';\n\nimport { Label } from '@/registry/base-nova/protoform/components/label';\nimport { Separator } from '@/registry/base-nova/protoform/components/separator';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\ninterface FieldContextValue {\n errorId: string | undefined;\n invalid: boolean;\n}\n\nconst FieldContext = createContext({ invalid: false, errorId: undefined });\n\n/**\n * Access field-level validation state from child components.\n * Returns `{ invalid, errorId }` — use `invalid` for `aria-invalid` and\n * `errorId` for `aria-describedby` on form controls.\n */\nexport function useFieldContext() {\n return useContext(FieldContext);\n}\n\nfunction FieldSet({ className, testId, ...props }: React.ComponentProps<'fieldset'> & SharedProps) {\n return (\n [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',\n className\n )}\n data-slot=\"field-set\"\n data-testid={testId}\n {...props}\n />\n );\n}\n\nfunction FieldLegend({\n className,\n variant = 'legend',\n ...props\n}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {\n return (\n \n );\n}\n\nfunction FieldGroup({ className, testId, ...props }: React.ComponentProps<'div'> & SharedProps) {\n return (\n [data-slot=field-group]]:gap-4',\n className\n )}\n data-slot=\"field-group\"\n data-testid={testId}\n {...props}\n />\n );\n}\n\nconst fieldVariants = cva('group/field flex w-full gap-3 data-[invalid=true]:text-destructive', {\n variants: {\n orientation: {\n vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],\n horizontal: [\n 'flex-row items-center',\n '[&>[data-slot=field-label]]:flex-auto',\n 'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',\n ],\n responsive: [\n '@md/field-group:flex-row flex-col @md/field-group:items-center @md/field-group:[&>*]:w-auto [&>*]:w-full [&>.sr-only]:w-auto',\n '@md/field-group:[&>[data-slot=field-label]]:flex-auto',\n '@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',\n ],\n },\n },\n defaultVariants: {\n orientation: 'vertical',\n },\n});\n\nfunction Field({\n className,\n orientation = 'vertical',\n testId,\n ...props\n}: React.ComponentProps<'div'> & VariantProps & SharedProps) {\n const dataProps = props as Record;\n const invalid = dataProps['data-invalid'] === true || dataProps['data-invalid'] === 'true';\n const errorId = useId();\n const ctx = useMemo(() => ({ invalid, errorId: invalid ? errorId : undefined }), [invalid, errorId]);\n\n return (\n \n {/* biome-ignore lint/a11y/useSemanticElements: part of field implementation */}\n \n \n );\n}\n\nfunction FieldContent({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n \n );\n}\n\nfunction FieldLabel({\n className,\n children,\n required,\n ...props\n}: React.ComponentProps & { required?: boolean }) {\n return (\n [data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',\n 'has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10',\n className\n )}\n data-slot=\"field-label\"\n {...props}\n >\n {children}\n {required ? (\n \n *\n \n ) : null}\n \n );\n}\n\nfunction FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n \n );\n}\n\n// Rendered as
instead of

so consumers can nest block-level components\n// (Text, Alert, Input, etc.) without triggering React's validateDOMNesting warnings.\nfunction FieldDescription({ className, testId, ...props }: React.ComponentProps<'div'> & SharedProps) {\n return (\n a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',\n className\n )}\n data-slot=\"field-description\"\n data-testid={testId}\n {...props}\n />\n );\n}\n\nfunction FieldSeparator({\n children,\n className,\n ...props\n}: React.ComponentProps<'div'> & {\n children?: React.ReactNode;\n}) {\n return (\n \n \n {children ? (\n \n {children}\n \n ) : null}\n

\n );\n}\n\nfunction FieldError({\n className,\n children,\n errors,\n testId,\n ...props\n}: React.ComponentProps<'div'> & {\n errors?: Array<{ message?: string } | undefined>;\n} & SharedProps) {\n const { errorId } = useContext(FieldContext);\n const content = useMemo(() => {\n if (children) {\n return children;\n }\n\n if (!errors?.length) {\n return null;\n }\n\n if (errors?.length === 1) {\n return errors[0]?.message;\n }\n\n return (\n
    \n {/* biome-ignore lint/suspicious/noArrayIndexKey: error messages are stable and order is maintained */}\n {errors.map((error, index) => error?.message &&
  • {error.message}
  • )}\n
\n );\n }, [children, errors]);\n\n if (!content) {\n return null;\n }\n\n return (\n \n {content}\n
\n );\n}\n\nexport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSeparator,\n FieldSet,\n FieldTitle,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/group/index.tsx", "content": "'use client';\nimport React, { createContext, useContext } from 'react';\n\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\ntype GroupPosition = 'first' | 'middle' | 'last';\n\ntype GroupContextValue = {\n position?: GroupPosition;\n attached: boolean;\n};\n\nconst GroupContext = createContext({\n position: undefined,\n attached: false,\n});\n\nconst useGroup = () => useContext(GroupContext);\n\nconst Group = ({\n children,\n className,\n testId,\n attached = false,\n}: {\n children: React.ReactNode;\n className?: string;\n attached?: boolean;\n} & SharedProps) => {\n const childrenArray = React.Children.toArray(children).filter((child) => React.isValidElement(child));\n const childCount = childrenArray.length;\n\n const content = childrenArray.map((child, index) => {\n const getPosition = (): GroupPosition | undefined => {\n if (!attached || childCount === 1) {\n return;\n }\n if (index === 0) {\n return 'first';\n }\n if (index === childCount - 1) {\n return 'last';\n }\n return 'middle';\n };\n\n const position = getPosition();\n const element = child as React.ReactElement;\n const key = element.key || `group-item-${index}`;\n\n return (\n \n {child}\n \n );\n });\n\n return (\n
\n {content}\n
\n );\n};\n\nexport { Group, type GroupContextValue, type GroupPosition, useGroup };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/input/index.tsx", "content": "/** biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: this is a complex component */\n'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Eye, EyeOff, Minus, Plus } from 'lucide-react';\nimport React, { createContext, useEffect, useState } from 'react';\n\nimport { Button } from '@/registry/base-nova/protoform/components/button';\nimport { useFieldContext } from '@/registry/base-nova/protoform/components/field';\nimport { useGroup } from '@/registry/base-nova/protoform/components/group';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nexport const inputVariants = cva(\n 'placeholder:!text-muted-foreground !border-input flex w-full min-w-0 border bg-transparent text-base outline-none transition-colors [-moz-appearance:textfield] selection:bg-selection selection:text-selection-foreground file:inline-flex file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',\n {\n variants: {\n size: {\n sm: 'h-7 px-2 py-1 text-sm file:h-5',\n md: 'h-8 px-2.5 py-1 file:h-6',\n lg: 'h-9 px-3 py-1 file:h-7',\n },\n variant: {\n standard: '',\n password: 'pr-10',\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'standard',\n },\n }\n);\n\nconst stepControlVariants = cva('flex items-center justify-center', {\n variants: {\n size: {\n sm: 'size-7 [&_svg]:size-3',\n md: 'size-8 [&_svg]:size-3.5',\n lg: 'size-9 [&_svg]:size-4',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n});\n\nconst inputContainerVariants = cva('', {\n variants: {\n layout: {\n standard: 'relative flex items-center',\n password: 'relative flex w-full flex-1',\n number: 'flex items-center gap-2',\n },\n },\n defaultVariants: {\n layout: 'standard',\n },\n});\n\nexport interface InputProps\n extends Omit, 'size'>,\n VariantProps,\n SharedProps {\n children?: React.ReactNode;\n containerClassName?: string;\n showStepControls?: boolean;\n}\n\nfunction useInputState(value: InputProps['value'], defaultValue: InputProps['defaultValue']) {\n const [internalValue, setInternalValue] = useState(value?.toString() || defaultValue?.toString() || '');\n const [showPassword, setShowPassword] = useState(false);\n\n useEffect(() => {\n if (value !== undefined) {\n setInternalValue(value.toString());\n }\n }, [value]);\n\n return { value: internalValue, setValue: setInternalValue, showPassword, setShowPassword };\n}\n\nfunction useNumberInputHandlers(\n value: string,\n setValue: React.Dispatch>,\n step: number,\n onChange?: React.ChangeEventHandler\n) {\n const createChangeEvent = (newValue: string): React.ChangeEvent =>\n ({\n target: { value: newValue },\n }) as React.ChangeEvent;\n\n const increment = () => {\n const currentValue = Number.parseFloat(value) || 0;\n const newValue = (currentValue + step).toString();\n setValue(newValue);\n onChange?.(createChangeEvent(newValue));\n };\n\n const decrement = () => {\n const currentValue = Number.parseFloat(value) || 0;\n const newValue = (currentValue - step).toString();\n setValue(newValue);\n onChange?.(createChangeEvent(newValue));\n };\n\n const handleInputChange = (e: React.ChangeEvent) => {\n setValue(e.target.value);\n onChange?.(e);\n };\n\n return { increment, decrement, handleInputChange };\n}\n\nconst Input = React.forwardRef(\n (\n {\n className,\n type,\n showStepControls,\n size,\n variant,\n testId,\n children,\n containerClassName,\n readOnly,\n defaultValue,\n ...props\n },\n ref\n ) => {\n const { value, setValue, showPassword, setShowPassword } = useInputState(props.value, defaultValue);\n const fieldCtx = useFieldContext();\n const [startWidth, setStartWidth] = useState();\n const [endWidth, setEndWidth] = useState();\n\n const isNumberInput = type === 'number';\n const isPasswordInput = type === 'password';\n const shouldShowControls = isNumberInput && showStepControls;\n const step = props.step ? Number(props.step) : 1;\n const inputVariant = isPasswordInput ? 'password' : variant;\n const { position: groupPosition, attached: groupAttached } = useGroup();\n const attached = groupAttached || isPasswordInput;\n\n const { increment, decrement, handleInputChange } = useNumberInputHandlers(value, setValue, step, props.onChange);\n\n // Map input size to a button icon size that fits comfortably inside the input\n // sm (h-8/32px) → icon-xs (24px), md (h-9/36px) → icon-sm (32px), lg (h-10/40px) → icon-sm (32px)\n const passwordToggleSize = size === 'sm' ? ('icon-xs' as const) : ('icon-sm' as const);\n\n let positionClasses = 'rounded-lg';\n if (attached && groupPosition === 'first') {\n positionClasses = 'rounded-r-none rounded-l-lg border-r-0';\n } else if (attached && groupPosition === 'last') {\n positionClasses = 'rounded-r-lg rounded-l-none border-l-0';\n } else if (attached && groupPosition === 'middle') {\n positionClasses = 'rounded-none border-r-0 border-l-0';\n }\n\n let inputType = type;\n if (isPasswordInput) {\n inputType = showPassword ? 'text' : 'password';\n }\n\n let layout: 'number' | 'password' | typeof variant = variant;\n if (shouldShowControls) {\n layout = 'number';\n } else if (isPasswordInput) {\n layout = 'password';\n }\n\n const inputElement = (\n \n );\n\n return (\n \n \n {inputElement}\n {children}\n {isPasswordInput ? (\n \n setShowPassword(!showPassword)}\n size={passwordToggleSize}\n type=\"button\"\n variant=\"ghost\"\n >\n {showPassword ? : }\n \n \n ) : null}\n {shouldShowControls ? (\n
\n \n \n \n \n \n \n
\n ) : null}\n
\n \n );\n }\n);\n\nconst inputEndClassNames = 'absolute inset-y-0 right-2 z-10 flex items-center pointer-events-none';\n\nconst InputContext = createContext<{\n setStartWidth: (width: number) => void;\n setEndWidth: (width: number) => void;\n startWidth: number | undefined;\n endWidth: number | undefined;\n}>({\n setStartWidth: () => {\n // Default no-op function\n },\n setEndWidth: () => {\n // Default no-op function\n },\n startWidth: undefined,\n endWidth: undefined,\n});\n\nconst useInputContext = () => {\n const context = React.useContext(InputContext);\n if (!context) {\n throw new Error('useInputContext must be used within an InputContextProvider');\n }\n return context;\n};\n\nconst InputStart = ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => {\n const { setStartWidth } = useInputContext();\n const startRef = React.useRef(null);\n\n useEffect(() => {\n setStartWidth(startRef.current?.offsetWidth ?? 0);\n }, [setStartWidth]);\n\n return (\n \n {children}\n \n );\n};\n\nconst InputEnd = ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => {\n const { setEndWidth } = useInputContext();\n const endRef = React.useRef(null);\n\n useEffect(() => {\n setEndWidth(endRef.current?.offsetWidth ?? 0);\n }, [setEndWidth]);\n\n return (\n \n {children}\n \n );\n};\n\nInput.displayName = 'Input';\n\nexport { Input, InputEnd, InputStart };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/input-group/index.tsx", "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport React from 'react';\n\nimport { Button } from '@/registry/base-nova/protoform/components/button';\nimport { Input } from '@/registry/base-nova/protoform/components/input';\nimport { Textarea } from '@/registry/base-nova/protoform/components/textarea';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nfunction InputGroup({ className, testId, ...props }: React.ComponentProps<'div'> & SharedProps) {\n const ref = React.useRef(null);\n const [hasBlockAlign, setHasBlockAlign] = React.useState(false);\n\n React.useEffect(() => {\n if (ref.current) {\n const blockAddon = ref.current.querySelector('[data-align=\"block-start\"], [data-align=\"block-end\"]');\n setHasBlockAlign(!!blockAddon);\n }\n }, []);\n\n return (\n // biome-ignore lint/a11y/useSemanticElements: part of input group implementation\n textarea]:h-auto',\n\n // Conditional alignment\n hasBlockAlign ? 'h-auto flex-col items-stretch' : 'items-center',\n\n // Variants based on alignment.\n 'has-[>[data-align=inline-start]]:[&>input]:pl-2',\n 'has-[>[data-align=inline-end]]:[&>input]:pr-2',\n 'has-[>[data-align=block-start]]:[&>input]:pb-3',\n 'has-[>[data-align=block-end]]:[&>input]:pt-3',\n\n // Focus state.\n 'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50',\n\n // Error state.\n 'has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',\n\n className\n )}\n data-slot=\"input-group\"\n data-testid={testId}\n ref={ref}\n role=\"group\"\n {...props}\n />\n );\n}\n\nconst inputGroupAddonVariants = cva(\n \"flex h-auto cursor-text select-none items-center gap-2 py-1.5 font-medium text-muted-foreground text-sm group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n align: {\n 'inline-start': 'order-first justify-start pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',\n 'inline-end': 'order-last justify-end pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',\n 'block-start':\n 'order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3',\n 'block-end': 'order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3',\n },\n },\n defaultVariants: {\n align: 'inline-start',\n },\n }\n);\n\nfunction InputGroupAddon({\n className,\n align = 'inline-start',\n ...props\n}: React.ComponentProps<'div'> & VariantProps) {\n return (\n {\n if ((e.target as HTMLElement).closest('button')) {\n return;\n }\n e.currentTarget.parentElement?.querySelector('input')?.focus();\n }}\n role=\"group\"\n {...props}\n />\n );\n}\n\nconst inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', {\n variants: {\n size: {\n xs: \"h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5\",\n sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',\n 'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',\n 'icon-sm': 'size-8 p-0 has-[>svg]:p-0',\n },\n },\n defaultVariants: {\n size: 'xs',\n },\n});\n\nfunction InputGroupButton({\n className,\n type = 'button',\n variant = 'ghost',\n size = 'xs',\n testId,\n ...props\n}: Omit, 'size'> & VariantProps & SharedProps) {\n return (\n \n );\n}\n\nfunction InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {\n return (\n \n );\n}\n\nfunction InputGroupInput({ className, testId, ...props }: Omit, 'size'> & SharedProps) {\n return (\n
\n \n
\n );\n}\n\nconst InputGroupTextarea = React.forwardRef & SharedProps>(\n ({ className, testId, ...props }, ref) => (\n \n )\n);\n\nInputGroupTextarea.displayName = 'InputGroupTextarea';\n\nexport { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/json-field/index.tsx", "content": "'use client';\n\nimport { Braces, FileEdit, SpellCheck, Trash2 } from 'lucide-react';\nimport Prism from 'prismjs';\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport EditorModule from 'react-simple-code-editor';\nimport { toast } from 'sonner';\n\nimport { Badge } from '@/registry/base-nova/protoform/components/badge';\nimport { Button } from '@/registry/base-nova/protoform/components/button';\nimport { Combobox, type ComboboxOption } from '@/registry/base-nova/protoform/components/combobox';\nimport { CopyButton } from '@/registry/base-nova/protoform/components/copy-button';\nimport { Input } from '@/registry/base-nova/protoform/components/input';\nimport { Heading, Text } from '@/registry/base-nova/protoform/components/typography';\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nfunction isCommonJsEditorModule(value: unknown): value is { default: typeof EditorModule } {\n return typeof value === 'object' && value !== null && 'default' in value;\n}\n\nconst Editor = isCommonJsEditorModule(EditorModule) ? EditorModule.default : EditorModule;\n\nconst JSON_PRISM_GRAMMAR: Prism.Grammar = {\n property: { pattern: /(^|[^\\\\])\"(?:\\\\.|[^\\\\\"\\r\\n])*\"(?=\\s*:)/, lookbehind: true, greedy: true },\n string: { pattern: /(^|[^\\\\])\"(?:\\\\.|[^\\\\\"\\r\\n])*\"(?!\\s*:)/, lookbehind: true, greedy: true },\n comment: { pattern: /\\/\\/.*|\\/\\*[\\s\\S]*?(?:\\*\\/|$)/, greedy: true },\n number: /-?\\b\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b/i,\n punctuation: /[{}[\\],]/,\n operator: /:/,\n boolean: /\\b(?:false|true)\\b/,\n null: { pattern: /\\bnull\\b/, alias: 'keyword' },\n};\n\nfunction highlightJson(code: string): string {\n return Prism.highlight(code, Prism.languages.json ?? JSON_PRISM_GRAMMAR, 'json');\n}\n\n// Regex for matching trailing 's' to create singular names\nconst TRAILING_S_REGEX = /s$/;\n\nfunction jsonEditorLabel(propertyName: string | undefined, path: string[]): string {\n const name = propertyName ?? path.at(-1) ?? 'value';\n return `${name.charAt(0).toUpperCase()}${name.slice(1)} JSON`;\n}\n\ntype CustomFieldConfig = {\n fieldName: string;\n options: { value: string; label: string }[];\n placeholder?: string;\n onCreateOption?: (\n newValue: string,\n path: string[],\n handleFieldChange: (path: string[], value: JSONValue) => void\n ) => Promise;\n};\n\ntype JSONFieldProps = {\n schema: JSONSchemaType;\n value: JSONValue;\n onChange: (value: JSONValue) => void;\n onBlur?: () => void;\n maxDepth?: number;\n showPlaceholder?: boolean;\n customFields?: CustomFieldConfig[];\n className?: string;\n} & Omit, 'onChange'> &\n SharedProps;\n\nconst isTypeSupported = (type: JSONSchemaType['type'], supportedTypes: string[]): boolean => {\n if (Array.isArray(type)) {\n return type.every((t) => supportedTypes.includes(t));\n }\n return typeof type === 'string' && supportedTypes.includes(type);\n};\n\nconst isSimpleObject = (schema: JSONSchemaType): boolean => {\n const supportedTypes = ['string', 'number', 'integer', 'boolean', 'null'];\n if (schema.type && isTypeSupported(schema.type, supportedTypes)) {\n return true;\n }\n if (schema.type === 'object') {\n // Allow objects with properties (even nested ones) to be considered \"simple\" for form rendering\n return !!schema.properties && Object.keys(schema.properties).length > 0;\n }\n if (schema.type === 'array') {\n // Allow arrays with defined item schemas to be considered \"simple\"\n return !!schema.items;\n }\n return false;\n};\n\nconst getArrayItemDefault = (schema: JSONSchemaType): JSONValue => {\n if ('default' in schema && schema.default !== undefined) {\n return schema.default;\n }\n\n switch (schema.type) {\n case 'string':\n return '';\n case 'number':\n case 'integer':\n return 0;\n case 'boolean':\n return false;\n case 'array':\n return [];\n case 'object':\n return {};\n case 'null':\n return null;\n default:\n return null;\n }\n};\n\nconst generateExampleData = (schema: JSONSchemaType): JSONValue => {\n if ('default' in schema && schema.default !== undefined) {\n return schema.default;\n }\n\n switch (schema.type) {\n case 'string':\n return (schema.examples?.[0] as string) || '';\n case 'number':\n case 'integer':\n return (schema.examples?.[0] as number) || 42;\n case 'boolean':\n return true;\n case 'array':\n if (schema.items) {\n return [generateExampleData(schema.items as JSONSchemaType)];\n }\n return [];\n case 'object':\n if (schema.properties) {\n const result: Record = {};\n for (const [key, propSchema] of Object.entries(schema.properties)) {\n result[key] = generateExampleData(propSchema as JSONSchemaType);\n }\n return result;\n }\n return {};\n case 'null':\n return null;\n default:\n return null;\n }\n};\n\nconst hasEmptyValues = (value: JSONValue, schema: JSONSchemaType): boolean => {\n if (!value) {\n return true;\n }\n\n if (schema.type === 'object' && typeof value === 'object' && !Array.isArray(value)) {\n const obj = value as Record;\n if (Object.keys(obj).length === 0) {\n return true;\n }\n\n // Check if all values are empty/default\n if (schema.properties) {\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive empty-value checking across all JSON types\n return Object.entries(schema.properties).every(([key, propSchema]) => {\n const val = obj[key];\n const subSchema = propSchema as JSONSchemaType;\n\n if (val === undefined || val === null) {\n return true;\n }\n if (subSchema.type === 'string' && val === '') {\n return true;\n }\n if ((subSchema.type === 'number' || subSchema.type === 'integer') && val === 0) {\n return true;\n }\n if (subSchema.type === 'boolean' && val === false) {\n return true;\n }\n if (subSchema.type === 'array' && Array.isArray(val) && val.length === 0) {\n return true;\n }\n if (subSchema.type === 'object' && hasEmptyValues(val, subSchema)) {\n return true;\n }\n\n return false;\n });\n }\n }\n\n if (schema.type === 'array' && Array.isArray(value)) {\n return value.length === 0;\n }\n\n return false;\n};\n\nconst JSONField = ({\n schema,\n value,\n onChange,\n onBlur,\n maxDepth = 3,\n showPlaceholder = true,\n customFields = [],\n className,\n testId,\n ref,\n ...rest\n}: JSONFieldProps) => {\n const [isJSONMode, setIsJSONMode] = useState(false);\n const [jsonError, setJSONError] = useState();\n\n // Store the raw JSON string to allow immediate feedback during typing\n // while deferring parsing until the user stops typing\n const [rawJSONValue, setRawJSONValue] = useState(() => {\n // Use example data when starting with empty values and showPlaceholder is true\n let initialValue: JSONValue;\n if (showPlaceholder && hasEmptyValues(value, schema)) {\n initialValue = generateExampleData(schema);\n } else {\n initialValue = value || (schema.type === 'array' ? [] : {});\n }\n return JSON.stringify(initialValue, null, 2);\n });\n\n // Use a ref to manage debouncing timeouts to avoid parsing JSON\n // on every keystroke which would be inefficient and error-prone\n const timeoutRef = useRef | null>(null);\n\n // Debounce JSON parsing and parent updates to handle typing gracefully\n const debouncedUpdateParent = useCallback(\n (jsonString: string) => {\n // Clear any existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n // Set a new timeout\n timeoutRef.current = setTimeout(() => {\n try {\n const parsed = JSON.parse(jsonString);\n onChange(parsed);\n setJSONError(undefined);\n } catch {\n // Don't set error during normal typing\n }\n }, 300);\n },\n [onChange]\n );\n\n // Update rawJSONValue when value prop changes\n useEffect(() => {\n // Use example data when the value is empty and showPlaceholder is true\n let displayValue: JSONValue;\n if (showPlaceholder && hasEmptyValues(value, schema)) {\n displayValue = generateExampleData(schema);\n } else {\n displayValue = value || (schema.type === 'array' ? [] : {});\n }\n setRawJSONValue(JSON.stringify(displayValue, null, 2));\n }, [value, schema, showPlaceholder]);\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: handles bidirectional JSON/Form mode switching with validation\n const handleSwitchToFormMode = () => {\n if (isJSONMode) {\n // When switching to Form mode, ensure we have valid JSON\n try {\n const parsed = JSON.parse(rawJSONValue);\n // Update the parent component's state with the parsed value\n onChange(parsed);\n // Switch to form mode\n setIsJSONMode(false);\n } catch (err) {\n setJSONError(err instanceof Error ? err.message : 'Invalid JSON');\n }\n } else {\n // When switching to JSON mode, generate example data if showPlaceholder is true and current value is empty\n let displayValue: JSONValue;\n if (showPlaceholder && hasEmptyValues(value, schema)) {\n displayValue = generateExampleData(schema);\n } else {\n displayValue = value || (schema.type === 'array' ? [] : {});\n }\n setRawJSONValue(JSON.stringify(displayValue, null, 2));\n setIsJSONMode(true);\n }\n };\n\n const formatJSON = () => {\n try {\n const jsonStr = rawJSONValue.trim();\n if (!jsonStr) {\n return;\n }\n const formatted = JSON.stringify(JSON.parse(jsonStr), null, 2);\n setRawJSONValue(formatted);\n debouncedUpdateParent(formatted);\n setJSONError(undefined);\n } catch (err) {\n setJSONError(err instanceof Error ? err.message : 'Invalid JSON');\n }\n };\n\n // biome-ignore lint/nursery/useMaxParams: Complex form rendering function with many context parameters\n const renderFormFields = (\n propSchema: JSONSchemaType,\n currentValue: JSONValue,\n path: string[] = [],\n depth = 0,\n parentSchema?: JSONSchemaType,\n propertyName?: string\n ): // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: renders form controls for all JSON schema types recursively\n React.ReactNode => {\n if (depth >= maxDepth && (propSchema.type === 'object' || propSchema.type === 'array')) {\n // Render as JSON editor when max depth is reached\n return (\n {\n try {\n const parsed = JSON.parse(newValue);\n handleFieldChange(path, parsed);\n setJSONError(undefined);\n } catch (err) {\n setJSONError(err instanceof Error ? err.message : 'Invalid JSON');\n }\n }}\n value={JSON.stringify(currentValue ?? (propSchema.type === 'array' ? [] : {}), null, 2)}\n />\n );\n }\n\n // Check if this property is required in the parent schema\n const isRequired = parentSchema?.required?.includes(propertyName || '') ?? false;\n\n let fieldType = propSchema.type;\n if (Array.isArray(fieldType)) {\n // Of the possible types, find the first non-null type to determine the control to render\n fieldType = fieldType.find((t) => t !== 'null') ?? fieldType[0];\n }\n\n switch (fieldType) {\n case 'string': {\n // Check for custom field configuration\n const customFieldConfig = customFields.find((field) => field.fieldName === propertyName);\n if (customFieldConfig) {\n // Auto-select if there's only one option and no current value\n // Use the default value instead of triggering state updates during render\n const effectiveValue = (() => {\n if (customFieldConfig.options.length === 1 && !currentValue) {\n return customFieldConfig.options[0]?.value ?? '';\n }\n return currentValue as string;\n })();\n\n return (\n {\n if (val || isRequired) {\n handleFieldChange(path, val);\n } else {\n handleFieldChange(path, undefined);\n }\n }}\n onCreateOption={(newValue) => {\n if (customFieldConfig.onCreateOption) {\n customFieldConfig.onCreateOption(newValue, path, handleFieldChange);\n } else {\n const newOption = { value: newValue, label: newValue };\n customFieldConfig.options.push(newOption);\n handleFieldChange(path, newValue);\n }\n }}\n options={customFieldConfig.options}\n placeholder={customFieldConfig.placeholder || 'Select an option...'}\n value={effectiveValue ?? ''}\n />\n );\n }\n\n if (propSchema.oneOf?.every((option) => typeof option.const === 'string' && typeof option.title === 'string')) {\n const oneOfOptions: ComboboxOption[] = propSchema.oneOf.map((option) => ({\n value: option.const as string,\n label: option.title as string,\n }));\n\n return (\n {\n if (val || isRequired) {\n handleFieldChange(path, val);\n } else {\n handleFieldChange(path, undefined);\n }\n }}\n onCreateOption={(newValue) => {\n const newOption = { value: newValue, label: newValue };\n oneOfOptions.push(newOption);\n handleFieldChange(path, newValue);\n }}\n options={oneOfOptions}\n placeholder=\"Select an option...\"\n value={(currentValue as string) ?? ''}\n />\n );\n }\n\n if (propSchema.enum) {\n const enumOptions: ComboboxOption[] = propSchema.enum.map((option) => ({\n value: option,\n label: option,\n }));\n\n return (\n {\n if (val || isRequired) {\n handleFieldChange(path, val);\n } else {\n handleFieldChange(path, undefined);\n }\n }}\n onCreateOption={(newValue) => {\n const newOption = { value: newValue, label: newValue };\n enumOptions.push(newOption);\n handleFieldChange(path, newValue);\n }}\n options={enumOptions}\n placeholder=\"Select an option...\"\n value={(currentValue as string) ?? ''}\n />\n );\n }\n\n let inputType = 'text';\n switch (propSchema.format) {\n case 'email':\n inputType = 'email';\n break;\n case 'uri':\n inputType = 'url';\n break;\n case 'date':\n inputType = 'date';\n break;\n case 'date-time':\n inputType = 'datetime-local';\n break;\n default:\n inputType = 'text';\n break;\n }\n\n return (\n {\n const val = e.target.value;\n // Always allow setting string values, including empty strings\n handleFieldChange(path, val);\n }}\n pattern={propSchema.pattern}\n placeholder={propSchema.description}\n required={isRequired}\n type={inputType}\n value={(currentValue as string) ?? ''}\n />\n );\n }\n\n case 'number':\n return (\n {\n const val = e.target.value;\n if (val || isRequired) {\n const num = Number(val);\n if (!Number.isNaN(num)) {\n handleFieldChange(path, num);\n }\n } else {\n handleFieldChange(path, undefined);\n }\n }}\n placeholder={propSchema.description}\n required={isRequired}\n type=\"number\"\n value={(currentValue as number)?.toString() ?? ''}\n />\n );\n\n case 'integer':\n return (\n {\n const val = e.target.value;\n if (val || isRequired) {\n const num = Number(val);\n if (!Number.isNaN(num) && Number.isInteger(num)) {\n handleFieldChange(path, num);\n }\n } else {\n handleFieldChange(path, undefined);\n }\n }}\n placeholder={propSchema.description}\n required={isRequired}\n step=\"1\"\n type=\"number\"\n value={(currentValue as number)?.toString() ?? ''}\n />\n );\n\n case 'boolean':\n return (\n handleFieldChange(path, e.target.checked)}\n required={isRequired}\n type=\"checkbox\"\n />\n );\n case 'null':\n return null;\n case 'object':\n if (!propSchema.properties) {\n return (\n {\n try {\n const parsed = JSON.parse(newValue);\n handleFieldChange(path, parsed);\n setJSONError(undefined);\n } catch (err) {\n setJSONError(err instanceof Error ? err.message : 'Invalid JSON');\n }\n }}\n value={JSON.stringify(currentValue ?? {}, null, 2)}\n />\n );\n }\n\n return (\n
\n {Object.entries(propSchema.properties).map(([key, subSchema]) => (\n
\n
\n \n {key}\n {propSchema.required?.includes(key) && *}\n \n \n {(subSchema as JSONSchemaType).type || 'unknown'}\n \n
\n {renderFormFields(\n subSchema as JSONSchemaType,\n (currentValue as Record)?.[key],\n [...path, key],\n depth + 1,\n propSchema,\n key\n )}\n
\n ))}\n
\n );\n case 'array': {\n let arrayValue = Array.isArray(currentValue) ? currentValue : [];\n if (!propSchema.items) {\n return null;\n }\n\n // Handle empty arrays without triggering state update during render\n if (arrayValue.length === 0) {\n const defaultValue = getArrayItemDefault(propSchema.items as JSONSchemaType);\n arrayValue = [defaultValue];\n }\n\n // If the array items are simple, render as form fields, otherwise use JSON editor\n if (isSimpleObject(propSchema.items)) {\n return (\n
\n {propSchema.description ? (\n \n {propSchema.description}\n \n ) : null}\n\n
\n {arrayValue.map((item, index) => {\n // Create a contextual name for the array item\n const itemTypeName =\n propSchema.items?.title ||\n propSchema.items?.description ||\n propertyName?.replace(TRAILING_S_REGEX, '') ||\n 'Item'; // Remove trailing 's' from property name\n const itemDisplayName = itemTypeName.charAt(0).toUpperCase() + itemTypeName.slice(1);\n\n return (\n // biome-ignore lint/suspicious/noArrayIndexKey: dynamic array items have no stable identifier\n
\n
\n \n {itemDisplayName} #{index + 1}\n \n {\n const newArray = [...arrayValue];\n newArray.splice(index, 1);\n handleFieldChange(path, newArray);\n }}\n size=\"sm\"\n type=\"button\"\n variant=\"outline\"\n >\n \n \n
\n
\n {propSchema.items?.type === 'object' && propSchema.items.properties\n ? Object.entries(propSchema.items.properties).map(([key, subSchema]) => (\n
\n
\n \n {key}\n \n \n {(subSchema as JSONSchemaType).type || 'unknown'}\n \n {propSchema.items?.required?.includes(key) && (\n *\n )}\n
\n {renderFormFields(\n subSchema as JSONSchemaType,\n (item as Record)?.[key],\n [...path, index.toString(), key],\n depth + 1,\n propSchema.items,\n key\n )}\n
\n ))\n : renderFormFields(\n propSchema.items as JSONSchemaType,\n item,\n [...path, index.toString()],\n depth + 1\n )}\n
\n
\n );\n })}\n {\n const defaultValue = getArrayItemDefault(propSchema.items as JSONSchemaType);\n handleFieldChange(path, [...arrayValue, defaultValue]);\n }}\n size=\"sm\"\n type=\"button\"\n variant=\"dashed\"\n >\n + Add{' '}\n {propSchema.items?.title ||\n propSchema.items?.description ||\n propertyName?.replace(TRAILING_S_REGEX, '') ||\n 'Item'}\n \n
\n
\n );\n }\n\n // For complex arrays, fall back to JSON editor\n return (\n {\n try {\n const parsed = JSON.parse(newValue);\n handleFieldChange(path, parsed);\n setJSONError(undefined);\n } catch (err) {\n setJSONError(err instanceof Error ? err.message : 'Invalid JSON');\n }\n }}\n value={JSON.stringify(currentValue ?? [], null, 2)}\n />\n );\n }\n default:\n return null;\n }\n };\n\n const handleFieldChange = (path: string[], fieldValue: JSONValue) => {\n if (path.length === 0) {\n onChange(fieldValue);\n return;\n }\n\n try {\n const newValue = updateValueAtPath(value, path, fieldValue);\n onChange(newValue);\n } catch {\n onChange(value);\n }\n };\n\n const shouldUseJSONMode =\n (schema.type === 'object' && (!schema.properties || Object.keys(schema.properties).length === 0)) ||\n (schema.type === 'array' && !schema.items);\n\n useEffect(() => {\n if (shouldUseJSONMode && !isJSONMode) {\n setIsJSONMode(true);\n }\n }, [shouldUseJSONMode, isJSONMode]);\n\n // Handle initialization of empty arrays with default values\n useEffect(() => {\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive schema traversal to initialize nested array defaults\n const initializeArrayDefaults = (currentSchema: JSONSchemaType, currentValue: JSONValue, path: string[] = []) => {\n if (currentSchema.type === 'array' && currentSchema.items) {\n const arrayValue = Array.isArray(currentValue) ? currentValue : [];\n if (arrayValue.length === 0) {\n const defaultValue = getArrayItemDefault(currentSchema.items as JSONSchemaType);\n const newValue = updateValueAtPath(value, path, [defaultValue]);\n onChange(newValue);\n }\n } else if (currentSchema.type === 'object' && currentSchema.properties) {\n for (const [key, subSchema] of Object.entries(currentSchema.properties)) {\n const subValue = (currentValue as Record)?.[key];\n initializeArrayDefaults(subSchema as JSONSchemaType, subValue, [...path, key]);\n }\n }\n };\n\n // Only initialize if we have a value and are not in JSON mode\n if (value !== undefined && !isJSONMode) {\n initializeArrayDefaults(schema, value);\n }\n }, [schema, value, onChange, isJSONMode]);\n\n // Handle auto-selection for custom fields with single options\n // biome-ignore lint/correctness/useExhaustiveDependencies: part of DynamicJSONForm implementation\n useEffect(() => {\n const syncAutoSelections = (currentSchema: JSONSchemaType, currentValue: JSONValue, path: string[] = []) => {\n if (currentSchema.type === 'object' && currentSchema.properties) {\n for (const [key, subSchema] of Object.entries(currentSchema.properties)) {\n const subValue = (currentValue as Record)?.[key];\n const customFieldConfig = customFields.find((field) => field.fieldName === key);\n\n if (customFieldConfig && customFieldConfig.options.length === 1 && !subValue) {\n const autoSelectedValue = customFieldConfig.options[0]?.value ?? '';\n handleFieldChange([...path, key], autoSelectedValue);\n }\n\n syncAutoSelections(subSchema as JSONSchemaType, subValue, [...path, key]);\n }\n }\n };\n\n if (value !== undefined && !isJSONMode && customFields.length > 0) {\n syncAutoSelections(schema, value);\n }\n }, [schema, value, customFields, isJSONMode]);\n\n return (\n // biome-ignore lint/a11y/noNoninteractiveElementInteractions: onBlur needed for react-hook-form field tracking\n // biome-ignore lint/a11y/noStaticElementInteractions: onBlur needed for react-hook-form field tracking\n
\n
\n {isJSONMode ? (\n <>\n \n toast.success('JSON copied', {\n description: 'The JSON data has been successfully copied to your clipboard.',\n })\n }\n size=\"sm\"\n type=\"button\"\n variant=\"outline\"\n >\n Copy JSON\n \n \n \n ) : null}\n\n \n
\n\n {isJSONMode ? (\n {\n // Always update local state\n setRawJSONValue(newValue);\n\n // Use the debounced function to attempt parsing and updating parent\n debouncedUpdateParent(newValue);\n }}\n value={rawJSONValue}\n />\n ) : (\n renderFormFields(schema, value)\n )}\n
\n );\n};\n\ntype JSONEditorProps = {\n value: string;\n onChange: (value: string) => void;\n error?: string;\n label: string;\n};\n\nconst JSONEditor = ({ value, onChange, error: externalError, label }: JSONEditorProps) => {\n const [editorContent, setEditorContent] = useState(value || '');\n const [internalError, setInternalError] = useState(undefined);\n const editorId = React.useId();\n\n useEffect(() => {\n setEditorContent(value || '');\n }, [value]);\n\n const handleEditorChange = (newContent: string) => {\n setEditorContent(newContent);\n setInternalError(undefined);\n onChange(newContent);\n };\n\n const displayError = internalError || externalError;\n\n return (\n
\n \n
\n \n
\n {displayError ? (\n \n {displayError}\n \n ) : null}\n
\n );\n};\n\ntype JSONValue = string | number | boolean | null | undefined | JSONValue[] | { [key: string]: JSONValue };\n\ntype JSONSchemaConst = {\n const: JSONValue;\n title?: string;\n description?: string;\n};\n\ntype JSONSchemaType = {\n type?:\n | 'string'\n | 'number'\n | 'integer'\n | 'boolean'\n | 'array'\n | 'object'\n | 'null'\n | ('string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null')[];\n title?: string;\n description?: string;\n required?: string[];\n default?: JSONValue;\n examples?: JSONValue[];\n properties?: Record;\n items?: JSONSchemaType;\n minimum?: number;\n maximum?: number;\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n format?: string;\n enum?: string[];\n const?: JSONValue;\n oneOf?: (JSONSchemaType | JSONSchemaConst)[];\n anyOf?: (JSONSchemaType | JSONSchemaConst)[];\n};\n\ntype JSONObject = { [key: string]: JSONValue };\n\nfunction updateValueAtPath(obj: JSONValue, path: string[], value: JSONValue): JSONValue {\n if (path.length === 0) {\n return value;\n }\n\n let mutableObj = obj;\n if (obj === null || obj === undefined) {\n mutableObj = Number.isNaN(Number(path[0])) ? {} : [];\n }\n\n if (Array.isArray(mutableObj)) {\n return updateArray(mutableObj, path, value);\n }\n if (typeof mutableObj === 'object' && mutableObj !== null) {\n return updateObject(mutableObj as JSONObject, path, value);\n }\n return mutableObj;\n}\n\nfunction updateArray(array: JSONValue[], path: string[], value: JSONValue): JSONValue[] {\n const [index, ...restPath] = path;\n const arrayIndex = Number(index);\n\n if (Number.isNaN(arrayIndex)) {\n return array;\n }\n\n if (arrayIndex < 0) {\n return array;\n }\n\n let newArray: JSONValue[] = [];\n for (let i = 0; i < array.length; i++) {\n newArray[i] = i in array ? array[i] : null;\n }\n\n if (arrayIndex >= newArray.length) {\n const extendedArray: JSONValue[] = new Array(arrayIndex).fill(null);\n // Copy over the existing elements (now guaranteed to be dense)\n for (let i = 0; i < newArray.length; i++) {\n extendedArray[i] = newArray[i];\n }\n newArray = extendedArray;\n }\n\n if (restPath.length === 0) {\n newArray[arrayIndex] = value;\n } else {\n newArray[arrayIndex] = updateValueAtPath(newArray[arrayIndex], restPath, value);\n }\n return newArray;\n}\n\nfunction updateObject(obj: JSONObject, path: string[], value: JSONValue): JSONObject {\n const [key, ...restPath] = path;\n\n if (typeof key !== 'string') {\n return obj;\n }\n\n const newObj = { ...obj };\n\n if (restPath.length === 0) {\n newObj[key] = value;\n } else {\n // Ensure key exists\n if (!(key in newObj)) {\n newObj[key] = {};\n }\n newObj[key] = updateValueAtPath(newObj[key], restPath, value);\n }\n return newObj;\n}\n\nexport type { CustomFieldConfig, JSONObject, JSONSchemaConst, JSONSchemaType, JSONValue };\nexport { JSONField };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/key-value-field/index.tsx", "content": "'use client';\n\nimport { Plus, X } from 'lucide-react';\nimport { type ReactNode, useMemo, useRef } from 'react';\n\nimport { Button } from '@/registry/base-nova/protoform/components/button';\nimport { Combobox, type ComboboxProps } from '@/registry/base-nova/protoform/components/combobox';\nimport { Input, type InputProps } from '@/registry/base-nova/protoform/components/input';\nimport { Label } from '@/registry/base-nova/protoform/components/label';\nimport { findDuplicateIndices, useInputListFocus } from '@/registry/base-nova/protoform/lib/input-utils';\nimport type { SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nexport type KeyValuePair = {\n key: string;\n value: string;\n};\n\ntype InputFieldConfig = { mode?: 'input' } & Omit;\ntype ComboboxFieldConfig = { mode: 'combobox' } & Omit;\nexport type KeyValueFieldConfig = InputFieldConfig | ComboboxFieldConfig;\n\nexport type KeyValueFieldError = {\n key?: string;\n value?: string;\n};\n\nexport interface KeyValueFieldProps extends SharedProps {\n addButtonLabel?: string;\n description?: ReactNode;\n disabled?: boolean;\n errors?: Array;\n keyFieldProps?: KeyValueFieldConfig;\n label?: ReactNode;\n maxItems?: number;\n onChange?: (value: KeyValuePair[]) => void;\n showAddButton?: boolean;\n value?: KeyValuePair[];\n valueFieldProps?: KeyValueFieldConfig;\n}\n\nfunction FieldRenderer({\n config,\n value,\n onChange,\n disabled,\n isInvalid,\n testId,\n}: {\n config: KeyValueFieldConfig;\n value: string;\n onChange: (value: string) => void;\n disabled?: boolean;\n isInvalid: boolean;\n testId?: string;\n}) {\n if (config.mode === 'combobox') {\n const { mode: _m, ...comboboxProps } = config;\n return ;\n }\n\n const { mode: _, ...inputProps } = config;\n return (\n onChange(e.target.value)}\n testId={testId}\n value={value}\n />\n );\n}\n\nfunction ErrorRow({ keyError, valueError }: { keyError?: string; valueError?: string }) {\n if (!(keyError || valueError)) {\n return null;\n }\n return (\n <>\n {keyError ?

{keyError}

: }\n {valueError ?

{valueError}

: }\n \n \n );\n}\n\nfunction KeyValueRow({\n pair,\n index,\n isDuplicate,\n isLast,\n error,\n disabled,\n keyFieldProps,\n valueFieldProps,\n testId,\n addButtonLabel,\n onKeyChange,\n onValueChange,\n onDelete,\n onAdd,\n}: {\n pair: KeyValuePair;\n index: number;\n isDuplicate: boolean;\n isLast: boolean;\n error?: KeyValueFieldError;\n disabled?: boolean;\n keyFieldProps: KeyValueFieldConfig;\n valueFieldProps: KeyValueFieldConfig;\n testId?: string;\n addButtonLabel: string;\n onKeyChange: (index: number, key: string) => void;\n onValueChange: (index: number, val: string) => void;\n onDelete: (index: number) => void;\n onAdd?: () => void;\n}) {\n const isKeyInvalid = Boolean(error?.key) || Boolean(pair.value && !pair.key) || isDuplicate;\n const isValueInvalid = Boolean(error?.value) || Boolean(pair.key && !pair.value);\n\n return (\n
\n onKeyChange(index, val)}\n testId={testId ? `${testId}-key-${index}` : undefined}\n value={pair.key}\n />\n onValueChange(index, val)}\n testId={testId ? `${testId}-value-${index}` : undefined}\n value={pair.value}\n />\n onDelete(index)}\n size=\"icon-sm\"\n type=\"button\"\n variant=\"ghost\"\n >\n \n \n \n {onAdd && isLast ? (\n \n \n {addButtonLabel}\n \n ) : null}\n
\n );\n}\n\nexport function KeyValueField({\n value = [],\n onChange,\n errors,\n label,\n description,\n addButtonLabel = 'Add',\n keyFieldProps = { placeholder: 'Key' },\n valueFieldProps = { placeholder: 'Value' },\n showAddButton = true,\n disabled,\n maxItems,\n testId,\n}: KeyValueFieldProps) {\n const containerRef = useRef(null);\n const { onAdd, onRemove } = useInputListFocus(containerRef);\n\n const duplicateIndices = useMemo(() => findDuplicateIndices(value, (pair) => pair.key), [value]);\n\n const isAtLimit = maxItems !== undefined && value.length >= maxItems;\n\n const handleAdd = () => {\n onChange?.([...value, { key: '', value: '' }]);\n onAdd();\n };\n\n const handleKeyChange = (index: number, key: string) => {\n const current = value[index];\n if (!current) {\n return;\n }\n const updated = [...value];\n updated[index] = { ...current, key };\n onChange?.(updated);\n };\n\n const handleValueChange = (index: number, val: string) => {\n const current = value[index];\n if (!current) {\n return;\n }\n const updated = [...value];\n updated[index] = { ...current, value: val };\n onChange?.(updated);\n };\n\n const handleDelete = (index: number) => {\n const updated = [...value];\n updated.splice(index, 1);\n onChange?.(updated);\n onRemove(index);\n };\n\n return (\n
\n {label || description ? (\n
\n {label ? : null}\n {description ?

{description}

: null}\n
\n ) : null}\n {value.map((pair, index) => (\n \n ))}\n {value.length === 0 && showAddButton && !isAtLimit ? (\n \n \n {addButtonLabel}\n \n ) : null}\n
\n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/label/index.tsx", "content": "import React from 'react';\n\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nconst Label = React.forwardRef & SharedProps>(\n ({ className, testId, ...props }, ref) => (\n \n )\n);\n\nLabel.displayName = 'Label';\n\nexport { Label };\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/multi-select/index.tsx", "content": "/** biome-ignore-all lint/a11y/useKeyWithClickEvents: part of multi select implementation */\n/** biome-ignore-all lint/a11y/noStaticElementInteractions: part of multi select implementation */\n'use client';\n\nimport { Popover as PopoverPrimitive } from '@base-ui/react/popover';\nimport { Check, ChevronDownIcon } from 'lucide-react';\nimport React from 'react';\nimport { createPortal } from 'react-dom';\n\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from '@/registry/base-nova/protoform/components/command';\nimport { TagsValue } from '@/registry/base-nova/protoform/components/tags';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/registry/base-nova/protoform/components/tooltip';\nimport { useControllableState } from '@/registry/base-nova/protoform/hooks/use-controllable-state';\nimport { narrowOpenChange, renderWithDataState } from '@/registry/base-nova/protoform/lib/base-ui-compat';\nimport { cn, type PortalContentProps, type PortalRootProps, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\nexport type MultiSelectOptionItem = {\n value: string;\n label?: React.ReactNode;\n selectedTestId?: string;\n testId?: string;\n};\n\ntype MultiSelectContextValue = {\n value: string[];\n\n open: boolean;\n\n onSelect: (value: string, item: MultiSelectOptionItem) => void;\n\n onDeselect: (value: string, item: MultiSelectOptionItem) => void;\n\n onSearch?: (keyword?: string) => void;\n\n filter?: boolean | ((keyword: string, current: string) => boolean);\n\n disabled?: boolean;\n\n maxCount?: number;\n\n itemCache: Map;\n};\n\nconst MultiSelectContext = React.createContext(undefined);\n\nfunction useMultiSelect() {\n const context = React.useContext(MultiSelectContext);\n\n if (!context) {\n throw new Error('useMultiSelect must be used within MultiSelectProvider');\n }\n\n return context;\n}\n\ntype MultiSelectProps = Omit, 'onOpenChange'> &\n SharedProps & {\n value?: string[];\n onValueChange?: (value: string[], items: MultiSelectOptionItem[]) => void;\n onSelect?: (value: string, item: MultiSelectOptionItem) => void;\n onDeselect?: (value: string, item: MultiSelectOptionItem) => void;\n defaultValue?: string[];\n onOpenChange?: (open: boolean) => void;\n onSearch?: (keyword: string | undefined) => void;\n filter?: boolean | ((keyword: string, current: string) => boolean);\n disabled?: boolean;\n maxCount?: number;\n };\n\nconst MultiSelect: React.FC = ({\n value: valueProp,\n onValueChange: onValueChangeProp,\n onDeselect: onDeselectProp,\n onSelect: onSelectProp,\n defaultValue,\n open: openProp,\n onOpenChange,\n defaultOpen,\n onSearch,\n filter,\n disabled,\n maxCount,\n ...popoverProps\n}) => {\n const itemCache = React.useRef(new Map()).current;\n\n const handleValueChange = React.useCallback(\n (state: string[]) => {\n if (onValueChangeProp) {\n // biome-ignore lint/style/noNonNullAssertion: part of multi-select implementation\n const items = state.map((v) => itemCache.get(v)!);\n\n onValueChangeProp(state, items);\n }\n },\n [onValueChangeProp, itemCache]\n );\n\n const [value, setValue] = useControllableState({\n prop: valueProp,\n defaultProp: defaultValue ?? [],\n onChange: handleValueChange,\n });\n\n const [open, setOpen] = useControllableState({\n prop: openProp,\n defaultProp: defaultOpen ?? false,\n onChange: onOpenChange,\n });\n\n const handleSelect = React.useCallback(\n (selectedValue: string, item: MultiSelectOptionItem) => {\n setValue((prev: string[]) => {\n if (prev.includes(selectedValue)) {\n return prev;\n }\n\n onSelectProp?.(selectedValue, item);\n\n return [...prev, selectedValue];\n });\n },\n [onSelectProp, setValue]\n );\n\n const handleDeselect = React.useCallback(\n (deselectedValue: string, item: MultiSelectOptionItem) => {\n setValue((prev: string[]) => {\n if (!prev.includes(deselectedValue)) {\n return prev;\n }\n\n onDeselectProp?.(deselectedValue, item);\n\n return prev.filter((v: string) => v !== deselectedValue);\n });\n },\n [onDeselectProp, setValue]\n );\n\n const contextValue = React.useMemo(\n () => ({\n value: value || [],\n open,\n onSearch,\n filter,\n disabled,\n maxCount,\n onSelect: handleSelect,\n onDeselect: handleDeselect,\n itemCache,\n }),\n [value, open, onSearch, filter, disabled, maxCount, handleSelect, handleDeselect, itemCache]\n );\n\n return (\n \n \n \n );\n};\n\nMultiSelect.displayName = 'MultiSelect';\n\ninterface MultiSelectTriggerProps extends React.ComponentPropsWithoutRef<'div'>, SharedProps {}\n\nfunction PreventClick(e: React.MouseEvent | React.TouchEvent) {\n e.preventDefault();\n e.stopPropagation();\n}\n\nconst MultiSelectTrigger = React.forwardRef, MultiSelectTriggerProps>(\n ({ className, children, testId, ...props }, forwardedRef) => {\n const { disabled } = useMultiSelect();\n\n return (\n {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n props.onClick?.(e as unknown as React.MouseEvent);\n }\n }\n }\n onTouchStart={disabled ? PreventClick : props.onTouchStart}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n >\n {children}\n \n
\n }\n />\n );\n }\n);\n\nMultiSelectTrigger.displayName = 'MultiSelectTrigger';\n\ninterface MultiSelectValueProps extends React.ComponentPropsWithoutRef<'div'>, SharedProps {\n maxDisplay?: number;\n maxItemLength?: number;\n options?: MultiSelectOption[];\n placeholder?: string;\n}\n\nconst MultiSelectValue = React.forwardRef, MultiSelectValueProps>(\n ({ className, placeholder, maxDisplay, maxItemLength, options, testId, ...props }, forwardRef) => {\n const { value, itemCache, onDeselect } = useMultiSelect();\n\n const renderRemain = maxDisplay && value.length > maxDisplay ? value.length - maxDisplay : 0;\n const renderItems = renderRemain ? value.slice(0, maxDisplay) : value;\n\n if (!value.length) {\n return {placeholder};\n }\n\n return (\n \n \n {renderItems.map((itemValue) => {\n const item = itemCache.get(itemValue) ?? findMultiSelectOption(options, itemValue);\n\n const content = item?.label || itemValue;\n\n // For React nodes, don't truncate - show full content\n const child =\n maxItemLength && typeof content === 'string' && content.length > maxItemLength\n ? `${content.slice(0, maxItemLength)}...`\n : content;\n\n // Determine if we should show a tooltip - only for truncated strings\n const shouldShowTooltip = maxItemLength && typeof content === 'string' && content.length > maxItemLength;\n\n const el = (\n {\n if (!item) {\n return;\n }\n\n onDeselect(itemValue, item);\n }}\n testId={item?.selectedTestId ?? (testId ? `${testId}-selected-${itemValue}` : undefined)}\n >\n {child}\n \n );\n\n if (shouldShowTooltip) {\n return (\n \n \n {el}\n \n \n {content}\n \n \n );\n }\n\n return el;\n })}\n {renderRemain ? +{renderRemain} : null}\n
\n \n );\n }\n);\n\nMultiSelectValue.displayName = 'MultiSelectValue';\n\nconst MultiSelectSearch = React.forwardRef<\n React.ComponentRef,\n React.ComponentPropsWithoutRef\n>((props, ref) => {\n const { onSearch } = useMultiSelect();\n\n return ;\n});\n\nMultiSelectSearch.displayName = 'MultiSelectSearch';\n\nconst MultiSelectList = React.forwardRef<\n React.ComponentRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\n\nMultiSelectList.displayName = 'MultiSelectList';\n\ninterface MultiSelectContentProps extends React.ComponentPropsWithoutRef, SharedProps {\n container?: Element;\n /**\n * @deprecated Base UI does not expose a Radix-style `onOpenAutoFocus` callback. Accepted for\n * API parity; the event is ignored.\n */\n onOpenAutoFocus?: (event: Event) => void;\n}\n\nconst MultiSelectContent = React.forwardRef, MultiSelectContentProps>(\n ({ className, children, container, testId, onOpenAutoFocus: _onOpenAutoFocus, ...props }, ref) => {\n const context = useMultiSelect();\n\n const fragmentRef = React.useRef(null);\n\n if (!fragmentRef.current && typeof window !== 'undefined') {\n fragmentRef.current = document.createDocumentFragment();\n }\n\n if (!context.open) {\n return fragmentRef.current ? createPortal({children}, fragmentRef.current) : null;\n }\n\n return (\n \n \n \n \n {children}\n \n \n \n \n );\n }\n);\n\nMultiSelectContent.displayName = 'MultiSelectContent';\n\ntype MultiSelectItemProps = React.ComponentPropsWithoutRef &\n Partial &\n SharedProps & {\n onSelect?: (value: string, item: MultiSelectOptionItem) => void;\n onDeselect?: (value: string, item: MultiSelectOptionItem) => void;\n };\n\nconst MultiSelectItem = React.forwardRef, MultiSelectItemProps>(\n (\n {\n value,\n onSelect: onSelectProp,\n onDeselect: onDeselectProp,\n children,\n label,\n disabled: disabledProp,\n className,\n selectedTestId,\n testId,\n ...props\n },\n forwardedRef\n ) => {\n const { value: contextValue, maxCount, onSelect, onDeselect, itemCache } = useMultiSelect();\n\n const item = React.useMemo(\n () =>\n value\n ? {\n value,\n label: label || (typeof children === 'string' ? children : undefined),\n selectedTestId,\n testId,\n }\n : undefined,\n [value, label, children, selectedTestId, testId]\n );\n\n const selected = Boolean(value && contextValue.includes(value));\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: part of multi-select implementation\n React.useEffect(() => {\n if (value) {\n // biome-ignore lint/style/noNonNullAssertion: part of multi-select implementation\n itemCache.set(value, item!);\n }\n }, [selected, value, item, itemCache]);\n\n const disabled = Boolean(disabledProp || (!selected && maxCount && contextValue.length >= maxCount));\n\n const handleClick = () => {\n if (selected) {\n // biome-ignore lint/style/noNonNullAssertion: part of multi-select implementation\n onDeselectProp?.(value!, item!);\n // biome-ignore lint/style/noNonNullAssertion: part of multi-select implementation\n onDeselect(value!, item!);\n } else {\n // biome-ignore lint/style/noNonNullAssertion: part of multi-select implementation\n itemCache.set(value!, item!);\n // biome-ignore lint/style/noNonNullAssertion: part of multi-select implementation\n onSelectProp?.(value!, item!);\n // biome-ignore lint/style/noNonNullAssertion: part of multi-select implementation\n onSelect(value!, item!);\n }\n };\n\n const labelText = typeof label === 'string' ? label : typeof children === 'string' ? children : undefined;\n const keywords = labelText && labelText !== value ? [labelText] : undefined;\n\n return (\n \n {children || label || value}\n {selected ? : null}\n \n );\n }\n);\n\nMultiSelectItem.displayName = 'MultiSelectItem';\n\nconst MultiSelectGroup = React.forwardRef<\n React.ComponentRef,\n React.ComponentPropsWithoutRef & SharedProps\n>(({ testId, ...props }, forwardRef) => );\n\nMultiSelectGroup.displayName = 'MultiSelectGroup';\n\nconst MultiSelectSeparator = React.forwardRef<\n React.ComponentRef,\n React.ComponentPropsWithoutRef\n>((props, forwardRef) => );\n\nMultiSelectSeparator.displayName = 'MultiSelectSeparator';\n\nconst MultiSelectEmpty = React.forwardRef<\n React.ComponentRef,\n React.ComponentPropsWithoutRef\n>(({ children = 'No Content', ...props }, forwardRef) => (\n \n {children}\n \n));\n\nMultiSelectEmpty.displayName = 'MultiSelectEmpty';\n\nexport type MultiSelectOptionSeparator = {\n type: 'separator';\n};\n\nexport type MultiSelectOptionGroup = {\n heading?: React.ReactNode;\n testId?: string;\n value?: string;\n children: MultiSelectOption[];\n};\n\nexport type MultiSelectOption =\n | Pick\n | MultiSelectOptionSeparator\n | MultiSelectOptionGroup;\n\nfunction findMultiSelectOption(\n options: MultiSelectOption[] | undefined,\n value: string\n): MultiSelectOptionItem | undefined {\n for (const option of options ?? []) {\n if ('type' in option) {\n continue;\n }\n if ('children' in option) {\n const item = findMultiSelectOption(option.children, value);\n if (item) {\n return item;\n }\n continue;\n }\n if (option.value === value) {\n return {\n label: option.label,\n selectedTestId: option.selectedTestId,\n testId: option.testId,\n value,\n };\n }\n }\n return;\n}\n\nfunction renderMultiSelectOptions(list: MultiSelectOption[]) {\n return list.map((option, index) => {\n if ('type' in option) {\n if (option.type === 'separator') {\n // biome-ignore lint/suspicious/noArrayIndexKey: part of multi-select implementation\n return ;\n }\n\n return null;\n }\n\n if ('children' in option) {\n return (\n \n {renderMultiSelectOptions(option.children)}\n \n );\n }\n\n return (\n \n {option.label}\n \n );\n });\n}\n\n// Simplified API for backend developers\ntype SimpleMultiSelectProps = PortalRootProps &\n SharedProps &\n Pick & {\n id?: string;\n options: MultiSelectOption[] | string[];\n value?: string[];\n onValueChange?: (value: string[]) => void;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n maxCount?: number;\n maxDisplay?: number;\n searchable?: boolean;\n width?: 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'auto';\n };\n\nconst widthClasses = {\n sm: 'w-48',\n md: 'w-64',\n lg: 'w-80',\n xl: 'w-96',\n full: 'w-full',\n auto: 'w-auto',\n};\n\nfunction SimpleMultiSelect({\n id,\n options,\n value,\n onValueChange,\n placeholder = 'Select items...',\n className,\n disabled,\n maxCount,\n maxDisplay,\n searchable = true,\n width = 'md',\n container,\n onOpenAutoFocus,\n open,\n defaultOpen,\n onOpenChange,\n testId,\n}: SimpleMultiSelectProps) {\n // Convert simple string array to option objects\n const normalizedOptions: MultiSelectOption[] = React.useMemo(\n () =>\n options.map((option) => {\n if (typeof option === 'string') {\n return { value: option, label: option };\n }\n return option;\n }),\n [options]\n );\n\n return (\n \n \n \n \n \n {searchable ? (\n \n ) : null}\n {renderMultiSelectOptions(normalizedOptions)}\n No items found\n \n \n );\n}\n\nexport {\n MultiSelect,\n MultiSelectContent,\n MultiSelectEmpty,\n MultiSelectGroup,\n MultiSelectItem,\n MultiSelectList,\n MultiSelectSearch,\n MultiSelectSeparator,\n MultiSelectTrigger,\n MultiSelectValue,\n renderMultiSelectOptions,\n SimpleMultiSelect,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/popover/index.tsx", "content": "'use client';\n\nimport { Popover as PopoverPrimitive } from '@base-ui/react/popover';\nimport { AnimatePresence, type HTMLMotionProps, motion, type Transition } from 'motion/react';\nimport React from 'react';\n\nimport { usePortalContainer } from '@/registry/base-nova/protoform/hooks/use-portal-container';\nimport { asChildTrigger, narrowOpenChange, renderWithDataState, Slot, useMirroredOpen } from '@/registry/base-nova/protoform/lib/base-ui-compat';\nimport { cn, type PortalContentProps, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\ntype PopoverContextType = {\n isOpen: boolean;\n};\n\nconst PopoverContext = React.createContext(undefined);\n\nconst usePopover = (): PopoverContextType => {\n const context = React.useContext(PopoverContext);\n if (!context) {\n throw new Error('usePopover must be used within a Popover');\n }\n return context;\n};\n\ntype PopoverAnchorContextType = {\n anchorRef: React.MutableRefObject;\n setHasAnchor: (hasAnchor: boolean) => void;\n hasAnchor: boolean;\n};\n\nconst PopoverAnchorContext = React.createContext(undefined);\n\ntype Side = 'top' | 'bottom' | 'left' | 'right';\ntype Align = 'start' | 'center' | 'end';\n\nconst getInitialPosition = (side: Side) => {\n switch (side) {\n case 'top':\n return { y: 15 };\n case 'bottom':\n return { y: -15 };\n case 'left':\n return { x: 15 };\n case 'right':\n return { x: -15 };\n default:\n return {};\n }\n};\n\ntype PopoverProps = Omit, 'onOpenChange' | 'children'> &\n SharedProps & {\n onOpenChange?: (open: boolean) => void;\n children?: React.ReactNode;\n };\n\nfunction Popover({ children, testId, onOpenChange, ...props }: PopoverProps) {\n const { isOpen, handleOpenChange } = useMirroredOpen(props?.open, props?.defaultOpen, onOpenChange);\n const anchorRef = React.useRef(null);\n const [hasAnchor, setHasAnchor] = React.useState(false);\n\n const anchorCtx = React.useMemo(\n () => ({ anchorRef, setHasAnchor, hasAnchor }),\n [hasAnchor]\n );\n\n return (\n \n \n \n {children}\n \n \n \n );\n}\n\ntype PopoverTriggerProps = React.ComponentProps &\n SharedProps & {\n asChild?: boolean;\n };\n\nfunction PopoverTrigger({ className, testId, ...props }: PopoverTriggerProps) {\n return (\n \n );\n}\n\ntype PopoverContentProps = React.ComponentProps &\n HTMLMotionProps<'div'> &\n SharedProps &\n Pick & {\n transition?: Transition;\n side?: Side;\n align?: Align;\n sideOffset?: number;\n alignOffset?: number;\n };\n\nfunction PopoverContent({\n className,\n align = 'center',\n side = 'bottom',\n sideOffset = 4,\n alignOffset,\n transition = { type: 'spring', stiffness: 300, damping: 25 },\n children,\n testId,\n container,\n onOpenAutoFocus: _onOpenAutoFocus,\n ...props\n}: PopoverContentProps) {\n const { isOpen } = usePopover();\n const initialPosition = getInitialPosition(side);\n const portalContainer = usePortalContainer();\n const anchorCtx = React.useContext(PopoverAnchorContext);\n\n return (\n \n {isOpen ? (\n \n \n \n \n {children}\n \n \n \n \n ) : null}\n \n );\n}\n\ntype PopoverAnchorProps = {\n asChild?: boolean;\n children?: React.ReactNode;\n render?: React.ReactElement;\n};\n\nfunction PopoverAnchor({ asChild, children, render }: PopoverAnchorProps) {\n const ctx = React.useContext(PopoverAnchorContext);\n\n const setRef = React.useCallback(\n (node: Element | null) => {\n if (ctx) {\n ctx.anchorRef.current = node;\n ctx.setHasAnchor(Boolean(node));\n }\n },\n [ctx]\n );\n\n const renderedChild =\n render && children !== undefined ? React.cloneElement(render, undefined, children) : render;\n const child = renderedChild ?? (asChild && React.isValidElement(children) ? children : undefined);\n\n if (child) {\n return (\n }>\n {child}\n \n );\n }\n\n return (\n
}>\n {children}\n
\n );\n}\n\nexport {\n Popover,\n PopoverAnchor,\n type PopoverAnchorProps,\n PopoverContent,\n type PopoverContentProps,\n type PopoverContextType,\n type PopoverProps,\n PopoverTrigger,\n type PopoverTriggerProps,\n usePopover,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/radio-group/index.tsx", "content": "import { Radio as RadioPrimitive } from '@base-ui/react/radio';\nimport { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group';\nimport { Circle } from 'lucide-react';\nimport { AnimatePresence, type HTMLMotionProps, motion, type Transition } from 'motion/react';\nimport React from 'react';\n\nimport { cn, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\n// Radix RadioGroup supported an `orientation` prop; Base UI's RadioGroup does not\n// declare one. Preserve the public API by accepting it and forwarding as\n// `aria-orientation` + data attribute.\ntype RadioGroupOrientation = 'vertical' | 'horizontal';\n\ntype RadioGroupProps = Omit, 'onValueChange'> &\n SharedProps & {\n orientation?: RadioGroupOrientation;\n onValueChange?: (value: string) => void;\n transition?: Transition;\n };\n\nfunction RadioGroup(allProps: RadioGroupProps) {\n const { className, orientation = 'vertical', testId, onValueChange, ...props } = allProps;\n\n const handleValueChange = React.useMemo(() => {\n if (!onValueChange) {\n return;\n }\n return (next: unknown) => onValueChange(next as string);\n }, [onValueChange]);\n\n // Radix parity: when consumers explicitly pass `value` (controlled mode) but\n // their source-of-truth starts as `undefined` (e.g. react-hook-form\n // `field.value` before the first change), Base UI's `useControlled` warns on\n // the undefined → string transition. Radix tolerated this silently. Normalize\n // undefined → '' only when `value` was explicitly passed — uncontrolled mode\n // via `defaultValue` (without `value`) keeps working unchanged.\n const hasValueProp = 'value' in allProps;\n const valueOverride = hasValueProp && allProps.value === undefined ? { value: '' } : undefined;\n\n return (\n \n );\n}\n\ntype RadioGroupIndicatorProps = React.ComponentProps & {\n transition: Transition;\n};\n\nfunction RadioGroupIndicator({ className, transition, ...props }: RadioGroupIndicatorProps) {\n return (\n \n \n \n \n \n \n \n );\n}\n\ntype RadioGroupItemProps = React.ComponentProps &\n HTMLMotionProps<'button'> &\n SharedProps & {\n transition?: Transition;\n variant?: 'card' | 'default';\n };\n\nfunction RadioGroupItem({\n children,\n className,\n transition = { duration: 0.15 },\n testId,\n variant = 'default',\n ...props\n}: RadioGroupItemProps) {\n return (\n )}\n nativeButton\n // biome-ignore lint/suspicious/noExplicitAny: Base UI render merges Root attrs for the consumer element\n render={(rootProps: Record, state: { checked?: boolean; disabled?: boolean }) => (\n \n {children}\n {variant === 'card' ? (\n \n \n \n ) : (\n \n )}\n \n )}\n />\n );\n}\n\nexport {\n RadioGroup,\n RadioGroupIndicator,\n type RadioGroupIndicatorProps,\n RadioGroupItem,\n type RadioGroupItemProps,\n type RadioGroupProps,\n};\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/select/index.tsx", "content": "'use client';\n\nimport { Select as SelectPrimitive } from '@base-ui/react/select';\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';\nimport React from 'react';\n\nimport { useGroup } from '@/registry/base-nova/protoform/components/group';\nimport { usePortalContainer } from '@/registry/base-nova/protoform/hooks/use-portal-container';\nimport { narrowOpenChange, renderWithDataState } from '@/registry/base-nova/protoform/lib/base-ui-compat';\nimport { cn, type PortalContentProps, type SharedProps } from '@/registry/base-nova/protoform/lib/utils';\n\ntype SelectRootProps = Omit<\n React.ComponentProps,\n 'defaultValue' | 'onOpenChange' | 'onValueChange' | 'value'\n> &\n SharedProps & {\n defaultValue?: string | null;\n onOpenChange?: (open: boolean) => void;\n onValueChange?: (value: string | null) => void;\n value?: string | null;\n };\n\n// Base UI types `value` as `unknown` because `Select.Root` is generic; the\n// registry pins it to `string | null`, so validate at the boundary.\nfunction adaptSelectValueChange(\n handler: ((value: string | null) => void) | undefined\n): ((value: unknown) => void) | undefined {\n if (!handler) {\n return;\n }\n return (value) => {\n if (value !== null && typeof value !== 'string') {\n throw new TypeError('Select values must be strings or null.');\n }\n handler(value);\n };\n}\n\nfunction Select({ testId, onOpenChange, onValueChange, ...props }: SelectRootProps) {\n return (\n \n );\n}\n\nSelect.displayName = 'Select';\n\nfunction SelectGroup({ testId, ...props }: React.ComponentProps & SharedProps) {\n return ;\n}\n\nSelectGroup.displayName = 'SelectGroup';\n\ntype SelectValueProps = Omit, 'children'> & {\n placeholder?: React.ReactNode;\n children?: React.ReactNode | ((value: unknown) => React.ReactNode);\n};\n\n// Base UI only resolves an item's label after the popup has mounted. Until then\n// it stringifies the raw value, which flashes `1` instead of `Any` for\n// enum-backed selects with a controlled value. Pass a render-prop child or an\n// `items` map on `