/** * FormRenderer — evidence-backed attributes (CE-WP-0010/0012). */ import { useMemo, useRef, useState, type ChangeEvent, type CSSProperties, } from "react"; import type { EvidenceTarget } from "@citation-evidence/engine/shared"; import { DEFAULT_CURRENCY, displayTypeLabel, formatOptionsText, normalizeAttributeType, parseAmountValue, parseOptionsText, parseSomeOfValue, serializeAmountValue, serializeSomeOfValue, typeNeedsOptions, type AttributeOption, type FormFieldSchema, type FormSchema, } from "./attribute-types"; import { FieldDefinitionForm, type FieldType } from "./FieldDefinitionForm"; import { useActiveState, type ActiveState } from "./state/active"; import { useRegisterRect } from "./visual-guide/react-hooks"; export type { AttributeOption, AttributeValueType, FormFieldSchema, FormSchema, } from "./attribute-types"; export { isAttributeValueType, normalizeFormSchema, normalizeAttributeType, parseAmountValue, serializeAmountValue, } from "./attribute-types"; const FILTER_MIN_CHARS = 3; function isFieldActive(state: ActiveState, fieldId: string): boolean { return ( state.activeTarget?.targetType === "form-field" && state.activeTarget?.targetId === fieldId ); } export function matchesTextFilter( query: string, parts: readonly (string | undefined | null)[], minChars = FILTER_MIN_CHARS, ): boolean { const q = query.trim(); if (q.length < minChars) return true; const needle = q.toLowerCase(); return parts.some((p) => p != null && String(p).toLowerCase().includes(needle)); } export interface FieldDefinitionPatch { readonly label: string; readonly type: FieldType; readonly options?: readonly AttributeOption[]; readonly defaultCurrency?: string; } export interface FormRendererProps { readonly schema: FormSchema; readonly values?: Readonly>; readonly onValueChange?: (fieldId: string, value: string) => void; readonly linkCounts?: Readonly>; readonly linkHints?: Readonly>; readonly showAddFieldForm?: boolean; readonly onRequestAddField?: () => void; readonly onConfirmAddField?: (patch: FieldDefinitionPatch) => void; readonly onCancelAddField?: () => void; readonly editingFieldId?: string | null; readonly onBeginEditField?: (fieldId: string) => void; readonly onSaveFieldEdit?: (fieldId: string, patch: FieldDefinitionPatch) => void; readonly onCancelFieldEdit?: () => void; } const iconButtonStyle: CSSProperties = { fontSize: 11, padding: "2px 6px", background: "white", border: "1px solid #888", borderRadius: 3, cursor: "pointer", lineHeight: 1, }; const controlStyle: CSSProperties = { width: "100%", boxSizing: "border-box", fontSize: 13, padding: 4, }; function ValueControl({ field, value, onChange, onFocus, }: { field: FormFieldSchema; value: string; onChange: (next: string) => void; onFocus: () => void; }) { const type = normalizeAttributeType(field.type); const id = `field-${field.id}`; if (type === "text-long") { return (