/** * The string field renderer. A plain string renders as a text input (its * `type` derived from any JSON-Schema `format`); an expression-annotated schema * renders the INJECTED expression door (a resting textarea with the visual-editor * button) when the host supplies one; a media-annotated schema renders the upload * control instead; and when the form supplies a completion provider, the input is * backed by argument autocomplete. * * NO JQ EDGE. This module — and every module under `schema-form`, and the package * barrel above them — names no jq type and imports `@tai42/jq-studio` neither * statically NOR dynamically. * The jq authoring door drags a heavy subgraph (the xyflow visual editor, a Web * Worker entry, and a multi-megabyte wasm engine), and a bundler EMITS that whole * subgraph for a dynamic import just as it does for a static one — `lazy` defers * the FETCH, never the emission. A consumer that bundles the SDK to render forms * would still ship the chunks, the worker file, and the wasm it can never run. * So the door arrives from the host instead, through * {@link ExpressionFieldContext} (or the form's `expressionField` prop), and an * annotated field with no injected door falls back to the plain string input. */ import type { ReactNode } from 'react'; import { Suspense, useCallback, useContext, useMemo, useState } from 'react'; import { CompletionInput } from '../components/completion-input'; import { Field } from '../components/field'; import { AlertTriangleIcon } from '../components/icons'; import { Textarea, TextInput } from '../components/inputs'; import { errorMessage } from '../errors'; import type { ExpressionFieldComponent, ExpressionInputShape } from './context'; import { CompletionProviderContext, ExpressionFieldContext } from './context'; import type { DateConstraints, ExpressionAnnotation, MediaUpload } from './field-model'; import { MediaField } from './media-field'; import type { CompletionProvider } from './SchemaForm'; import type { FormInputMode } from './types'; export function StringField({ heading, description, error, format, inputMode, multiline, maxLength, minLength, media, expression, date, argName, value, required, onChange, }: { heading: string; description: string | undefined; error: string | undefined; format: string | undefined; inputMode: FormInputMode | undefined; multiline: boolean; maxLength: number | undefined; minLength: number | undefined; media: MediaUpload | undefined; expression: ExpressionAnnotation | undefined; date: DateConstraints | undefined; argName: string; value: unknown; required: boolean; onChange: (value: unknown) => void; }): ReactNode { const control = stringControl(format, inputMode); const completionProvider = useContext(CompletionProviderContext); const expressionField = useContext(ExpressionFieldContext); const current = typeof value === 'string' ? value : ''; // An empty optional field emits `undefined` (drops the key); a required one // keeps the empty string so validation can flag it. const emit = (next: string): void => { onChange(next === '' && !required ? undefined : next); }; // An expression-annotated field renders the injected door — the resting control // plus its visual-editor button. Checked before `media` because the expression // annotation is the more specific, deliberate opt-in (`x-tai42-expression`); // a schema carrying both is contradictory, and an expression is text, not an // upload. Not reachable together with the completion provider path: an // expression is authored, not completed from server suggestions. // // With no door injected the annotation has no effect: the field falls through to the // ordinary paths below, exactly as a malformed annotation does, so a form // renders the same bytes either way. if (expression !== undefined && expressionField !== undefined) { return ( ); } // A media-annotated field renders the upload control instead of a text box. if (media !== undefined) { return ( ); } return ( {completionProvider !== undefined ? ( ) : multiline ? ( // `control: textarea` draws a multi-line control; the length bounds draw // natively, and a live counter shows the maximum (below).