@meistrari/minuta-nuxt 0.1.0 → 1.1.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.
@@ -120,6 +120,54 @@ function absTop(el, sheet) {
120
120
  return top;
121
121
  }
122
122
  const HEADING_TAGS = /* @__PURE__ */ new Set(["H1", "H2", "H3", "H4"]);
123
+ const SPLITTABLE_TAGS = /* @__PURE__ */ new Set(["P"]);
124
+ const MIN_LINES_PER_SIDE = 2;
125
+ function findInlineSplit(view, dom, availablePx, fromLine = 0, minHead = MIN_LINES_PER_SIDE, minTail = MIN_LINES_PER_SIDE) {
126
+ if (!SPLITTABLE_TAGS.has(dom.tagName))
127
+ return null;
128
+ let rects;
129
+ try {
130
+ const range = document.createRange();
131
+ range.selectNodeContents(dom);
132
+ rects = Array.from(range.getClientRects()).filter((r) => r.height > 0 && r.width > 0);
133
+ } catch {
134
+ return null;
135
+ }
136
+ if (rects.length < minHead + minTail)
137
+ return null;
138
+ const lines = [];
139
+ for (const r of rects) {
140
+ const last2 = lines[lines.length - 1];
141
+ if (last2 && r.top < last2.bottom - 1) {
142
+ last2.bottom = Math.max(last2.bottom, r.bottom);
143
+ last2.left = Math.min(last2.left, r.left);
144
+ } else {
145
+ lines.push({ top: r.top, bottom: r.bottom, left: r.left });
146
+ }
147
+ }
148
+ if (lines.length < minHead + minTail)
149
+ return null;
150
+ const scale = dom.offsetHeight > 0 ? dom.getBoundingClientRect().height / dom.offsetHeight : 1;
151
+ if (!Number.isFinite(scale) || scale <= 0)
152
+ return null;
153
+ const originTop = lines[0].top;
154
+ const first = fromLine + minHead;
155
+ const last = lines.length - minTail;
156
+ let cut = -1;
157
+ for (let i = first; i <= last; i++) {
158
+ if ((lines[i].bottom - originTop) / scale > availablePx) {
159
+ cut = i;
160
+ break;
161
+ }
162
+ }
163
+ if (cut < first || cut > last)
164
+ return null;
165
+ const line = lines[cut];
166
+ const found = view.posAtCoords({ left: line.left + 1, top: (line.top + line.bottom) / 2 });
167
+ if (!found)
168
+ return null;
169
+ return { pos: found.pos, usedPx: (lines[cut - 1].bottom - originTop) / scale, line: cut };
170
+ }
123
171
  function pageSafetyPx(contentHeight) {
124
172
  return Math.max(32, Math.round(contentHeight * 0.08));
125
173
  }
@@ -180,10 +228,10 @@ export function computePocBreaks(view, cfg, sheet) {
180
228
  return;
181
229
  const top = absTop(dom, sheet) - baseY - chromeAbove(absTop(dom, sheet));
182
230
  const notes = [];
183
- child.descendants((inner) => {
231
+ child.descendants((inner, innerPos) => {
184
232
  if (inner.type.name === "footnoteRef" && inner.attrs.text) {
185
233
  noteCounter += 1;
186
- notes.push({ n: noteCounter, text: String(inner.attrs.text), size: inner.attrs.size ?? null });
234
+ notes.push({ n: noteCounter, text: String(inner.attrs.text), size: inner.attrs.size ?? null, at: pos + 1 + innerPos });
187
235
  }
188
236
  return true;
189
237
  });
@@ -193,6 +241,7 @@ export function computePocBreaks(view, cfg, sheet) {
193
241
  top,
194
242
  height: dom.offsetHeight,
195
243
  notes,
244
+ dom,
196
245
  // margem superior conta como parte da página que o item abre —
197
246
  // senão a "página" do preview fica mais alta que um A4 real e
198
247
  // o DOCX (página física) divergiria
@@ -217,9 +266,9 @@ export function computePocBreaks(view, cfg, sheet) {
217
266
  let forcedBreakPending = false;
218
267
  let pageNotes = [];
219
268
  const notesH = (notes) => measurer && notes.length ? measurer.height(notes) : 0;
220
- const closePage = (pos, filler) => {
269
+ const closePage = (pos, filler, soft = false) => {
221
270
  const height = notesH(pageNotes);
222
- breaks.push({ pos, filler: Math.max(0, filler - height), pageNo, notes: pageNotes, notesH: height });
271
+ breaks.push({ pos, filler: Math.max(0, filler - height), pageNo, notes: pageNotes, notesH: height, soft });
223
272
  pageNo++;
224
273
  pageNotes = [];
225
274
  };
@@ -244,6 +293,41 @@ export function computePocBreaks(view, cfg, sheet) {
244
293
  pageStart = startWithMargin;
245
294
  forcedBreakPending = false;
246
295
  } else if (item.top > pageStart && itemEnd - pageStart > capacity) {
296
+ let split = findInlineSplit(view, item.dom, capacity - (item.top - pageStart));
297
+ if (split) {
298
+ let pending = [...item.notes];
299
+ let lastLine = split.line;
300
+ while (split) {
301
+ lastLine = split.line;
302
+ const usedEnd = item.top + split.usedPx;
303
+ const cutAt = split.pos;
304
+ const stay = pending.filter((n) => n.at === void 0 || n.at < cutAt);
305
+ pending = pending.filter((n) => !stay.includes(n));
306
+ pageNotes.push(...stay);
307
+ closePage(cutAt, fullH - (usedEnd - pageStart), true);
308
+ pageStart = usedEnd;
309
+ if (itemEnd - pageStart <= contentH - notesH(pending))
310
+ break;
311
+ const fromTop = pageStart - item.top;
312
+ split = findInlineSplit(
313
+ view,
314
+ item.dom,
315
+ fromTop + contentH - notesH(pending),
316
+ split.line,
317
+ 1,
318
+ 1
319
+ );
320
+ if (!split)
321
+ split = findInlineSplit(view, item.dom, fromTop + contentH, lastLine, 1, 1);
322
+ }
323
+ pageNotes.push(...pending);
324
+ forcedBreakPending = false;
325
+ while (item.top + item.height - pageStart > contentH) {
326
+ pageStart += contentH;
327
+ pageNo++;
328
+ }
329
+ continue;
330
+ }
247
331
  let breakIndex = i;
248
332
  if (shouldMoveBreakToHeading({ heading: items[i - 1], item, pageStart, capacity }))
249
333
  breakIndex = i - 1;
@@ -1,3 +1,6 @@
1
+ import { extractTemplateVariables, resolveTemplateVariables } from '../utils/templateVariables.js';
2
+ export type { MinutaVariableDef } from '../utils/templateVariables.js';
3
+ export { extractTemplateVariables, resolveTemplateVariables };
1
4
  export interface MinutaFile {
2
5
  name: string;
3
6
  url: string;
@@ -43,6 +46,8 @@ export interface AnnotationAnchorState {
43
46
  trecho?: string;
44
47
  manual?: boolean;
45
48
  trechoOriginal?: string;
49
+ /** sinônimo de `onde` que já apareceu numa geração fora do contrato */
50
+ secao?: string;
46
51
  }
47
52
  export interface MinutaAnalysis {
48
53
  resumo?: string;
@@ -88,10 +93,14 @@ export declare function useMinutaApi(): {
88
93
  fetchMinutaById: (id: string) => Promise<Minuta>;
89
94
  createMinuta: (input: {
90
95
  name: string;
91
- files: MinutaFile[];
96
+ files?: MinutaFile[];
92
97
  context?: string;
93
98
  agentId?: string;
94
- }) => Promise<Minuta>;
99
+ templateHtml?: string;
100
+ variables?: Record<string, string | null>;
101
+ }) => Promise<Minuta & {
102
+ missingVariables?: string[];
103
+ }>;
95
104
  updateMinuta: (id: string, patch: {
96
105
  name?: string;
97
106
  contentHtml?: string;
@@ -1,4 +1,6 @@
1
1
  import { $fetch } from "ofetch";
2
+ import { extractTemplateVariables, resolveTemplateVariables } from "../utils/templateVariables.js";
3
+ export { extractTemplateVariables, resolveTemplateVariables };
2
4
  export function useMinutaApi() {
3
5
  const base = "/api/minuta";
4
6
  function fetchMinutas() {
@@ -8,7 +10,10 @@ export function useMinutaApi() {
8
10
  return $fetch(`${base}/${encodeURIComponent(id)}`);
9
11
  }
10
12
  function createMinuta(input) {
11
- return $fetch(base, { method: "POST", body: input });
13
+ return $fetch(base, {
14
+ method: "POST",
15
+ body: { files: [], ...input }
16
+ });
12
17
  }
13
18
  function updateMinuta(id, patch) {
14
19
  return $fetch(`${base}/${encodeURIComponent(id)}`, { method: "PATCH", body: patch });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Variáveis de template da minuta — o formato do placeholder e a resolução dele.
3
+ *
4
+ * O template é um documento normal do editor; onde entra um valor há um NÓ
5
+ * atômico (`templateVariable`, ver editorExtensions), serializado como
6
+ * `<span data-minuta-var="CHAVE" data-minuta-var-label="Rótulo">Rótulo</span>`.
7
+ *
8
+ * Por que nó e não `#CHAVE` no texto: no editor rico o texto é fatiado em
9
+ * marks. Negritar metade de um `#PROCESSO` produz
10
+ * `<b>#PRO</b>CESSO`, e qualquer substituição textual passa reto — em silêncio,
11
+ * só naquele template, só naquela linha. Como nó atômico o placeholder não tem
12
+ * "metade": seleciona inteiro, apaga inteiro, formata inteiro.
13
+ *
14
+ * ATENÇÃO: este arquivo é DUPLICADO em
15
+ * `packages/server/src/standalone-minuta/template-variables.ts` — o servidor
16
+ * resolve na criação da minuta e não pode importar o pacote publicado no npm.
17
+ * Mudou aqui, muda lá (e vice-versa); há teste de paridade no server.
18
+ */
19
+ /** Definição de uma variável disponível — vem do app hospedeiro, não do CDB. */
20
+ export type MinutaVariableDef = {
21
+ /** chave usada no documento e nos valores (ex.: `PROCESSO`) */
22
+ key: string;
23
+ /** rótulo exibido no chip e no seletor (ex.: `Nº do Processo`) */
24
+ label: string;
25
+ /** de onde o valor sai — vira o tooltip do chip */
26
+ description?: string;
27
+ /** agrupador opcional no seletor (ex.: `Processo`, `Réu`) */
28
+ group?: string;
29
+ };
30
+ export declare function escapeHtml(value: string): string;
31
+ /**
32
+ * Chaves das variáveis usadas no documento, na ordem em que aparecem e sem
33
+ * repetição. É o que substitui o `extractPeticaoVariaveisFromTexto` de quem
34
+ * hospeda: o host guarda esta lista junto com o template e não precisa ler HTML.
35
+ */
36
+ export declare function extractTemplateVariables(html: string | null | undefined): string[];
37
+ /**
38
+ * Troca os placeholders pelos valores. O que NÃO tem valor continua sendo um
39
+ * placeholder vivo no documento — não vira texto vazio nem some.
40
+ *
41
+ * É de propósito: uma variável sem valor é um buraco na peça, e o lugar de
42
+ * mostrar isso é o próprio parágrafo em que ela está, não um botão desabilitado
43
+ * no rodapé do modal. Como o nó sobrevive, `extractTemplateVariables` na minuta
44
+ * gerada responde "o que ainda falta", e mandar os valores de novo resolve.
45
+ */
46
+ export declare function resolveTemplateVariables(html: string | null | undefined, values: Record<string, string | null | undefined> | null | undefined): {
47
+ html: string;
48
+ missing: string[];
49
+ };
@@ -0,0 +1,36 @@
1
+ const VARIABLE_TAG_RE = /<span\b[^>]*\bdata-minuta-var="([^"]*)"[^>]*>[^<]*<\/span>/g;
2
+ export function escapeHtml(value) {
3
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4
+ }
5
+ function decodeAttr(value) {
6
+ return value.replace(/&quot;/g, '"').replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
7
+ }
8
+ export function extractTemplateVariables(html) {
9
+ if (!html)
10
+ return [];
11
+ const keys = [];
12
+ for (const match of html.matchAll(VARIABLE_TAG_RE)) {
13
+ const key = decodeAttr(match[1] ?? "").trim();
14
+ if (key && !keys.includes(key))
15
+ keys.push(key);
16
+ }
17
+ return keys;
18
+ }
19
+ export function resolveTemplateVariables(html, values) {
20
+ if (!html)
21
+ return { html: "", missing: [] };
22
+ const missing = [];
23
+ const resolved = html.replace(VARIABLE_TAG_RE, (tag, rawKey) => {
24
+ const key = decodeAttr(rawKey ?? "").trim();
25
+ if (!key)
26
+ return tag;
27
+ const value = values?.[key];
28
+ if (typeof value !== "string" || !value.trim()) {
29
+ if (!missing.includes(key))
30
+ missing.push(key);
31
+ return tag;
32
+ }
33
+ return escapeHtml(value).replace(/\r?\n/g, "<br>");
34
+ });
35
+ return { html: resolved, missing };
36
+ }
package/package.json CHANGED
@@ -1,100 +1,101 @@
1
1
  {
2
- "name": "@meistrari/minuta-nuxt",
3
- "version": "0.1.0",
4
- "description": "Embed do editor de minuta (docx paginado A4) do defesa.ai para apps Nuxt — componente + proxy Nitro, sem segredos no browser",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/meistrari/case-defense-builder.git",
8
- "directory": "packages/minuta-nuxt"
9
- },
10
- "license": "UNLICENSED",
11
- "type": "module",
12
- "exports": {
13
- ".": {
14
- "types": "./dist/types.d.mts",
15
- "import": "./dist/module.mjs"
2
+ "name": "@meistrari/minuta-nuxt",
3
+ "version": "1.1.0",
4
+ "description": "Embed do editor de minuta (docx paginado A4) do defesa.ai para apps Nuxt — componente + proxy Nitro, sem segredos no browser",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/meistrari/case-defense-builder.git",
8
+ "directory": "packages/minuta-nuxt"
16
9
  },
17
- "./runtime/*": {
18
- "types": "./dist/runtime/*.d.ts",
19
- "import": "./dist/runtime/*.js"
20
- }
21
- },
22
- "main": "./dist/module.mjs",
23
- "typesVersions": {
24
- "*": {
25
- ".": [
26
- "./dist/types.d.mts"
27
- ]
28
- }
29
- },
30
- "files": [
31
- "dist"
32
- ],
33
- "scripts": {
34
- "prepack": "nuxt-module-build build",
35
- "build": "nuxt-module-build build",
36
- "dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground",
37
- "dev": "nuxi dev playground"
38
- },
39
- "dependencies": {
40
- "@fontsource-variable/inter": "5.3.0",
41
- "@nuxt/kit": "^3.17.7",
42
- "@phosphor-icons/vue": "2.2.1",
43
- "@tiptap/core": "2.11.9",
44
- "@tiptap/extension-table": "2.11.9",
45
- "@tiptap/extension-table-cell": "2.11.9",
46
- "@tiptap/extension-table-header": "2.11.9",
47
- "@tiptap/extension-table-row": "2.11.9",
48
- "@tiptap/pm": "2.11.9",
49
- "@tiptap/starter-kit": "2.11.9",
50
- "@tiptap/vue-3": "2.11.9",
51
- "defu": "^6.1.4",
52
- "docx": "9.7.1",
53
- "h3": "^1.15.0",
54
- "ofetch": "^1.4.1"
55
- },
56
- "overrides": {
57
- "@tiptap/core": "2.11.9",
58
- "@tiptap/pm": "2.11.9",
59
- "@tiptap/extension-blockquote": "2.11.9",
60
- "@tiptap/extension-bold": "2.11.9",
61
- "@tiptap/extension-bullet-list": "2.11.9",
62
- "@tiptap/extension-code": "2.11.9",
63
- "@tiptap/extension-code-block": "2.11.9",
64
- "@tiptap/extension-document": "2.11.9",
65
- "@tiptap/extension-dropcursor": "2.11.9",
66
- "@tiptap/extension-gapcursor": "2.11.9",
67
- "@tiptap/extension-hard-break": "2.11.9",
68
- "@tiptap/extension-heading": "2.11.9",
69
- "@tiptap/extension-history": "2.11.9",
70
- "@tiptap/extension-horizontal-rule": "2.11.9",
71
- "@tiptap/extension-italic": "2.11.9",
72
- "@tiptap/extension-list-item": "2.11.9",
73
- "@tiptap/extension-ordered-list": "2.11.9",
74
- "@tiptap/extension-paragraph": "2.11.9",
75
- "@tiptap/extension-strike": "2.11.9",
76
- "@tiptap/extension-text": "2.11.9",
77
- "@tiptap/extension-text-style": "2.11.9",
78
- "@tiptap/extension-bubble-menu": "2.11.9",
79
- "@tiptap/extension-floating-menu": "2.11.9"
80
- },
81
- "devDependencies": {
82
- "@nuxt/module-builder": "^0.8.4",
83
- "nuxt": "~3.17.7",
84
- "pino-pretty": "^13.1.3",
85
- "typescript": "^5.8.2",
86
- "vue": "^3.5.13",
87
- "@meistrari/chat-nuxt": "3.14.0-rc.2"
88
- },
89
- "publishConfig": {
90
- "access": "public"
91
- },
92
- "peerDependencies": {
93
- "@meistrari/chat-nuxt": ">=3.13.0"
94
- },
95
- "peerDependenciesMeta": {
96
- "@meistrari/chat-nuxt": {
97
- "optional": true
10
+ "license": "UNLICENSED",
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/types.d.mts",
15
+ "import": "./dist/module.mjs"
16
+ },
17
+ "./runtime/*": {
18
+ "types": "./dist/runtime/*.d.ts",
19
+ "import": "./dist/runtime/*.js"
20
+ }
21
+ },
22
+ "main": "./dist/module.mjs",
23
+ "typesVersions": {
24
+ "*": {
25
+ ".": [
26
+ "./dist/types.d.mts"
27
+ ]
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "prepack": "nuxt-module-build build",
35
+ "build": "nuxt-module-build build",
36
+ "dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground",
37
+ "dev": "nuxi dev playground"
38
+ },
39
+ "dependencies": {
40
+ "@fontsource-variable/inter": "5.3.0",
41
+ "@nuxt/kit": "^3.17.7",
42
+ "@phosphor-icons/vue": "2.2.1",
43
+ "@tiptap/core": "2.11.9",
44
+ "@tiptap/extension-table": "2.11.9",
45
+ "@tiptap/extension-table-cell": "2.11.9",
46
+ "@tiptap/extension-table-header": "2.11.9",
47
+ "@tiptap/extension-table-row": "2.11.9",
48
+ "@tiptap/pm": "2.11.9",
49
+ "@tiptap/starter-kit": "2.11.9",
50
+ "@tiptap/vue-3": "2.11.9",
51
+ "defu": "^6.1.4",
52
+ "docx": "9.7.1",
53
+ "h3": "^1.15.0",
54
+ "ofetch": "^1.4.1"
55
+ },
56
+ "overrides": {
57
+ "@tiptap/core": "2.11.9",
58
+ "@tiptap/pm": "2.11.9",
59
+ "@tiptap/extension-blockquote": "2.11.9",
60
+ "@tiptap/extension-bold": "2.11.9",
61
+ "@tiptap/extension-bullet-list": "2.11.9",
62
+ "@tiptap/extension-code": "2.11.9",
63
+ "@tiptap/extension-code-block": "2.11.9",
64
+ "@tiptap/extension-document": "2.11.9",
65
+ "@tiptap/extension-dropcursor": "2.11.9",
66
+ "@tiptap/extension-gapcursor": "2.11.9",
67
+ "@tiptap/extension-hard-break": "2.11.9",
68
+ "@tiptap/extension-heading": "2.11.9",
69
+ "@tiptap/extension-history": "2.11.9",
70
+ "@tiptap/extension-horizontal-rule": "2.11.9",
71
+ "@tiptap/extension-italic": "2.11.9",
72
+ "@tiptap/extension-list-item": "2.11.9",
73
+ "@tiptap/extension-ordered-list": "2.11.9",
74
+ "@tiptap/extension-paragraph": "2.11.9",
75
+ "@tiptap/extension-strike": "2.11.9",
76
+ "@tiptap/extension-text": "2.11.9",
77
+ "@tiptap/extension-text-style": "2.11.9",
78
+ "@tiptap/extension-bubble-menu": "2.11.9",
79
+ "@tiptap/extension-floating-menu": "2.11.9"
80
+ },
81
+ "devDependencies": {
82
+ "@nuxt/module-builder": "^0.8.4",
83
+ "nuxt": "~3.17.7",
84
+ "pino-pretty": "^13.1.3",
85
+ "typescript": "^5.8.2",
86
+ "vue": "^3.5.13",
87
+ "@meistrari/chat-nuxt": "3.14.0-rc.2"
88
+ },
89
+ "publishConfig": {
90
+ "access": "public"
91
+ },
92
+ "peerDependencies": {
93
+ "@meistrari/chat-nuxt": ">=3.13.0",
94
+ "@meistrari/tela-build": ">=1.63.0"
95
+ },
96
+ "peerDependenciesMeta": {
97
+ "@meistrari/chat-nuxt": {
98
+ "optional": true
99
+ }
98
100
  }
99
- }
100
101
  }