@meistrari/minuta-nuxt 0.1.0 → 1.0.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.
@@ -0,0 +1,149 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref, watch } from 'vue'
3
+ import type { MinutaVariableDef } from '../utils/templateVariables'
4
+
5
+ /**
6
+ * Seletor de variável do template — o popover que abre pelo `/` no texto ou
7
+ * pelo botão da toolbar.
8
+ *
9
+ * A busca é por RÓTULO, não por chave: quem monta o template pensa em "número
10
+ * do processo", não em `PROCESSO`. A chave aparece ao lado porque é ela que o
11
+ * host recebe de volta, e ver as duas juntas é o que ensina o vocabulário.
12
+ */
13
+
14
+ const props = defineProps<{
15
+ variables: MinutaVariableDef[]
16
+ /** posição absoluta dentro do container do editor */
17
+ top: number
18
+ left: number
19
+ /** texto digitado depois do `/` (busca ao vivo do gatilho no documento) */
20
+ query?: string
21
+ }>()
22
+
23
+ const emit = defineEmits<{ select: [variable: MinutaVariableDef], close: [] }>()
24
+
25
+ const search = ref('')
26
+ const activeIndex = ref(0)
27
+ const searchInput = ref<HTMLInputElement | null>(null)
28
+
29
+ /** o gatilho `/` filtra pelo que está sendo digitado NO DOCUMENTO; o botão, pelo campo */
30
+ const term = computed(() => (props.query ?? search.value).trim().toLowerCase())
31
+
32
+ const filtered = computed(() => {
33
+ if (!term.value)
34
+ return props.variables
35
+ return props.variables.filter(v =>
36
+ v.label.toLowerCase().includes(term.value)
37
+ || v.key.toLowerCase().includes(term.value)
38
+ || (v.description?.toLowerCase().includes(term.value) ?? false),
39
+ )
40
+ })
41
+
42
+ /** lista achatada com cabeçalhos de grupo — o índice ativo anda só nos itens */
43
+ const rows = computed(() => {
44
+ const out: Array<{ kind: 'group', label: string } | { kind: 'item', variable: MinutaVariableDef, index: number }> = []
45
+ let index = 0
46
+ let lastGroup: string | null = null
47
+ for (const variable of filtered.value) {
48
+ const group = variable.group ?? null
49
+ if (group && group !== lastGroup)
50
+ out.push({ kind: 'group', label: group })
51
+ lastGroup = group
52
+ out.push({ kind: 'item', variable, index })
53
+ index++
54
+ }
55
+ return out
56
+ })
57
+
58
+ watch(filtered, () => {
59
+ activeIndex.value = 0
60
+ })
61
+
62
+ /**
63
+ * Aberto pelo BOTÃO (sem `query`), o filtro é o campo daqui e ele precisa do
64
+ * foco — quem abriu não estava digitando no documento. Aberto pelo GATILHO `/`,
65
+ * o foco fica no editor de propósito: a pessoa continua digitando o texto do
66
+ * filtro no próprio parágrafo, e é o pai que encaminha as teclas para cá.
67
+ *
68
+ * `onMounted`, e não um watcher `immediate`: no watcher o ref do input ainda
69
+ * não existe, e o foco caía no editor — daí o que se digitava virar texto do
70
+ * documento em vez de busca.
71
+ */
72
+ onMounted(() => {
73
+ if (props.query === undefined)
74
+ searchInput.value?.focus()
75
+ })
76
+
77
+ function move(delta: number) {
78
+ const total = filtered.value.length
79
+ if (!total)
80
+ return
81
+ activeIndex.value = (activeIndex.value + delta + total) % total
82
+ }
83
+
84
+ function choose(index = activeIndex.value) {
85
+ const variable = filtered.value[index]
86
+ if (variable)
87
+ emit('select', variable)
88
+ }
89
+
90
+ /** o gatilho `/` mantém o foco no editor — o pai delega as teclas para cá */
91
+ defineExpose({ move, choose, hasResults: () => filtered.value.length > 0 })
92
+ </script>
93
+
94
+ <template>
95
+ <div
96
+ class="poc-varpicker"
97
+ :style="{ top: `${top}px`, left: `${left}px` }"
98
+ @click.stop
99
+ >
100
+ <input
101
+ v-if="query === undefined"
102
+ ref="searchInput"
103
+ v-model="search"
104
+ class="poc-varpicker__search"
105
+ placeholder="Buscar variável…"
106
+ spellcheck="false"
107
+ @keydown.down.prevent="move(1)"
108
+ @keydown.up.prevent="move(-1)"
109
+ @keydown.enter.prevent="choose()"
110
+ @keydown.esc.prevent="emit('close')"
111
+ >
112
+ <div v-else class="poc-varpicker__hint">
113
+ Variável<span v-if="query">: <b>{{ query }}</b></span>
114
+ </div>
115
+
116
+ <!-- `mousedown.prevent` só na lista: na raiz ele impediria o clique de
117
+ focar o próprio campo de busca. Aqui ele serve para o clique num
118
+ item não tirar o foco de onde está (editor ou campo) antes do
119
+ `click` — o que fecharia o popover pelo caminho de "clicou fora". -->
120
+ <div class="poc-varpicker__list" @mousedown.prevent>
121
+ <p v-if="!filtered.length" class="poc-varpicker__empty">
122
+ Nenhuma variável encontrada.
123
+ </p>
124
+ <template v-for="(row, i) in rows" :key="i">
125
+ <div v-if="row.kind === 'group'" class="poc-varpicker__group">
126
+ {{ row.label }}
127
+ </div>
128
+ <button
129
+ v-else
130
+ type="button"
131
+ class="poc-varpicker__item"
132
+ :class="{ 'poc-varpicker__item--on': row.index === activeIndex }"
133
+ @mouseenter="activeIndex = row.index"
134
+ @click="choose(row.index)"
135
+ >
136
+ <span class="poc-varpicker__label">{{ row.variable.label }}</span>
137
+ <span class="poc-varpicker__key">{{ row.variable.key }}</span>
138
+ <span v-if="row.variable.description" class="poc-varpicker__desc">
139
+ {{ row.variable.description }}
140
+ </span>
141
+ </button>
142
+ </template>
143
+ </div>
144
+ </div>
145
+ </template>
146
+
147
+ <style scoped>
148
+ .poc-varpicker{background:#fff;border:1px solid #e5e7eb;border-radius:10px;box-shadow:0 12px 24px -8px rgba(0,0,0,.25);display:flex;flex-direction:column;font-size:13px;max-height:300px;overflow:hidden;position:absolute;width:320px;z-index:60}.poc-varpicker__search{border:0;border-bottom:1px solid #f3f4f6;font-family:inherit;font-size:13px;outline:none;padding:10px 12px}.poc-varpicker__hint{border-bottom:1px solid #f3f4f6;color:#6b7280;font-size:12px;padding:8px 12px}.poc-varpicker__list{flex:1;min-height:0;overflow-y:auto;padding:4px}.poc-varpicker__empty{color:#6b7280;font-size:12px;margin:0;padding:12px}.poc-varpicker__group{color:#9ca3af;font-size:11px;font-weight:600;letter-spacing:.04em;padding:8px 8px 4px;text-transform:uppercase}.poc-varpicker__item{background:none;border:0;border-radius:6px;cursor:pointer;display:grid;font-family:inherit;gap:2px 8px;grid-template-columns:1fr auto;padding:6px 8px;text-align:left;width:100%}.poc-varpicker__item--on{background:#f3f4f6}.poc-varpicker__label{color:#111827;font-weight:500}.poc-varpicker__key{color:#9ca3af;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}.poc-varpicker__desc{color:#6b7280;display:-webkit-box;font-size:11px;grid-column:1/-1;-webkit-line-clamp:2;line-height:1.4;overflow:hidden;-webkit-box-orient:vertical}
149
+ </style>
@@ -11,7 +11,6 @@ export interface AnnotationTarget {
11
11
  /** título da seção onde o item aparece (fallback) */
12
12
  onde?: string | null;
13
13
  }
14
- /** range PM de um recorte verbatim do documento (busca whitespace-tolerante) */
15
14
  export declare function findTextRange(editor: Editor, needle: string): {
16
15
  from: number;
17
16
  to: number;
@@ -50,18 +50,24 @@ function findRange(text, posAt, needle) {
50
50
  }
51
51
  return null;
52
52
  }
53
+ function plainNeedle(needle) {
54
+ if (!needle.includes("<") && !needle.includes("&"))
55
+ return needle;
56
+ const doc = new DOMParser().parseFromString(needle, "text/html");
57
+ return doc.body.textContent ?? needle;
58
+ }
53
59
  export function findTextRange(editor, needle) {
54
60
  const { text, posAt } = buildTextIndex(editor);
55
61
  if (!text.length)
56
62
  return null;
57
- return findRange(text, posAt, needle);
63
+ return findRange(text, posAt, plainNeedle(needle));
58
64
  }
59
65
  export function locateAnnotationDetailed(editor, target) {
60
66
  const { text, posAt } = buildTextIndex(editor);
61
67
  if (!text.length)
62
68
  return null;
63
69
  if (target.trecho) {
64
- const range = findRange(text, posAt, target.trecho);
70
+ const range = findRange(text, posAt, plainNeedle(target.trecho));
65
71
  if (range)
66
72
  return { range, matchedBy: "trecho" };
67
73
  }
@@ -227,7 +233,8 @@ export function setUserEditRanges(editor, ranges) {
227
233
  export function scrollToAnnotation(editor, from, scrollContainer) {
228
234
  try {
229
235
  const coords = editor.view.coordsAtPos(from);
230
- if (scrollContainer) {
236
+ const rolavel = !!scrollContainer && scrollContainer.scrollHeight > scrollContainer.clientHeight + 4;
237
+ if (scrollContainer && rolavel) {
231
238
  const rect = scrollContainer.getBoundingClientRect();
232
239
  scrollContainer.scrollTo({
233
240
  top: scrollContainer.scrollTop + (coords.top - rect.top) - rect.height * 0.3,
@@ -202,6 +202,12 @@ function inlineToRuns(node, base, ctx) {
202
202
  }
203
203
  return;
204
204
  }
205
+ if (child.type.name === "templateVariable") {
206
+ const label = child.attrs.label || child.attrs.varKey || "";
207
+ if (label)
208
+ runs.push(new TextRun({ text: `[${label}]`, ...runOptions(base) }));
209
+ return;
210
+ }
205
211
  if (child.type.name === "variableChip") {
206
212
  if (child.attrs.text)
207
213
  runs.push(new TextRun({ text: child.attrs.text, ...runOptions(base) }));
@@ -327,7 +333,9 @@ function textBlockToParagraph(node, pos, ctx, opts = {}) {
327
333
  const runs = inlineToRuns(node, base, ctx);
328
334
  const paragraph = new Paragraph({
329
335
  children: runs,
330
- alignment: m?.align ?? parseAlignment(style),
336
+ // sem alinhamento explícito, corpo de peça vai justificado (o Word
337
+ // usaria esquerda e o .docx sairia diferente da praxe e do preview)
338
+ alignment: m?.align ?? parseAlignment(style) ?? (opts.heading ? void 0 : AlignmentType.JUSTIFIED),
331
339
  spacing,
332
340
  indent: total ? { left: total } : void 0,
333
341
  heading: opts.heading ? headingLevelFor(opts.heading) : void 0,
@@ -1,6 +1,8 @@
1
1
  import type { Editor } from '@tiptap/core';
2
2
  import { Extension as TiptapExtension, Mark as TiptapMark, Node as TiptapNode } from '@tiptap/core';
3
+ import type { EditorView as PMEditorView } from '@tiptap/pm/view';
3
4
  import type { PocPageConfig } from './paginationPlugin.js';
5
+ import type { MinutaVariableDef } from '../utils/templateVariables.js';
4
6
  /**
5
7
  * Extensões Tiptap da PoC do editor de minuta.
6
8
  *
@@ -8,6 +10,7 @@ import type { PocPageConfig } from './paginationPlugin.js';
8
10
  * - minutaBlock: wrapper com proveniência (blockId/título do bloco do template)
9
11
  * - htmlIsland: tabelas/listas/imagens/svg preservados como ilhas não-editáveis (HTML cru)
10
12
  * - variableChip: <span data-variable-id> vira nó atômico — a variável continua "viva"
13
+ * - templateVariable: <span data-minuta-var> — placeholder NÃO resolvido do template
11
14
  * - inlineStyle/styledDiv/passthrough: preservam o CSS inline do pipeline de render
12
15
  */
13
16
  /**
@@ -39,6 +42,26 @@ export declare const PocHtmlIsland: TiptapNode<any, any>;
39
42
  */
40
43
  export declare const PocMissingImage: TiptapNode<any, any>;
41
44
  export declare const PocVariableChip: TiptapNode<any, any>;
45
+ /**
46
+ * Placeholder de variável do TEMPLATE — o `#NOME_VARIAVEL` de antigamente, só
47
+ * que como nó atômico em vez de texto (ver `utils/templateVariables.ts` para o
48
+ * porquê e para o formato).
49
+ *
50
+ * O catálogo (`options.catalog`) vem do app hospedeiro: o CDB não sabe o que é
51
+ * `SIGLA_FORO`, só sabe que existe um lugar no documento com esse nome. O nó
52
+ * guarda o rótulo junto (para um template abrir legível sem catálogo, ex.: no
53
+ * histórico), mas quem manda na exibição é o catálogo — renomear uma variável
54
+ * no host reflete em todo template já salvo, sem migrar documento nenhum.
55
+ *
56
+ * Sobrevive à geração: `resolveTemplateVariables` só troca o que tem valor, e o
57
+ * que sobra continua sendo este nó — um buraco visível no parágrafo em que ele
58
+ * está, e não um erro genérico no rodapé de um modal.
59
+ */
60
+ /** o catálogo pode chegar depois do mount (host que carrega async) — daí o getter */
61
+ export type MinutaVariableCatalog = MinutaVariableDef[] | (() => MinutaVariableDef[]);
62
+ export declare const PocTemplateVariable: TiptapNode<{
63
+ catalog: MinutaVariableCatalog;
64
+ }, any>;
42
65
  /**
43
66
  * Nota de rodapé: o render devolve `<sup data-footnote="texto">n</sup>` e o
44
67
  * export transforma em nota de rodapé DE VERDADE no Word (o pipeline legado faz
@@ -53,4 +76,18 @@ export declare const PocInlineStyle: TiptapMark<any, any>;
53
76
  export declare const PocUnderline: TiptapMark<any, any>;
54
77
  /** Preserva style/class inline nos blocos de texto (o pipeline de render inlina todo o CSS). */
55
78
  export declare const PocStylePassthrough: TiptapExtension<any, any>;
56
- export declare function buildPocExtensions(pageConfig?: PocPageConfig): (TiptapNode<any, any> | TiptapMark<any, any> | TiptapExtension<any, any>)[];
79
+ export declare function buildPocExtensions(pageConfig?: PocPageConfig, options?: {
80
+ /** catálogo de variáveis do host — habilita os chips do modo template */
81
+ variables?: MinutaVariableCatalog;
82
+ /**
83
+ * Primeiro a ver o teclado, ANTES do keymap do editor. Devolver `true`
84
+ * consome a tecla.
85
+ *
86
+ * Existe porque `editorProps.handleKeyDown` não serve: o `someProp` do
87
+ * ProseMirror percorre os plugins antes das props diretas, então o keymap
88
+ * do StarterKit trata o Enter (quebra o parágrafo) e o handler da view
89
+ * nunca roda. Quem precisa de um popover dirigido pelo teclado com o foco
90
+ * ainda no editor — o seletor de variável — depende disso.
91
+ */
92
+ handleKeyDown?: (view: PMEditorView, event: KeyboardEvent) => boolean;
93
+ }): (TiptapNode<any, any> | TiptapMark<any, any> | TiptapExtension<any, any>)[];
@@ -4,6 +4,7 @@ import Table from "@tiptap/extension-table";
4
4
  import TableCell from "@tiptap/extension-table-cell";
5
5
  import TableHeader from "@tiptap/extension-table-header";
6
6
  import TableRow from "@tiptap/extension-table-row";
7
+ import { Plugin as PMPlugin } from "@tiptap/pm/state";
7
8
  import { sanitizeRichHtml } from "../utils/sanitizeRichHtml.js";
8
9
  import { PocPaginationPlugin } from "./paginationPlugin.js";
9
10
  export const POC_ASSET_SWAP = "pocAssetSwap";
@@ -272,6 +273,74 @@ export const PocVariableChip = TiptapNode.create({
272
273
  };
273
274
  }
274
275
  });
276
+ function readCatalog(catalog) {
277
+ return typeof catalog === "function" ? catalog() : catalog;
278
+ }
279
+ export const PocTemplateVariable = TiptapNode.create({
280
+ name: "templateVariable",
281
+ inline: true,
282
+ group: "inline",
283
+ atom: true,
284
+ selectable: true,
285
+ addOptions() {
286
+ return { catalog: [] };
287
+ },
288
+ addAttributes() {
289
+ return {
290
+ varKey: { default: null },
291
+ label: { default: null }
292
+ };
293
+ },
294
+ parseHTML() {
295
+ return [{
296
+ // acima do PocInlineStyle (40): é nó, não span estilizado
297
+ tag: "span[data-minuta-var]",
298
+ priority: 115,
299
+ getAttrs: (el) => {
300
+ const key = el.getAttribute("data-minuta-var")?.trim();
301
+ if (!key)
302
+ return false;
303
+ return {
304
+ varKey: key,
305
+ label: el.getAttribute("data-minuta-var-label")?.trim() || el.textContent?.trim() || key
306
+ };
307
+ }
308
+ }];
309
+ },
310
+ renderHTML({ node }) {
311
+ const key = node.attrs.varKey ?? "";
312
+ const label = node.attrs.label || key;
313
+ return ["span", {
314
+ "data-minuta-var": key,
315
+ "data-minuta-var-label": label,
316
+ "class": "poc-tplvar"
317
+ }, label];
318
+ },
319
+ addNodeView() {
320
+ return ({ node }) => {
321
+ const key = node.attrs.varKey ?? "";
322
+ const catalog = readCatalog(this.options.catalog);
323
+ const def = catalog.find((item) => item.key === key);
324
+ const dom = document.createElement("span");
325
+ dom.className = "poc-tplvar";
326
+ dom.contentEditable = "false";
327
+ dom.setAttribute("data-minuta-var", key);
328
+ dom.textContent = def?.label || node.attrs.label || key;
329
+ dom.title = def?.description ? `${key} \u2014 ${def.description}` : key;
330
+ if (catalog.length && !def)
331
+ dom.classList.add("poc-tplvar--unknown");
332
+ return { dom };
333
+ };
334
+ },
335
+ addCommands() {
336
+ return {
337
+ insertTemplateVariable: (variable) => ({ commands }) => commands.insertContent({
338
+ type: "templateVariable",
339
+ attrs: { varKey: variable.key, label: variable.label }
340
+ })
341
+ };
342
+ }
343
+ });
275
344
  export const PocFootnoteRef = TiptapNode.create({
276
345
  name: "footnoteRef",
277
346
  inline: true,
@@ -391,7 +460,7 @@ export const PocInlineStyle = TiptapMark.create({
391
460
  tag: "span",
392
461
  priority: 40,
393
462
  getAttrs: (el) => {
394
- if (el.hasAttribute("data-variable-id"))
463
+ if (el.hasAttribute("data-variable-id") || el.hasAttribute("data-minuta-var"))
395
464
  return false;
396
465
  const style = el.getAttribute("style");
397
466
  const cls = el.getAttribute("class");
@@ -436,7 +505,7 @@ export const PocStylePassthrough = TiptapExtension.create({
436
505
  }];
437
506
  }
438
507
  });
439
- export function buildPocExtensions(pageConfig) {
508
+ export function buildPocExtensions(pageConfig, options) {
440
509
  const extensions = [
441
510
  PocDocument,
442
511
  TiptapStarterKit.configure({ document: false }),
@@ -448,12 +517,25 @@ export function buildPocExtensions(pageConfig) {
448
517
  PocHtmlIsland,
449
518
  PocMissingImage,
450
519
  PocVariableChip,
520
+ PocTemplateVariable.configure({ catalog: options?.variables ?? [] }),
451
521
  PocFootnoteRef,
452
522
  PocStyledDiv,
453
523
  PocInlineStyle,
454
524
  PocUnderline,
455
525
  PocStylePassthrough
456
526
  ];
527
+ const handleKeyDown = options?.handleKeyDown;
528
+ if (handleKeyDown) {
529
+ extensions.push(TiptapExtension.create({
530
+ name: "pocKeyDownFirst",
531
+ // acima do StarterKit (100): o ExtensionManager ordena os plugins
532
+ // por prioridade, e é essa ordem que o someProp percorre
533
+ priority: 1e3,
534
+ addProseMirrorPlugins() {
535
+ return [new PMPlugin({ props: { handleKeyDown } })];
536
+ }
537
+ }));
538
+ }
457
539
  if (pageConfig) {
458
540
  extensions.push(TiptapExtension.create({
459
541
  name: "pocPagination",
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/minuta-nuxt",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Embed do editor de minuta (docx paginado A4) do defesa.ai para apps Nuxt — componente + proxy Nitro, sem segredos no browser",
5
5
  "repository": {
6
6
  "type": "git",