@meistrari/minuta-editor 1.2.0 → 1.3.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
@@ -2,7 +2,8 @@
2
2
 
3
3
  O editor de minuta do **defesa.ai** sem framework: a folha A4 paginada de
4
4
  verdade, edição rica, cabeçalho/rodapé, placeholders de variável e export
5
- DOCX com as mesmas quebras de página do preview. Monta em qualquer app —
5
+ DOCX que reflui no Word como na folha (só a quebra manual do template é
6
+ forçada; título leva keep-with-next). Monta em qualquer app —
6
7
  React, Vue, Svelte, vanilla — com uma chamada:
7
8
 
8
9
  ```ts
@@ -33,6 +34,8 @@ const editor = createMinutaEditor(el, {
33
34
  chrome: { header: '', footer: '', showPageNumber: true },
34
35
  variables: [...], // catálogo → liga o modo template
35
36
  uploadImage: async file => ({ src, vaultSrc }) | null,
37
+ imageFallback: 'dataUri', // 'none' = upload falhou → não insere, só onImageUploadError
38
+ onImageUploadError: (error, file) => {},
36
39
  toolbar: true, // false = o host monta a barra dele
37
40
  onReady: tiptap => {},
38
41
  onChange: () => {},
@@ -53,9 +56,10 @@ editor.setBlockClickWhileEditing(bool)
53
56
  // variáveis do case marcadas no documento (<span|img data-variable-id>, aviso de imagem faltando)
54
57
  editor.scrollToVariable(id) // → boolean (rola e dá um flash)
55
58
  editor.setVariableValue(id, texto) // → nº de ocorrências (o aviso de "faltando" vira chip)
56
- editor.setVariableImage(id, src) // → nº de ocorrências (o aviso de imagem vira <img>)
59
+ editor.setVariableImage(id, src, vaultSrc?) // → nº de ocorrências (o aviso de imagem vira <img>; vaultSrc → data-vault-src)
60
+ editor.setImageFallback('dataUri' | 'none')
57
61
  editor.variablesInDocument() // → string[]
58
- editor.refreshVaultImages(vaultUrl => Promise<string>) // renova o src das imagens inseridas (data-vault-src)
62
+ editor.refreshVaultImages(vaultUrl => Promise<string>) // renova o src das imagens do vault (data-vault-src ou src="vault://"); sem undo/onChange
59
63
 
60
64
  editor.schedulePaginate() // repagina (após mudar algo que afeta altura por fora)
61
65
  editor.totalPages()
@@ -1,5 +1,5 @@
1
- import { f as footnoteHtmlToRuns, p as pocLoneCellIndex } from '../shared/minuta-editor.b2512170.mjs';
2
- import { BorderStyle, TextRun, ExternalHyperlink, Paragraph, Document, Header, Packer, AlignmentType, PageBreak, LineRuleType, Table, TableRow, TableCell, WidthType, TableLayoutType, Footer, FootnoteReferenceRun, VerticalAlign, ImageRun, HorizontalPositionRelativeFrom, VerticalPositionRelativeFrom, TextWrappingType, FrameAnchorType, PageNumber, HeadingLevel } from 'docx';
1
+ import { f as footnoteHtmlToRuns, p as pocLoneCellIndex, a as pocLooksLikeTitle } from '../shared/minuta-editor.d9d0c0ce.mjs';
2
+ import { BorderStyle, TextRun, ExternalHyperlink, Paragraph, Document, Header, Packer, AlignmentType, LineRuleType, Table, TableRow, TableCell, WidthType, TableLayoutType, Footer, FootnoteReferenceRun, VerticalAlign, ImageRun, HorizontalPositionRelativeFrom, VerticalPositionRelativeFrom, TextWrappingType, FrameAnchorType, PageNumber, HeadingLevel } from 'docx';
3
3
  import '@tiptap/pm/state';
4
4
  import '@tiptap/pm/view';
5
5
 
@@ -117,7 +117,7 @@ function measureEl(el) {
117
117
  // Piso = 1.0em: protege o caso patológico (0.7em) SEM inflar entrelinha
118
118
  // justa (1.0em, comum em citações) — com 1.15em o export ficava mais
119
119
  // alto que o preview mediu, o conteúdo derramava no Word e a quebra
120
- // explícita seguinte abria página quase vazia (TLF ¶36/¶52).
120
+ // explícita seguinte abria página quase vazia.
121
121
  lineExactTw: lineHpx && Number.isFinite(lineHpx) ? Math.round(Math.max(lineHpx, fontPx) * PX_TO_TWIP) : null,
122
122
  afterTw: Math.round((Number.parseFloat(cs.marginBottom) || 0) * PX_TO_TWIP),
123
123
  beforeTw: Math.round((Number.parseFloat(cs.marginTop) || 0) * PX_TO_TWIP),
@@ -257,7 +257,7 @@ function domInlineToRuns(root, fallback = {}) {
257
257
  runs.push(new TextRun({
258
258
  text,
259
259
  // sem rFonts o run cai no default Arial do Document — a fonte
260
- // REAL do preview (ex.: Calibri do TLF) tem que viajar no run
260
+ // REAL do preview (ex.: Calibri) tem que viajar no run
261
261
  font: primaryFont(cs) ?? fallback.font,
262
262
  size: Math.round((Number.parseFloat(cs.fontSize) || 16) / PX_PER_PT * 2),
263
263
  color: cssColorToHex(cs.color) ?? fallback.color,
@@ -328,6 +328,7 @@ function textBlockToParagraph(node, pos, ctx, opts = {}) {
328
328
  spacing.before = Math.max(0, spacing.before - ctx.lastAfterTw);
329
329
  ctx.lastAfterTw = spacing.after ?? 0;
330
330
  const runs = inlineToRuns(node, base, ctx);
331
+ const keepNext = Boolean(opts.heading || m && pocLooksLikeTitle(m.el)) || void 0;
331
332
  const paragraph = new Paragraph({
332
333
  children: runs,
333
334
  // sem alinhamento explícito, corpo de peça vai justificado (o Word
@@ -336,7 +337,8 @@ function textBlockToParagraph(node, pos, ctx, opts = {}) {
336
337
  spacing,
337
338
  indent: total ? { left: total } : void 0,
338
339
  heading: opts.heading ? headingLevelFor(opts.heading) : void 0,
339
- pageBreakBefore: breakBefore
340
+ pageBreakBefore: breakBefore,
341
+ keepNext
340
342
  });
341
343
  ctx.meta.set(paragraph, { spacer: runs.length === 0 && !opts.heading, breakBefore });
342
344
  return paragraph;
@@ -703,7 +705,7 @@ function collectImageIslands(node, pos) {
703
705
  }
704
706
  return out.length ? out : null;
705
707
  }
706
- async function inlineImagesParagraph(islands, innerPx, ctx) {
708
+ async function inlineImagesParagraphs(islands, innerPx, ctx) {
707
709
  const GAP_PX = 8;
708
710
  const items = [];
709
711
  for (const { node, pos } of islands) {
@@ -711,27 +713,41 @@ async function inlineImagesParagraph(islands, innerPx, ctx) {
711
713
  const src = await srcToDataUrl(img.getAttribute("src") || "");
712
714
  if (!src)
713
715
  continue;
714
- const live = domAt(ctx, pos)?.querySelector("img");
716
+ const islandEl = domAt(ctx, pos);
717
+ const live = islandEl?.querySelector("img");
715
718
  const styleW = Number.parseFloat(getProp(img.getAttribute("style"), "width") ?? "");
716
719
  const widthPx = live?.offsetWidth || 0 || (Number.isFinite(styleW) && styleW > 0 ? styleW : 0) || Math.min(innerPx, 200);
717
- items.push({ src, widthPx });
720
+ const zoom = islandEl ? zoomFactor(islandEl) : 1;
721
+ const top = islandEl ? islandEl.getBoundingClientRect().top / zoom : Number.NaN;
722
+ items.push({ src, widthPx, top });
718
723
  }
719
724
  if (!items.length)
720
- return null;
721
- const total = items.reduce((a, b) => a + b.widthPx, 0) + GAP_PX * (items.length - 1);
722
- const scale = innerPx > 0 && total > innerPx ? (innerPx - GAP_PX * (items.length - 1)) / (total - GAP_PX * (items.length - 1)) : 1;
723
- const runs = [];
724
- for (let i = 0; i < items.length; i++) {
725
- const run = await dataUrlToImageRun(items[i].src, Math.max(1, Math.round(items[i].widthPx * scale)));
726
- if (!run)
727
- continue;
725
+ return [];
726
+ const lines = [];
727
+ for (const item of items) {
728
+ const line = lines[lines.length - 1];
729
+ if (line && (Number.isNaN(item.top) || Number.isNaN(line[0].top) || Math.abs(item.top - line[0].top) <= 8))
730
+ line.push(item);
731
+ else
732
+ lines.push([item]);
733
+ }
734
+ const paragraphs = [];
735
+ for (const line of lines) {
736
+ const total = line.reduce((a, b) => a + b.widthPx, 0) + GAP_PX * (line.length - 1);
737
+ const scale = innerPx > 0 && total > innerPx ? (innerPx - GAP_PX * (line.length - 1)) / (total - GAP_PX * (line.length - 1)) : 1;
738
+ const runs = [];
739
+ for (const item of line) {
740
+ const run = await dataUrlToImageRun(item.src, Math.max(1, Math.round(item.widthPx * scale)));
741
+ if (!run)
742
+ continue;
743
+ if (runs.length)
744
+ runs.push(new TextRun({ text: " " }));
745
+ runs.push(run);
746
+ }
728
747
  if (runs.length)
729
- runs.push(new TextRun({ text: " " }));
730
- runs.push(run);
748
+ paragraphs.push(new Paragraph({ children: runs, alignment: AlignmentType.CENTER, spacing: { after: 0, before: 0 } }));
731
749
  }
732
- if (!runs.length)
733
- return null;
734
- return new Paragraph({ children: runs, alignment: AlignmentType.CENTER, spacing: { after: 0, before: 0 } });
750
+ return paragraphs;
735
751
  }
736
752
  async function tableToDocx(node, pos, contentWidthPx, ctx) {
737
753
  const tableDomRaw = domAt(ctx, pos);
@@ -785,9 +801,7 @@ async function tableToDocx(node, pos, contentWidthPx, ctx) {
785
801
  endPos += next.nodeSize;
786
802
  end++;
787
803
  }
788
- const para = await inlineImagesParagraph(islands, cellInnerPx, cellCtx);
789
- if (para)
790
- cellContent.push(para);
804
+ cellContent.push(...await inlineImagesParagraphs(islands, cellInnerPx, cellCtx));
791
805
  childPos = endPos;
792
806
  k = end - 1;
793
807
  continue;
@@ -937,9 +951,8 @@ async function nodeToDocx(node, pos, contentWidthPx, ctx) {
937
951
  }
938
952
  case "paragraph": {
939
953
  if (node.textContent.trim() === "PAGEBREAK") {
940
- if (ctx.breakSet.size)
941
- return [];
942
- return [new Paragraph({ children: [new PageBreak()] })];
954
+ ctx.pending.breakBefore = true;
955
+ return [];
943
956
  }
944
957
  return [textBlockToParagraph(node, pos, ctx)];
945
958
  }
@@ -1303,11 +1316,13 @@ async function pocExportDocx(editor, margins, chrome, options = {}) {
1303
1316
  const doc = editor.state.doc;
1304
1317
  const ctx = {
1305
1318
  view: editor.view,
1306
- // quebras `soft` (dentro de um parágrafo) não viram pageBreakBefore:
1307
- // partir o parágrafo em dois no Word entregaria ao advogado um arquivo
1308
- // que não reflui ao editar. Ali o Word quebra sozinho, no mesmo lugar
1309
- // ou a uma linha de distância.
1310
- breakSet: new Set((options.breaks ?? []).filter((b) => !b.soft).map((b) => b.pos)),
1319
+ // só a quebra pedida pelo template vira pageBreakBefore. As demais
1320
+ // (entre blocos ou entre linhas de um parágrafo) ficam por conta do
1321
+ // Word: ele parte parágrafo, lista e tabela sozinho e empurra imagem
1322
+ // só quando ela de fato não cabe na página DELE. Forçar a quebra do
1323
+ // preview deixava o arquivo com o pé da página vazio sempre que o
1324
+ // Word cabia mais (ou menos) texto que o browser.
1325
+ breakSet: new Set((options.breaks ?? []).filter((b) => b.manual).map((b) => b.pos)),
1311
1326
  pending: { breakBefore: false },
1312
1327
  lastAfterTw: 0,
1313
1328
  meta: /* @__PURE__ */ new WeakMap(),
package/dist/docx.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { a as PocDocxChrome, b as PocDocxExportOptions, u as pocExportDocx } from './shared/minuta-editor.e223a421.mjs';
1
+ export { a as PocDocxChrome, b as PocDocxExportOptions, w as pocExportDocx } from './shared/minuta-editor.9e65d163.mjs';
2
2
  import '@tiptap/core';
3
3
  import '@tiptap/pm/model';
4
4
  import '@tiptap/pm/view';
package/dist/docx.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { a as PocDocxChrome, b as PocDocxExportOptions, u as pocExportDocx } from './shared/minuta-editor.e223a421.js';
1
+ export { a as PocDocxChrome, b as PocDocxExportOptions, w as pocExportDocx } from './shared/minuta-editor.9e65d163.js';
2
2
  import '@tiptap/core';
3
3
  import '@tiptap/pm/model';
4
4
  import '@tiptap/pm/view';
package/dist/docx.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  export { pocExportDocx } from './chunks/docxExport.mjs';
2
- import './shared/minuta-editor.b2512170.mjs';
2
+ import './shared/minuta-editor.d9d0c0ce.mjs';
3
3
  import '@tiptap/pm/state';
4
4
  import '@tiptap/pm/view';
5
5
  import 'docx';
package/dist/index.d.mts CHANGED
@@ -19,8 +19,8 @@
19
19
  /// <reference types="@tiptap/extension-bubble-menu" />
20
20
  import { Node, Editor, Mark, Extension } from '@tiptap/core';
21
21
  import { EditorView } from '@tiptap/pm/view';
22
- import { P as PocPageConfig } from './shared/minuta-editor.e223a421.mjs';
23
- export { e as PAGINATE_META, a as PocDocxChrome, b as PocDocxExportOptions, q as PocLoneRow, d as PocPageBreak, c as PocPageNote, t as PocPaginationPlugin, s as breaksEqual, h as buildPocFooterEl, g as buildPocHeaderEl, j as buildPocNotesEl, k as computePocBreaks, r as computePocLoneRows, i as isPocChromeCanvas, l as isPocFlexRow, f as pocChromeFlowHeight, o as pocLoneCellIndex, m as pocNodeHasVisibleContent, n as pocNodePaintsBox, p as pocPageKey } from './shared/minuta-editor.e223a421.mjs';
22
+ import { P as PocPageConfig } from './shared/minuta-editor.9e65d163.mjs';
23
+ export { e as PAGINATE_META, a as PocDocxChrome, b as PocDocxExportOptions, s as PocLoneRow, d as PocPageBreak, c as PocPageNote, v as PocPaginationPlugin, u as breaksEqual, h as buildPocFooterEl, g as buildPocHeaderEl, j as buildPocNotesEl, l as computePocBreaks, t as computePocLoneRows, i as isPocChromeCanvas, n as isPocFlexRow, f as pocChromeFlowHeight, m as pocDescendsInto, r as pocLoneCellIndex, k as pocLooksLikeTitle, o as pocNodeHasVisibleContent, q as pocNodePaintsBox, p as pocPageKey } from './shared/minuta-editor.9e65d163.mjs';
24
24
  import '@tiptap/pm/model';
25
25
  import '@tiptap/pm/state';
26
26
 
@@ -248,6 +248,16 @@ interface MinutaEditorOptions {
248
248
  src: string;
249
249
  vaultSrc: string;
250
250
  } | null>) | null;
251
+ /**
252
+ * O que fazer quando `uploadImage` falha (lança ou devolve `null`) ou não
253
+ * existe. `'dataUri'` (default) embute a imagem em base64 — conveniência
254
+ * do editor sem backend. `'none'` não insere nada: o host recebe
255
+ * `onImageUploadError` e decide (mostrar erro, tentar de novo). Um host
256
+ * cujo documento canônico depende de `vault://` quer `'none'`.
257
+ */
258
+ imageFallback?: 'dataUri' | 'none';
259
+ /** o upload da imagem da toolbar falhou (ou devolveu `null`); com `imageFallback: 'none'` nada foi inserido */
260
+ onImageUploadError?: (error: unknown, file: File) => void;
251
261
  /** barra de formatação (default: true). `false` = o host monta a dele sobre `tiptap` */
252
262
  toolbar?: boolean;
253
263
  /** realce de proveniência de bloco no hover (só faz sentido no modo estruturado) */
@@ -321,16 +331,24 @@ interface MinutaEditor {
321
331
  /**
322
332
  * Troca a imagem de uma variável de imagem: o `<img data-variable-id>` ganha
323
333
  * o `src` novo, e um aviso de imagem faltando com esse id vira a imagem.
324
- * Devolve quantas ocorrências mudaram.
334
+ * `vaultSrc` (a `vault://` durável) entra como `data-vault-src`: é o que
335
+ * `refreshVaultImages` renova e o que o DOCX do servidor usa quando o `src`
336
+ * presignado expira. Omiti-lo REMOVE a ref durável que estivesse na tag —
337
+ * ela descrevia a imagem antiga. Devolve quantas ocorrências mudaram.
325
338
  */
326
- setVariableImage: (variableId: string, src: string) => number;
339
+ setVariableImage: (variableId: string, src: string, vaultSrc?: string) => number;
340
+ /** troca o modo de fallback do upload de imagem depois de criado (ver `imageFallback`) */
341
+ setImageFallback: (mode: 'dataUri' | 'none') => void;
327
342
  /** ids marcados no documento, na ordem, sem repetição: chips de texto, imagens e imagens faltando */
328
343
  variablesInDocument: () => string[];
329
344
  /**
330
- * Renova o `src` das imagens inseridas no editor (as que têm `data-vault-src`):
331
- * o presignado do upload expira, então quem reabre uma edição salva passa
332
- * um resolvedor `vault://` → URL nova (no defesa.ai, `POST /data/vault/download-url`).
333
- * Devolve quantos nós mudaram. Não entra no undo nem dispara `onChange`.
345
+ * Renova o `src` das imagens do vault: as que têm `data-vault-src` (inseridas
346
+ * no editor, presignado expirado) e as salvas com `src="vault://…"` (a forma
347
+ * durável, que o browser não abre — ganham `data-vault-src` e um `src` novo).
348
+ * Quem reabre uma edição salva passa um resolvedor `vault://` → URL nova (no
349
+ * defesa.ai, `POST /data/vault/download-url`). Devolve quantos nós mudaram.
350
+ * Não entra no undo nem dispara `onChange`; chame `getHTML()` depois se
351
+ * guarda uma cópia do documento.
334
352
  */
335
353
  refreshVaultImages: (resolveUrl: (vaultUrl: string) => Promise<string>) => Promise<number>;
336
354
  /** repagina (o host chama após mudar algo que afeta altura fora do editor) */
@@ -442,8 +460,9 @@ interface RefreshVaultImagesOptions {
442
460
  /**
443
461
  * Re-resolve todas as imagens `vault://` do documento: as inseridas pela
444
462
  * toolbar entram com `src` presignado (funciona agora) e `data-vault-src`
445
- * durável; como o presignado expira, quem reabre uma edição salva chama isto
446
- * no load. `resolveUrl` recebe a `vault://` e devolve a URL nova (no defesa.ai,
463
+ * durável, e uma edição salva pode trazer só `src="vault://…"`. Como o
464
+ * presignado expira, quem reabre uma edição salva chama isto no load.
465
+ * `resolveUrl` recebe a `vault://` e devolve a URL nova (no defesa.ai,
447
466
  * `POST /data/vault/download-url`). Devolve quantos nós mudaram.
448
467
  *
449
468
  * Fora do histórico de undo: renovar um link não é uma edição do advogado.
@@ -472,5 +491,18 @@ declare function escapeVaultAttr(value: string): string;
472
491
  declare function decodeVaultAttr(value: string): string;
473
492
  declare function vaultImageHtml(src: string, vaultUrl: string): string;
474
493
 
475
- export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPageConfig, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocUnderline, PocVariableChip, buildPocExtensions, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, iconSvg, refreshVaultImages, renumberPocFootnotes, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, variableChipHtml, vaultImageHtml };
494
+ /**
495
+ * A `vault://` de uma tag `<img>`: o `data-vault-src` (imagem inserida no
496
+ * editor, presignado + ref durável) ou um `src="vault://…"` (a forma durável
497
+ * salva sem presignado — o browser não abre). Decodificada; null se não há.
498
+ */
499
+ declare function vaultRefOf(tag: string): string | null;
500
+ /** `src` novo, mantendo (ou criando) o `data-vault-src` — a ref durável fica */
501
+ declare function swapVaultSrc(tag: string, vaultUrl: string, freshUrl: string): string;
502
+ /** todas as `vault://` referenciadas por `<img>` num trecho de HTML, sem repetição */
503
+ declare function collectVaultRefs(html: string): string[];
504
+ /** aplica as URLs resolvidas às `<img>` do trecho; tags sem resolução ficam como estão */
505
+ declare function renewVaultImageTags(html: string, resolved: Map<string, string>): string;
506
+
507
+ export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPageConfig, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocUnderline, PocVariableChip, buildPocExtensions, collectVaultRefs, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, iconSvg, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, swapVaultSrc, variableChipHtml, vaultImageHtml, vaultRefOf };
476
508
  export type { FootnoteRun, MinutaChromeText, MinutaEditor, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, PocKeepWithNextItem, RefreshVaultImagesOptions, VariableClickKind };
package/dist/index.d.ts CHANGED
@@ -19,8 +19,8 @@
19
19
  /// <reference types="@tiptap/extension-bubble-menu" />
20
20
  import { Node, Editor, Mark, Extension } from '@tiptap/core';
21
21
  import { EditorView } from '@tiptap/pm/view';
22
- import { P as PocPageConfig } from './shared/minuta-editor.e223a421.js';
23
- export { e as PAGINATE_META, a as PocDocxChrome, b as PocDocxExportOptions, q as PocLoneRow, d as PocPageBreak, c as PocPageNote, t as PocPaginationPlugin, s as breaksEqual, h as buildPocFooterEl, g as buildPocHeaderEl, j as buildPocNotesEl, k as computePocBreaks, r as computePocLoneRows, i as isPocChromeCanvas, l as isPocFlexRow, f as pocChromeFlowHeight, o as pocLoneCellIndex, m as pocNodeHasVisibleContent, n as pocNodePaintsBox, p as pocPageKey } from './shared/minuta-editor.e223a421.js';
22
+ import { P as PocPageConfig } from './shared/minuta-editor.9e65d163.js';
23
+ export { e as PAGINATE_META, a as PocDocxChrome, b as PocDocxExportOptions, s as PocLoneRow, d as PocPageBreak, c as PocPageNote, v as PocPaginationPlugin, u as breaksEqual, h as buildPocFooterEl, g as buildPocHeaderEl, j as buildPocNotesEl, l as computePocBreaks, t as computePocLoneRows, i as isPocChromeCanvas, n as isPocFlexRow, f as pocChromeFlowHeight, m as pocDescendsInto, r as pocLoneCellIndex, k as pocLooksLikeTitle, o as pocNodeHasVisibleContent, q as pocNodePaintsBox, p as pocPageKey } from './shared/minuta-editor.9e65d163.js';
24
24
  import '@tiptap/pm/model';
25
25
  import '@tiptap/pm/state';
26
26
 
@@ -248,6 +248,16 @@ interface MinutaEditorOptions {
248
248
  src: string;
249
249
  vaultSrc: string;
250
250
  } | null>) | null;
251
+ /**
252
+ * O que fazer quando `uploadImage` falha (lança ou devolve `null`) ou não
253
+ * existe. `'dataUri'` (default) embute a imagem em base64 — conveniência
254
+ * do editor sem backend. `'none'` não insere nada: o host recebe
255
+ * `onImageUploadError` e decide (mostrar erro, tentar de novo). Um host
256
+ * cujo documento canônico depende de `vault://` quer `'none'`.
257
+ */
258
+ imageFallback?: 'dataUri' | 'none';
259
+ /** o upload da imagem da toolbar falhou (ou devolveu `null`); com `imageFallback: 'none'` nada foi inserido */
260
+ onImageUploadError?: (error: unknown, file: File) => void;
251
261
  /** barra de formatação (default: true). `false` = o host monta a dele sobre `tiptap` */
252
262
  toolbar?: boolean;
253
263
  /** realce de proveniência de bloco no hover (só faz sentido no modo estruturado) */
@@ -321,16 +331,24 @@ interface MinutaEditor {
321
331
  /**
322
332
  * Troca a imagem de uma variável de imagem: o `<img data-variable-id>` ganha
323
333
  * o `src` novo, e um aviso de imagem faltando com esse id vira a imagem.
324
- * Devolve quantas ocorrências mudaram.
334
+ * `vaultSrc` (a `vault://` durável) entra como `data-vault-src`: é o que
335
+ * `refreshVaultImages` renova e o que o DOCX do servidor usa quando o `src`
336
+ * presignado expira. Omiti-lo REMOVE a ref durável que estivesse na tag —
337
+ * ela descrevia a imagem antiga. Devolve quantas ocorrências mudaram.
325
338
  */
326
- setVariableImage: (variableId: string, src: string) => number;
339
+ setVariableImage: (variableId: string, src: string, vaultSrc?: string) => number;
340
+ /** troca o modo de fallback do upload de imagem depois de criado (ver `imageFallback`) */
341
+ setImageFallback: (mode: 'dataUri' | 'none') => void;
327
342
  /** ids marcados no documento, na ordem, sem repetição: chips de texto, imagens e imagens faltando */
328
343
  variablesInDocument: () => string[];
329
344
  /**
330
- * Renova o `src` das imagens inseridas no editor (as que têm `data-vault-src`):
331
- * o presignado do upload expira, então quem reabre uma edição salva passa
332
- * um resolvedor `vault://` → URL nova (no defesa.ai, `POST /data/vault/download-url`).
333
- * Devolve quantos nós mudaram. Não entra no undo nem dispara `onChange`.
345
+ * Renova o `src` das imagens do vault: as que têm `data-vault-src` (inseridas
346
+ * no editor, presignado expirado) e as salvas com `src="vault://…"` (a forma
347
+ * durável, que o browser não abre — ganham `data-vault-src` e um `src` novo).
348
+ * Quem reabre uma edição salva passa um resolvedor `vault://` → URL nova (no
349
+ * defesa.ai, `POST /data/vault/download-url`). Devolve quantos nós mudaram.
350
+ * Não entra no undo nem dispara `onChange`; chame `getHTML()` depois se
351
+ * guarda uma cópia do documento.
334
352
  */
335
353
  refreshVaultImages: (resolveUrl: (vaultUrl: string) => Promise<string>) => Promise<number>;
336
354
  /** repagina (o host chama após mudar algo que afeta altura fora do editor) */
@@ -442,8 +460,9 @@ interface RefreshVaultImagesOptions {
442
460
  /**
443
461
  * Re-resolve todas as imagens `vault://` do documento: as inseridas pela
444
462
  * toolbar entram com `src` presignado (funciona agora) e `data-vault-src`
445
- * durável; como o presignado expira, quem reabre uma edição salva chama isto
446
- * no load. `resolveUrl` recebe a `vault://` e devolve a URL nova (no defesa.ai,
463
+ * durável, e uma edição salva pode trazer só `src="vault://…"`. Como o
464
+ * presignado expira, quem reabre uma edição salva chama isto no load.
465
+ * `resolveUrl` recebe a `vault://` e devolve a URL nova (no defesa.ai,
447
466
  * `POST /data/vault/download-url`). Devolve quantos nós mudaram.
448
467
  *
449
468
  * Fora do histórico de undo: renovar um link não é uma edição do advogado.
@@ -472,5 +491,18 @@ declare function escapeVaultAttr(value: string): string;
472
491
  declare function decodeVaultAttr(value: string): string;
473
492
  declare function vaultImageHtml(src: string, vaultUrl: string): string;
474
493
 
475
- export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPageConfig, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocUnderline, PocVariableChip, buildPocExtensions, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, iconSvg, refreshVaultImages, renumberPocFootnotes, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, variableChipHtml, vaultImageHtml };
494
+ /**
495
+ * A `vault://` de uma tag `<img>`: o `data-vault-src` (imagem inserida no
496
+ * editor, presignado + ref durável) ou um `src="vault://…"` (a forma durável
497
+ * salva sem presignado — o browser não abre). Decodificada; null se não há.
498
+ */
499
+ declare function vaultRefOf(tag: string): string | null;
500
+ /** `src` novo, mantendo (ou criando) o `data-vault-src` — a ref durável fica */
501
+ declare function swapVaultSrc(tag: string, vaultUrl: string, freshUrl: string): string;
502
+ /** todas as `vault://` referenciadas por `<img>` num trecho de HTML, sem repetição */
503
+ declare function collectVaultRefs(html: string): string[];
504
+ /** aplica as URLs resolvidas às `<img>` do trecho; tags sem resolução ficam como estão */
505
+ declare function renewVaultImageTags(html: string, resolved: Map<string, string>): string;
506
+
507
+ export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPageConfig, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocUnderline, PocVariableChip, buildPocExtensions, collectVaultRefs, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, footnoteHtmlToRuns, footnoteSizePt, iconSvg, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, shouldMoveBreakToHeading, swapVaultSrc, variableChipHtml, vaultImageHtml, vaultRefOf };
476
508
  export type { FootnoteRun, MinutaChromeText, MinutaEditor, MinutaEditorOptions, MinutaHeader, MinutaMargins, MinutaVariableCatalog, MinutaVariableDef, MinutaVariableRef, PocKeepWithNextItem, RefreshVaultImagesOptions, VariableClickKind };
package/dist/index.mjs CHANGED
@@ -22,8 +22,8 @@ import TableCell from '@tiptap/extension-table-cell';
22
22
  import TableHeader from '@tiptap/extension-table-header';
23
23
  import TableRow from '@tiptap/extension-table-row';
24
24
  import { Plugin } from '@tiptap/pm/state';
25
- import { s as sanitizeRichHtml, P as PocPaginationPlugin, a as sanitizeFootnoteHtml, c as computePocBreaks, b as pocPageKey, d as breaksEqual, e as PAGINATE_META } from './shared/minuta-editor.b2512170.mjs';
26
- export { j as buildPocFooterEl, h as buildPocHeaderEl, k as buildPocNotesEl, o as computePocLoneRows, f as footnoteHtmlToRuns, q as footnoteSizePt, i as isPocChromeCanvas, l as isPocFlexRow, g as pocChromeFlowHeight, p as pocLoneCellIndex, m as pocNodeHasVisibleContent, n as pocNodePaintsBox, r as shouldMoveBreakToHeading } from './shared/minuta-editor.b2512170.mjs';
25
+ import { s as sanitizeRichHtml, P as PocPaginationPlugin, b as sanitizeFootnoteHtml, c as computePocBreaks, d as pocPageKey, e as breaksEqual, g as PAGINATE_META } from './shared/minuta-editor.d9d0c0ce.mjs';
26
+ export { k as buildPocFooterEl, j as buildPocHeaderEl, l as buildPocNotesEl, r as computePocLoneRows, f as footnoteHtmlToRuns, t as footnoteSizePt, i as isPocChromeCanvas, n as isPocFlexRow, h as pocChromeFlowHeight, m as pocDescendsInto, p as pocLoneCellIndex, a as pocLooksLikeTitle, o as pocNodeHasVisibleContent, q as pocNodePaintsBox, u as shouldMoveBreakToHeading } from './shared/minuta-editor.d9d0c0ce.mjs';
27
27
  import '@tiptap/pm/view';
28
28
 
29
29
  const POC_ASSET_SWAP = "pocAssetSwap";
@@ -682,16 +682,39 @@ function vaultImageHtml(src, vaultUrl) {
682
682
  function escapeAttr(value) {
683
683
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
684
684
  }
685
- function escapeRegExp(value) {
686
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
685
+ function vaultRefOf(tag) {
686
+ const durable = tag.match(/\sdata-vault-src="([^"]*)"/)?.[1];
687
+ if (durable)
688
+ return decodeVaultAttr(durable);
689
+ const src = tag.match(/\ssrc="([^"]*)"/)?.[1];
690
+ if (src && decodeVaultAttr(src).startsWith("vault://"))
691
+ return decodeVaultAttr(src);
692
+ return null;
693
+ }
694
+ function swapVaultSrc(tag, vaultUrl, freshUrl) {
695
+ const fresh = ` src="${escapeAttr(freshUrl)}"`;
696
+ const withSrc = /\ssrc="[^"]*"/.test(tag) ? tag.replace(/\ssrc="[^"]*"/, fresh) : tag.replace(/^<img\b/, `<img${fresh}`);
697
+ if (/\sdata-vault-src="/.test(withSrc))
698
+ return withSrc;
699
+ return withSrc.replace(/^<img\b/, `<img data-vault-src="${escapeVaultAttr(vaultUrl)}"`);
687
700
  }
688
- function swapSrcKeepRef(html, attrValue, freshUrl) {
689
- const needle = `data-vault-src="${attrValue}"`;
690
- if (!html.includes(needle))
691
- return html;
692
- const tagRe = new RegExp(`<img\\b[^>]*?${escapeRegExp(needle)}[^>]*?>`, "g");
693
- return html.replace(tagRe, (tag) => tag.replace(/\ssrc="[^"]*"/, ` src="${escapeAttr(freshUrl)}"`));
701
+ function collectVaultRefs(html) {
702
+ const refs = /* @__PURE__ */ new Set();
703
+ for (const [tag] of html.matchAll(/<img\b[^>]*>/g)) {
704
+ const ref = vaultRefOf(tag);
705
+ if (ref)
706
+ refs.add(ref);
707
+ }
708
+ return [...refs];
694
709
  }
710
+ function renewVaultImageTags(html, resolved) {
711
+ return html.replace(/<img\b[^>]*>/g, (tag) => {
712
+ const ref = vaultRefOf(tag);
713
+ const fresh = ref ? resolved.get(ref) : void 0;
714
+ return ref && fresh ? swapVaultSrc(tag, ref, fresh) : tag;
715
+ });
716
+ }
717
+
695
718
  async function refreshVaultImages(editor, resolveUrl, options = {}) {
696
719
  if (!editor || editor.isDestroyed)
697
720
  return 0;
@@ -699,15 +722,15 @@ async function refreshVaultImages(editor, resolveUrl, options = {}) {
699
722
  editor.state.doc.descendants((node) => {
700
723
  if (node.type.name !== "htmlIsland")
701
724
  return;
702
- for (const m of String(node.attrs.html ?? "").matchAll(/data-vault-src="([^"]+)"/g))
703
- refs.add(m[1]);
725
+ for (const ref of collectVaultRefs(String(node.attrs.html ?? "")))
726
+ refs.add(ref);
704
727
  });
705
728
  if (!refs.size)
706
729
  return 0;
707
730
  const resolved = /* @__PURE__ */ new Map();
708
- await Promise.all([...refs].map(async (attrValue) => {
731
+ await Promise.all([...refs].map(async (vaultUrl) => {
709
732
  try {
710
- const fresh = await resolveUrl(decodeVaultAttr(attrValue));
733
+ const fresh = await resolveUrl(vaultUrl);
711
734
  if (!fresh)
712
735
  return;
713
736
  await new Promise((done) => {
@@ -718,9 +741,9 @@ async function refreshVaultImages(editor, resolveUrl, options = {}) {
718
741
  setTimeout(finish, 4e3);
719
742
  img.src = fresh;
720
743
  });
721
- resolved.set(attrValue, fresh);
744
+ resolved.set(vaultUrl, fresh);
722
745
  } catch (error) {
723
- console.error("minuta: falha ao resolver imagem do vault", attrValue, error);
746
+ console.error("minuta: falha ao resolver imagem do vault", vaultUrl, error);
724
747
  }
725
748
  }));
726
749
  if (!resolved.size || editor.isDestroyed)
@@ -730,17 +753,10 @@ async function refreshVaultImages(editor, resolveUrl, options = {}) {
730
753
  editor.state.doc.descendants((node, pos) => {
731
754
  if (node.type.name !== "htmlIsland")
732
755
  return;
733
- let html = String(node.attrs.html ?? "");
734
- let changed = false;
735
- for (const [attrValue, freshUrl] of resolved) {
736
- const next = swapSrcKeepRef(html, attrValue, freshUrl);
737
- if (next !== html) {
738
- html = next;
739
- changed = true;
740
- }
741
- }
742
- if (changed) {
743
- tr.setNodeMarkup(pos, void 0, { ...node.attrs, html });
756
+ const html = String(node.attrs.html ?? "");
757
+ const next = renewVaultImageTags(html, resolved);
758
+ if (next !== html) {
759
+ tr.setNodeMarkup(pos, void 0, { ...node.attrs, html: next });
744
760
  touched++;
745
761
  }
746
762
  });
@@ -1577,6 +1593,14 @@ function createMinutaEditor(element, opts) {
1577
1593
  };
1578
1594
  reader.readAsDataURL(file);
1579
1595
  }
1596
+ let imageFallback = opts.imageFallback ?? "dataUri";
1597
+ function onImageUploadFailed(error, file) {
1598
+ opts.onImageUploadError?.(error, file);
1599
+ if (imageFallback === "none")
1600
+ return;
1601
+ console.error("[minuta-editor] upload de imagem falhou, embutindo como data URI", error);
1602
+ insertDataUriImage(file);
1603
+ }
1580
1604
  function onImageFile(event) {
1581
1605
  const input = event.target;
1582
1606
  const file = input.files?.[0];
@@ -1590,13 +1614,13 @@ function createMinutaEditor(element, opts) {
1590
1614
  tiptap?.chain().focus().insertContent(vaultImageHtml(uploaded.src, uploaded.vaultSrc)).run();
1591
1615
  return;
1592
1616
  }
1617
+ onImageUploadFailed(new Error("uploadImage returned null"), file);
1593
1618
  } catch (error) {
1594
- console.error("[minuta-editor] upload de imagem falhou, embutindo como data URI", error);
1619
+ onImageUploadFailed(error, file);
1595
1620
  }
1596
- insertDataUriImage(file);
1597
1621
  })();
1598
1622
  } else {
1599
- insertDataUriImage(file);
1623
+ onImageUploadFailed(new Error("no uploadImage configured"), file);
1600
1624
  }
1601
1625
  input.value = "";
1602
1626
  }
@@ -1797,7 +1821,7 @@ function createMinutaEditor(element, opts) {
1797
1821
  ed.view.dispatch(tr);
1798
1822
  return count;
1799
1823
  }
1800
- function setVariableImage(variableId, src) {
1824
+ function setVariableImage(variableId, src, vaultSrc) {
1801
1825
  const ed = tiptap;
1802
1826
  if (!ed || ed.isDestroyed)
1803
1827
  return 0;
@@ -1815,7 +1839,8 @@ function createMinutaEditor(element, opts) {
1815
1839
  if (node.type.name === "missingImage") {
1816
1840
  if (!islandType)
1817
1841
  continue;
1818
- const html = `<img src="${escapeAttr(src)}" alt="${escapeAttr(variableId)}" data-variable-id="${escapeAttr(variableId)}" data-parent-block-id="${escapeAttr(String(node.attrs.parentBlockId ?? ""))}" style="max-width: 100%; height: auto;" />`;
1842
+ const vaultAttr = vaultSrc ? ` data-vault-src="${escapeVaultAttr(vaultSrc)}"` : "";
1843
+ const html = `<img src="${escapeAttr(src)}"${vaultAttr} alt="${escapeAttr(variableId)}" data-variable-id="${escapeAttr(variableId)}" data-parent-block-id="${escapeAttr(String(node.attrs.parentBlockId ?? ""))}" style="max-width: 100%; height: auto;" />`;
1819
1844
  tr.replaceWith(pos, pos + node.nodeSize, islandType.create({ html }));
1820
1845
  count++;
1821
1846
  continue;
@@ -1825,8 +1850,13 @@ function createMinutaEditor(element, opts) {
1825
1850
  const imgs = tpl.content.querySelectorAll(`img[data-variable-id="${CSS.escape(variableId)}"]`);
1826
1851
  if (!imgs.length)
1827
1852
  continue;
1828
- for (const img of imgs)
1853
+ for (const img of imgs) {
1829
1854
  img.setAttribute("src", src);
1855
+ if (vaultSrc)
1856
+ img.setAttribute("data-vault-src", vaultSrc);
1857
+ else
1858
+ img.removeAttribute("data-vault-src");
1859
+ }
1830
1860
  tr.setNodeMarkup(pos, void 0, { ...node.attrs, html: tpl.innerHTML });
1831
1861
  count += imgs.length;
1832
1862
  }
@@ -2032,6 +2062,9 @@ function createMinutaEditor(element, opts) {
2032
2062
  scrollToVariable,
2033
2063
  setVariableValue,
2034
2064
  setVariableImage,
2065
+ setImageFallback(mode) {
2066
+ imageFallback = mode;
2067
+ },
2035
2068
  variablesInDocument,
2036
2069
  refreshVaultImages(resolveUrl) {
2037
2070
  const ed = tiptap;
@@ -2058,4 +2091,4 @@ function createMinutaEditor(element, opts) {
2058
2091
  return api;
2059
2092
  }
2060
2093
 
2061
- export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, PAGINATE_META, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPaginationPlugin, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocUnderline, PocVariableChip, breaksEqual, buildPocExtensions, computePocBreaks, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, iconSvg, pocPageKey, refreshVaultImages, renumberPocFootnotes, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, variableChipHtml, vaultImageHtml };
2094
+ export { A4_HEIGHT_PX, A4_WIDTH_PX, DEFAULT_MARGINS, DOC_FONTS, ICON_PATHS, PAGINATE_META, POC_ASSET_SWAP, PocDocument, PocFootnoteRef, PocHtmlIsland, PocInlineStyle, PocMinutaBlock, PocMissingImage, PocPaginationPlugin, PocStylePassthrough, PocStyledDiv, PocTemplateVariable, PocUnderline, PocVariableChip, breaksEqual, buildPocExtensions, collectVaultRefs, computePocBreaks, createMinutaEditor, decodeVaultAttr, dispatchVariableClick, escapeHtml, escapeVaultAttr, extractTemplateVariables, iconSvg, pocPageKey, refreshVaultImages, renewVaultImageTags, renumberPocFootnotes, resolveTemplateVariables, sanitizeFootnoteHtml, sanitizeRichHtml, swapVaultSrc, variableChipHtml, vaultImageHtml, vaultRefOf };
@@ -19,11 +19,15 @@ import { PluginKey, Plugin } from '@tiptap/pm/state';
19
19
  * algoritmo sempre enxerga o fluxo "sem paginação" e converge (recomputar
20
20
  * com chrome na tela produz os mesmos breaks → sem loop de layout).
21
21
  *
22
- * Regras de quebra:
23
- * - quebra somente entre nós (parágrafo/título/ilha) — nó mais alto que a
24
- * página flui e as páginas internas são contadas, sem chrome no meio;
25
- * - keep-with-next: título não fica órfão no fim da página (a quebra sobe
26
- * para antes do título).
22
+ * Regras de quebra (as mesmas que o Word aplica ao refluir o documento):
23
+ * - a unidade é o bloco-FOLHA: parágrafo, título, ilha (imagem/lista/svg),
24
+ * linha de colunas, linha de tabela. Citação, container simples e lista
25
+ * real são só recipientes — a quebra entra neles e cai entre os filhos;
26
+ * - parágrafo parte entre linhas (viúvas/órfãs de 2 linhas), como no Word;
27
+ * - nó-folha mais alto que a página flui e as páginas internas são contadas,
28
+ * sem chrome no meio;
29
+ * - keep-with-next: título (h1–h4 ou parágrafo curto em negrito/caixa alta)
30
+ * não fica órfão no fim da página — a quebra sobe para antes dele.
27
31
  */
28
32
  interface PocPageConfig {
29
33
  pageHeight: number;
@@ -74,6 +78,13 @@ interface PocPageBreak {
74
78
  * Word quebra naturalmente, e o preview só antecipa onde.
75
79
  */
76
80
  soft?: boolean;
81
+ /**
82
+ * quebra pedida pelo TEMPLATE (parágrafo-marcador "PAGEBREAK"). É a única
83
+ * que vira pageBreakBefore no DOCX: as demais são só a previsão do preview
84
+ * de onde o Word vai quebrar, e o Word decide sozinho — forçá-las deixava
85
+ * o pé da página vazio sempre que ele media o conteúdo diferente daqui.
86
+ */
87
+ manual?: boolean;
77
88
  /** notas cujos marcadores estão nesta página — desenhadas acima do rodapé */
78
89
  notes?: PocPageNote[];
79
90
  /** altura reservada pelas notas (já descontada do filler) */
@@ -100,6 +111,14 @@ declare function buildPocHeaderEl(cfg: PocPageConfig): HTMLElement;
100
111
  declare function buildPocFooterEl(cfg: PocPageConfig, pageNo: number, total: number): HTMLElement;
101
112
  /** área de notas de rodapé: filete + notas, no pé da área útil (como no Word) */
102
113
  declare function buildPocNotesEl(notes: PocPageNote[]): HTMLElement;
114
+ /**
115
+ * O bloco é um título, para efeito de keep-with-next? h1–h4, ou um parágrafo
116
+ * de UMA linha, curto, todo em negrito ou em caixa alta — é assim que os
117
+ * templates marcam seções ("PRELIMINARMENTE", "DO MÉRITO"): com `<p>`
118
+ * estilizado, não com heading. O export usa a mesma régua para pedir ao Word
119
+ * que mantenha o título junto do parágrafo seguinte (keepNext).
120
+ */
121
+ declare function pocLooksLikeTitle(dom: HTMLElement): boolean;
103
122
  declare function computePocBreaks(view: EditorView, cfg: PocPageConfig, sheet: HTMLElement): {
104
123
  breaks: PocPageBreak[];
105
124
  total: number;
@@ -109,6 +128,13 @@ declare function computePocBreaks(view: EditorView, cfg: PocPageConfig, sheet: H
109
128
  }[];
110
129
  lastNotes: PocPageNote[];
111
130
  };
131
+ /**
132
+ * O nó é só um RECIPIENTE para a paginação — a quebra entra nele e cai entre
133
+ * os filhos? Citação, lista real, tabela (entre linhas) e container simples
134
+ * sem borda nem fundo. Linha de colunas e caixa pintada (borda/fundo) são
135
+ * unidades visuais: partem só se forem mais altas que a página.
136
+ */
137
+ declare function pocDescendsInto(node: Node): boolean;
112
138
  declare function isPocFlexRow(node: Node): boolean;
113
139
  /** conteúdo visível = texto, imagem com src preenchido ou svg (imgs vazias do pipeline não contam) */
114
140
  declare function pocNodeHasVisibleContent(node: Node): boolean;
@@ -116,7 +142,7 @@ declare function pocNodeHasVisibleContent(node: Node): boolean;
116
142
  * A célula/linha PINTA alguma coisa (borda ou fundo)?
117
143
  *
118
144
  * Uma coluna sem texto mas com borda não é espaço sobrando: é estrutura. A
119
- * folha de rosto da Telefônica desenha uma timeline com `border-right` numa
145
+ * folha de rosto de um template desenha uma timeline com `border-right` numa
120
146
  * coluna VAZIA (o eixo central) e `border-bottom` alternando entre as duas —
121
147
  * colapsar a coluna "vazia" apaga o eixo e centraliza o que devia ficar
122
148
  * alternando esquerda/direita.
@@ -162,5 +188,5 @@ declare function pocExportDocx(editor: Editor, margins: {
162
188
  left: number;
163
189
  }, chrome?: PocDocxChrome, options?: PocDocxExportOptions): Promise<Blob>;
164
190
 
165
- export { PAGINATE_META as e, pocChromeFlowHeight as f, buildPocHeaderEl as g, buildPocFooterEl as h, isPocChromeCanvas as i, buildPocNotesEl as j, computePocBreaks as k, isPocFlexRow as l, pocNodeHasVisibleContent as m, pocNodePaintsBox as n, pocLoneCellIndex as o, pocPageKey as p, computePocLoneRows as r, breaksEqual as s, PocPaginationPlugin as t, pocExportDocx as u };
166
- export type { PocPageConfig as P, PocDocxChrome as a, PocDocxExportOptions as b, PocPageNote as c, PocPageBreak as d, PocLoneRow as q };
191
+ export { PAGINATE_META as e, pocChromeFlowHeight as f, buildPocHeaderEl as g, buildPocFooterEl as h, isPocChromeCanvas as i, buildPocNotesEl as j, pocLooksLikeTitle as k, computePocBreaks as l, pocDescendsInto as m, isPocFlexRow as n, pocNodeHasVisibleContent as o, pocPageKey as p, pocNodePaintsBox as q, pocLoneCellIndex as r, computePocLoneRows as t, breaksEqual as u, PocPaginationPlugin as v, pocExportDocx as w };
192
+ export type { PocPageConfig as P, PocDocxChrome as a, PocDocxExportOptions as b, PocPageNote as c, PocPageBreak as d, PocLoneRow as s };
@@ -19,11 +19,15 @@ import { PluginKey, Plugin } from '@tiptap/pm/state';
19
19
  * algoritmo sempre enxerga o fluxo "sem paginação" e converge (recomputar
20
20
  * com chrome na tela produz os mesmos breaks → sem loop de layout).
21
21
  *
22
- * Regras de quebra:
23
- * - quebra somente entre nós (parágrafo/título/ilha) — nó mais alto que a
24
- * página flui e as páginas internas são contadas, sem chrome no meio;
25
- * - keep-with-next: título não fica órfão no fim da página (a quebra sobe
26
- * para antes do título).
22
+ * Regras de quebra (as mesmas que o Word aplica ao refluir o documento):
23
+ * - a unidade é o bloco-FOLHA: parágrafo, título, ilha (imagem/lista/svg),
24
+ * linha de colunas, linha de tabela. Citação, container simples e lista
25
+ * real são só recipientes — a quebra entra neles e cai entre os filhos;
26
+ * - parágrafo parte entre linhas (viúvas/órfãs de 2 linhas), como no Word;
27
+ * - nó-folha mais alto que a página flui e as páginas internas são contadas,
28
+ * sem chrome no meio;
29
+ * - keep-with-next: título (h1–h4 ou parágrafo curto em negrito/caixa alta)
30
+ * não fica órfão no fim da página — a quebra sobe para antes dele.
27
31
  */
28
32
  interface PocPageConfig {
29
33
  pageHeight: number;
@@ -74,6 +78,13 @@ interface PocPageBreak {
74
78
  * Word quebra naturalmente, e o preview só antecipa onde.
75
79
  */
76
80
  soft?: boolean;
81
+ /**
82
+ * quebra pedida pelo TEMPLATE (parágrafo-marcador "PAGEBREAK"). É a única
83
+ * que vira pageBreakBefore no DOCX: as demais são só a previsão do preview
84
+ * de onde o Word vai quebrar, e o Word decide sozinho — forçá-las deixava
85
+ * o pé da página vazio sempre que ele media o conteúdo diferente daqui.
86
+ */
87
+ manual?: boolean;
77
88
  /** notas cujos marcadores estão nesta página — desenhadas acima do rodapé */
78
89
  notes?: PocPageNote[];
79
90
  /** altura reservada pelas notas (já descontada do filler) */
@@ -100,6 +111,14 @@ declare function buildPocHeaderEl(cfg: PocPageConfig): HTMLElement;
100
111
  declare function buildPocFooterEl(cfg: PocPageConfig, pageNo: number, total: number): HTMLElement;
101
112
  /** área de notas de rodapé: filete + notas, no pé da área útil (como no Word) */
102
113
  declare function buildPocNotesEl(notes: PocPageNote[]): HTMLElement;
114
+ /**
115
+ * O bloco é um título, para efeito de keep-with-next? h1–h4, ou um parágrafo
116
+ * de UMA linha, curto, todo em negrito ou em caixa alta — é assim que os
117
+ * templates marcam seções ("PRELIMINARMENTE", "DO MÉRITO"): com `<p>`
118
+ * estilizado, não com heading. O export usa a mesma régua para pedir ao Word
119
+ * que mantenha o título junto do parágrafo seguinte (keepNext).
120
+ */
121
+ declare function pocLooksLikeTitle(dom: HTMLElement): boolean;
103
122
  declare function computePocBreaks(view: EditorView, cfg: PocPageConfig, sheet: HTMLElement): {
104
123
  breaks: PocPageBreak[];
105
124
  total: number;
@@ -109,6 +128,13 @@ declare function computePocBreaks(view: EditorView, cfg: PocPageConfig, sheet: H
109
128
  }[];
110
129
  lastNotes: PocPageNote[];
111
130
  };
131
+ /**
132
+ * O nó é só um RECIPIENTE para a paginação — a quebra entra nele e cai entre
133
+ * os filhos? Citação, lista real, tabela (entre linhas) e container simples
134
+ * sem borda nem fundo. Linha de colunas e caixa pintada (borda/fundo) são
135
+ * unidades visuais: partem só se forem mais altas que a página.
136
+ */
137
+ declare function pocDescendsInto(node: Node): boolean;
112
138
  declare function isPocFlexRow(node: Node): boolean;
113
139
  /** conteúdo visível = texto, imagem com src preenchido ou svg (imgs vazias do pipeline não contam) */
114
140
  declare function pocNodeHasVisibleContent(node: Node): boolean;
@@ -116,7 +142,7 @@ declare function pocNodeHasVisibleContent(node: Node): boolean;
116
142
  * A célula/linha PINTA alguma coisa (borda ou fundo)?
117
143
  *
118
144
  * Uma coluna sem texto mas com borda não é espaço sobrando: é estrutura. A
119
- * folha de rosto da Telefônica desenha uma timeline com `border-right` numa
145
+ * folha de rosto de um template desenha uma timeline com `border-right` numa
120
146
  * coluna VAZIA (o eixo central) e `border-bottom` alternando entre as duas —
121
147
  * colapsar a coluna "vazia" apaga o eixo e centraliza o que devia ficar
122
148
  * alternando esquerda/direita.
@@ -162,5 +188,5 @@ declare function pocExportDocx(editor: Editor, margins: {
162
188
  left: number;
163
189
  }, chrome?: PocDocxChrome, options?: PocDocxExportOptions): Promise<Blob>;
164
190
 
165
- export { PAGINATE_META as e, pocChromeFlowHeight as f, buildPocHeaderEl as g, buildPocFooterEl as h, isPocChromeCanvas as i, buildPocNotesEl as j, computePocBreaks as k, isPocFlexRow as l, pocNodeHasVisibleContent as m, pocNodePaintsBox as n, pocLoneCellIndex as o, pocPageKey as p, computePocLoneRows as r, breaksEqual as s, PocPaginationPlugin as t, pocExportDocx as u };
166
- export type { PocPageConfig as P, PocDocxChrome as a, PocDocxExportOptions as b, PocPageNote as c, PocPageBreak as d, PocLoneRow as q };
191
+ export { PAGINATE_META as e, pocChromeFlowHeight as f, buildPocHeaderEl as g, buildPocFooterEl as h, isPocChromeCanvas as i, buildPocNotesEl as j, pocLooksLikeTitle as k, computePocBreaks as l, pocDescendsInto as m, isPocFlexRow as n, pocNodeHasVisibleContent as o, pocPageKey as p, pocNodePaintsBox as q, pocLoneCellIndex as r, computePocLoneRows as t, breaksEqual as u, PocPaginationPlugin as v, pocExportDocx as w };
192
+ export type { PocPageConfig as P, PocDocxChrome as a, PocDocxExportOptions as b, PocPageNote as c, PocPageBreak as d, PocLoneRow as s };
@@ -223,7 +223,7 @@ function chromeContentEl(html, cfg) {
223
223
  if (isPocChromeCanvas(clean)) {
224
224
  const canvas = document.createElement("div");
225
225
  canvas.className = "poc-chrome-canvas";
226
- canvas.style.cssText = `position:absolute;top:0;left:${-cfg.margins.left}px;width:794px;height:100%;pointer-events:none;`;
226
+ canvas.style.cssText = `position:absolute;top:0;left:0;width:794px;height:100%;pointer-events:none;`;
227
227
  canvas.innerHTML = clean;
228
228
  return canvas;
229
229
  }
@@ -237,6 +237,7 @@ function buildPocHeaderEl(cfg) {
237
237
  header.className = "poc-page-headerarea";
238
238
  header.style.height = `${cfg.margins.top}px`;
239
239
  header.style.position = "relative";
240
+ padToMargins(header, cfg);
240
241
  header.style.justifyContent = cfg.headerPosition === "center" ? "center" : cfg.headerPosition === "right" ? "flex-end" : "flex-start";
241
242
  header.title = "Clique para editar o cabe\xE7alho";
242
243
  header.addEventListener("click", editHint("header"));
@@ -250,7 +251,7 @@ function buildPocHeaderEl(cfg) {
250
251
  img.style.display = "inline-block";
251
252
  header.appendChild(img);
252
253
  }
253
- const content = chromeContentEl(cfg.headerText, cfg);
254
+ const content = chromeContentEl(cfg.headerText);
254
255
  if (content.className === "poc-chrome-text" && !content.innerHTML && !cfg.headerLogoUrl)
255
256
  content.innerHTML = "&nbsp;";
256
257
  header.appendChild(content);
@@ -261,10 +262,11 @@ function buildPocFooterEl(cfg, pageNo, total) {
261
262
  footer.className = "poc-page-footerarea";
262
263
  footer.style.height = `${cfg.margins.bottom}px`;
263
264
  footer.style.position = "relative";
265
+ padToMargins(footer, cfg);
264
266
  footer.title = "Clique para editar o rodap\xE9";
265
267
  footer.addEventListener("click", editHint("footer"));
266
268
  const showCustom = !(cfg.footerScope === "first" && pageNo !== 1);
267
- const custom = showCustom ? chromeContentEl(cfg.footerText, cfg) : document.createElement("div");
269
+ const custom = showCustom ? chromeContentEl(cfg.footerText) : document.createElement("div");
268
270
  if (!showCustom)
269
271
  custom.className = "poc-chrome-text";
270
272
  footer.appendChild(custom);
@@ -290,26 +292,81 @@ function buildPocNotesEl(notes) {
290
292
  }
291
293
  return box;
292
294
  }
295
+ function padToMargins(el, cfg) {
296
+ el.style.boxSizing = "border-box";
297
+ el.style.paddingLeft = `${cfg.margins.left}px`;
298
+ el.style.paddingRight = `${cfg.margins.right}px`;
299
+ }
293
300
  function buildChrome(cfg, brk, total) {
294
301
  const chrome = document.createElement("div");
295
302
  chrome.className = "poc-page-chrome";
296
303
  chrome.contentEditable = "false";
297
304
  chrome.style.height = `${brk.filler + (brk.notesH ?? 0) + pocChromeFlowHeight(cfg)}px`;
305
+ chrome.style.boxSizing = "border-box";
306
+ chrome.style.width = `calc(100% + ${cfg.margins.left + cfg.margins.right}px)`;
307
+ chrome.style.marginLeft = `-${cfg.margins.left}px`;
298
308
  const filler = document.createElement("div");
299
309
  filler.style.height = `${brk.filler}px`;
300
310
  chrome.appendChild(filler);
301
- if (brk.notes?.length)
302
- chrome.appendChild(buildPocNotesEl(brk.notes));
311
+ if (brk.notes?.length) {
312
+ const notes = buildPocNotesEl(brk.notes);
313
+ padToMargins(notes, cfg);
314
+ chrome.appendChild(notes);
315
+ }
303
316
  chrome.appendChild(buildPocFooterEl(cfg, brk.pageNo, total));
304
317
  const band = document.createElement("div");
305
318
  band.className = "poc-page-gapband";
306
319
  band.style.height = `${cfg.gap}px`;
307
- band.style.marginLeft = `-${cfg.margins.left}px`;
308
- band.style.marginRight = `-${cfg.margins.right}px`;
309
320
  chrome.appendChild(band);
310
321
  chrome.appendChild(buildPocHeaderEl(cfg));
322
+ queueMicrotask(() => alignChromeToSheet(chrome));
311
323
  return chrome;
312
324
  }
325
+ function alignChromeToSheet(chrome) {
326
+ if (!chrome.isConnected)
327
+ return;
328
+ const sheet = chrome.closest(".poc-sheet");
329
+ if (!sheet)
330
+ return;
331
+ const currentMl = Number.parseFloat(chrome.style.marginLeft) || 0;
332
+ const naturalLeft = absLeft(chrome, sheet) - currentMl;
333
+ const ml = -naturalLeft;
334
+ if (Math.abs(ml - currentMl) > 0.5)
335
+ chrome.style.marginLeft = `${ml}px`;
336
+ chrome.style.width = `${sheet.clientWidth}px`;
337
+ }
338
+ function buildChromeWidget(view, cfg, brk, total) {
339
+ const chrome = buildChrome(cfg, brk, total);
340
+ let table = null;
341
+ try {
342
+ const parent = view.state.doc.resolve(brk.pos).parent;
343
+ if (parent.type.name === "table")
344
+ table = parent;
345
+ } catch {
346
+ table = null;
347
+ }
348
+ if (!table)
349
+ return chrome;
350
+ let cols = 0;
351
+ table.firstChild?.forEach((cell2) => {
352
+ cols += Math.max(1, Number(cell2.attrs.colspan) || 1);
353
+ });
354
+ const row = document.createElement("tr");
355
+ row.className = "poc-page-chrome-row";
356
+ row.contentEditable = "false";
357
+ const cell = document.createElement("td");
358
+ cell.className = "poc-page-chrome-cell";
359
+ cell.colSpan = Math.max(1, cols);
360
+ cell.appendChild(chrome);
361
+ row.appendChild(cell);
362
+ return row;
363
+ }
364
+ function pocChromeHeightInside(el) {
365
+ let sum = 0;
366
+ for (const chrome of el.querySelectorAll(".poc-page-chrome"))
367
+ sum += chrome.offsetHeight;
368
+ return sum;
369
+ }
313
370
  function absTop(el, sheet) {
314
371
  let top = 0;
315
372
  let node = el;
@@ -319,7 +376,35 @@ function absTop(el, sheet) {
319
376
  }
320
377
  return top;
321
378
  }
379
+ function absLeft(el, sheet) {
380
+ let left = 0;
381
+ let node = el;
382
+ while (node && node !== sheet) {
383
+ left += node.offsetLeft;
384
+ node = node.offsetParent;
385
+ }
386
+ return left;
387
+ }
322
388
  const HEADING_TAGS = /* @__PURE__ */ new Set(["H1", "H2", "H3", "H4"]);
389
+ function pocLooksLikeTitle(dom) {
390
+ if (HEADING_TAGS.has(dom.tagName))
391
+ return true;
392
+ if (dom.tagName !== "P")
393
+ return false;
394
+ const text = (dom.textContent || "").trim();
395
+ if (!text || text.length > 120)
396
+ return false;
397
+ const cs = getComputedStyle(dom);
398
+ const fontPx = Number.parseFloat(cs.fontSize) || 16;
399
+ const lineHpx = cs.lineHeight === "normal" ? fontPx * 1.3 : Number.parseFloat(cs.lineHeight) || fontPx * 1.3;
400
+ if (dom.offsetHeight > lineHpx * 1.6)
401
+ return false;
402
+ const boldChars = Array.from(dom.querySelectorAll("b, strong")).reduce((n, el) => n + (el.textContent || "").trim().length, 0);
403
+ const bold = (Number.parseInt(cs.fontWeight) || 400) >= 600 || boldChars >= text.length * 0.9;
404
+ const letters = text.replace(/[^\p{L}]/gu, "");
405
+ const upper = cs.textTransform === "uppercase" || letters.length >= 4 && letters === letters.toUpperCase();
406
+ return bold || upper;
407
+ }
323
408
  const SPLITTABLE_TAGS = /* @__PURE__ */ new Set(["P"]);
324
409
  const MIN_LINES_PER_SIDE = 2;
325
410
  function findInlineSplit(view, dom, availablePx, fromLine = 0, minHead = MIN_LINES_PER_SIDE, minTail = MIN_LINES_PER_SIDE) {
@@ -333,21 +418,28 @@ function findInlineSplit(view, dom, availablePx, fromLine = 0, minHead = MIN_LIN
333
418
  } catch {
334
419
  return null;
335
420
  }
336
- if (rects.length < minHead + minTail)
421
+ const chromes = Array.from(dom.querySelectorAll(".poc-page-chrome")).map((c) => c.getBoundingClientRect()).filter((r) => r.height > 0);
422
+ const insideChrome = (r) => chromes.some((c) => r.top >= c.top - 0.5 && r.bottom <= c.bottom + 0.5);
423
+ const chromeAbove = (top) => chromes.reduce((sum, c) => sum + (c.top < top ? c.height : 0), 0);
424
+ const flowRects = rects.filter((r) => !insideChrome(r)).map((r) => ({ top: r.top - chromeAbove(r.top), bottom: r.bottom - chromeAbove(r.top), left: r.left, liveTop: r.top, liveBottom: r.bottom }));
425
+ if (flowRects.length < minHead + minTail)
337
426
  return null;
338
427
  const lines = [];
339
- for (const r of rects) {
428
+ for (const r of flowRects) {
340
429
  const last2 = lines[lines.length - 1];
341
430
  if (last2 && r.top < last2.bottom - 1) {
342
431
  last2.bottom = Math.max(last2.bottom, r.bottom);
432
+ last2.liveBottom = Math.max(last2.liveBottom, r.liveBottom);
343
433
  last2.left = Math.min(last2.left, r.left);
344
434
  } else {
345
- lines.push({ top: r.top, bottom: r.bottom, left: r.left });
435
+ lines.push({ top: r.top, bottom: r.bottom, left: r.left, liveTop: r.liveTop, liveBottom: r.liveBottom });
346
436
  }
347
437
  }
348
438
  if (lines.length < minHead + minTail)
349
439
  return null;
350
- const scale = dom.offsetHeight > 0 ? dom.getBoundingClientRect().height / dom.offsetHeight : 1;
440
+ const flowHeightPx = dom.offsetHeight - pocChromeHeightInside(dom);
441
+ const flowRectPx = dom.getBoundingClientRect().height - chromes.reduce((sum, c) => sum + c.height, 0);
442
+ const scale = flowHeightPx > 0 ? flowRectPx / flowHeightPx : 1;
351
443
  if (!Number.isFinite(scale) || scale <= 0)
352
444
  return null;
353
445
  const originTop = lines[0].top;
@@ -363,13 +455,13 @@ function findInlineSplit(view, dom, availablePx, fromLine = 0, minHead = MIN_LIN
363
455
  if (cut < first || cut > last)
364
456
  return null;
365
457
  const line = lines[cut];
366
- const found = view.posAtCoords({ left: line.left + 1, top: (line.top + line.bottom) / 2 });
458
+ const found = view.posAtCoords({ left: line.left + 1, top: (line.liveTop + line.liveBottom) / 2 });
367
459
  if (!found)
368
460
  return null;
369
461
  return { pos: found.pos, usedPx: (lines[cut - 1].bottom - originTop) / scale, line: cut };
370
462
  }
371
463
  function pageSafetyPx(contentHeight) {
372
- return Math.max(32, Math.round(contentHeight * 0.08));
464
+ return Math.max(8, Math.round(contentHeight * 0.02));
373
465
  }
374
466
  const NOTES_RULE_PX = 4;
375
467
  function createNotesMeasurer(sheet, contentWidth) {
@@ -418,12 +510,14 @@ function computePocBreaks(view, cfg, sheet) {
418
510
  const items = [];
419
511
  const baseY = cfg.margins.top;
420
512
  let noteCounter = 0;
421
- view.state.doc.forEach((block, blockOffset) => {
422
- if (block.type.name !== "minutaBlock") {
423
- return;
424
- }
425
- block.forEach((child, childOffset) => {
426
- const pos = blockOffset + 1 + childOffset;
513
+ const collect = (parent, parentPos, firstChain) => {
514
+ parent.forEach((child, offset, index) => {
515
+ const pos = parentPos + 1 + offset;
516
+ const chainStart = index === 0 ? firstChain ?? parentPos : null;
517
+ if (pocDescendsInto(child)) {
518
+ collect(child, pos, chainStart);
519
+ return;
520
+ }
427
521
  const dom = view.nodeDOM(pos);
428
522
  if (!(dom instanceof HTMLElement))
429
523
  return;
@@ -440,21 +534,24 @@ function computePocBreaks(view, cfg, sheet) {
440
534
  pos,
441
535
  size: child.nodeSize,
442
536
  top,
443
- height: dom.offsetHeight,
537
+ // sem o chrome que uma quebra suave anterior deixou dentro do
538
+ // parágrafo (o fluxo "sem paginação" é o que se mede)
539
+ height: dom.offsetHeight - pocChromeHeightInside(dom),
444
540
  notes,
445
541
  dom,
446
542
  // margem superior conta como parte da página que o item abre —
447
543
  // senão a "página" do preview fica mais alta que um A4 real e
448
544
  // o DOCX (página física) divergiria
449
545
  marginTop: Number.parseFloat(getComputedStyle(dom).marginTop) || 0,
450
- heading: HEADING_TAGS.has(dom.tagName),
546
+ heading: pocLooksLikeTitle(dom),
451
547
  marker: (dom.textContent || "").trim() === "PAGEBREAK",
452
- // quebra que cai no 1º filho de um bloco vai ANTES do bloco
453
- // (o chrome fora do div do bloco — senão o outline/label de
454
- // proveniência "vaza" para a página anterior)
455
- blockStartPos: childOffset === 0 ? blockOffset : null
548
+ breakPos: chainStart ?? pos
456
549
  });
457
550
  });
551
+ };
552
+ view.state.doc.forEach((block, blockOffset) => {
553
+ if (block.type.name === "minutaBlock")
554
+ collect(block, blockOffset, null);
458
555
  });
459
556
  if (!items.length)
460
557
  return { breaks: [], total: 1, markers: [], lastNotes: [] };
@@ -467,9 +564,9 @@ function computePocBreaks(view, cfg, sheet) {
467
564
  let forcedBreakPending = false;
468
565
  let pageNotes = [];
469
566
  const notesH = (notes) => measurer && notes.length ? measurer.height(notes) : 0;
470
- const closePage = (pos, filler, soft = false) => {
567
+ const closePage = (pos, filler, soft = false, manual = false) => {
471
568
  const height = notesH(pageNotes);
472
- breaks.push({ pos, filler: Math.max(0, filler - height), pageNo, notes: pageNotes, notesH: height, soft });
569
+ breaks.push({ pos, filler: Math.max(0, filler - height), pageNo, notes: pageNotes, notesH: height, soft, manual });
473
570
  pageNo++;
474
571
  pageNotes = [];
475
572
  };
@@ -490,7 +587,7 @@ function computePocBreaks(view, cfg, sheet) {
490
587
  const capacity = contentH - notesH([...pageNotes, ...item.notes]);
491
588
  if (forcedBreakPending && item.top > pageStart) {
492
589
  const startWithMargin = item.top - item.marginTop;
493
- closePage(item.blockStartPos ?? item.pos, fullH - (startWithMargin - pageStart));
590
+ closePage(item.breakPos, fullH - (startWithMargin - pageStart), false, true);
494
591
  pageStart = startWithMargin;
495
592
  forcedBreakPending = false;
496
593
  } else if (item.top > pageStart && itemEnd - pageStart > capacity) {
@@ -546,7 +643,7 @@ function computePocBreaks(view, cfg, sheet) {
546
643
  moved.push(note);
547
644
  }
548
645
  }
549
- closePage(target.blockStartPos ?? target.pos, fullH - (startWithMargin - pageStart));
646
+ closePage(target.breakPos, fullH - (startWithMargin - pageStart));
550
647
  pageNotes = moved;
551
648
  pageStart = startWithMargin;
552
649
  if (breakIndex < i) {
@@ -570,6 +667,20 @@ function pocStyleProp(style, prop) {
570
667
  const match = style.match(new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]+)`, "i"));
571
668
  return match?.[1]?.trim();
572
669
  }
670
+ function pocDescendsInto(node) {
671
+ switch (node.type.name) {
672
+ case "blockquote":
673
+ case "bulletList":
674
+ case "orderedList":
675
+ case "listItem":
676
+ case "table":
677
+ return true;
678
+ case "styledDiv":
679
+ return !isPocFlexRow(node) && !pocNodePaintsBox(node);
680
+ default:
681
+ return false;
682
+ }
683
+ }
573
684
  function isPocFlexRow(node) {
574
685
  if (node.type.name !== "styledDiv")
575
686
  return false;
@@ -687,7 +798,7 @@ function PocPaginationPlugin(cfg) {
687
798
  decorations(state) {
688
799
  const pag = pocPageKey.getState(state);
689
800
  const decorations = (pag?.breaks ?? []).map(
690
- (brk) => Decoration.widget(brk.pos, () => buildChrome(cfg, brk, pag.total), {
801
+ (brk) => Decoration.widget(brk.pos, (view) => buildChromeWidget(view, cfg, brk, pag.total), {
691
802
  side: -1,
692
803
  key: `chrome-${brk.pageNo}-${Math.round(brk.filler)}-${brk.notes?.length ?? 0}-${pag.total}-v${cfg.version ?? 0}`
693
804
  })
@@ -708,4 +819,4 @@ function PocPaginationPlugin(cfg) {
708
819
  });
709
820
  }
710
821
 
711
- export { PocPaginationPlugin as P, sanitizeFootnoteHtml as a, pocPageKey as b, computePocBreaks as c, breaksEqual as d, PAGINATE_META as e, footnoteHtmlToRuns as f, pocChromeFlowHeight as g, buildPocHeaderEl as h, isPocChromeCanvas as i, buildPocFooterEl as j, buildPocNotesEl as k, isPocFlexRow as l, pocNodeHasVisibleContent as m, pocNodePaintsBox as n, computePocLoneRows as o, pocLoneCellIndex as p, footnoteSizePt as q, shouldMoveBreakToHeading as r, sanitizeRichHtml as s };
822
+ export { PocPaginationPlugin as P, pocLooksLikeTitle as a, sanitizeFootnoteHtml as b, computePocBreaks as c, pocPageKey as d, breaksEqual as e, footnoteHtmlToRuns as f, PAGINATE_META as g, pocChromeFlowHeight as h, isPocChromeCanvas as i, buildPocHeaderEl as j, buildPocFooterEl as k, buildPocNotesEl as l, pocDescendsInto as m, isPocFlexRow as n, pocNodeHasVisibleContent as o, pocLoneCellIndex as p, pocNodePaintsBox as q, computePocLoneRows as r, sanitizeRichHtml as s, footnoteSizePt as t, shouldMoveBreakToHeading as u };
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .poc-editor-root .poc-ins{background:rgba(34,197,94,.16);text-decoration:underline;text-decoration-color:#16a34a}.poc-editor-root .poc-del{background:rgba(239,68,68,.12);color:#b91c1c;text-decoration:line-through}.poc-editor-root .poc-bubble-btn{border-radius:6px;font-size:13px;padding:4px 8px;transition:all .15s;transition-timing-function:ease}.poc-editor-root .poc-toolbar{align-items:center;display:flex;flex-wrap:wrap;gap:2px;padding:4px 12px}@media (max-width:640px){.poc-editor-root .poc-toolbar{flex-wrap:nowrap;overflow-x:auto;padding:4px 8px;scrollbar-width:none}.poc-editor-root .poc-toolbar::-webkit-scrollbar{display:none}.poc-editor-root .poc-toolbar>*{flex:none}}.poc-editor-root .poc-tool{align-items:center;border-radius:6px;display:flex;font-size:13px;height:28px;justify-content:center;min-width:28px;padding-left:6px;padding-right:6px;position:relative;transition:all .15s;transition-timing-function:ease}.poc-editor-root .poc-tool-sep{background:#dee3e7;height:20px;margin:0 6px;width:1px}.poc-editor-root .poc-tool-select{border-radius:6px;font-size:12px;height:28px;padding-left:6px;padding-right:6px}.poc-editor-root .poc-color-underline{background:#dc2626;border-radius:1px;bottom:4px;height:3px;left:7px;position:absolute;right:7px}.poc-editor-root .poc-palette{background:#fff;border:.5px solid #dee3e7;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.12);display:flex;flex-wrap:wrap;gap:2px;left:0;padding:8px;position:absolute;top:32px;width:230px;z-index:50}.poc-editor-root .poc-palette-swatch{border:1px solid rgba(0,0,0,.1);border-radius:3px;cursor:pointer;height:19px;width:19px}.poc-editor-root .poc-palette-swatch:hover{transform:scale(1.2)}.poc-editor-root .poc-palette-custom{align-items:center;color:#565d69;cursor:pointer;display:flex;font-size:12px;margin-top:6px;width:100%}.poc-editor-root .poc-color-swatch{border:1px solid rgba(0,0,0,.12);border-radius:4px;cursor:pointer;height:16px;margin:0 2px;width:16px}.poc-editor-root .poc-color-swatch:hover{transform:scale(1.15)}.poc-editor-root .poc-first-header{align-items:center;cursor:pointer;display:flex;gap:12px;position:absolute;top:0}.poc-editor-root .poc-first-header[style*=center]{justify-content:center}.poc-editor-root .poc-first-header[style*=right]{justify-content:flex-end}.poc-editor-root .poc-first-header:hover,.poc-editor-root .poc-last-footer:hover,.poc-editor-root .poc-page-footerarea:hover,.poc-editor-root .poc-page-headerarea:hover{background:rgba(99,102,241,.06);outline:1px dashed #c7d2fe}.poc-editor-root .poc-last-footer{align-items:center;bottom:0;color:#94a3b8;cursor:pointer;display:flex;font-size:11px;justify-content:space-between;position:absolute}.poc-editor-root .poc-chrome-text{color:#64748b;font-size:11px}.poc-editor-root .poc-chrome-text img{display:inline-block;max-height:48px;max-width:200px;vertical-align:middle}.poc-editor-root .poc-pagebreak-marker{border-top:1px dashed #a5b4fc;font-size:0!important;height:16px;line-height:0!important;margin:6px 0!important;position:relative}.poc-editor-root .poc-pagebreak-marker:after{background:#fff;color:#818cf8;content:"quebra de página do template";font-size:10px;left:50%;line-height:16px;padding:0 8px;position:absolute;top:-8px;transform:translateX(-50%)}.poc-editor-root .poc-page-chrome{display:block;pointer-events:none;text-align:initial;text-indent:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.poc-editor-root .poc-page-footerarea,.poc-editor-root .poc-page-headerarea{cursor:pointer;pointer-events:auto}.poc-editor-root .poc-page-footerarea{align-items:center;color:#94a3b8;display:flex;font-size:11px;justify-content:space-between}.poc-editor-root .poc-page-gapband{background:#eceef1;box-shadow:inset 0 6px 6px -6px rgba(0,0,0,.25),inset 0 -6px 6px -6px rgba(0,0,0,.25)}.poc-editor-root .poc-page-headerarea{align-items:center;display:flex}.poc-editor-root .poc-page-headerarea[style*=center]{justify-content:center}.poc-editor-root .poc-page-headerarea[style*=right]{justify-content:flex-end}.poc-editor-root .ProseMirror{white-space:pre-wrap;word-wrap:break-word}.poc-editor-root h1{color:#0f172a;font-size:24px;font-weight:640;letter-spacing:-.3px;line-height:32px;margin:0 0 20px}.poc-editor-root h2{color:#0f172a;font-size:19px;font-weight:620;letter-spacing:-.2px;line-height:26px;margin:28px 0 14px}.poc-editor-root h3{color:#0f172a;font-size:17px;font-weight:600;letter-spacing:-.1px;line-height:24px;margin:24px 0 12px}.poc-editor-root h1:first-child,.poc-editor-root h2:first-child,.poc-editor-root h3:first-child{margin-top:0}.poc-editor-root strong{font-weight:540}.poc-editor-root p{color:#000;line-height:28px;line-height:1.7em;margin-bottom:.75em;text-align:justify}.poc-editor-root blockquote,.poc-editor-root p{font-size:18px;font-weight:400;letter-spacing:-.4px}.poc-editor-root blockquote{line-height:28px;margin-bottom:2em;margin-left:35%}.poc-editor-root ol,.poc-editor-root ul{margin-bottom:2em}.poc-editor-root li{list-style-position:inside;margin-bottom:16px;margin-left:48px}.poc-editor-root ul>li{list-style-type:disc}.poc-editor-root ol>li{list-style-type:decimal}.poc-editor-root table{border:.5px solid #d4d9df;max-width:100%;table-layout:fixed;width:100%}.poc-editor-root th{font-weight:700}.poc-editor-root td,.poc-editor-root th{color:inherit;font-size:inherit;overflow-wrap:anywhere;padding:12px;text-align:left}.poc-editor-root td{line-height:inherit}.poc-editor-root td p,.poc-editor-root th p{color:inherit;font-size:inherit;line-height:inherit;margin-bottom:0}.poc-editor-root img{display:block;max-width:100%}.poc-editor-root .poc-island--img>img,.poc-editor-root .poc-island>img:only-child{margin-left:auto;margin-right:auto}.poc-editor-root .poc-chrome-canvas{overflow:hidden}.poc-editor-root .poc-chrome-canvas [data-el]{position:absolute}.poc-editor-root .poc-chrome-canvas [data-el=text]{color:#64748b;font-size:11px;line-height:1.4;text-align:left;white-space:pre-wrap}.poc-editor-root img[data-vault-src]{animation:poc-img-loading 1.4s ease-in-out infinite}@keyframes poc-img-loading{0%,to{opacity:.5}50%{opacity:.9}}.poc-editor-root .poc-lone-row{justify-content:center!important}.poc-editor-root .poc-lone-row>:not(.poc-lone-cell){flex:0 0 0!important;min-width:0!important;padding:0!important}.poc-editor-root .poc-lone-row>.poc-lone-cell{align-items:center!important;flex:0 0 auto!important;max-width:100%;min-width:0}.poc-editor-root [data-horizontal-align=left] .poc-island--img>img,.poc-editor-root [data-horizontal-align=left] .poc-island>img:only-child{margin-left:0}.poc-editor-root [data-horizontal-align=right] .poc-island--img>img,.poc-editor-root [data-horizontal-align=right] .poc-island>img:only-child{margin-right:0}.poc-editor-root .poc-review-cursor .poc-minuta-block{cursor:pointer}.poc-editor-root .poc-minuta-block{border-radius:4px;position:relative}.poc-editor-root .poc-minuta-block:hover{outline:1px dashed #c7d2fe;outline-offset:6px}.poc-editor-root.poc-no-block-origin .poc-minuta-block:hover{outline:none}.poc-editor-root.poc-no-block-origin .poc-minuta-block:hover:before{content:none}.poc-editor-root .poc-minuta-block:hover:before{background:#eef2ff;border:1px solid #c7d2fe;border-radius:4px;color:#6366f1;content:attr(data-block-title);font-size:10px;letter-spacing:.04em;padding:1px 8px;pointer-events:none;position:absolute;right:0;text-transform:uppercase;top:-22px;z-index:3}.poc-editor-root .poc-varchip{background:rgba(59,130,246,.06);border-radius:3px;box-shadow:inset 0 0 0 1px rgba(59,130,246,.25);cursor:pointer;transition:background .15s ease}.poc-editor-root .poc-varchip:hover{background:rgba(59,130,246,.16)}.poc-editor-root .poc-tplvar{background:rgba(251,191,36,.16);border-radius:3px;box-shadow:inset 0 0 0 1px rgba(180,83,9,.35);color:#78350f;display:inline;padding:0 2px;white-space:pre-wrap}.poc-editor-root .poc-tplvar--unknown{background:rgba(244,63,94,.12);box-shadow:inset 0 0 0 1px rgba(190,18,60,.45);color:#881337;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.poc-editor-root .ProseMirror-selectednode.poc-tplvar{box-shadow:inset 0 0 0 2px rgba(180,83,9,.75)}.poc-editor-root .poc-missing-image .missing-image-warning:hover{background-color:rgba(254,174,0,.09);border-color:rgba(223,139,13,.85)}.poc-editor-root .poc-missing-image__remove{background:hsla(0,0%,100%,.75);border:none;border-radius:999px;color:#773613;cursor:pointer;font-size:16px;height:22px;line-height:1;opacity:0;position:absolute;right:4px;top:4px;transition:opacity .15s ease,background .15s ease;width:22px}.poc-editor-root .poc-missing-image:hover .poc-missing-image__remove{opacity:1}.poc-editor-root .poc-missing-image__remove:hover{background:rgba(254,174,0,.35)}.poc-editor-root .poc-missing-image.ProseMirror-selectednode .missing-image-warning{outline:2px solid rgba(223,139,13,.9);outline-offset:2px}.poc-editor-root .poc-page-notes{color:#1c242d;font-size:10.67px;line-height:1.25;padding-top:6px}.poc-editor-root .poc-page-notes:before{border-top:.5px solid #9aa4ae;content:"";display:block;margin-bottom:6px;width:30%}.poc-editor-root .poc-page-note{color:#1c242d;font-size:10.67px;font-weight:400;line-height:1.25;margin:0;text-align:justify}.poc-editor-root .poc-page-note sup{font-size:.8em;line-height:0;vertical-align:super}.poc-editor-root .poc-page-note a{color:#0563c1;text-decoration:underline}.poc-editor-root .poc-footnote{color:#1d4ed8;cursor:help;font-size:.7em;line-height:0;vertical-align:super}.poc-editor-root .poc-island{position:relative}.poc-editor-root .poc-island--img{cursor:grab}.poc-editor-root .poc-img-handle{background:#3b82f6;border:2px solid #fff;border-radius:3px;bottom:-6px;box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:nwse-resize;height:12px;opacity:0;position:absolute;right:-6px;transition:opacity .12s ease;width:12px}.poc-editor-root .poc-island--img.ProseMirror-selectednode .poc-img-handle,.poc-editor-root .poc-island--img:hover .poc-img-handle{opacity:1}.poc-editor-root .poc-island.ProseMirror-selectednode{outline:2px solid #93c5fd;outline-offset:2px}.poc-editor-root .h-full{height:100%}.poc-editor-root .w-full{width:100%}.poc-editor-root .w-1px{width:1px}.poc-editor-root .w-64px{width:64px}.poc-editor-root .h-16px{height:16px}.poc-editor-root .flex{display:flex}.poc-editor-root .flex-col{flex-direction:column}.poc-editor-root .flex-1{flex:1 1 0%}.poc-editor-root .items-center{align-items:center}.poc-editor-root .gap-2px{gap:2px}.poc-editor-root .min-h-0{min-height:0}.poc-editor-root .overflow-y-auto{overflow-y:auto}.poc-editor-root .relative{position:relative}.poc-editor-root .hidden{display:none}.poc-editor-root .inline-block{display:inline-block}.poc-editor-root .align-middle{vertical-align:middle}.poc-editor-root .ml-auto{margin-left:auto}.poc-editor-root .mx-auto{margin-left:auto;margin-right:auto}.poc-editor-root .mx-2px{margin-left:2px;margin-right:2px}.poc-editor-root .p-2px{padding:2px}.poc-editor-root .py-24px{padding-bottom:24px;padding-top:24px}.poc-editor-root .bg-white{background:#fff}.poc-editor-root .bg-gray-120{background:#ebedf0}.poc-editor-root .bg-gray-200{background:#e5e7eb}.poc-editor-root .border{border-style:solid;border-width:1px}.poc-editor-root .border-gray-200{border-color:#e5e7eb}.poc-editor-root .rounded-8px{border-radius:8px}.poc-editor-root .shadow-md{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1)}.poc-editor-root .shadow-lg{box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1)}.poc-editor-root .font-bold{font-weight:700}.poc-editor-root .font-semibold{font-weight:600}.poc-editor-root .italic{font-style:italic}.poc-editor-root .underline{text-decoration:underline}.poc-editor-root .line-through{text-decoration:line-through}.poc-editor-root .text-red-600{color:#dc2626}.poc-editor-root .body-13-regular{font-size:13px;font-weight:400}.poc-editor-root{font-family:Inter Variable,Inter,-apple-system,Segoe UI,Roboto,sans-serif}.poc-editor-root .poc-toolbar{background:#fafbfc;border-bottom:.5px solid #dee3e7}.poc-editor-root .poc-tool{background:transparent;border:none;cursor:pointer}.poc-editor-root .poc-tool:hover{background:#ebedf0}.poc-editor-root .poc-tool--on{background:#e2e8f0}.poc-editor-root .poc-tool-select{background:#fff;border:.5px solid #d4d9df;cursor:pointer}.poc-editor-root .ProseMirror{outline:none}.poc-editor-root td,.poc-editor-root th{border:.5px solid #d4d9df}.poc-editor-root .poc-footnote-pop{background:#fff;border:.5px solid #d4d9df;border-radius:10px;box-shadow:0 10px 25px -5px rgba(0,0,0,.18);padding:10px 12px;position:absolute;width:364px;z-index:40}.poc-editor-root .poc-footnote-pop__head{color:#6b7280;font-size:11px;font-weight:650;letter-spacing:.04em;margin-bottom:6px;text-transform:uppercase}.poc-editor-root .poc-footnote-pop__toolbar{align-items:center;display:flex;gap:4px;margin-bottom:6px}.poc-editor-root .poc-footnote-pop__tool{background:transparent;border:none;border-radius:6px;color:#374151;cursor:pointer;font-size:12px;height:24px;width:24px}.poc-editor-root .poc-footnote-pop__tool:hover{background:rgba(28,36,45,.06)}.poc-editor-root .poc-footnote-pop__sep{background:#e5e7eb;height:16px;margin:0 2px;width:1px}.poc-editor-root .poc-footnote-pop__select{background:#fff;border:1px solid #e5e7eb;border-radius:6px;color:#374151;font-size:11px;height:24px;max-width:108px;outline:none}.poc-editor-root .poc-footnote-pop__select--narrow{max-width:58px}.poc-editor-root .poc-footnote-pop__swatch{border:1px solid rgba(0,0,0,.15);border-radius:4px;cursor:pointer;height:14px;padding:0;width:14px}.poc-editor-root .poc-footnote-pop__input{border:1px solid #e5e7eb;border-radius:8px;color:#111827;font-size:12.5px;line-height:1.5;max-height:160px;min-height:58px;outline:none;overflow-y:auto;padding:6px 8px;width:100%}.poc-editor-root .poc-footnote-pop__input:focus{border-color:#a5b4fc}.poc-editor-root .poc-footnote-pop__input:empty:before{color:#9ca3af;content:attr(data-placeholder)}.poc-editor-root .poc-footnote-pop__actions{align-items:center;display:flex;gap:8px;margin-top:8px}.poc-editor-root .poc-footnote-pop__spacer{flex:1}.poc-editor-root .poc-footnote-pop__actions button{background:transparent;border:none;border-radius:8px;color:#565d69;cursor:pointer;font-size:12px;font-weight:500;padding:4px 12px}.poc-editor-root .poc-footnote-pop__remove{color:#b91c1c!important}.poc-editor-root .poc-footnote-pop__save{background:rgba(0,0,0,.88)!important;color:#fff!important}.poc-editor-root{display:flex;flex-direction:column;height:100%;width:100%}.poc-editor-root .w-118px{width:118px}.poc-editor-root .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-editor-root .poc-varpicker__search{border:0;border-bottom:1px solid #f3f4f6;font-family:inherit;font-size:13px;outline:none;padding:10px 12px}.poc-editor-root .poc-varpicker__hint{border-bottom:1px solid #f3f4f6;color:#6b7280;font-size:12px;padding:8px 12px}.poc-editor-root .poc-varpicker__list{flex:1;min-height:0;overflow-y:auto;padding:4px}.poc-editor-root .poc-varpicker__empty{color:#6b7280;font-size:12px;margin:0;padding:12px}.poc-editor-root .poc-varpicker__group{color:#9ca3af;font-size:11px;font-weight:600;letter-spacing:.04em;padding:8px 8px 4px;text-transform:uppercase}.poc-editor-root .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-editor-root .poc-varpicker__item--on{background:#f3f4f6}.poc-editor-root .poc-varpicker__label{color:#111827;font-weight:500}.poc-editor-root .poc-varpicker__key{color:#9ca3af;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}.poc-editor-root .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}.poc-editor-root [hidden]{display:none!important}.poc-editor-root .poc-varchip--flash{background:rgba(59,130,246,.35);transition:background .9s ease}
1
+ .poc-editor-root .poc-ins{background:rgba(34,197,94,.16);text-decoration:underline;text-decoration-color:#16a34a}.poc-editor-root .poc-del{background:rgba(239,68,68,.12);color:#b91c1c;text-decoration:line-through}.poc-editor-root .poc-bubble-btn{border-radius:6px;font-size:13px;padding:4px 8px;transition:all .15s;transition-timing-function:ease}.poc-editor-root .poc-toolbar{align-items:center;display:flex;flex-wrap:wrap;gap:2px;padding:4px 12px}@media (max-width:640px){.poc-editor-root .poc-toolbar{flex-wrap:nowrap;overflow-x:auto;padding:4px 8px;scrollbar-width:none}.poc-editor-root .poc-toolbar::-webkit-scrollbar{display:none}.poc-editor-root .poc-toolbar>*{flex:none}}.poc-editor-root .poc-tool{align-items:center;border-radius:6px;display:flex;font-size:13px;height:28px;justify-content:center;min-width:28px;padding-left:6px;padding-right:6px;position:relative;transition:all .15s;transition-timing-function:ease}.poc-editor-root .poc-tool-sep{background:#dee3e7;height:20px;margin:0 6px;width:1px}.poc-editor-root .poc-tool-select{border-radius:6px;font-size:12px;height:28px;padding-left:6px;padding-right:6px}.poc-editor-root .poc-color-underline{background:#dc2626;border-radius:1px;bottom:4px;height:3px;left:7px;position:absolute;right:7px}.poc-editor-root .poc-palette{background:#fff;border:.5px solid #dee3e7;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.12);display:flex;flex-wrap:wrap;gap:2px;left:0;padding:8px;position:absolute;top:32px;width:230px;z-index:50}.poc-editor-root .poc-palette-swatch{border:1px solid rgba(0,0,0,.1);border-radius:3px;cursor:pointer;height:19px;width:19px}.poc-editor-root .poc-palette-swatch:hover{transform:scale(1.2)}.poc-editor-root .poc-palette-custom{align-items:center;color:#565d69;cursor:pointer;display:flex;font-size:12px;margin-top:6px;width:100%}.poc-editor-root .poc-color-swatch{border:1px solid rgba(0,0,0,.12);border-radius:4px;cursor:pointer;height:16px;margin:0 2px;width:16px}.poc-editor-root .poc-color-swatch:hover{transform:scale(1.15)}.poc-editor-root .poc-first-header{align-items:center;cursor:pointer;display:flex;gap:12px;position:absolute;top:0}.poc-editor-root .poc-first-header[style*=center]{justify-content:center}.poc-editor-root .poc-first-header[style*=right]{justify-content:flex-end}.poc-editor-root .poc-first-header:hover,.poc-editor-root .poc-last-footer:hover,.poc-editor-root .poc-page-footerarea:hover,.poc-editor-root .poc-page-headerarea:hover{background:rgba(99,102,241,.06);outline:1px dashed #c7d2fe}.poc-editor-root .poc-last-footer{align-items:center;bottom:0;color:#94a3b8;cursor:pointer;display:flex;font-size:11px;justify-content:space-between;position:absolute}.poc-editor-root .poc-chrome-text{color:#64748b;font-size:11px}.poc-editor-root .poc-chrome-text img{display:inline-block;max-height:48px;max-width:200px;vertical-align:middle}.poc-editor-root .poc-pagebreak-marker{border-top:1px dashed #a5b4fc;font-size:0!important;height:16px;line-height:0!important;margin:6px 0!important;position:relative}.poc-editor-root .poc-pagebreak-marker:after{background:#fff;color:#818cf8;content:"quebra de página do template";font-size:10px;left:50%;line-height:16px;padding:0 8px;position:absolute;top:-8px;transform:translateX(-50%)}.poc-editor-root .poc-page-chrome{color:initial;display:block;font-style:normal;font-weight:400;letter-spacing:normal;line-height:normal;pointer-events:none;text-align:initial;text-indent:0;text-transform:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:normal}.poc-editor-root .poc-page-chrome>.poc-page-footerarea:not(:hover),.poc-editor-root .poc-page-chrome>.poc-page-headerarea:not(:hover),.poc-editor-root .poc-page-chrome>.poc-page-notes{background:#fff}.poc-editor-root tr.poc-page-chrome-row>td.poc-page-chrome-cell{background:transparent!important;border:0!important;padding:0!important;vertical-align:top}.poc-editor-root .poc-page-footerarea,.poc-editor-root .poc-page-headerarea{cursor:pointer;pointer-events:auto}.poc-editor-root .poc-page-footerarea{align-items:center;color:#94a3b8;display:flex;font-size:11px;justify-content:space-between}.poc-editor-root .poc-page-gapband{background:#eceef1;box-shadow:inset 0 6px 6px -6px rgba(0,0,0,.25),inset 0 -6px 6px -6px rgba(0,0,0,.25)}.poc-editor-root .poc-page-headerarea{align-items:center;display:flex}.poc-editor-root .poc-page-headerarea[style*=center]{justify-content:center}.poc-editor-root .poc-page-headerarea[style*=right]{justify-content:flex-end}.poc-editor-root .ProseMirror{white-space:pre-wrap;word-wrap:break-word}.poc-editor-root h1{color:#0f172a;font-size:24px;font-weight:640;letter-spacing:-.3px;line-height:32px;margin:0 0 20px}.poc-editor-root h2{color:#0f172a;font-size:19px;font-weight:620;letter-spacing:-.2px;line-height:26px;margin:28px 0 14px}.poc-editor-root h3{color:#0f172a;font-size:17px;font-weight:600;letter-spacing:-.1px;line-height:24px;margin:24px 0 12px}.poc-editor-root h1:first-child,.poc-editor-root h2:first-child,.poc-editor-root h3:first-child{margin-top:0}.poc-editor-root strong{font-weight:540}.poc-editor-root p{color:#000;line-height:28px;line-height:1.7em;margin-bottom:.75em;text-align:justify}.poc-editor-root blockquote,.poc-editor-root p{font-size:18px;font-weight:400;letter-spacing:-.4px}.poc-editor-root blockquote{line-height:28px;margin-bottom:2em;margin-left:35%}.poc-editor-root ol,.poc-editor-root ul{margin-bottom:2em}.poc-editor-root li{list-style-position:inside;margin-bottom:16px;margin-left:48px}.poc-editor-root ul>li{list-style-type:disc}.poc-editor-root ol>li{list-style-type:decimal}.poc-editor-root table{border:.5px solid #d4d9df;max-width:100%;table-layout:fixed;width:100%}.poc-editor-root th{font-weight:700}.poc-editor-root td,.poc-editor-root th{color:inherit;font-size:inherit;overflow-wrap:anywhere;padding:12px;text-align:left}.poc-editor-root td{line-height:inherit}.poc-editor-root td p,.poc-editor-root th p{color:inherit;font-size:inherit;line-height:inherit;margin-bottom:0}.poc-editor-root img{display:block;max-width:100%}.poc-editor-root .poc-island--img>img,.poc-editor-root .poc-island>img:only-child{margin-left:auto;margin-right:auto}.poc-editor-root .poc-chrome-canvas{overflow:hidden}.poc-editor-root .poc-chrome-canvas [data-el]{position:absolute}.poc-editor-root .poc-chrome-canvas [data-el=text]{color:#64748b;font-size:11px;line-height:1.4;text-align:left;white-space:pre-wrap}.poc-editor-root img[src^="vault://"]{animation:poc-img-loading 1.4s ease-in-out infinite}@keyframes poc-img-loading{0%,to{opacity:.5}50%{opacity:.9}}.poc-editor-root .poc-lone-row{justify-content:center!important}.poc-editor-root .poc-lone-row>:not(.poc-lone-cell){flex:0 0 0!important;min-width:0!important;padding:0!important}.poc-editor-root .poc-lone-row>.poc-lone-cell{align-items:center!important;flex:0 0 auto!important;max-width:100%;min-width:0}.poc-editor-root [data-horizontal-align=left] .poc-island--img>img,.poc-editor-root [data-horizontal-align=left] .poc-island>img:only-child{margin-left:0}.poc-editor-root [data-horizontal-align=right] .poc-island--img>img,.poc-editor-root [data-horizontal-align=right] .poc-island>img:only-child{margin-right:0}.poc-editor-root .poc-review-cursor .poc-minuta-block{cursor:pointer}.poc-editor-root .poc-minuta-block{border-radius:4px;position:relative}.poc-editor-root .poc-minuta-block:hover{outline:1px dashed #c7d2fe;outline-offset:6px}.poc-editor-root.poc-no-block-origin .poc-minuta-block:hover{outline:none}.poc-editor-root.poc-no-block-origin .poc-minuta-block:hover:before{content:none}.poc-editor-root .poc-minuta-block:hover:before{background:#eef2ff;border:1px solid #c7d2fe;border-radius:4px;color:#6366f1;content:attr(data-block-title);font-size:10px;letter-spacing:.04em;padding:1px 8px;pointer-events:none;position:absolute;right:0;text-transform:uppercase;top:-22px;z-index:3}.poc-editor-root .poc-varchip{background:rgba(59,130,246,.06);border-radius:3px;box-shadow:inset 0 0 0 1px rgba(59,130,246,.25);cursor:pointer;transition:background .15s ease}.poc-editor-root .poc-varchip:hover{background:rgba(59,130,246,.16)}.poc-editor-root .poc-tplvar{background:rgba(251,191,36,.16);border-radius:3px;box-shadow:inset 0 0 0 1px rgba(180,83,9,.35);color:#78350f;display:inline;padding:0 2px;white-space:pre-wrap}.poc-editor-root .poc-tplvar--unknown{background:rgba(244,63,94,.12);box-shadow:inset 0 0 0 1px rgba(190,18,60,.45);color:#881337;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.poc-editor-root .ProseMirror-selectednode.poc-tplvar{box-shadow:inset 0 0 0 2px rgba(180,83,9,.75)}.poc-editor-root .poc-missing-image .missing-image-warning:hover{background-color:rgba(254,174,0,.09);border-color:rgba(223,139,13,.85)}.poc-editor-root .poc-missing-image__remove{background:hsla(0,0%,100%,.75);border:none;border-radius:999px;color:#773613;cursor:pointer;font-size:16px;height:22px;line-height:1;opacity:0;position:absolute;right:4px;top:4px;transition:opacity .15s ease,background .15s ease;width:22px}.poc-editor-root .poc-missing-image:hover .poc-missing-image__remove{opacity:1}.poc-editor-root .poc-missing-image__remove:hover{background:rgba(254,174,0,.35)}.poc-editor-root .poc-missing-image.ProseMirror-selectednode .missing-image-warning{outline:2px solid rgba(223,139,13,.9);outline-offset:2px}.poc-editor-root .poc-page-notes{color:#1c242d;font-size:10.67px;line-height:1.25;padding-top:6px}.poc-editor-root .poc-page-notes:before{border-top:.5px solid #9aa4ae;content:"";display:block;margin-bottom:6px;width:30%}.poc-editor-root .poc-page-note{color:#1c242d;font-size:10.67px;font-weight:400;line-height:1.25;margin:0;text-align:justify}.poc-editor-root .poc-page-note sup{font-size:.8em;line-height:0;vertical-align:super}.poc-editor-root .poc-page-note a{color:#0563c1;text-decoration:underline}.poc-editor-root .poc-footnote{color:#1d4ed8;cursor:help;font-size:.7em;line-height:0;vertical-align:super}.poc-editor-root .poc-island{position:relative}.poc-editor-root .poc-island--img{cursor:grab}.poc-editor-root .poc-img-handle{background:#3b82f6;border:2px solid #fff;border-radius:3px;bottom:-6px;box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:nwse-resize;height:12px;opacity:0;position:absolute;right:-6px;transition:opacity .12s ease;width:12px}.poc-editor-root .poc-island--img.ProseMirror-selectednode .poc-img-handle,.poc-editor-root .poc-island--img:hover .poc-img-handle{opacity:1}.poc-editor-root .poc-island.ProseMirror-selectednode{outline:2px solid #93c5fd;outline-offset:2px}.poc-editor-root .h-full{height:100%}.poc-editor-root .w-full{width:100%}.poc-editor-root .w-1px{width:1px}.poc-editor-root .w-64px{width:64px}.poc-editor-root .h-16px{height:16px}.poc-editor-root .flex{display:flex}.poc-editor-root .flex-col{flex-direction:column}.poc-editor-root .flex-1{flex:1 1 0%}.poc-editor-root .items-center{align-items:center}.poc-editor-root .gap-2px{gap:2px}.poc-editor-root .min-h-0{min-height:0}.poc-editor-root .overflow-y-auto{overflow-y:auto}.poc-editor-root .relative{position:relative}.poc-editor-root .hidden{display:none}.poc-editor-root .inline-block{display:inline-block}.poc-editor-root .align-middle{vertical-align:middle}.poc-editor-root .ml-auto{margin-left:auto}.poc-editor-root .mx-auto{margin-left:auto;margin-right:auto}.poc-editor-root .mx-2px{margin-left:2px;margin-right:2px}.poc-editor-root .p-2px{padding:2px}.poc-editor-root .py-24px{padding-bottom:24px;padding-top:24px}.poc-editor-root .bg-white{background:#fff}.poc-editor-root .bg-gray-120{background:#ebedf0}.poc-editor-root .bg-gray-200{background:#e5e7eb}.poc-editor-root .border{border-style:solid;border-width:1px}.poc-editor-root .border-gray-200{border-color:#e5e7eb}.poc-editor-root .rounded-8px{border-radius:8px}.poc-editor-root .shadow-md{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1)}.poc-editor-root .shadow-lg{box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1)}.poc-editor-root .font-bold{font-weight:700}.poc-editor-root .font-semibold{font-weight:600}.poc-editor-root .italic{font-style:italic}.poc-editor-root .underline{text-decoration:underline}.poc-editor-root .line-through{text-decoration:line-through}.poc-editor-root .text-red-600{color:#dc2626}.poc-editor-root .body-13-regular{font-size:13px;font-weight:400}.poc-editor-root{font-family:Inter Variable,Inter,-apple-system,Segoe UI,Roboto,sans-serif}.poc-editor-root .poc-toolbar{background:#fafbfc;border-bottom:.5px solid #dee3e7}.poc-editor-root .poc-tool{background:transparent;border:none;cursor:pointer}.poc-editor-root .poc-tool:hover{background:#ebedf0}.poc-editor-root .poc-tool--on{background:#e2e8f0}.poc-editor-root .poc-tool-select{background:#fff;border:.5px solid #d4d9df;cursor:pointer}.poc-editor-root .ProseMirror{outline:none}.poc-editor-root td,.poc-editor-root th{border:.5px solid #d4d9df}.poc-editor-root .poc-footnote-pop{background:#fff;border:.5px solid #d4d9df;border-radius:10px;box-shadow:0 10px 25px -5px rgba(0,0,0,.18);padding:10px 12px;position:absolute;width:364px;z-index:40}.poc-editor-root .poc-footnote-pop__head{color:#6b7280;font-size:11px;font-weight:650;letter-spacing:.04em;margin-bottom:6px;text-transform:uppercase}.poc-editor-root .poc-footnote-pop__toolbar{align-items:center;display:flex;gap:4px;margin-bottom:6px}.poc-editor-root .poc-footnote-pop__tool{background:transparent;border:none;border-radius:6px;color:#374151;cursor:pointer;font-size:12px;height:24px;width:24px}.poc-editor-root .poc-footnote-pop__tool:hover{background:rgba(28,36,45,.06)}.poc-editor-root .poc-footnote-pop__sep{background:#e5e7eb;height:16px;margin:0 2px;width:1px}.poc-editor-root .poc-footnote-pop__select{background:#fff;border:1px solid #e5e7eb;border-radius:6px;color:#374151;font-size:11px;height:24px;max-width:108px;outline:none}.poc-editor-root .poc-footnote-pop__select--narrow{max-width:58px}.poc-editor-root .poc-footnote-pop__swatch{border:1px solid rgba(0,0,0,.15);border-radius:4px;cursor:pointer;height:14px;padding:0;width:14px}.poc-editor-root .poc-footnote-pop__input{border:1px solid #e5e7eb;border-radius:8px;color:#111827;font-size:12.5px;line-height:1.5;max-height:160px;min-height:58px;outline:none;overflow-y:auto;padding:6px 8px;width:100%}.poc-editor-root .poc-footnote-pop__input:focus{border-color:#a5b4fc}.poc-editor-root .poc-footnote-pop__input:empty:before{color:#9ca3af;content:attr(data-placeholder)}.poc-editor-root .poc-footnote-pop__actions{align-items:center;display:flex;gap:8px;margin-top:8px}.poc-editor-root .poc-footnote-pop__spacer{flex:1}.poc-editor-root .poc-footnote-pop__actions button{background:transparent;border:none;border-radius:8px;color:#565d69;cursor:pointer;font-size:12px;font-weight:500;padding:4px 12px}.poc-editor-root .poc-footnote-pop__remove{color:#b91c1c!important}.poc-editor-root .poc-footnote-pop__save{background:rgba(0,0,0,.88)!important;color:#fff!important}.poc-editor-root{display:flex;flex-direction:column;height:100%;width:100%}.poc-editor-root .w-118px{width:118px}.poc-editor-root .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-editor-root .poc-varpicker__search{border:0;border-bottom:1px solid #f3f4f6;font-family:inherit;font-size:13px;outline:none;padding:10px 12px}.poc-editor-root .poc-varpicker__hint{border-bottom:1px solid #f3f4f6;color:#6b7280;font-size:12px;padding:8px 12px}.poc-editor-root .poc-varpicker__list{flex:1;min-height:0;overflow-y:auto;padding:4px}.poc-editor-root .poc-varpicker__empty{color:#6b7280;font-size:12px;margin:0;padding:12px}.poc-editor-root .poc-varpicker__group{color:#9ca3af;font-size:11px;font-weight:600;letter-spacing:.04em;padding:8px 8px 4px;text-transform:uppercase}.poc-editor-root .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-editor-root .poc-varpicker__item--on{background:#f3f4f6}.poc-editor-root .poc-varpicker__label{color:#111827;font-weight:500}.poc-editor-root .poc-varpicker__key{color:#9ca3af;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}.poc-editor-root .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}.poc-editor-root [hidden]{display:none!important}.poc-editor-root .poc-varchip--flash{background:rgba(59,130,246,.35);transition:background .9s ease}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/minuta-editor",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Editor de minuta do defesa.ai sem framework: folha A4 paginada, edição rica, placeholders de variável e export DOCX fiel ao preview — monta em qualquer app (React, Vue, vanilla)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,7 +32,8 @@
32
32
  "prepack": "bun run build",
33
33
  "build": "unbuild && node scripts/add-type-refs.mjs",
34
34
  "typecheck": "tsc --noEmit",
35
- "test:pack": "node scripts/verify-pack.mjs"
35
+ "test:pack": "node scripts/verify-pack.mjs",
36
+ "test": "bun test src"
36
37
  },
37
38
  "dependencies": {
38
39
  "@tiptap/core": "2.11.9",