@meistrari/minuta-react 1.2.0 → 1.4.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 +70 -7
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +6 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ um `Blob` de `.docx`. Buscar o documento, persistir a edição e baixar o arquiv
|
|
|
35
35
|
| `chrome` | — | `{ header, footer, headerScope, footerScope, showPageNumber }` |
|
|
36
36
|
| `variables` | — | catálogo `{ key, label, description?, group? }[]` → liga o modo template |
|
|
37
37
|
| `uploadImage` | — | `(file) => Promise<{ src, vaultSrc } \| null>` |
|
|
38
|
+
| `imageFallback` | `'dataUri'` | `'none'` = upload falhou → não insere nada, só `onImageUploadError` |
|
|
39
|
+
| `onImageUploadError` | — | `(error, file) => void` |
|
|
38
40
|
| `toolbar` | `true` | `false` = você monta a sua sobre `ref.current.tiptap` |
|
|
39
41
|
| `blockClickWhileEditing` | `false` | dispara `onBlockClick` também em edição (padrão: só em leitura) |
|
|
40
42
|
| `onChange` | — | `(html) => void`, a cada edição |
|
|
@@ -71,6 +73,28 @@ const t = editor.current?.tiptap
|
|
|
71
73
|
<MinutaEditor ref={editor} html={html} toolbar={false} onTransaction={rerender} />
|
|
72
74
|
```
|
|
73
75
|
|
|
76
|
+
### Numerar parágrafo
|
|
77
|
+
|
|
78
|
+
O número dos parágrafos da peça ("12. A Ré…") é um nó próprio, não texto:
|
|
79
|
+
continua a sequência do template e sai como numeração nativa no Word. Com a
|
|
80
|
+
toolbar do pacote há o botão **1.**; numa toolbar própria:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { hasLeadingTopicNumber, toggleTopicNumber } from '@meistrari/minuta-react'
|
|
84
|
+
|
|
85
|
+
// `editor.current` é null até o mount: proteja a leitura no render
|
|
86
|
+
const tiptap = editor.current?.tiptap
|
|
87
|
+
<Button type={tiptap && hasLeadingTopicNumber(tiptap) ? 'primary' : 'default'}
|
|
88
|
+
onClick={() => tiptap && toggleTopicNumber(tiptap)}>1.</Button>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
(`toggleTopicNumber` equivale a `tiptap.chain().focus().toggleTopicNumber().run()`;
|
|
92
|
+
há também `setTopicNumber()` e `unsetTopicNumber()`.) Enter no fim de um
|
|
93
|
+
parágrafo numerado já cria o seguinte numerado, sem botão. **Não** use a
|
|
94
|
+
"lista numerada" (`toggleOrderedList`) para isso: ela cria uma lista com recuo
|
|
95
|
+
próprio, fora da sequência da peça.
|
|
96
|
+
|
|
97
|
+
|
|
74
98
|
Os tipos de `chain().toggleBold()` viajam com o pacote — não precisa importar
|
|
75
99
|
`@tiptap/*` no seu código.
|
|
76
100
|
|
|
@@ -81,13 +105,21 @@ de conta no defesa.ai:
|
|
|
81
105
|
|
|
82
106
|
```
|
|
83
107
|
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, … }
|
|
108
|
+
POST /data/case/:id/editions { name, html } → cria a edição → { id, updatedAt, contentHash, … }
|
|
109
|
+
PATCH /data/case/:id/editions/:id → salva → { updatedAt, updatedBy, contentHash, … }
|
|
86
110
|
{ html, baseUpdatedAt } 400 sem o token; 409 se outro salvou antes
|
|
111
|
+
GET /data/case/:id/editions → [{ id, updatedAt, updatedBy, contentHash, … }] sem html
|
|
87
112
|
POST /data/case/:id/editions/:id/versions → marco no histórico (o PATCH NÃO cria versão)
|
|
113
|
+
{ comment?, baseUpdatedAt? } com baseUpdatedAt: 409 se a revisão mudou; já versionada → 200 reused
|
|
88
114
|
POST /data/case/:id/docx → .docx do estado atual (a edição, se existir)
|
|
89
115
|
```
|
|
90
116
|
|
|
117
|
+
`contentHash` é o sha256 do `html` exatamente como foi gravado (o servidor não
|
|
118
|
+
normaliza): num save que deu timeout, compare com o hash do que você enviou
|
|
119
|
+
pela listagem, sem baixar o documento. `baseUpdatedAt` no `POST …/versions`
|
|
120
|
+
amarra o checkpoint à revisão que você viu: a versão nunca sai com o seu nome
|
|
121
|
+
e o texto que outra pessoa salvou depois, e repetir a chamada não duplica.
|
|
122
|
+
|
|
91
123
|
```tsx
|
|
92
124
|
// abrir
|
|
93
125
|
const doc = await api.get(`/case/${caseId}/html`)
|
|
@@ -175,20 +207,51 @@ Sobre o fluxo de "enviar variáveis de volta": com uma edição viva o documento
|
|
|
175
207
|
modelo do defesa.ai), ou você mantém os dois vivos com `setVariableValue` +
|
|
176
208
|
salva a edição. Decisão de UX do seu app; as duas funcionam.
|
|
177
209
|
|
|
210
|
+
### Voltar ao checklist sem perder a edição
|
|
211
|
+
|
|
212
|
+
Esqueceu uma tese ou variável no meio da edição? Mude o caso (`PATCH
|
|
213
|
+
/data/case/:id`), busque o documento que o checklist gera agora e aplique sobre
|
|
214
|
+
o texto editado — só o que mudou entra, e trecho que o advogado editou vira
|
|
215
|
+
pergunta em vez de ser sobrescrito:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
const { html } = await api.get(`/data/case/${caseId}/html?source=blocks`)
|
|
219
|
+
const result = editor.current!.applyChecklist(html)
|
|
220
|
+
for (const c of result.conflicts) // 'replace' | 'remove' — a UI é sua
|
|
221
|
+
editor.current!.resolveChecklistConflict(c.blockId, 'keep' /* ou 'apply' */)
|
|
222
|
+
// DEPOIS das decisões: bloco inserido ou aceito pode trazer imagem vault://
|
|
223
|
+
await editor.current!.refreshVaultImages(resolver)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Para isso funcionar a edição precisa nascer marcada: depois de montar o editor
|
|
227
|
+
com o HTML do checklist, antes do primeiro save, chame
|
|
228
|
+
`editor.current!.stampGenerated()`.
|
|
229
|
+
|
|
178
230
|
### Imagens inseridas pelo advogado
|
|
179
231
|
|
|
180
232
|
`uploadImage` devolve `{ src, vaultSrc }`: o editor grava a tag com o `src`
|
|
181
|
-
(presignado, expira) e `data-vault-src` (a `vault://`, durável).
|
|
182
|
-
|
|
233
|
+
(presignado, expira) e `data-vault-src` (a `vault://`, durável). Persista o
|
|
234
|
+
`getHTML()` como está: o `POST /data/case/:id/docx` usa o `data-vault-src` e
|
|
235
|
+
resolve `vault://` sozinho. Ao reabrir uma edição salva, renove os links antes
|
|
236
|
+
de mostrar:
|
|
183
237
|
|
|
184
238
|
```tsx
|
|
185
239
|
// resolver: POST /data/vault/download-url { vaultUrl } → { downloadUrl } (pelo seu backend)
|
|
186
240
|
await editor.current!.refreshVaultImages(vaultUrl => api.post('/vault/download-url', { vaultUrl }).then(r => r.downloadUrl))
|
|
187
241
|
```
|
|
188
242
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
243
|
+
Renova tanto `data-vault-src` quanto imagens salvas só com `src="vault://…"`
|
|
244
|
+
(que ganham o `data-vault-src`), mantendo a referência durável. Não entra no
|
|
245
|
+
undo nem dispara `onChange`: chame `getHTML()` depois se guarda uma cópia. O
|
|
246
|
+
`exportDocx()` do browser baixa pelo `src`, então renove antes de exportar.
|
|
247
|
+
|
|
248
|
+
Se o upload falhar (ou `uploadImage` devolver `null`), o padrão é embutir a
|
|
249
|
+
imagem em base64. Um app cujo documento canônico depende de `vault://` não
|
|
250
|
+
quer isso: `imageFallback="none"` não insere nada e `onImageUploadError` recebe
|
|
251
|
+
o erro e o arquivo para você mostrar e deixar tentar de novo.
|
|
252
|
+
|
|
253
|
+
`setVariableImage(id, src, vaultSrc)` grava o `data-vault-src` junto — use a
|
|
254
|
+
forma de três argumentos para a imagem sobreviver à expiração do `src`.
|
|
192
255
|
|
|
193
256
|
## Next.js (App Router)
|
|
194
257
|
|
package/dist/index.d.mts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import * as react from 'react';
|
|
19
19
|
import { CSSProperties } from 'react';
|
|
20
20
|
import { MinutaMargins, MinutaHeader, MinutaChromeText, MinutaVariableCatalog, MinutaEditorOptions, MinutaVariableRef, MinutaEditor as MinutaEditor$1 } from '@meistrari/minuta-editor';
|
|
21
|
-
export { MinutaChromeText, MinutaEditor as MinutaEditorInstance, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, extractTemplateVariables, resolveTemplateVariables } from '@meistrari/minuta-editor';
|
|
21
|
+
export { MinutaChromeText, MinutaEditor as MinutaEditorInstance, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, extractTemplateVariables, hasLeadingTopicNumber, resolveTemplateVariables, toggleTopicNumber } from '@meistrari/minuta-editor';
|
|
22
22
|
|
|
23
23
|
interface MinutaEditorProps {
|
|
24
24
|
/**
|
|
@@ -32,6 +32,9 @@ interface MinutaEditorProps {
|
|
|
32
32
|
chrome?: Partial<MinutaChromeText>;
|
|
33
33
|
variables?: MinutaVariableCatalog;
|
|
34
34
|
uploadImage?: MinutaEditorOptions['uploadImage'];
|
|
35
|
+
/** `'dataUri'` (default) embute em base64 quando o upload falha; `'none'` não insere e avisa por `onImageUploadError` */
|
|
36
|
+
imageFallback?: MinutaEditorOptions['imageFallback'];
|
|
37
|
+
onImageUploadError?: MinutaEditorOptions['onImageUploadError'];
|
|
35
38
|
/** barra de formatação do pacote (default: true). `false` = monte a sua sobre `ref.tiptap` */
|
|
36
39
|
toolbar?: boolean;
|
|
37
40
|
showBlockOrigin?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import * as react from 'react';
|
|
19
19
|
import { CSSProperties } from 'react';
|
|
20
20
|
import { MinutaMargins, MinutaHeader, MinutaChromeText, MinutaVariableCatalog, MinutaEditorOptions, MinutaVariableRef, MinutaEditor as MinutaEditor$1 } from '@meistrari/minuta-editor';
|
|
21
|
-
export { MinutaChromeText, MinutaEditor as MinutaEditorInstance, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, extractTemplateVariables, resolveTemplateVariables } from '@meistrari/minuta-editor';
|
|
21
|
+
export { MinutaChromeText, MinutaEditor as MinutaEditorInstance, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, extractTemplateVariables, hasLeadingTopicNumber, resolveTemplateVariables, toggleTopicNumber } from '@meistrari/minuta-editor';
|
|
22
22
|
|
|
23
23
|
interface MinutaEditorProps {
|
|
24
24
|
/**
|
|
@@ -32,6 +32,9 @@ interface MinutaEditorProps {
|
|
|
32
32
|
chrome?: Partial<MinutaChromeText>;
|
|
33
33
|
variables?: MinutaVariableCatalog;
|
|
34
34
|
uploadImage?: MinutaEditorOptions['uploadImage'];
|
|
35
|
+
/** `'dataUri'` (default) embute em base64 quando o upload falha; `'none'` não insere e avisa por `onImageUploadError` */
|
|
36
|
+
imageFallback?: MinutaEditorOptions['imageFallback'];
|
|
37
|
+
onImageUploadError?: MinutaEditorOptions['onImageUploadError'];
|
|
35
38
|
/** barra de formatação do pacote (default: true). `false` = monte a sua sobre `ref.tiptap` */
|
|
36
39
|
toolbar?: boolean;
|
|
37
40
|
showBlockOrigin?: boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { forwardRef, useRef, useLayoutEffect, useEffect, useImperativeHandle, createElement } from 'react';
|
|
3
3
|
import { createMinutaEditor } from '@meistrari/minuta-editor';
|
|
4
|
-
export { extractTemplateVariables, resolveTemplateVariables } from '@meistrari/minuta-editor';
|
|
4
|
+
export { extractTemplateVariables, hasLeadingTopicNumber, resolveTemplateVariables, toggleTopicNumber } from '@meistrari/minuta-editor';
|
|
5
5
|
|
|
6
6
|
const MinutaEditor = forwardRef((props, ref) => {
|
|
7
7
|
const host = useRef(null);
|
|
@@ -21,6 +21,8 @@ const MinutaEditor = forwardRef((props, ref) => {
|
|
|
21
21
|
variables: props.variables,
|
|
22
22
|
// lido na hora do upload, não no mount: o host pode trocar a função
|
|
23
23
|
uploadImage: (file) => callbacks.current.uploadImage ? callbacks.current.uploadImage(file) : Promise.resolve(null),
|
|
24
|
+
imageFallback: props.imageFallback,
|
|
25
|
+
onImageUploadError: (error, file) => callbacks.current.onImageUploadError?.(error, file),
|
|
24
26
|
toolbar: props.toolbar,
|
|
25
27
|
showBlockOrigin: props.showBlockOrigin,
|
|
26
28
|
blockClickWhileEditing: props.blockClickWhileEditing,
|
|
@@ -64,6 +66,9 @@ const MinutaEditor = forwardRef((props, ref) => {
|
|
|
64
66
|
useEffect(() => {
|
|
65
67
|
instance.current?.setBlockClickWhileEditing(props.blockClickWhileEditing === true);
|
|
66
68
|
}, [props.blockClickWhileEditing]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
instance.current?.setImageFallback(props.imageFallback ?? "dataUri");
|
|
71
|
+
}, [props.imageFallback]);
|
|
67
72
|
useImperativeHandle(ref, () => instance.current);
|
|
68
73
|
return createElement("div", {
|
|
69
74
|
ref: host,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/minuta-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test:pack": "node scripts/verify-pack.mjs"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@meistrari/minuta-editor": "^1.
|
|
31
|
+
"@meistrari/minuta-editor": "^1.7.0",
|
|
32
32
|
"@tiptap/core": "2.11.9",
|
|
33
33
|
"@tiptap/extension-blockquote": "2.11.9",
|
|
34
34
|
"@tiptap/extension-bold": "2.11.9",
|