Declarative form: it takes fields and values and reports changes through onChange. The shape of the form is data, not JSX.
fieldsFromConfigSchema and fieldsFromIntegrationManifest turn them into IngotFieldSpec[].<label> and its own onChange.integer, number, boolean, secret) to behave the same on every screen.space-y; compose IngotFieldInput yourself.IngotField.IngotFieldInput is cheaper outright and you do not have to invent a testIdPrefix.fields: readonly IngotFieldSpec[] — The field descriptors. Array order is on-screen order.values: Record<string, unknown> — Current values keyed by field.key.onChange: (key: string, next: unknown) => void — The form is controlled — the caller owns the state.testIdPrefix: string — An input's data-testid is `${testIdPrefix}-${key}`.renderOptions: IngotFieldInput['renderOptions'] — The picker for options fields. Without it such a field behaves as text.secretPlaceholder: (field: IngotFieldSpec) => string — Translated placeholder for a secret field — the Ingot has no translations.className: string — The form wrapper. Defaults to space-y-4.inputClassName: string — Screens differ in field width, not in form shape.labelClassName: string — The field label. Defaults to block text-sm.