# SchemaForms — LLMs.txt > Este arquivo descreve tudo que um LLM precisa saber para implementar, estender e integrar > o monorepo **@schema-forms-data** corretamente: arquitetura, tipos, APIs, padrões de código, > e receitas prontas de uso. --- ## 1. Visão Geral SchemaForms é um monorepo que separa **definição** (JSON/TypeScript) de formulários de sua **renderização** (React). Um formulário é descrito como um `FormSchema` — um objeto tipado — que pode ser exibido via `` ou construído visualmente via ``. **Pacotes:** | Pacote | Responsabilidade | Deps principais | |--------|-----------------|-----------------| | `@schema-forms-data/core` | Tipos TypeScript, enums, `generateId()` | nenhuma | | `@schema-forms-data/templates` | CSS vars de tema, `TemplateProvider`, preset-blocks | core | | `@schema-forms-data/renderer` | ``, hooks, validação, 24+ campos | react-hook-form, lucide-react, zod | | `@schema-forms-data/builder` | Drag-and-drop editor, ``, conversores | @dnd-kit, lucide-react, radix-ui, tailwind | | `@schema-forms-data/react` | Meta-pacote que re-exporta tudo | todos acima | --- ## 2. Schema — Estrutura de dados Um formulário é um **`FormSchema`**, que aninha `FormStep` → `FormContainer` → `FormField`. ```ts // packages/core/src/types/formSchema.ts interface FormSchema { id: string; nome: string; descricao?: string; eventoId?: string | null; status: FormSchemaStatus; // 'rascunho' | 'ativo' | 'inativo' template?: string | null; // ID do template visual, ex: "moderno" steps: FormStep[]; version?: number; } interface FormStep { id: string; titulo: string; descricao?: string; icone?: string; ordem: number; showLabel?: boolean; containers: FormContainer[]; } interface FormContainer { id: string; nome?: string; titulo: string; descricao?: string; icone?: string; ordem: number; colunas?: 1 | 2 | 3 | 4; // colunas internas dos campos tamanho?: number; // largura 1-12 na grade de 12 cols do step inicioColuna?: number; // coluna de início (1-12) showLabel?: boolean; showAsCard?: boolean; repeatable?: boolean; // container repetível (array de registros) minItems?: number; maxItems?: number; itemLabel?: string; condicional?: FieldConditionalExpr; campos: FormField[]; } interface FormField { id: string; nome: string; // chave no objeto de valores do formulário label: string; tipo: FieldType; obrigatorio: boolean; tamanho: number; // colunas ocupadas (1-12) inicioColuna?: number; ordem: number; placeholder?: string; hint?: string; defaultValue?: string | number | boolean; opcoes?: FieldOption[]; // para select, radio, checkbox_group, autocomplete validacao?: FieldValidation; // regras built-in validate?: FieldValidatorConfig[]; // validators customizados (bloqueiam submit) warn?: FieldValidatorConfig[]; // validators de aviso (não bloqueiam) condicional?: FieldConditionalExpr; // condição de exibição setValues?: ConditionalSetValue[]; // preenche outros campos quando condição ativa mascara?: MaskType; mascaraCustom?: string; locked?: boolean; // campo de preset — nome/tipo não editáveis no builder isReadOnly?: boolean; isDisabled?: boolean; clearedValue?: string | number | boolean | null; // valor ao ocultar por condicional initialValue?: string | number | boolean; resolvePropsKey?: string; // chave para FieldResolvers dinâmicos subSchema?: SubFormSchema; // para tipo sub_form subFields?: FormField[]; // para tipo field_array itemLabel?: string; minItems?: number; maxItems?: number; addLabel?: string; step?: number; // para slider minValue?: number; maxValue?: number; maxRating?: number; // para rating dateRangeStartLabel?: string; dateRangeEndLabel?: string; } ``` ### 2.1 Enum FieldType (todos os valores) ```ts enum FieldType { TEXTO = 'texto', TEXTAREA = 'textarea', NUMBER = 'number', EMAIL = 'email', PASSWORD = 'password', TELEFONE = 'telefone', CPF = 'cpf', CEP = 'cep', DATE = 'date', DATETIME = 'datetime', TIME = 'time', DATE_RANGE = 'date_range', SELECT = 'select', AUTOCOMPLETE = 'autocomplete', RADIO = 'radio', CHECKBOX = 'checkbox', CHECKBOX_GROUP = 'checkbox_group', SWITCH = 'switch', SLIDER = 'slider', RATING = 'rating', COLOR = 'color', FILE = 'file', HIDDEN = 'hidden', FIELD_ARRAY = 'field_array', // array de sub-objetos PARTICIPATION_TYPE = 'participation_type', PAYMENT_METHOD = 'payment_method', TERMS = 'terms', SUB_FORM = 'sub_form', } ``` ### 2.2 Validação built-in ```ts interface FieldValidation { minLength?: number; maxLength?: number; min?: number; max?: number; regex?: string; regexMessage?: string; minDate?: string; // ISO date string maxDate?: string; fileTypes?: string[]; // MIME types aceitos maxFileSize?: number; // bytes minAge?: number; maxAge?: number; } ``` ### 2.3 Condicionais Uma condição pode ser uma **folha** ou um **grupo AND/OR** recursivo: ```ts // Folha interface FieldConditional { campoId: string; operador: 'igual' | 'diferente' | 'contem' | 'naoContem' | 'vazio' | 'naoVazio' | 'maiorQue' | 'menorQue' | 'maiorOuIgual' | 'menorOuIgual'; valor?: string | number | boolean; source?: 'campo' | 'evento'; // 'evento' lê de externalData } // Grupo interface FieldConditionGroup { and?: FieldConditionalExpr[]; or?: FieldConditionalExpr[]; } type FieldConditionalExpr = FieldConditional | FieldConditionGroup; ``` Avaliação via `evaluateFieldCondition(cond, values, externalData)`: - `source: 'evento'` → lê de `externalData[campoId]` - Grupos AND/OR são recursivos - Retorna `true` se `cond` é `undefined` --- ## 3. FormRenderer — Renderizador ### 3.1 Instalação e uso mínimo ```bash npm install @schema-forms-data/renderer react-hook-form lucide-react ``` ```tsx import { FormRenderer } from "@schema-forms-data/renderer"; import type { FormSchema } from "@schema-forms-data/core"; const schema: FormSchema = { id: "meu-form", nome: "Cadastro", status: "ativo", steps: [ { id: "step-1", titulo: "Dados", ordem: 0, containers: [ { id: "c-1", titulo: "Informações", ordem: 0, campos: [ { id: "nome", nome: "nome", label: "Nome completo", tipo: "texto", obrigatorio: true, tamanho: 12, ordem: 0, }, ], }, ], }, ], }; export default function App() { return ( { console.log(values); // { nome: "..." } }} /> ); } ``` ### 3.2 Props completas do FormRenderer ```ts interface FormRendererProps { schema: FormSchema; // OBRIGATÓRIO initialValues?: Record; initialStep?: number; onSubmitStep?: (stepIndex: number, data: Record) => Promise; onComplete?: (data: Record) => Promise; template?: string | null; // ID do template, ex: "moderno" formTitle?: string; externalData?: Record; // dados externos para interpolação e condicionais fieldErrors?: Record; // erros externos por nome de campo uploadFile?: (file: File, fieldName: string) => Promise; cepLookup?: (cep: string) => Promise; resolveTermsUploadUrl?: (uploadId: string) => Promise; fieldResolvers?: FieldResolvers; validatorMapper?: ValidatorMapper; componentMapper?: ComponentMapper; onValuesChange?: (values: Record) => void; className?: string; StepIndicator?: React.ComponentType; } ``` ### 3.3 ComponentMapper — Substituir componentes de campo Para substituir o componente de um tipo de campo com implementação própria: ```tsx import { useController } from "react-hook-form"; import type { ComponentMapper, FieldComponentProps } from "@schema-forms-data/renderer"; function MeuTextField({ name, control, field }: FieldComponentProps) { const { field: ctrl, fieldState } = useController({ name, control }); return (
{fieldState.error && {fieldState.error.message}}
); } const componentMapper: ComponentMapper = { texto: MeuTextField, email: MeuTextField, // qualquer FieldType ou string customizada }; ``` `ComponentMapper` tem **prioridade máxima** — sobrescreve todos os componentes padrão. ### 3.4 ValidatorMapper — Validators customizados ```ts type FieldValidatorFn = ( value: unknown, allValues: Record, config: FieldValidatorConfig, externalData: Record, ) => string | undefined | Promise; type ValidatorMapper = Record; ``` No schema, campo `validate` ou `warn`: ```json { "validate": [ { "type": "meu_validador", "message": "Valor inválido", "min": 10 } ] } ``` No renderer: ```tsx const validatorMapper: ValidatorMapper = { meu_validador: async (value, allValues, config, externalData) => { if (Number(value) < config.min) return config.message ?? "Valor muito baixo"; return undefined; }, }; ``` `validate[]` bloqueia submit. `warn[]` exibe aviso mas **não bloqueia**. ### 3.5 FieldResolvers — Props dinâmicas por campo ```ts type FieldResolver = ( field: FormField, formValues: Record, externalData: Record, ) => Partial; type FieldResolvers = Record; ``` Conecte um campo via `resolvePropsKey`: ```json { "resolvePropsKey": "minhaChave" } ``` ```tsx const fieldResolvers: FieldResolvers = { minhaChave: (field, values, external) => ({ opcoes: external.minhasOpcoes ?? [], isDisabled: !values.outroField, }), }; ``` ### 3.6 Template vars — Interpolação dinâmica Labels, placeholders, hints e valores padrão aceitam `{{evento.xxx}}` ou `{{event.xxx}}`: ```json { "label": "Inscrição para {{evento.nome}}", "defaultValue": "Evento em {{evento.localEvento}}" } ``` Passe os dados via `externalData`: ```tsx ``` ### 3.7 CEP lookup ```tsx { const res = await fetch(`https://viacep.com.br/ws/${cep}/json/`); const data = await res.json(); return { logradouro: data.logradouro, bairro: data.bairro, cidade: data.localidade, estado: data.uf, erro: !!data.erro, }; }} /> ``` Campos com `tipo: 'cep'` disparam o lookup automaticamente e preenchem campos irmãos pelo `nome`: `logradouro`, `bairro`, `cidade`, `estado`. ### 3.8 Upload de arquivo ```tsx { const formData = new FormData(); formData.append("file", file); const res = await fetch("/api/upload", { method: "POST", body: formData }); const { url } = await res.json(); return url; // string URL retornada }} /> ``` ### 3.9 StepIndicator customizado ```tsx interface StepIndicatorProps { steps: Array<{ id: string; label?: string; icone?: string }>; currentStep: number; onStepClick?: (index: number) => void; } function MeuIndicador({ steps, currentStep, onStepClick }: StepIndicatorProps) { return (
{steps.map((s, i) => ( ))}
); } ``` --- ## 4. Hooks do Renderer Todos os hooks abaixo devem ser usados **dentro** de um ``. ### 4.1 useFormApi ```ts interface FormApi { change: (name: string, value: unknown) => void; submit: () => void; reset: (values?: Record) => void; getState: () => FormStateSnapshot; } const api = useFormApi(); api.change("nome", "João"); api.submit(); api.reset({ nome: "" }); ``` ### 4.2 useFormState ```ts interface FormStateValue { values: Record; errors: Record; warnings: Record; dirty: boolean; valid: boolean; submitting: boolean; } const state = useFormState(); console.log(state.values.nome); ``` ### 4.3 useField ```ts interface FieldState { value: unknown; error?: string; warning?: string; dirty: boolean; touched: boolean; valid: boolean; } const fieldState = useField("nome"); ``` ### 4.4 useFieldApi ```ts interface FieldApiReturn { input: { name: string; value: unknown; onChange: ...; onBlur: ...; ref: ... }; meta: { error?: string; warning?: string; touched: boolean; dirty: boolean; valid: boolean }; } const { input, meta } = useFieldApi("nome"); return ; ``` ### 4.5 FormSpy ```tsx import { FormSpy } from "@schema-forms-data/renderer"; // Padrão 1 — render prop
{JSON.stringify(state.values, null, 2)}
} /> // Padrão 2 — children como função {(state) => {state.valid ? "✓" : "×"}} // Padrão 3 — callback console.log(state.values)} /> ``` --- ## 5. Templates visuais ### 5.1 Templates disponíveis `moderno`, `minimalista`, `card`, `banner`, `acampamento`, `acampamento_imersivo`, `corporativo`, `festival`, `webinar`, `retiro`, `conferencia`, `workshop`, `social`, `gala` ### 5.2 Aplicar template ```tsx ``` Ou no `BuilderProvider`: ```tsx ``` ### 5.3 TemplateProvider manual ```tsx import { TemplateProvider, getTemplateConfig } from "@schema-forms-data/templates"; const config = getTemplateConfig("corporativo"); {/* qualquer componente que use useTemplate() */} ``` ### 5.4 CSS vars injetadas pelo TemplateProvider ```css --t-primary /* cor primária */ --t-primary-hover /* hover da primária */ --t-accent /* cor de destaque */ --t-bg /* fundo geral */ --t-surface /* fundo de cards/containers */ --t-text /* texto principal */ --t-text-muted /* texto secundário */ --t-border /* bordas */ --t-error /* vermelho de erro */ ``` --- ## 6. Builder visual ### 6.1 Uso mínimo ```tsx import { BuilderProvider, Canvas, ConfigPanel, Palette, } from "@schema-forms-data/builder"; export default function BuilderPage() { return ( console.log(schema)}>
); } ``` ### 6.2 Props do BuilderProvider ```ts interface BuilderProviderProps { initialConfigs?: Record; initialContainers?: ContainersTree; initialTemplateId?: string; schemaId?: string; uploadTermsPdf?: (file: File, schemaId: string) => Promise; onSchemaChange?: (schema: FormSchema) => void; } ``` ### 6.3 Carregar schema existente no builder ```tsx import { formSchemaToBuilder, BuilderProvider } from "@schema-forms-data/builder"; const { dndState, configs, templateId } = formSchemaToBuilder(meuFormSchema); ``` ### 6.4 Exportar schema a partir do builder ```tsx import { useBuilder, builderToFormSchema } from "@schema-forms-data/builder"; function ExportButton() { const { containers, configs, previewTemplateId } = useBuilder(); const handleExport = () => { const schema = builderToFormSchema( { containers }, // DndState — passa containers dentro do objeto configs, { id: "meu-form", nome: "Meu Formulário", status: "ativo", template: previewTemplateId, } ); console.log(JSON.stringify(schema, null, 2)); }; return ; } ``` ### 6.5 Hook useBuilder ```ts const { // Estado containers, // ContainersTree — árvore de containers DnD configs, // Record selectedId, // ID do item selecionado no canvas showPreview, // boolean previewTemplateId, // string | null schemaId, // string | undefined canUndo, // boolean canRedo, // boolean // Leitura getConfig, // (id: string) => ItemConfig getAllFieldIds, // () => string[] // Seleção setSelected, // (id: string | null) => void // Ações de mutação addItem, // (componentType, containerId, atIndex?) => void addPresetBlock, // (block: PresetBlock, stepId: string) => void addPresetStepBlock, // (block: PresetStepBlock) => void moveCanvasItem, // (itemId, from, to, atIndex?) => void reorderInContainer, // (containerId, newChildren: string[]) => void removeItem, // (id: string) => void moveItem, // (id: string, direction: 'up' | 'down') => void updateConfig, // (id: string, partial: Partial) => void setShowPreview, // (v: boolean) => void setPreviewTemplateId, // (id: string | null) => void undo, // () => void redo, // () => void pushSnapshot, // () => void — salva snapshot manual no histórico } = useBuilder(); ``` > **Nota:** `containers` é a mesma árvore que `DndState.containers`. Para passar ao `builderToFormSchema`, envolva: `builderToFormSchema({ containers }, configs, meta)`. ### 6.6 ItemConfig — configuração interna de cada nó do builder ```ts interface ItemConfig { label: string; name?: string; // nome do campo (chave no objeto de valores) description?: string; placeholder?: string; hint?: string; required?: boolean; fieldType?: string; // FieldType string defaultValue?: string | number | boolean; columns?: 1 | 2 | 3 | 4; tamanho?: number; inicioColuna?: number; showLabel?: boolean; showAsCard?: boolean; descricao?: string; icone?: string; mascara?: string; mascaraCustom?: string; validation?: ItemValidation; options?: FieldOption[]; condition?: ItemCondition; locked?: boolean; visualStyle?: 'default' | 'card'; subFields?: Array<{ id: string; label: string; name: string; type: string; required?: boolean; placeholder?: string; options?: FieldOption[]; }>; itemLabel?: string; minItems?: number; maxItems?: number; addLabel?: string; subSchemaTitle?: string; subSchemaFields?: FormField[]; termoTexto?: string; termoPdfUrl?: string; termoPdfUploadId?: string; isReadOnly?: boolean; isDisabled?: boolean; clearedValue?: string | number | boolean | null; initialValue?: string | number | boolean; resolvePropsKey?: string; validate?: FieldValidatorConfig[]; warn?: FieldValidatorConfig[]; step?: number; minValue?: number; maxValue?: number; maxRating?: number; dateRangeStartLabel?: string; dateRangeEndLabel?: string; } ``` --- ## 7. Preset Blocks Blocos pré-configurados prontos para arrastar na paleta do builder. ### 7.1 Preset Blocks disponíveis (containers) | ID | Nome | Campos | |----|------|--------| | `preset_personal` | Dados Pessoais | nome, email, telefone, cpf, data_nascimento | | `preset_emergency_array` | Contatos de Emergência (array) | nome, parentesco, telefone | | `preset_emergency` | Contatos de Emergência | nome, parentesco, telefone | | `preset_address` | Endereço | cep, logradouro, numero, complemento, bairro, cidade, estado | | `preset_responsible` | Responsável | responsavel_nome, parentesco, telefone, cpf | | `preset_health` | Informações de Saúde | tipo_sanguineo, plano_saude, alergias, medicamentos, obs | | `preset_participation` | Participação | restricao_alimentar, tamanho_camiseta, observacoes | | `preset_payment` | Informações de Pagamento | forma_pagamento, comprovante_pagamento | | `preset_participation_type` | Tipo de Participação | participation_type (locked) | ### 7.2 Preset Step Blocks disponíveis (múltiplos steps) | ID | Nome | Conteúdo | |----|------|---------| | `preset_steps_participation_payment` | Participação + Pagamento | Step 1: tipo_participacao · Step 2: forma_pagamento | ### 7.3 Usar preset programaticamente ```tsx import { useBuilder } from "@schema-forms-data/builder"; import { getPresetById } from "@schema-forms-data/templates"; function AddPersonalBlock({ stepId }: { stepId: string }) { const { addPresetBlock } = useBuilder(); const handleAdd = () => { const preset = getPresetById("preset_personal"); if (preset) addPresetBlock(preset, stepId); }; return ; } ``` ### 7.4 Inserir preset diretamente no schema (sem builder) ```ts import { PRESET_BLOCKS } from "@schema-forms-data/templates"; import { generateId } from "@schema-forms-data/core"; function buildPresetContainer(presetId: string): FormContainer { const preset = PRESET_BLOCKS.find(b => b.id === presetId)!; return { ...preset.containerTemplate, id: generateId(), campos: preset.containerTemplate.campos.map(f => ({ ...f, id: generateId() })), }; } ``` --- ## 8. Conversores builder ↔ schema ### 8.1 builderToFormSchema ```ts import { builderToFormSchema } from "@schema-forms-data/builder"; // containers vem de useBuilder() ou de dndState.containers const schema: FormSchema = builderToFormSchema( { containers }, // DndState — envolva containers no objeto configs, // Record { id: "form-id", nome: "Nome do formulário", status: "ativo", template: "moderno", } ); ``` Percurso: `root → steps (por ordem) → containers (por ordem) → campos (por ordem)`. Condensadores internos: - `ItemCondition → FieldConditionalExpr` (suporta `extraConditions` com AND/OR) - `ItemConfig → FormField` (mapeamento completo de todas as props) - Containers `repeatable = true` preservados como `repeatable` ### 8.2 formSchemaToBuilder ```ts import { formSchemaToBuilder } from "@schema-forms-data/builder"; const { dndState, configs, templateId } = formSchemaToBuilder(schema); // Retorna: // dndState.containers — Record // configs — Record // templateId — string | null (do schema.template) ``` --- ## 9. Árquitetura interna — ContainerRenderer e grade O `ContainerRenderer` renderiza campos em uma **grade de 12 colunas CSS**: ```html
``` `FieldWidget` faz dispatch para o componente certo conforme `field.tipo`. Se `componentMapper[field.tipo]` existe, usa ele; senão cai no padrão. Antes de renderizar, cada campo passa por: 1. `evaluateFieldCondition` — se falso, campo não é renderizado e seu valor é limpo (`clearedValue`) 2. `interpolateField` — substitui `{{evento.xxx}}` com `externalData` 3. `fieldResolvers[resolvePropsKey]` — mescla props dinâmicas --- ## 10. Árvore do builder (DndState) ```ts interface DndState { containers: Record; draggingElement?: string | null; } ``` Hierarquia especial de IDs: - `"root"` — sempre existe; `children` são IDs de steps - IDs com prefixo `step_` → nó de step - IDs com prefixo `container_` → nó de container - IDs com prefixo `field_` → nó de campo - IDs com prefixo `field_array_` → campo array (também tem entrada em `containers` para sub-campos) ```ts // Exemplo de estrutura { "root": { children: ["step_abc"] }, "step_abc": { children: ["container_xyz"] }, "container_xyz": { children: ["field_name", "field_email"] }, } ``` `getItemType(id)` detecta o tipo pelo prefixo. `generateId()` gera UUID v4 (ou fallback se `crypto.randomUUID` indisponível). --- ## 11. Formulários multi-step — fluxo 1. `FormRenderer` mantém `currentStep: number` e `allValues: Record`. 2. Cada step é um `
` independente com `react-hook-form` (`FormProvider`). 3. Ao submeter um step: - `makeStepResolver` valida todos os campos do step. - `onSubmitStep(stepIndex, stepValues)` é chamado (await). - `allValues` é atualizado com os valores do step. - Se não for o último: avança para `currentStep + 1`. - Se for o último: chama `onComplete(allValues)`. 4. Campos ocultos por condicional têm seu valor zerado para `clearedValue` (ou `undefined`). 5. `setValues` é disparado reativamente quando a condição do campo-gatilho muda de falso para verdadeiro. --- ## 12. Validação — camadas ``` ┌────────────────────────────────────────────────────────────┐ │ Camada 1 — Built-in (formResolver.ts) │ │ required, minLength, maxLength, min, max, minDate, │ │ maxDate, minAge, maxAge, regex, email, CPF, CEP, │ │ TELEFONE, fileTypes, maxFileSize, CHECKBOX, SWITCH, │ │ SLIDER, RATING, DATE_RANGE, PARTICIPATION_TYPE, │ │ CHECKBOX_GROUP, field_array │ ├────────────────────────────────────────────────────────────┤ │ Camada 2 — ValidatorMapper (customizado pelo usuário) │ │ field.validate[] → pipeline assíncrono │ │ Retorna string de erro ou undefined │ ├────────────────────────────────────────────────────────────┤ │ Camada 3 — Warnings (warn[]) │ │ Mesmo pipeline do validate[], mas não bloqueia submit. │ │ Exibidos como avisos visuais no campo. │ └────────────────────────────────────────────────────────────┘ ``` Implementado via **Zod `superRefine` assíncrono** dentro de `makeStepResolver`. --- ## 13. Classes CSS dos componentes de campo O renderer usa prefixo `sfr-*` para suas classes CSS próprias. Componentes de campo usam Tailwind CSS internamente. As variáveis `--t-*` do template são aplicadas via `style` no `TemplateProvider`. --- ## 14. Padrões de nomeamento - IDs de campos no schema: UUIDs gerados por `generateId()` — ex: `"3a8e1f2c-..."` - `nome` do campo: identificador legível em camelCase ou snake_case — ex: `"nomeCompleto"`, `"data_nascimento"` - `nome` é a chave no objeto `values` retornado pelo formulário - Templates: slugs em kebab-case — `"moderno"`, `"acampamento_imersivo"` - Presets: prefixo `preset_` — `"preset_personal"`, `"preset_address"` --- ## 15. Tipos especiais de campo ### field_array Array de sub-objetos. Cada item contém os campos de `subFields`. ```json { "id": "uuid", "nome": "contatos", "label": "Contatos", "tipo": "field_array", "obrigatorio": false, "tamanho": 12, "ordem": 0, "itemLabel": "Contato", "minItems": 1, "maxItems": 5, "addLabel": "Adicionar contato", "subFields": [ { "id": "uuid-1", "nome": "nome", "label": "Nome", "tipo": "texto", "obrigatorio": true, "tamanho": 6, "ordem": 0 }, { "id": "uuid-2", "nome": "telefone", "label": "Telefone", "tipo": "telefone", "obrigatorio": true, "tamanho": 6, "ordem": 1 } ] } ``` ### participation_type Campo que interage com `payment_method`. Armazena JSON: ```json { "tipo": "todos_os_dias", "data": null, "genero": null } ``` ### payment_method Campo que lê `allValues['tipo_participacao']` para computar `valorTotal`. Armazena JSON: ```json { "metodo": "pix", "valorTotal": 15000 } ``` ### terms Exibe um termos e condições com checkbox de aceite. - `termoTexto`: texto markdown inline - `termoPdfUrl`: URL pública de PDF - `termoPdfUploadId`: ID para resolver via `resolveTermsUploadUrl` --- ## 16. Exemplo completo — formulário de evento ```tsx import { FormRenderer } from "@schema-forms-data/renderer"; import type { FormSchema } from "@schema-forms-data/core"; const schema: FormSchema = { id: "inscricao-evento", nome: "Inscrição no Evento", status: "ativo", template: "moderno", steps: [ { id: "step-dados", titulo: "Dados Pessoais", ordem: 0, containers: [ { id: "c-pessoais", titulo: "Informações pessoais", ordem: 0, colunas: 2, campos: [ { id: "f-nome", nome: "nome", label: "Nome completo", tipo: "texto", obrigatorio: true, tamanho: 12, ordem: 0 }, { id: "f-email", nome: "email", label: "E-mail", tipo: "email", obrigatorio: true, tamanho: 6, ordem: 1 }, { id: "f-tel", nome: "telefone", label: "Telefone", tipo: "telefone", obrigatorio: true, tamanho: 6, ordem: 2 }, { id: "f-cpf", nome: "cpf", label: "CPF", tipo: "cpf", obrigatorio: true, tamanho: 6, ordem: 3 }, { id: "f-nasc", nome: "data_nascimento", label: "Data de nascimento", tipo: "date", obrigatorio: true, tamanho: 6, ordem: 4, validacao: { minAge: 18 }, }, ], }, ], }, { id: "step-endereco", titulo: "Endereço", ordem: 1, containers: [ { id: "c-end", titulo: "Endereço", ordem: 0, campos: [ { id: "f-cep", nome: "cep", label: "CEP", tipo: "cep", obrigatorio: true, tamanho: 4, ordem: 0 }, { id: "f-rua", nome: "logradouro", label: "Rua", tipo: "texto", obrigatorio: false, tamanho: 8, ordem: 1 }, { id: "f-num", nome: "numero", label: "Número", tipo: "texto", obrigatorio: true, tamanho: 3, ordem: 2 }, { id: "f-comp", nome: "complemento", label: "Complemento", tipo: "texto", obrigatorio: false, tamanho: 5, ordem: 3 }, { id: "f-bai", nome: "bairro", label: "Bairro", tipo: "texto", obrigatorio: false, tamanho: 4, ordem: 4 }, { id: "f-cid", nome: "cidade", label: "Cidade", tipo: "texto", obrigatorio: false, tamanho: 6, ordem: 5 }, { id: "f-est", nome: "estado", label: "Estado", tipo: "select", obrigatorio: false, tamanho: 2, ordem: 6, opcoes: [ { valor: "SP", label: "SP" }, { valor: "RJ", label: "RJ" }, { valor: "MG", label: "MG" }, { valor: "RS", label: "RS" }, ], }, ], }, ], }, { id: "step-termos", titulo: "Termos", ordem: 2, containers: [ { id: "c-termos", titulo: "Confirme sua inscrição", ordem: 0, campos: [ { id: "f-termos", nome: "aceite_termos", label: "Termos e condições", tipo: "terms", obrigatorio: true, tamanho: 12, ordem: 0, termoTexto: "Ao se inscrever você concorda com as regras do evento.", }, ], }, ], }, ], }; export default function InscricaoPage() { return ( { const r = await fetch(`https://viacep.com.br/ws/${cep}/json/`); const d = await r.json(); return { logradouro: d.logradouro, bairro: d.bairro, cidade: d.localidade, estado: d.uf }; }} onSubmitStep={async (stepIndex, data) => { // salvar rascunho por step se necessário }} onComplete={async (values) => { const res = await fetch("/api/inscricao", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(values), }); if (!res.ok) throw new Error("Falha ao inscrever"); }} /> ); } ``` --- ## 17. Exemplo completo — builder com export ```tsx import { BuilderProvider, Canvas, ConfigPanel, Palette, useBuilder, builderToFormSchema, } from "@schema-forms-data/builder"; function SaveButton({ schemaId }: { schemaId: string }) { const { containers, configs, previewTemplateId } = useBuilder(); const save = async () => { const schema = builderToFormSchema({ containers }, configs, { id: schemaId, nome: "Formulário", status: "rascunho", template: previewTemplateId, }); await fetch("/api/schemas", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(schema), }); }; return ; } export default function PageBuilder() { return ( { // retorna URL do PDF armazenado return "/uploads/termos.pdf"; }} onSchemaChange={(schema) => { console.log("Schema atualizado:", schema); }} >
); } ``` --- ## 18. Condicionais — exemplos práticos ### Campo visível somente se outro campo tem valor específico ```json { "condicional": { "campoId": "tipo_participante", "operador": "igual", "valor": "menor" } } ``` ### Campo visível se campo NÃO está vazio ```json { "condicional": { "campoId": "email", "operador": "naoVazio" } } ``` ### Condição composta (AND) ```json { "condicional": { "and": [ { "campoId": "idade", "operador": "maiorOuIgual", "valor": 18 }, { "campoId": "aceito_termos", "operador": "igual", "valor": "true" } ] } } ``` ### Condição baseada em dado externo (evento) ```json { "condicional": { "campoId": "evento.permiteMenores", "operador": "igual", "valor": "true", "source": "evento" } } ``` ### setValues — preencher campos ao ativar condição ```json { "condicional": { "campoId": "usa_plano", "operador": "igual", "valor": "false" }, "setValues": [{ "campo": "plano_saude", "valor": "Não informado" }] } ``` --- ## 19. Geração de ID ```ts import { generateId } from "@schema-forms-data/core"; const id = generateId(); // UUID v4 — funciona em browser e Node ``` --- ## 20. Dependências externas ### Renderer - `react-hook-form` — gerenciamento de formulário - `zod` — validação via resolver customizado - `lucide-react` — ícones nos campos e step indicator - `date-fns` — validações de data (interno) ### Builder - `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities` — drag and drop - `@radix-ui/*` — primitivos de UI (Popover, Select, Dialog, etc.) - `tailwindcss` — estilos - `clsx`, `tailwind-merge` — utilitário de classes CSS - `lucide-react` — ícones --- ## 21. Guia rápido por caso de uso | Objetivo | O que usar | |----------|-----------| | Renderizar schema existente | `` | | Construir schema visualmente | `` + `` + `` + `` | | Converter schema para builder | `formSchemaToBuilder(schema)` | | Converter builder para schema | `builderToFormSchema({ containers }, configs, meta)` | | Substituir componente de campo | `componentMapper` prop do `FormRenderer` | | Adicionar validação customizada | `validatorMapper` + `validate[]` no campo | | Adicionar aviso (não bloqueia) | `validatorMapper` + `warn[]` no campo | | Props dinâmicas no campo | `fieldResolvers` + `resolvePropsKey` no campo | | Buscar CEP automaticamente | `cepLookup` prop do `FormRenderer` | | Upload de arquivo | `uploadFile` prop do `FormRenderer` | | Observar valores do formulário | `` ou `useFormState()` | | Mudar valor via código | `useFormApi().change(name, value)` | | Tema visual | `template` prop ou `TemplateProvider` | | Template vars nos labels | `externalData` + sintaxe `{{evento.xxx}}` | | Inserir bloco pré-pronto | `addPresetBlock(preset, stepId)` no builder | | Gerar ID único | `generateId()` do core | --- ## 22. DnD Setup — integrar `DndContext` com o builder O `BuilderProvider` **não inclui** o `DndContext` internamente. Você precisa criar o contexto e implementar os handlers de arrasto. ### Estrutura ``` DndContext ← você controla (sensors + handlers) └── BuilderProvider ├── Palette ← itens draggable (fonte) ├── Canvas ← zonas de drop └── ConfigPanel ``` ### Os três formatos de `active.data.current` | Formato | Quando | Payload | |---|---|---| | `isPaletteItem: true` | Arrastando campo da Palette | `{ isPaletteItem: true, componentType: "text" }` | | `isPalettePreset: true` | Arrastando bloco preset | `{ isPalettePreset: true, presetId: "preset_personal" }` | | `isCanvasItem: true` | Movendo item no canvas | `{ isCanvasItem: true, type: "step"\|"field", id, fromContainer }` | ### IDs das zonas de drop (`over.id`) - `"root"` — área raiz, para steps - `"zone-"` — dentro de um step, para campos/containers - `""` — sobre um campo existente (reordenação) ### Implementação mínima do `handleDragEnd` ```tsx const handleDragEnd = ({ active, over }: DragEndEvent) => { if (!over) return; const data = active.data.current as Record; const overId = String(over.id); if (data?.isPalettePreset) { if (overId.startsWith("zone-")) addPresetBlock(preset, overId.slice(5)); return; } if (data?.isPaletteItem) { const target = overId === "root" ? "root" : overId.startsWith("zone-") ? overId.slice(5) : null; if (target) addItem(data.componentType as string, target); return; } if (data?.isCanvasItem) { const { type, id, fromContainer } = data as { type: string; id: string; fromContainer: string }; if (type === "step") { const kids = containers.root?.children ?? []; const oldIdx = kids.indexOf(id), newIdx = kids.indexOf(overId); if (oldIdx !== -1 && newIdx !== -1 && oldIdx !== newIdx) reorderInContainer("root", arrayMove(kids, oldIdx, newIdx)); return; } if (type === "field") { let toContainer = fromContainer; let targetField: string | null = null; if (overId.startsWith("zone-")) { toContainer = overId.slice(5); } else { const parent = findParentContainer(overId, containers); if (parent) { toContainer = parent; targetField = overId; } } if (toContainer === fromContainer && targetField) { const kids = containers[fromContainer]?.children ?? []; const oldIdx = kids.indexOf(id), newIdx = kids.indexOf(targetField); if (oldIdx !== -1 && newIdx !== -1 && oldIdx !== newIdx) reorderInContainer(fromContainer, arrayMove(kids, oldIdx, newIdx)); } else if (toContainer !== fromContainer) { const idx = targetField ? (containers[toContainer]?.children ?? []).indexOf(targetField) : undefined; moveCanvasItem(id, fromContainer, toContainer, idx ?? undefined); } } } }; ``` ### Por que `DragOverlay` é obrigatório O canvas usa `overflow: auto`. Sem `DragOverlay` (que renderiza via portal no `body`), o ghost do drag é cortado pelas bordas de scroll. Use `dropAnimation={null}` para desativar a animação de retorno. ```tsx {activeDrag && } ```