/** * FormRenderer — renders a FormSchema as evidence-backed attributes. * * Each attribute registers itself with the rect registry under * `kind="field"` and the field's `id`, so the SVG visual guide can * draw curves from the active attribute to its linked evidence card and * on to the source highlight. * * CE-WP-0007-T10/T11: add/edit flows use FieldDefinitionForm. * CE-WP-0010: user-facing "Attributes" vocabulary, `Key (type)` labels, * and a ≥3-character filter next to the caption. */ import { useMemo, useRef, useState, type ChangeEvent, type CSSProperties, } from "react"; import type { EvidenceTarget } from "@citation-evidence/engine/shared"; import { FieldDefinitionForm, type FieldType } from "./FieldDefinitionForm"; import { useActiveState, type ActiveState } from "./state/active"; import { useRegisterRect } from "./visual-guide/react-hooks"; const FILTER_MIN_CHARS = 3; function isFieldActive(state: ActiveState, fieldId: string): boolean { return ( state.activeTarget?.targetType === "form-field" && state.activeTarget?.targetId === fieldId ); } /** Case-insensitive substring match; empty / short queries match everything. */ 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 FormFieldSchema { readonly type: "text" | "textarea" | "date"; readonly id: string; readonly label: string; } export interface FormSchema { readonly id: string; readonly title: string; readonly fields: readonly FormFieldSchema[]; } export interface FieldDefinitionPatch { readonly label: string; readonly type: FieldType; } 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, }; function FieldRow({ field, value, linkCount, linkHint, isActive, isEditing, editLabel, editType, onChange, onFocus, onBeginEdit, onChangeEditLabel, onChangeEditType, onSaveEdit, onCancelEdit, }: { field: FormFieldSchema; value: string; linkCount: number; linkHint?: string; isActive: boolean; isEditing: boolean; editLabel: string; editType: FieldType; onChange: (next: string) => void; onFocus: () => void; onBeginEdit: () => void; onChangeEditLabel: (next: string) => void; onChangeEditType: (next: FieldType) => void; onSaveEdit: () => void; onCancelEdit: () => void; }) { const ref = useRef(null); useRegisterRect("field", field.id, ref); if (isEditing) { return (
); } const sharedProps = { id: `field-${field.id}`, value, onFocus, onChange: (e: ChangeEvent) => onChange(e.target.value), style: { width: "100%", boxSizing: "border-box" as const, fontSize: 13, padding: 4 }, }; return (
{field.type === "textarea" ? (