@meistrari/minuta-react 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,13 +36,16 @@ um `Blob` de `.docx`. Buscar o documento, persistir a edição e baixar o arquiv
36
36
  | `variables` | — | catálogo `{ key, label, description?, group? }[]` → liga o modo template |
37
37
  | `uploadImage` | — | `(file) => Promise<{ src, vaultSrc } \| null>` |
38
38
  | `toolbar` | `true` | `false` = você monta a sua sobre `ref.current.tiptap` |
39
+ | `blockClickWhileEditing` | `false` | dispara `onBlockClick` também em edição (padrão: só em leitura) |
39
40
  | `onChange` | — | `(html) => void`, a cada edição |
40
41
  | `onTransaction` | — | qualquer transação, inclusive só de seleção — para toolbar própria |
41
42
  | `onReady`, `onEditChrome`, `onBlockClick`, `onError` | — | |
42
43
 
43
44
  O `ref` entrega a instância do editor: `getHTML()`, `exportDocx()` (→ `Blob`),
44
45
  `usedVariables()`, `setEditable()`, `setChrome()`, `tiptap` (a instância do
45
- Tiptap — escotilha de fuga), `root`/`container`/`sheet` (os elementos).
46
+ Tiptap — escotilha de fuga), `root`/`container`/`sheet` (os elementos) — e a
47
+ ponte formulário ↔ documento: `scrollToVariable()`, `setVariableValue()`,
48
+ `setVariableImage()`, `variablesInDocument()` (abaixo).
46
49
 
47
50
  ### Por que o `html` é lido uma vez
48
51
 
@@ -77,10 +80,12 @@ Tudo pelo seu backend, com `x-api-key` + `x-org-id` — seus usuários não prec
77
80
  de conta no defesa.ai:
78
81
 
79
82
  ```
80
- GET /data/case/:id/html → { html, chrome, margins, source, editionId }
81
- POST /data/case/:id/editions { name, html } → cria a edição (o documento lapidado)
82
- PATCH /data/case/:id/editions/:editionId { html } → salva (versão automática)
83
- POST /data/case/:id/docx → .docx do estado atual (a edição, se existir)
83
+ GET /data/case/:id/html → { html, chrome, margins, source, editionId, variablesMarked, markedVariables }
84
+ POST /data/case/:id/editions { name, html } → cria a edição → { id, updatedAt, … }
85
+ PATCH /data/case/:id/editions/:id → salva → { updatedAt, updatedBy, … }
86
+ { html, baseUpdatedAt } 400 sem o token; 409 se outro salvou antes
87
+ POST /data/case/:id/editions/:id/versions → marco no histórico (o PATCH NÃO cria versão)
88
+ POST /data/case/:id/docx → .docx do estado atual (a edição, se existir)
84
89
  ```
85
90
 
86
91
  ```tsx
@@ -88,15 +93,19 @@ POST /data/case/:id/docx → .docx do estado atual (a e
88
93
  const doc = await api.get(`/case/${caseId}/html`)
89
94
  <MinutaEditor key={caseId} html={doc.html} chrome={doc.chrome ?? undefined} margins={doc.margins ?? undefined} onChange={setHtml} />
90
95
 
91
- // salvar (debounce do seu lado)
92
- await api.patch(`/case/${caseId}/editions/${editionId}`, { html })
96
+ // salvar (debounce do seu lado): baseUpdatedAt = o updatedAt da última resposta
97
+ const saved = await api.patch(`/case/${caseId}/editions/${editionId}`, { html, baseUpdatedAt })
98
+ baseUpdatedAt = saved.updatedAt
93
99
 
94
100
  // baixar — fiel ao preview, gerado no browser
95
101
  const blob = await editor.current!.exportDocx()
96
102
  ```
97
103
 
98
- Mande `x-user-email` / `x-user-name` nas chamadas de edição para a autoria
99
- ficar registrada.
104
+ Mande `x-user-email` / `x-user-name` em toda chamada feita em nome de alguém:
105
+ nas rotas de edição viram `createdBy` (criação), `updatedBy` (último save de
106
+ conteúdo), `createdBy` de cada versão e `deletedBy` (descarte); no
107
+ `PATCH /data/case/:id` viram `updatedByEmail`/`updatedByName` do case. Sem os
108
+ headers a autoria fica `null` — nunca "a integração".
100
109
 
101
110
  ## Formulário ↔ documento
102
111
 
@@ -106,8 +115,9 @@ e o editor trata a marca como nó atômico. Isso dá as duas direções sem casa
106
115
  texto (que erra quando o valor coincide com algo que já existia no template):
107
116
 
108
117
  ```tsx
109
- // documento → formulário: clicou no trecho, foca o campo
110
- <MinutaEditor ref={editor} html={html} onVariableClick={v => focusField(v.variableId)} />
118
+ // documento → formulário: clicou no trecho, foca o campo. `kind` diz o que foi
119
+ // clicado: 'text' (chip), 'image' (<img> marcada) ou 'missingImage' (aviso)
120
+ <MinutaEditor ref={editor} html={html} onVariableClick={v => focusField(v.variableId, v.kind)} />
111
121
 
112
122
  // formulário → documento: focou o campo, rola até o trecho
113
123
  editor.current!.scrollToVariable('nome_cliente')
@@ -123,12 +133,63 @@ edição criada a partir de HTML já resolvido (sem marcas) a ponte não existe,
123
133
  é melhor saber antes de montar a tela do que descobrir com
124
134
  `variablesInDocument()` vazio.
125
135
 
136
+ ### O que é marcado, e com que id
137
+
138
+ | no template | no HTML | id da marca |
139
+ |---|---|---|
140
+ | `<variable name="comarca">` | `<span data-variable-id data-parent-block-id>` (chip) | `comarca` |
141
+ | variável `compute` | idem — o valor é calculado antes do render | o id da compute |
142
+ | `<for item="c" in="contratos"> <variable name="c.numero">` | um chip por linha, com `data-variable-index` | `contratos[0].numero`, `contratos[1].numero`… |
143
+ | `<img variable="img_doc">` com valor | `<img data-variable-id data-parent-block-id>` | `img_doc` |
144
+ | `<img variable="img_doc">` sem valor | aviso `missing-image-warning` (nó `missingImage` no editor) | `img_doc` |
145
+ | `<variable name="valor">` sem valor | aviso vermelho `<span data-variable-id data-parent-block-id data-invalid-variable>` | `valor` |
146
+ | cada bloco do template | `<div data-minuta-block data-block-id data-block-title>` | `data-block-id` = id do bloco no `templateSpec` |
147
+
148
+ O aviso de texto sem valor é uma marca como as outras: entra em
149
+ `markedVariables`, dispara `onVariableClick` (`kind: 'text'`) e
150
+ `setVariableValue` o converte em chip comum ao receber valor. Booleans não
151
+ viram marca: eles decidem se um trecho ou bloco é emitido; a regra fica em
152
+ `displayCondition` e nos `<if condition>` do template (podem envolver várias
153
+ variáveis).
154
+
155
+ ```tsx
156
+ // array: uma linha de cada vez
157
+ editor.current!.setVariableValue('contratos[1].numero', '375586874')
158
+
159
+ // texto sem valor: o aviso vermelho vira chip
160
+ editor.current!.setVariableValue('valor', 'R$ 1.000,00')
161
+
162
+ // imagem: troca a existente, ou transforma o aviso de "faltando" em imagem
163
+ editor.current!.setVariableImage('img_doc', urlAssinada) // → nº de ocorrências
164
+ editor.current!.scrollToVariable('img_faltando') // acha o aviso também
165
+
166
+ // bloco: com blockClickWhileEditing o clique em edição também avisa
167
+ <MinutaEditor blockClickWhileEditing onBlockClick={id => openChecklistBlock(id)} … />
168
+ ```
169
+
126
170
  Sobre o fluxo de "enviar variáveis de volta": com uma edição viva o documento
127
- **é** a edição — o `PATCH` de variáveis no case não re-renderiza a peça (senão
128
- apagaria o texto do advogado). Ou o formulário trava quando a lapidação começa
129
- (o modelo do defesa.ai), ou você mantém os dois vivos com `setVariableValue` +
171
+ **é** a edição — o `PATCH /data/case/:id` de variáveis não re-renderiza a peça
172
+ (senão apagaria o texto do advogado). A API aceita a mudança e devolve
173
+ `activeEdition` (`{ id, name, updatedAt }` da edição que ficou como estava, ou
174
+ `null`) para o app decidir: ou o formulário trava quando a lapidação começa (o
175
+ modelo do defesa.ai), ou você mantém os dois vivos com `setVariableValue` +
130
176
  salva a edição. Decisão de UX do seu app; as duas funcionam.
131
177
 
178
+ ### Imagens inseridas pelo advogado
179
+
180
+ `uploadImage` devolve `{ src, vaultSrc }`: o editor grava a tag com o `src`
181
+ (presignado, expira) e `data-vault-src` (a `vault://`, durável). Ao reabrir uma
182
+ edição salva, renove os links antes de mostrar:
183
+
184
+ ```tsx
185
+ // resolver: POST /data/vault/download-url { vaultUrl } → { downloadUrl } (pelo seu backend)
186
+ await editor.current!.refreshVaultImages(vaultUrl => api.post('/vault/download-url', { vaultUrl }).then(r => r.downloadUrl))
187
+ ```
188
+
189
+ Não entra no undo nem dispara `onChange`. O `exportDocx()` do browser baixa
190
+ pelo `src`, então renove antes de exportar também; o `POST /data/case/:id/docx`
191
+ resolve `vault://` sozinho.
192
+
132
193
  ## Next.js (App Router)
133
194
 
134
195
  O entry publicado carrega `'use client'`: dá para importar o componente direto
package/dist/index.d.mts CHANGED
@@ -35,6 +35,8 @@ interface MinutaEditorProps {
35
35
  /** barra de formatação do pacote (default: true). `false` = monte a sua sobre `ref.tiptap` */
36
36
  toolbar?: boolean;
37
37
  showBlockOrigin?: boolean;
38
+ /** dispara `onBlockClick` também em edição (padrão: só em leitura) */
39
+ blockClickWhileEditing?: boolean;
38
40
  onReady?: MinutaEditorOptions['onReady'];
39
41
  /** o documento mudou por edição — recebe o HTML atual */
40
42
  onChange?: (html: string) => void;
package/dist/index.d.ts CHANGED
@@ -35,6 +35,8 @@ interface MinutaEditorProps {
35
35
  /** barra de formatação do pacote (default: true). `false` = monte a sua sobre `ref.tiptap` */
36
36
  toolbar?: boolean;
37
37
  showBlockOrigin?: boolean;
38
+ /** dispara `onBlockClick` também em edição (padrão: só em leitura) */
39
+ blockClickWhileEditing?: boolean;
38
40
  onReady?: MinutaEditorOptions['onReady'];
39
41
  /** o documento mudou por edição — recebe o HTML atual */
40
42
  onChange?: (html: string) => void;
package/dist/index.mjs CHANGED
@@ -23,6 +23,7 @@ const MinutaEditor = forwardRef((props, ref) => {
23
23
  uploadImage: (file) => callbacks.current.uploadImage ? callbacks.current.uploadImage(file) : Promise.resolve(null),
24
24
  toolbar: props.toolbar,
25
25
  showBlockOrigin: props.showBlockOrigin,
26
+ blockClickWhileEditing: props.blockClickWhileEditing,
26
27
  onReady: (tiptap) => callbacks.current.onReady?.(tiptap),
27
28
  onChange: () => callbacks.current.onChange?.(editor.getHTML()),
28
29
  onTransaction: () => callbacks.current.onTransaction?.(),
@@ -60,6 +61,9 @@ const MinutaEditor = forwardRef((props, ref) => {
60
61
  useEffect(() => {
61
62
  instance.current?.setShowBlockOrigin(props.showBlockOrigin !== false);
62
63
  }, [props.showBlockOrigin]);
64
+ useEffect(() => {
65
+ instance.current?.setBlockClickWhileEditing(props.blockClickWhileEditing === true);
66
+ }, [props.blockClickWhileEditing]);
63
67
  useImperativeHandle(ref, () => instance.current);
64
68
  return createElement("div", {
65
69
  ref: host,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/minuta-react",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Componente React do editor de minuta do defesa.ai — casca fina sobre @meistrari/minuta-editor",
5
5
  "repository": {
6
6
  "type": "git",