@meistrari/minuta-nuxt 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +106 -0
  2. package/dist/module.cjs +5 -0
  3. package/dist/module.d.mts +18 -0
  4. package/dist/module.d.ts +18 -0
  5. package/dist/module.json +9 -0
  6. package/dist/module.mjs +45 -0
  7. package/dist/runtime/components/MinutaAnnotationsGutter.vue +425 -0
  8. package/dist/runtime/components/MinutaChatEditCard.vue +124 -0
  9. package/dist/runtime/components/MinutaChromeCanvas.vue +549 -0
  10. package/dist/runtime/components/MinutaChromeEditor.vue +100 -0
  11. package/dist/runtime/components/MinutaDocumentViewer.vue +90 -0
  12. package/dist/runtime/components/MinutaDocxEditor.vue +1158 -0
  13. package/dist/runtime/components/MinutaEmbed.vue +783 -0
  14. package/dist/runtime/components/MinutaIcon.vue +53 -0
  15. package/dist/runtime/composables/annotationAnchors.d.ts +73 -0
  16. package/dist/runtime/composables/annotationAnchors.js +243 -0
  17. package/dist/runtime/composables/chatEditOps.d.ts +37 -0
  18. package/dist/runtime/composables/chatEditOps.js +61 -0
  19. package/dist/runtime/composables/docxExport.d.ts +24 -0
  20. package/dist/runtime/composables/docxExport.js +1292 -0
  21. package/dist/runtime/composables/editorExtensions.d.ts +56 -0
  22. package/dist/runtime/composables/editorExtensions.js +466 -0
  23. package/dist/runtime/composables/paginationPlugin.d.ts +126 -0
  24. package/dist/runtime/composables/paginationPlugin.js +421 -0
  25. package/dist/runtime/composables/useMinutaApi.d.ts +112 -0
  26. package/dist/runtime/composables/useMinutaApi.js +68 -0
  27. package/dist/runtime/composables/vaultImages.d.ts +12 -0
  28. package/dist/runtime/composables/vaultImages.js +85 -0
  29. package/dist/runtime/composables/versionDiff.d.ts +34 -0
  30. package/dist/runtime/composables/versionDiff.js +62 -0
  31. package/dist/runtime/server/api/minuta/[...path].d.ts +2 -0
  32. package/dist/runtime/server/api/minuta/[...path].js +6 -0
  33. package/dist/runtime/server/api/minuta/index.d.ts +2 -0
  34. package/dist/runtime/server/api/minuta/index.js +3 -0
  35. package/dist/runtime/server/utils/minuta-proxy.d.ts +10 -0
  36. package/dist/runtime/server/utils/minuta-proxy.js +63 -0
  37. package/dist/runtime/utils/footnoteHtml.d.ts +26 -0
  38. package/dist/runtime/utils/footnoteHtml.js +117 -0
  39. package/dist/runtime/utils/pocKeepWithNext.d.ts +44 -0
  40. package/dist/runtime/utils/pocKeepWithNext.js +9 -0
  41. package/dist/runtime/utils/sanitizeRichHtml.d.ts +10 -0
  42. package/dist/runtime/utils/sanitizeRichHtml.js +73 -0
  43. package/dist/types.d.mts +7 -0
  44. package/dist/types.d.ts +7 -0
  45. package/package.json +100 -0
@@ -0,0 +1,1158 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
3
+ import { Editor as TiptapEditor, BubbleMenu as TiptapBubbleMenu } from '@tiptap/vue-3'
4
+ import { sanitizeRichHtml } from '../utils/sanitizeRichHtml'
5
+ import { vaultImageHtml } from '../composables/vaultImages'
6
+ import { sanitizeFootnoteHtml } from '../utils/footnoteHtml'
7
+ import MinutaIcon from './MinutaIcon.vue'
8
+ import type { Editor } from '@tiptap/core'
9
+ import type { PocPageConfig, PocPageNote } from '../composables/paginationPlugin'
10
+ import { POC_ASSET_SWAP, buildPocExtensions, renumberPocFootnotes } from '../composables/editorExtensions'
11
+ import {
12
+ PAGINATE_META,
13
+ breaksEqual,
14
+ computePocBreaks,
15
+ pocPageKey,
16
+ } from '../composables/paginationPlugin'
17
+
18
+ const props = defineProps<{
19
+ initialHtml: string
20
+ margins: { top: number, right: number, bottom: number, left: number }
21
+ header: { logoUrl: string | null, position: 'left' | 'center' | 'right' }
22
+ editable: boolean
23
+ chromeText: { header: string, footer: string, headerScope?: 'all' | 'first', footerScope?: 'all' | 'first', showPageNumber?: boolean }
24
+ /** realce de proveniência de bloco no hover (só faz sentido no modo estruturado) */
25
+ showBlockOrigin?: boolean
26
+ /**
27
+ * upload da imagem inserida pela toolbar (ex.: Vault). Quando presente, a
28
+ * tag entra com src presignado + data-vault-src durável; sem ele (ou em
29
+ * falha), a imagem vira data-URI embutida no documento.
30
+ */
31
+ uploadImage?: ((file: File) => Promise<{ src: string, vaultSrc: string } | null>) | null
32
+ }>()
33
+
34
+ const emit = defineEmits<{
35
+ ready: [editor: Editor]
36
+ changed: []
37
+ blockClick: [blockId: string]
38
+ }>()
39
+
40
+ /** Na fase de revisão (read-only), clicar num bloco abre o bloco no checklist. */
41
+ function onSheetClick(event: MouseEvent) {
42
+ if (props.editable)
43
+ return
44
+ const target = event.target as HTMLElement
45
+ if (target.closest('.poc-varchip') || target.closest('.poc-page-headerarea')
46
+ || target.closest('.poc-page-footerarea') || target.closest('.poc-first-header')
47
+ || target.closest('.poc-last-footer')) {
48
+ return
49
+ }
50
+ const block = target.closest('[data-block-id]')
51
+ if (block)
52
+ emit('blockClick', block.getAttribute('data-block-id')!)
53
+ }
54
+
55
+ const A4_WIDTH_PX = 794
56
+ const A4_HEIGHT_PX = 1123
57
+ const PAGE_GAP_PX = 32
58
+
59
+ const pageConfig: PocPageConfig = {
60
+ pageHeight: A4_HEIGHT_PX,
61
+ margins: props.margins,
62
+ gap: PAGE_GAP_PX,
63
+ headerLogoUrl: props.header.logoUrl,
64
+ headerPosition: props.header.position,
65
+ headerText: props.chromeText.header,
66
+ footerText: props.chromeText.footer,
67
+ headerScope: props.chromeText.headerScope ?? 'all',
68
+ footerScope: props.chromeText.footerScope ?? 'all',
69
+ showPageNumber: props.chromeText.showPageNumber !== false,
70
+ version: 0,
71
+ }
72
+
73
+ const editor = shallowRef<Editor | null>(null)
74
+ const editorHost = ref<HTMLElement | null>(null)
75
+ const sheetRef = ref<HTMLElement | null>(null)
76
+ const createError = ref<string | null>(null)
77
+ const totalPages = ref(1)
78
+ const uiTick = ref(0)
79
+ /** notas de rodapé cujas chamadas caem na ÚLTIMA página (sem chrome próprio) */
80
+ const lastPageNotes = ref<PocPageNote[]>([])
81
+
82
+ /** Reconstrói o chrome (cabeçalhos/rodapés) com os textos atuais, sem repaginar. */
83
+ function refreshChrome() {
84
+ const ed = editor.value
85
+ if (!ed)
86
+ return
87
+ pageConfig.headerLogoUrl = props.header.logoUrl
88
+ pageConfig.headerPosition = props.header.position
89
+ pageConfig.headerText = props.chromeText.header
90
+ pageConfig.footerText = props.chromeText.footer
91
+ pageConfig.headerScope = props.chromeText.headerScope ?? 'all'
92
+ pageConfig.footerScope = props.chromeText.footerScope ?? 'all'
93
+ pageConfig.showPageNumber = props.chromeText.showPageNumber !== false
94
+ pageConfig.version = (pageConfig.version ?? 0) + 1
95
+ const current = pocPageKey.getState(ed.state)
96
+ if (current)
97
+ ed.view.dispatch(ed.state.tr.setMeta(PAGINATE_META, { ...current, breaks: [...current.breaks] }))
98
+ }
99
+
100
+ watch(
101
+ () => [props.chromeText.header, props.chromeText.footer, props.chromeText.headerScope, props.chromeText.footerScope, props.chromeText.showPageNumber, props.header.logoUrl, props.header.position],
102
+ () => refreshChrome(),
103
+ )
104
+
105
+ // --- paginação: agenda a medição fora do ciclo do PM (rAF) e só despacha se os breaks mudaram ---
106
+ // (lone rows — linha com uma única célula preenchida — agora são node
107
+ // decorations do plugin de paginação: sobrevivem a qualquer redraw do PM)
108
+ let paginateScheduled = false
109
+ function schedulePaginate() {
110
+ if (paginateScheduled || !editor.value || !sheetRef.value)
111
+ return
112
+ paginateScheduled = true
113
+ requestAnimationFrame(() => {
114
+ paginateScheduled = false
115
+ const ed = editor.value
116
+ const sheet = sheetRef.value
117
+ if (!ed || ed.isDestroyed || !sheet)
118
+ return
119
+ const { breaks, total, markers, lastNotes } = computePocBreaks(ed.view, pageConfig, sheet)
120
+ const notesChanged = lastNotes.length !== lastPageNotes.value.length
121
+ if (notesChanged)
122
+ lastPageNotes.value = lastNotes
123
+ const current = pocPageKey.getState(ed.state)
124
+ if (current && breaksEqual(current.breaks, breaks) && current.total === total
125
+ && current.markers.length === markers.length && !notesChanged) {
126
+ return
127
+ }
128
+ totalPages.value = total
129
+ ed.view.dispatch(ed.state.tr.setMeta(PAGINATE_META, { breaks, total, markers }))
130
+ })
131
+ }
132
+
133
+ let contentObserver: ResizeObserver | null = null
134
+
135
+ onMounted(() => {
136
+ // chamada de nota clicada (nodeview das extensões) → abre o popover
137
+ window.addEventListener('poc:edit-footnote', onEditFootnoteEvent)
138
+ try {
139
+ // TiptapEditor = classe Editor de @tiptap/vue-3 (auto-import do nuxt-tiptap-editor)
140
+ editor.value = new TiptapEditor({
141
+ element: editorHost.value!,
142
+ content: props.initialHtml,
143
+ editable: props.editable,
144
+ extensions: buildPocExtensions(pageConfig),
145
+ onCreate: ({ editor }) => {
146
+ emit('ready', editor as Editor)
147
+ nextTick(() => {
148
+ schedulePaginate()
149
+ const pmEl = (editor as Editor).view.dom as HTMLElement
150
+ contentObserver = new ResizeObserver(() => schedulePaginate())
151
+ contentObserver.observe(pmEl)
152
+ })
153
+ },
154
+ onTransaction: ({ transaction }) => {
155
+ uiTick.value++
156
+ if (transaction.docChanged) {
157
+ // troca de imagem fria (load progressivo) não é edição do
158
+ // advogado: repagina, mas não marca o doc como sujo — senão
159
+ // o autosave dispararia sozinho durante o carregamento
160
+ if (!transaction.getMeta(POC_ASSET_SWAP))
161
+ emit('changed')
162
+ schedulePaginate()
163
+ }
164
+ },
165
+ }) as unknown as Editor
166
+ }
167
+ catch (error: any) {
168
+ createError.value = error?.message ?? String(error)
169
+ console.error('[poc] falha ao criar editor:', error)
170
+ }
171
+ })
172
+
173
+ onBeforeUnmount(() => {
174
+ contentObserver?.disconnect()
175
+ editor.value?.destroy()
176
+ })
177
+
178
+ watch(() => props.editable, (value) => {
179
+ editor.value?.setEditable(value)
180
+ })
181
+
182
+ // --- zoom (mesma abordagem do preview: CSS zoom quando o painel é mais estreito que A4) ---
183
+ const containerRef = ref<HTMLElement | null>(null)
184
+ const zoom = ref(1)
185
+ let resizeObserver: ResizeObserver | null = null
186
+ watch(containerRef, (el) => {
187
+ resizeObserver?.disconnect()
188
+ resizeObserver = null
189
+ if (!el)
190
+ return
191
+ resizeObserver = new ResizeObserver(() => {
192
+ zoom.value = Math.min(1, (el.clientWidth - 48) / A4_WIDTH_PX)
193
+ })
194
+ resizeObserver.observe(el)
195
+ }, { immediate: true })
196
+ onUnmounted(() => {
197
+ window.removeEventListener('poc:edit-footnote', onEditFootnoteEvent)
198
+ resizeObserver?.disconnect()
199
+ })
200
+
201
+ const sheetStyle = computed(() => ({
202
+ width: `${A4_WIDTH_PX}px`,
203
+ minHeight: `${A4_HEIGHT_PX}px`,
204
+ padding: `${props.margins.top}px ${props.margins.right}px ${props.margins.bottom}px ${props.margins.left}px`,
205
+ boxSizing: 'border-box' as const,
206
+ zoom: zoom.value,
207
+ }))
208
+
209
+ /** chrome em formato canvas (editor estilo Word): coordenadas no frame da página */
210
+ function isChromeCanvas(html: string | undefined) {
211
+ return !!html && html.includes('data-el=')
212
+ }
213
+
214
+ const chromeCanvasStyle = computed(() => ({
215
+ position: 'absolute' as const,
216
+ top: '0',
217
+ left: `${-props.margins.left}px`,
218
+ width: '794px',
219
+ height: '100%',
220
+ pointerEvents: 'none' as const,
221
+ }))
222
+
223
+ const firstHeaderStyle = computed(() => ({
224
+ height: `${props.margins.top}px`,
225
+ left: `${props.margins.left}px`,
226
+ right: `${props.margins.right}px`,
227
+ textAlign: props.header.position,
228
+ lineHeight: `${props.margins.top}px`,
229
+ }))
230
+
231
+ const lastFooterStyle = computed(() => ({
232
+ height: `${props.margins.bottom}px`,
233
+ left: `${props.margins.left}px`,
234
+ right: `${props.margins.right}px`,
235
+ }))
236
+
237
+ /** as notas da última página encostam no rodapé, dentro da área útil */
238
+ const lastNotesStyle = computed(() => ({
239
+ position: 'absolute' as const,
240
+ bottom: `${props.margins.bottom}px`,
241
+ left: `${props.margins.left}px`,
242
+ right: `${props.margins.right}px`,
243
+ }))
244
+
245
+ function editChrome(part: 'header' | 'footer') {
246
+ window.dispatchEvent(new CustomEvent('poc:edit-chrome', { detail: { part } }))
247
+ }
248
+
249
+ // --- toolbar estilo Google Docs (etapa de edição) ---
250
+ const PALETTE = [
251
+ '#000000',
252
+ '#434343',
253
+ '#666666',
254
+ '#999999',
255
+ '#B7B7B7',
256
+ '#CCCCCC',
257
+ '#D9D9D9',
258
+ '#EFEFEF',
259
+ '#F3F3F3',
260
+ '#FFFFFF',
261
+ '#980000',
262
+ '#FF0000',
263
+ '#FF9900',
264
+ '#FFFF00',
265
+ '#00FF00',
266
+ '#00FFFF',
267
+ '#4A86E8',
268
+ '#0000FF',
269
+ '#9900FF',
270
+ '#FF00FF',
271
+ '#E6B8AF',
272
+ '#F4CCCC',
273
+ '#FCE5CD',
274
+ '#FFF2CC',
275
+ '#D9EAD3',
276
+ '#D0E0E3',
277
+ '#C9DAF8',
278
+ '#CFE2F3',
279
+ '#D9D2E9',
280
+ '#EAD1DC',
281
+ '#CC4125',
282
+ '#E06666',
283
+ '#F6B26B',
284
+ '#FFD966',
285
+ '#93C47D',
286
+ '#76A5AF',
287
+ '#6D9EEB',
288
+ '#6FA8DC',
289
+ '#8E7CC3',
290
+ '#C27BA0',
291
+ '#A61C00',
292
+ '#CC0000',
293
+ '#E69138',
294
+ '#F1C232',
295
+ '#6AA84F',
296
+ '#45818E',
297
+ '#3C78D8',
298
+ '#3D85C6',
299
+ '#674EA7',
300
+ '#A64D79',
301
+ ]
302
+
303
+ const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 24, 30, 36]
304
+
305
+ /**
306
+ * tamanho (pt) efetivo da seleção: mark inlineStyle manda; senão o computed
307
+ * do bloco onde o cursor está (o pipeline inlina font-size nos parágrafos)
308
+ */
309
+ const currentFontSizePt = computed<number | null>(() => {
310
+ void uiTick.value
311
+ const ed = editor.value
312
+ if (!ed)
313
+ return null
314
+ const markStyle: string | undefined = ed.getAttributes('inlineStyle')?.style
315
+ const fromMark = markStyle?.match(/font-size:\s*([\d.]+)\s*(pt|px)/i)
316
+ if (fromMark) {
317
+ const value = Number.parseFloat(fromMark[1])
318
+ return Math.round(fromMark[2].toLowerCase() === 'pt' ? value : value / (96 / 72))
319
+ }
320
+ try {
321
+ const { node, offset } = ed.view.domAtPos(ed.state.selection.from)
322
+ const el = node.nodeType === Node.TEXT_NODE
323
+ ? node.parentElement
324
+ : (node.childNodes[offset] instanceof HTMLElement ? node.childNodes[offset] as HTMLElement : node as HTMLElement)
325
+ if (el instanceof HTMLElement)
326
+ return Math.round(Number.parseFloat(getComputedStyle(el).fontSize) / (96 / 72))
327
+ }
328
+ catch {
329
+ // posição sem DOM (ex.: seleção de nó atômico)
330
+ }
331
+ return null
332
+ })
333
+
334
+ const openPalette = ref<'color' | 'highlight' | null>(null)
335
+
336
+ const active = computed(() => {
337
+ void uiTick.value
338
+ const ed = editor.value
339
+ if (!ed)
340
+ return { bold: false, italic: false, underline: false, strike: false, bullet: false, ordered: false, heading: 0 }
341
+ return {
342
+ bold: ed.isActive('bold'),
343
+ italic: ed.isActive('italic'),
344
+ underline: ed.isActive('underline'),
345
+ strike: ed.isActive('strike'),
346
+ bullet: ed.isActive('bulletList'),
347
+ ordered: ed.isActive('orderedList'),
348
+ heading: ed.isActive('heading', { level: 1 }) ? 1 : ed.isActive('heading', { level: 2 }) ? 2 : ed.isActive('heading', { level: 3 }) ? 3 : 0,
349
+ }
350
+ })
351
+
352
+ /** aplica uma propriedade de estilo inline preservando as demais (cor + tamanho convivem) */
353
+ function mergeInlineStyle(prop: string, value: string) {
354
+ const ed = editor.value
355
+ if (!ed)
356
+ return
357
+ const current: string = ed.getAttributes('inlineStyle')?.style || ''
358
+ const cleaned = current.replace(new RegExp(`${prop}:\\s*[^;]+;?`, 'gi'), '').trim()
359
+ const style = `${cleaned}${cleaned && !cleaned.endsWith(';') ? ';' : ''} ${prop}: ${value};`.trim()
360
+ ed.chain().focus().setMark('inlineStyle', { style }).run()
361
+ }
362
+
363
+ function setTextColor(color: string) {
364
+ mergeInlineStyle('color', color)
365
+ openPalette.value = null
366
+ }
367
+
368
+ function setHighlight(color: string) {
369
+ mergeInlineStyle('background-color', color)
370
+ openPalette.value = null
371
+ }
372
+
373
+ function setFontSize(event: Event) {
374
+ const value = (event.target as HTMLSelectElement).value
375
+ if (value)
376
+ mergeInlineStyle('font-size', `${value}pt`)
377
+ }
378
+
379
+ /** mesmas fontes do canvas de cabeçalho/rodapé — o export lê a fonte por run */
380
+ const DOC_FONTS = ['Arial', 'Times New Roman', 'Calibri', 'Cambria', 'Georgia', 'Garamond', 'Verdana', 'Courier New']
381
+
382
+ const currentFontFamily = computed<string | null>(() => {
383
+ void uiTick.value
384
+ const ed = editor.value
385
+ if (!ed)
386
+ return null
387
+ const markStyle: string | undefined = ed.getAttributes('inlineStyle')?.style
388
+ const fromMark = markStyle?.match(/font-family:\s*([^;]+)/i)
389
+ if (fromMark)
390
+ return fromMark[1].split(',')[0].trim().replace(/["']/g, '')
391
+ try {
392
+ const { node } = ed.view.domAtPos(ed.state.selection.from)
393
+ const el = node.nodeType === Node.TEXT_NODE ? node.parentElement : node as HTMLElement
394
+ const family = el ? getComputedStyle(el).fontFamily : ''
395
+ return family.split(',')[0]?.trim().replace(/["']/g, '') || null
396
+ }
397
+ catch {
398
+ return null
399
+ }
400
+ })
401
+
402
+ function setFontFamily(event: Event) {
403
+ const value = (event.target as HTMLSelectElement).value
404
+ if (value)
405
+ mergeInlineStyle('font-family', value)
406
+ }
407
+
408
+ // --- notas de rodapé: popover ancorado no documento (estilo Google Docs) ---
409
+ const footnoteEditor = ref<{ pos: number, text: string, top: number, left: number, isNew: boolean } | null>(null)
410
+ const footnoteInputRef = ref<HTMLDivElement | null>(null)
411
+
412
+ /** personalização da nota: mesmos tamanhos/cores acessíveis num clique */
413
+ const FOOTNOTE_SIZES = [7, 8, 9, 10, 11, 12]
414
+ const FOOTNOTE_COLORS = ['#000000', '#6b7280', '#b91c1c', '#1d4ed8', '#047857']
415
+
416
+ function openFootnoteEditor(pos: number, isNew: boolean) {
417
+ const ed = editor.value
418
+ const sheet = sheetRef.value
419
+ if (!ed || ed.isDestroyed || !sheet)
420
+ return
421
+ const node = ed.state.doc.nodeAt(pos)
422
+ if (!node || node.type.name !== 'footnoteRef')
423
+ return
424
+ const coords = ed.view.coordsAtPos(pos)
425
+ const sheetRect = sheet.getBoundingClientRect()
426
+ footnoteEditor.value = {
427
+ pos,
428
+ text: node.attrs.text,
429
+ top: coords.bottom - sheetRect.top + 6,
430
+ left: Math.max(8, Math.min(coords.left - sheetRect.left, sheetRect.width - 372)),
431
+ isNew,
432
+ }
433
+ nextTick(() => {
434
+ const el = footnoteInputRef.value
435
+ if (!el)
436
+ return
437
+ el.innerHTML = sanitizeFootnoteHtml(footnoteEditor.value?.text ?? '')
438
+ el.focus()
439
+ // caret no fim do conteúdo
440
+ const sel = window.getSelection()
441
+ if (sel) {
442
+ const range = document.createRange()
443
+ range.selectNodeContents(el)
444
+ range.collapse(false)
445
+ sel.removeAllRanges()
446
+ sel.addRange(range)
447
+ }
448
+ })
449
+ }
450
+
451
+ /** B/I/U no trecho selecionado dentro do popover */
452
+ function footnoteExec(command: 'bold' | 'italic' | 'underline') {
453
+ footnoteInputRef.value?.focus()
454
+ document.execCommand(command)
455
+ }
456
+
457
+ /** fonte/tamanho/cor no trecho selecionado (span com estilo permitido) */
458
+ function applyFootnoteStyle(prop: 'font-family' | 'font-size' | 'color', value: string) {
459
+ const el = footnoteInputRef.value
460
+ const sel = window.getSelection()
461
+ if (!el || !sel || sel.rangeCount === 0)
462
+ return
463
+ const range = sel.getRangeAt(0)
464
+ if (!el.contains(range.commonAncestorContainer) || range.collapsed)
465
+ return
466
+ const span = document.createElement('span')
467
+ span.style.setProperty(prop, value)
468
+ span.appendChild(range.extractContents())
469
+ range.insertNode(span)
470
+ sel.removeAllRanges()
471
+ const next = document.createRange()
472
+ next.selectNodeContents(span)
473
+ sel.addRange(next)
474
+ el.focus()
475
+ }
476
+
477
+ function onFootnoteFontChange(event: Event) {
478
+ const value = (event.target as HTMLSelectElement).value
479
+ if (value)
480
+ applyFootnoteStyle('font-family', value)
481
+ ;(event.target as HTMLSelectElement).value = ''
482
+ }
483
+
484
+ function onFootnoteSizeChange(event: Event) {
485
+ const value = (event.target as HTMLSelectElement).value
486
+ if (value)
487
+ applyFootnoteStyle('font-size', `${value}pt`)
488
+ ;(event.target as HTMLSelectElement).value = ''
489
+ }
490
+
491
+ /** insere a chamada no cursor e abre o popover pra digitar a nota */
492
+ function insertFootnote() {
493
+ const ed = editor.value
494
+ if (!ed)
495
+ return
496
+ // colapsa a seleção (inserir com texto selecionado o substituiria)
497
+ const to = ed.state.selection.to
498
+ ed.chain().focus().setTextSelection(to).insertContent({ type: 'footnoteRef', attrs: { number: '?', text: '' } }).run()
499
+ renumberPocFootnotes(ed)
500
+ const pos = ed.state.selection.from - 1
501
+ openFootnoteEditor(pos, true)
502
+ }
503
+
504
+ function saveFootnote() {
505
+ const state = footnoteEditor.value
506
+ const ed = editor.value
507
+ // lê o conteúdo formatado ANTES de fechar o popover
508
+ const html = footnoteInputRef.value ? sanitizeFootnoteHtml(footnoteInputRef.value.innerHTML) : state?.text ?? ''
509
+ footnoteEditor.value = null
510
+ if (!state || !ed || ed.isDestroyed)
511
+ return
512
+ const node = ed.state.doc.nodeAt(state.pos)
513
+ if (!node || node.type.name !== 'footnoteRef')
514
+ return
515
+ const text = html.trim()
516
+ if (!text)
517
+ ed.chain().focus().deleteRange({ from: state.pos, to: state.pos + node.nodeSize }).run()
518
+ else
519
+ ed.view.dispatch(ed.state.tr.setNodeMarkup(state.pos, undefined, { ...node.attrs, text }))
520
+ renumberPocFootnotes(ed)
521
+ }
522
+
523
+ function cancelFootnote() {
524
+ const state = footnoteEditor.value
525
+ const ed = editor.value
526
+ footnoteEditor.value = null
527
+ // cancelar uma nota recém-criada (ainda vazia) remove a chamada
528
+ if (state?.isNew && ed && !ed.isDestroyed) {
529
+ const node = ed.state.doc.nodeAt(state.pos)
530
+ if (node?.type.name === 'footnoteRef' && !node.attrs.text)
531
+ ed.chain().focus().deleteRange({ from: state.pos, to: state.pos + node.nodeSize }).run()
532
+ renumberPocFootnotes(ed)
533
+ }
534
+ }
535
+
536
+ function removeFootnote() {
537
+ if (footnoteInputRef.value)
538
+ footnoteInputRef.value.innerHTML = ''
539
+ saveFootnote()
540
+ }
541
+
542
+ function onEditFootnoteEvent(event: Event) {
543
+ if (!props.editable)
544
+ return
545
+ const { pos } = (event as CustomEvent).detail || {}
546
+ if (typeof pos === 'number')
547
+ openFootnoteEditor(pos, false)
548
+ }
549
+
550
+ /** clicar na PRÓPRIA nota no pé da página também abre a edição */
551
+ function onNotesClick(event: MouseEvent) {
552
+ if (!props.editable)
553
+ return
554
+ const line = (event.target as HTMLElement).closest?.('.poc-page-note')
555
+ if (!line)
556
+ return
557
+ const number = line.querySelector('sup')?.textContent?.trim()
558
+ const ed = editor.value
559
+ if (!number || !ed)
560
+ return
561
+ let target: number | null = null
562
+ ed.state.doc.descendants((node, pos) => {
563
+ if (target != null)
564
+ return false
565
+ if (node.type.name === 'footnoteRef' && node.attrs.number === number) {
566
+ target = pos
567
+ return false
568
+ }
569
+ return true
570
+ })
571
+ if (target != null)
572
+ openFootnoteEditor(target, false)
573
+ }
574
+
575
+ /** alinhamento por bloco: mescla text-align no style dos parágrafos/títulos selecionados */
576
+ function setAlignment(align: 'left' | 'center' | 'right' | 'justify') {
577
+ const ed = editor.value
578
+ if (!ed)
579
+ return
580
+ const { state, view } = ed
581
+ const { from, to } = state.selection
582
+ const tr = state.tr
583
+ state.doc.nodesBetween(from, to, (node, pos) => {
584
+ if (node.type.name === 'paragraph' || node.type.name === 'heading') {
585
+ const style = (node.attrs.style || '').replace(/text-align:\s*[^;]+;?/gi, '').trim()
586
+ const merged = `${style}${style && !style.endsWith(';') ? ';' : ''} text-align: ${align};`.trim()
587
+ tr.setNodeMarkup(pos, undefined, { ...node.attrs, style: merged })
588
+ }
589
+ })
590
+ view.dispatch(tr)
591
+ ed.commands.focus()
592
+ }
593
+
594
+ // inserir imagem do computador (vira ilha: arrastável e redimensionável)
595
+ const imageInput = ref<HTMLInputElement | null>(null)
596
+
597
+ function pickImage() {
598
+ imageInput.value?.click()
599
+ }
600
+
601
+ function insertDataUriImage(file: File) {
602
+ const reader = new FileReader()
603
+ reader.onload = () => {
604
+ editor.value?.chain().focus().insertContent(
605
+ `<img src="${reader.result}" style="width: 420px; height: auto; max-width: 100%;">`,
606
+ ).run()
607
+ }
608
+ reader.readAsDataURL(file)
609
+ }
610
+
611
+ function onImageFile(event: Event) {
612
+ const file = (event.target as HTMLInputElement).files?.[0]
613
+ if (!file || !file.type.startsWith('image/'))
614
+ return
615
+ if (props.uploadImage) {
616
+ void (async () => {
617
+ try {
618
+ const uploaded = await props.uploadImage!(file)
619
+ if (uploaded) {
620
+ editor.value?.chain().focus().insertContent(vaultImageHtml(uploaded.src, uploaded.vaultSrc)).run()
621
+ return
622
+ }
623
+ }
624
+ catch (error) {
625
+ console.error('[minuta-nuxt] upload de imagem falhou, embutindo como data URI', error)
626
+ }
627
+ insertDataUriImage(file)
628
+ })()
629
+ }
630
+ else {
631
+ insertDataUriImage(file)
632
+ }
633
+ ;(event.target as HTMLInputElement).value = ''
634
+ }
635
+
636
+ /** insere a quebra de página manual do template (o engine e o DOCX respeitam) */
637
+ function insertPageBreak() {
638
+ editor.value?.chain().focus().insertContent('<p>PAGEBREAK</p>').run()
639
+ }
640
+
641
+ function clearFormatting() {
642
+ editor.value?.chain().focus().unsetAllMarks().run()
643
+ }
644
+
645
+ const TEXT_COLORS = [
646
+ { label: 'Preto', value: '#1C242D' },
647
+ { label: 'Vermelho', value: '#DC2626' },
648
+ { label: 'Azul', value: '#2563EB' },
649
+ { label: 'Verde', value: '#16A34A' },
650
+ { label: 'Laranja', value: '#EA580C' },
651
+ ]
652
+
653
+ function applyColor(color: string) {
654
+ editor.value?.chain().focus().setMark('inlineStyle', { style: `color: ${color}` }).run()
655
+ }
656
+
657
+ function clearColor() {
658
+ editor.value?.chain().focus().unsetMark('inlineStyle').run()
659
+ }
660
+
661
+ defineExpose({ editor, schedulePaginate, refreshChrome, containerRef })
662
+ </script>
663
+
664
+ <template>
665
+ <div class="h-full w-full flex flex-col poc-editor-root" :class="{ 'poc-no-block-origin': props.showBlockOrigin === false }">
666
+ <!-- toolbar de edição (estilo Google Docs) -->
667
+ <div v-if="editable" class="poc-toolbar">
668
+ <button class="poc-tool" title="Desfazer" @click="editor?.chain().focus().undo().run()">
669
+ <MinutaIcon name="i-ph-arrow-u-up-left" />
670
+ </button>
671
+ <button class="poc-tool" title="Refazer" @click="editor?.chain().focus().redo().run()">
672
+ <MinutaIcon name="i-ph-arrow-u-up-right" />
673
+ </button>
674
+ <div class="poc-tool-sep" />
675
+ <select class="poc-tool-select w-118px" title="Fonte" :value="currentFontFamily ?? ''" @change="setFontFamily">
676
+ <option value="" disabled>
677
+ Fonte
678
+ </option>
679
+ <option v-if="currentFontFamily && !DOC_FONTS.includes(currentFontFamily)" :value="currentFontFamily">
680
+ {{ currentFontFamily }}
681
+ </option>
682
+ <option v-for="font in DOC_FONTS" :key="font" :value="font" :style="{ fontFamily: font }">
683
+ {{ font }}
684
+ </option>
685
+ </select>
686
+
687
+ <select class="poc-tool-select w-64px" title="Tamanho da fonte" :value="currentFontSizePt ?? ''" @change="setFontSize">
688
+ <option value="" disabled>
689
+ Tam.
690
+ </option>
691
+ <option v-if="currentFontSizePt != null && !FONT_SIZES.includes(currentFontSizePt)" :value="currentFontSizePt">
692
+ {{ currentFontSizePt }}
693
+ </option>
694
+ <option v-for="size in FONT_SIZES" :key="size" :value="size">
695
+ {{ size }}
696
+ </option>
697
+ </select>
698
+ <div class="poc-tool-sep" />
699
+ <button class="poc-tool font-bold" :class="{ 'poc-tool--on': active.bold }" title="Negrito" @click="editor?.chain().focus().toggleBold().run()">
700
+ B
701
+ </button>
702
+ <button class="poc-tool italic" :class="{ 'poc-tool--on': active.italic }" title="Itálico" @click="editor?.chain().focus().toggleItalic().run()">
703
+ I
704
+ </button>
705
+ <button class="poc-tool underline" :class="{ 'poc-tool--on': active.underline }" title="Sublinhado" @click="editor?.chain().focus().toggleMark('underline').run()">
706
+ U
707
+ </button>
708
+ <button class="poc-tool line-through" :class="{ 'poc-tool--on': active.strike }" title="Tachado" @click="editor?.chain().focus().toggleStrike().run()">
709
+ S
710
+ </button>
711
+ <div class="poc-tool-sep" />
712
+ <div class="relative">
713
+ <button class="poc-tool" title="Cor do texto" @click="openPalette = openPalette === 'color' ? null : 'color'">
714
+ <span class="font-semibold">A</span>
715
+ <span class="poc-color-underline" />
716
+ </button>
717
+ <div v-if="openPalette === 'color'" class="poc-palette">
718
+ <button v-for="c in PALETTE" :key="c" class="poc-palette-swatch" :style="{ background: c }" :title="c" @click="setTextColor(c)" />
719
+ <label class="poc-palette-custom">
720
+ Personalizada
721
+ <input type="color" @change="setTextColor(($event.target as HTMLInputElement).value)">
722
+ </label>
723
+ </div>
724
+ </div>
725
+ <div class="relative">
726
+ <button class="poc-tool" title="Cor de destaque" @click="openPalette = openPalette === 'highlight' ? null : 'highlight'">
727
+ <MinutaIcon name="i-ph-highlighter" />
728
+ </button>
729
+ <div v-if="openPalette === 'highlight'" class="poc-palette">
730
+ <button v-for="c in PALETTE" :key="c" class="poc-palette-swatch" :style="{ background: c }" :title="c" @click="setHighlight(c)" />
731
+ <label class="poc-palette-custom">
732
+ Personalizada
733
+ <input type="color" @change="setHighlight(($event.target as HTMLInputElement).value)">
734
+ </label>
735
+ </div>
736
+ </div>
737
+ <div class="poc-tool-sep" />
738
+ <button class="poc-tool" title="Alinhar à esquerda" @click="setAlignment('left')">
739
+ <MinutaIcon name="i-ph-text-align-left" />
740
+ </button>
741
+ <button class="poc-tool" title="Centralizar" @click="setAlignment('center')">
742
+ <MinutaIcon name="i-ph-text-align-center" />
743
+ </button>
744
+ <button class="poc-tool" title="Alinhar à direita" @click="setAlignment('right')">
745
+ <MinutaIcon name="i-ph-text-align-right" />
746
+ </button>
747
+ <button class="poc-tool" title="Justificar" @click="setAlignment('justify')">
748
+ <MinutaIcon name="i-ph-text-align-justify" />
749
+ </button>
750
+ <div class="poc-tool-sep" />
751
+ <button class="poc-tool" :class="{ 'poc-tool--on': active.bullet }" title="Lista com marcadores" @click="editor?.chain().focus().toggleBulletList().run()">
752
+ <MinutaIcon name="i-ph-list-bullets" />
753
+ </button>
754
+ <button class="poc-tool" :class="{ 'poc-tool--on': active.ordered }" title="Lista numerada" @click="editor?.chain().focus().toggleOrderedList().run()">
755
+ <MinutaIcon name="i-ph-list-numbers" />
756
+ </button>
757
+ <div class="poc-tool-sep" />
758
+ <button class="poc-tool" title="Inserir imagem" @click="pickImage">
759
+ <MinutaIcon name="i-ph-image" />
760
+ </button>
761
+ <input ref="imageInput" type="file" accept="image/*" class="hidden" @change="onImageFile">
762
+ <button class="poc-tool" title="Inserir nota de rodapé" @click="insertFootnote">
763
+ <MinutaIcon name="i-ph-note" />
764
+ </button>
765
+ <button class="poc-tool" title="Inserir quebra de página" @click="insertPageBreak">
766
+ <MinutaIcon name="i-ph-arrows-out-line-vertical" />
767
+ </button>
768
+ <button class="poc-tool" title="Limpar formatação" @click="clearFormatting">
769
+ <MinutaIcon name="i-ph-eraser" />
770
+ </button>
771
+ </div>
772
+
773
+ <div ref="containerRef" class="relative min-h-0 w-full flex-1 overflow-y-auto bg-gray-120 py-24px" @click="openPalette = null">
774
+ <div ref="sheetRef" class="poc-sheet relative mx-auto bg-white shadow-md" :style="sheetStyle" @click="onNotesClick">
775
+ <!-- cabeçalho da 1ª página (na margem superior da folha) -->
776
+ <div class="poc-first-header" :style="firstHeaderStyle" title="Clique para editar o cabeçalho" @click="editChrome('header')">
777
+ <img
778
+ v-if="header.logoUrl"
779
+ :src="header.logoUrl"
780
+ :style="{ maxHeight: `${Math.max(24, margins.top - 36)}px`, maxWidth: '50%' }"
781
+ class="inline-block align-middle"
782
+ >
783
+ <!-- HTML persistido → sanitizado (stored XSS) -->
784
+ <!-- eslint-disable-next-line vue/no-v-html -->
785
+ <div v-if="isChromeCanvas(chromeText.header)" class="poc-chrome-canvas" :style="chromeCanvasStyle" v-html="sanitizeRichHtml(chromeText.header)" />
786
+ <!-- eslint-disable-next-line vue/no-v-html -->
787
+ <div v-else class="poc-chrome-text" v-html="sanitizeRichHtml(chromeText.header)" />
788
+ </div>
789
+
790
+ <p v-if="createError" class="body-13-regular text-red-600">
791
+ Falha ao montar o editor: {{ createError }}
792
+ </p>
793
+ <div ref="editorHost" :class="{ 'poc-review-cursor': !editable }" @click="onSheetClick" />
794
+
795
+ <!-- notas cujas chamadas caem na última página (as demais moram no chrome) -->
796
+ <div v-if="lastPageNotes.length" class="poc-page-notes poc-last-notes" :style="lastNotesStyle">
797
+ <!-- conteúdo da nota já sai sanitizado do render; re-sanitiza porque
798
+ também pode vir de edição salva no servidor -->
799
+ <!-- eslint-disable-next-line vue/no-v-html -->
800
+ <p
801
+ v-for="note in lastPageNotes"
802
+ :key="note.n"
803
+ class="poc-page-note"
804
+ :style="note.size ? { fontSize: `${note.size}pt` } : undefined"
805
+ v-html="`<sup>${note.n}</sup> ${sanitizeFootnoteHtml(note.text)}`"
806
+ />
807
+ </div>
808
+
809
+ <!-- rodapé da última página (na margem inferior da folha) -->
810
+ <div class="poc-last-footer" :style="lastFooterStyle" title="Clique para editar o rodapé" @click="editChrome('footer')">
811
+ <template v-if="!(chromeText.footerScope === 'first' && totalPages > 1)">
812
+ <!-- eslint-disable-next-line vue/no-v-html -->
813
+ <div v-if="isChromeCanvas(chromeText.footer)" class="poc-chrome-canvas" :style="chromeCanvasStyle" v-html="sanitizeRichHtml(chromeText.footer)" />
814
+ <!-- eslint-disable-next-line vue/no-v-html -->
815
+ <div v-else class="poc-chrome-text" v-html="sanitizeRichHtml(chromeText.footer)" />
816
+ </template>
817
+ <div v-else class="poc-chrome-text" />
818
+ <span v-if="chromeText.showPageNumber !== false" class="ml-auto">Página {{ totalPages }} de {{ totalPages }}</span>
819
+ </div>
820
+ <slot name="gutter" />
821
+
822
+ <!-- popover de edição da nota de rodapé (ancorado na chamada) -->
823
+ <div
824
+ v-if="footnoteEditor"
825
+ class="poc-footnote-pop"
826
+ :style="{ top: `${footnoteEditor.top}px`, left: `${footnoteEditor.left}px` }"
827
+ @click.stop
828
+ >
829
+ <div class="poc-footnote-pop__head">
830
+ Nota de rodapé
831
+ </div>
832
+ <div class="poc-footnote-pop__toolbar">
833
+ <button class="poc-footnote-pop__tool font-bold" type="button" title="Negrito" @mousedown.prevent @click="footnoteExec('bold')">
834
+ B
835
+ </button>
836
+ <button class="poc-footnote-pop__tool italic" type="button" title="Itálico" @mousedown.prevent @click="footnoteExec('italic')">
837
+ I
838
+ </button>
839
+ <button class="poc-footnote-pop__tool underline" type="button" title="Sublinhado" @mousedown.prevent @click="footnoteExec('underline')">
840
+ U
841
+ </button>
842
+ <span class="poc-footnote-pop__sep" />
843
+ <select class="poc-footnote-pop__select" title="Fonte do trecho selecionado" @mousedown.stop @change="onFootnoteFontChange">
844
+ <option value="">
845
+ Fonte
846
+ </option>
847
+ <option v-for="font in DOC_FONTS" :key="font" :value="font" :style="{ fontFamily: font }">
848
+ {{ font }}
849
+ </option>
850
+ </select>
851
+ <select class="poc-footnote-pop__select poc-footnote-pop__select--narrow" title="Tamanho do trecho selecionado" @mousedown.stop @change="onFootnoteSizeChange">
852
+ <option value="">
853
+ Tam.
854
+ </option>
855
+ <option v-for="size in FOOTNOTE_SIZES" :key="size" :value="size">
856
+ {{ size }}
857
+ </option>
858
+ </select>
859
+ <span class="poc-footnote-pop__sep" />
860
+ <button
861
+ v-for="color in FOOTNOTE_COLORS"
862
+ :key="color"
863
+ class="poc-footnote-pop__swatch"
864
+ type="button"
865
+ :style="{ background: color }"
866
+ :title="`Cor ${color}`"
867
+ @mousedown.prevent
868
+ @click="applyFootnoteStyle('color', color)"
869
+ />
870
+ </div>
871
+ <div
872
+ ref="footnoteInputRef"
873
+ class="poc-footnote-pop__input"
874
+ contenteditable="true"
875
+ data-placeholder="Escreva a nota…"
876
+ @keydown.enter.exact.prevent="saveFootnote"
877
+ @keydown.esc.prevent="cancelFootnote"
878
+ />
879
+ <div class="poc-footnote-pop__actions">
880
+ <button v-if="!footnoteEditor.isNew" class="poc-footnote-pop__remove" type="button" @click="removeFootnote">
881
+ Remover
882
+ </button>
883
+ <span class="poc-footnote-pop__spacer" />
884
+ <button class="poc-footnote-pop__cancel" type="button" @click="cancelFootnote">
885
+ Cancelar
886
+ </button>
887
+ <button class="poc-footnote-pop__save" type="button" @click="saveFootnote">
888
+ Salvar
889
+ </button>
890
+ </div>
891
+ </div>
892
+ </div>
893
+
894
+ <TiptapBubbleMenu v-if="editor && editable" :editor="editor" :tippy-options="{ duration: 100 }">
895
+ <div class="flex items-center gap-2px rounded-8px border border-gray-200 bg-white p-2px shadow-lg">
896
+ <button
897
+ class="poc-bubble-btn font-bold"
898
+ :class="{ 'poc-bubble-btn--active': editor.isActive('bold') }"
899
+ @click="editor.chain().focus().toggleBold().run()"
900
+ >
901
+ B
902
+ </button>
903
+ <button
904
+ class="poc-bubble-btn italic"
905
+ :class="{ 'poc-bubble-btn--active': editor.isActive('italic') }"
906
+ @click="editor.chain().focus().toggleItalic().run()"
907
+ >
908
+ I
909
+ </button>
910
+ <button
911
+ class="poc-bubble-btn underline"
912
+ :class="{ 'poc-bubble-btn--active': editor.isActive('underline') }"
913
+ @click="editor.chain().focus().toggleMark('underline').run()"
914
+ >
915
+ U
916
+ </button>
917
+ <div class="mx-2px h-16px w-1px bg-gray-200" />
918
+ <button
919
+ v-for="color in TEXT_COLORS"
920
+ :key="color.value"
921
+ class="poc-color-swatch"
922
+ :style="{ background: color.value }"
923
+ :title="color.label"
924
+ @click="applyColor(color.value)"
925
+ />
926
+ <button class="poc-bubble-btn" title="Remover cor" @click="clearColor">
927
+ <MinutaIcon name="i-ph-eraser" />
928
+ </button>
929
+ </div>
930
+ </TiptapBubbleMenu>
931
+ </div>
932
+ </div>
933
+ </template>
934
+
935
+ <style scoped>
936
+ :deep(.poc-ins){background:rgba(34,197,94,.16);text-decoration:underline;text-decoration-color:#16a34a}:deep(.poc-del){background:rgba(239,68,68,.12);color:#b91c1c;text-decoration:line-through}.poc-bubble-btn{border-radius:6px;font-size:13px;padding:4px 8px;transition:all .15s;transition-timing-function:ease}.poc-toolbar{background:#fafbfc;border-bottom:.5px solid #dee3e7;flex-wrap:wrap;gap:2px;padding:4px 12px}.poc-tool,.poc-toolbar{align-items:center;display:flex}.poc-tool{border-radius:6px;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-tool-sep{background:#dee3e7;height:20px;margin:0 6px;width:1px}.poc-tool-select{background:#fff;border-radius:6px;font-size:12px;height:28px;padding-left:6px;padding-right:6px}.poc-color-underline{background:#dc2626;border-radius:1px;bottom:4px;height:3px;left:7px;position:absolute;right:7px}.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-palette-swatch{border:1px solid rgba(0,0,0,.1);border-radius:3px;cursor:pointer;height:19px;width:19px}.poc-palette-swatch:hover{transform:scale(1.2)}.poc-palette-custom{align-items:center;color:#565d69;cursor:pointer;display:flex;font-size:12px;margin-top:6px;width:100%}.poc-color-swatch{border:1px solid rgba(0,0,0,.12);border-radius:4px;cursor:pointer;height:16px;margin:0 2px;width:16px}.poc-color-swatch:hover{transform:scale(1.15)}.poc-first-header{align-items:center;cursor:pointer;display:flex;gap:12px;position:absolute;top:0}.poc-first-header[style*=center]{justify-content:center}.poc-first-header[style*=right]{justify-content:flex-end}.poc-first-header:hover,.poc-last-footer:hover,:deep(.poc-page-footerarea:hover),:deep(.poc-page-headerarea:hover){background:rgba(99,102,241,.06);outline:1px dashed #c7d2fe}.poc-last-footer{align-items:center;bottom:0;color:#94a3b8;cursor:pointer;display:flex;font-size:11px;justify-content:space-between;position:absolute}.poc-chrome-text,:deep(.poc-chrome-text){color:#64748b;font-size:11px}.poc-chrome-text :deep(img),:deep(.poc-chrome-text img){display:inline-block;max-height:48px;max-width:200px;vertical-align:middle}:deep(.poc-pagebreak-marker){border-top:1px dashed #a5b4fc;font-size:0!important;height:16px;line-height:0!important;margin:6px 0!important;position:relative}:deep(.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%)}:deep(.poc-page-chrome){pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}:deep(.poc-page-footerarea),:deep(.poc-page-headerarea){cursor:pointer;pointer-events:auto}:deep(.poc-page-footerarea){align-items:center;color:#94a3b8;display:flex;font-size:11px;justify-content:space-between}:deep(.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)}:deep(.poc-page-headerarea){align-items:center;display:flex}:deep(.poc-page-headerarea[style*=center]){justify-content:center}:deep(.poc-page-headerarea[style*=right]){justify-content:flex-end}:deep(.ProseMirror){outline:none;white-space:pre-wrap;word-wrap:break-word}:deep(h1){color:#0f172a;font-size:16px;font-weight:540;letter-spacing:-.2px;line-height:20px;margin-bottom:20px}:deep(h2){color:#0f172a;font-size:14px;font-size:24px;font-weight:540;letter-spacing:-.15px;line-height:16px;margin-bottom:20px}:deep(h3){color:#0f172a;font-size:12px;font-size:20px;font-weight:540;letter-spacing:-.1px;line-height:14px;margin-bottom:20px}:deep(strong){font-weight:540}:deep(p){color:#000;font-size:18px;font-weight:400;letter-spacing:-.4px;line-height:28px;line-height:1.85em;margin-bottom:2em}:deep(blockquote){font-size:18px;font-weight:400;letter-spacing:-.4px;line-height:28px;margin-bottom:2em;margin-left:35%}:deep(ol),:deep(ul){margin-bottom:2em}:deep(li){list-style-position:inside;list-style-type:disc;margin-bottom:16px;margin-left:48px}:deep(table){border:.5px solid #d4d9df;max-width:100%;table-layout:fixed;width:100%}:deep(th){color:inherit;font-size:inherit;font-weight:700;overflow-wrap:anywhere;padding:12px;text-align:left}:deep(td){color:inherit;font-size:inherit;line-height:inherit;overflow-wrap:anywhere;padding:12px;text-align:left}:deep(td p),:deep(th p){color:inherit;font-size:inherit;line-height:inherit;margin-bottom:0}:deep(img){display:block;max-width:100%}:deep(.poc-island--img>img),:deep(.poc-island>img:only-child){margin-left:auto;margin-right:auto}:deep(.poc-chrome-canvas){overflow:hidden}:deep(.poc-chrome-canvas [data-el]){position:absolute}:deep(.poc-chrome-canvas [data-el=text]){color:#64748b;font-size:11px;line-height:1.4;text-align:left;white-space:pre-wrap}:deep(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}}:deep(.poc-lone-row){justify-content:center!important}:deep(.poc-lone-row>:not(.poc-lone-cell)){flex:0 0 0!important;min-width:0!important;padding:0!important}:deep(.poc-lone-row>.poc-lone-cell){align-items:center!important;flex:0 0 auto!important;max-width:100%;min-width:0}:deep([data-horizontal-align=left] .poc-island--img>img),:deep([data-horizontal-align=left] .poc-island>img:only-child){margin-left:0}:deep([data-horizontal-align=right] .poc-island--img>img),:deep([data-horizontal-align=right] .poc-island>img:only-child){margin-right:0}.poc-review-cursor :deep(.poc-minuta-block){cursor:pointer}:deep(.poc-minuta-block){border-radius:4px;position:relative}:deep(.poc-minuta-block:hover){outline:1px dashed #c7d2fe;outline-offset:6px}.poc-no-block-origin :deep(.poc-minuta-block:hover){outline:none}.poc-no-block-origin :deep(.poc-minuta-block:hover):before{content:none}:deep(.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}:deep(.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}:deep(.poc-varchip:hover){background:rgba(59,130,246,.16)}:deep(.poc-missing-image .missing-image-warning:hover){background-color:rgba(254,174,0,.09);border-color:rgba(223,139,13,.85)}:deep(.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}:deep(.poc-missing-image:hover .poc-missing-image__remove){opacity:1}:deep(.poc-missing-image__remove:hover){background:rgba(254,174,0,.35)}:deep(.poc-missing-image.ProseMirror-selectednode .missing-image-warning){outline:2px solid rgba(223,139,13,.9);outline-offset:2px}.poc-page-notes,:deep(.poc-page-notes){color:#1c242d;font-size:10.67px;line-height:1.25;padding-top:6px}.poc-page-notes:before,:deep(.poc-page-notes):before{border-top:.5px solid #9aa4ae;content:"";display:block;margin-bottom:6px;width:30%}.poc-page-note,:deep(.poc-page-note){color:#1c242d;font-size:10.67px;font-weight:400;line-height:1.25;margin:0;text-align:justify}.poc-page-note sup,:deep(.poc-page-note sup){font-size:.8em;line-height:0;vertical-align:super}.poc-page-note a,:deep(.poc-page-note a){color:#0563c1;text-decoration:underline}:deep(.poc-footnote){color:#1d4ed8;cursor:help;font-size:.7em;line-height:0;vertical-align:super}:deep(.poc-island){position:relative}:deep(.poc-island--img){cursor:grab}:deep(.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}:deep(.poc-island--img.ProseMirror-selectednode .poc-img-handle),:deep(.poc-island--img:hover .poc-img-handle){opacity:1}:deep(.poc-island.ProseMirror-selectednode){outline:2px solid #93c5fd;outline-offset:2px}
937
+ </style>
938
+
939
+ <style scoped>
940
+ /* utilities que no CDB vêm do UnoCSS — aqui o package se basta */
941
+ .h-full { height: 100%; }
942
+ .w-full { width: 100%; }
943
+ .w-1px { width: 1px; }
944
+ .w-64px { width: 64px; }
945
+ .h-16px { height: 16px; }
946
+ .flex { display: flex; }
947
+ .flex-col { flex-direction: column; }
948
+ .flex-1 { flex: 1 1 0%; }
949
+ .items-center { align-items: center; }
950
+ .gap-2px { gap: 2px; }
951
+ .min-h-0 { min-height: 0; }
952
+ .overflow-y-auto { overflow-y: auto; }
953
+ .relative { position: relative; }
954
+ .hidden { display: none; }
955
+ .inline-block { display: inline-block; }
956
+ .align-middle { vertical-align: middle; }
957
+ .ml-auto { margin-left: auto; }
958
+ .mx-auto { margin-left: auto; margin-right: auto; }
959
+ .mx-2px { margin-left: 2px; margin-right: 2px; }
960
+ .p-2px { padding: 2px; }
961
+ .py-24px { padding-top: 24px; padding-bottom: 24px; }
962
+ .bg-white { background: #fff; }
963
+ .bg-gray-120 { background: #ebedf0; }
964
+ .bg-gray-200 { background: #e5e7eb; }
965
+ .border { border-width: 1px; border-style: solid; }
966
+ .border-gray-200 { border-color: #e5e7eb; }
967
+ .rounded-8px { border-radius: 8px; }
968
+ .shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); }
969
+ .shadow-lg { box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); }
970
+ .font-bold { font-weight: 700; }
971
+ .font-semibold { font-weight: 600; }
972
+ .italic { font-style: italic; }
973
+ .underline { text-decoration: underline; }
974
+ .line-through { text-decoration: line-through; }
975
+ .text-red-600 { color: #dc2626; }
976
+ .body-13-regular { font-size: 13px; font-weight: 400; }
977
+ </style>
978
+
979
+ <style scoped>
980
+ /* acabamento de paridade com o defesa.ai (hover states + fonte + detalhes
981
+ que o conversor de utilities não cobre) */
982
+ .poc-editor-root {
983
+ font-family: 'Inter Variable', Inter, -apple-system, 'Segoe UI', Roboto, sans-serif;
984
+ }
985
+
986
+ .poc-toolbar {
987
+ background: #fafbfc;
988
+ border-bottom: 0.5px solid #dee3e7;
989
+ }
990
+
991
+ .poc-tool {
992
+ border: none;
993
+ background: transparent;
994
+ cursor: pointer;
995
+ }
996
+
997
+ .poc-tool:hover {
998
+ background: #ebedf0;
999
+ }
1000
+
1001
+ .poc-tool--on {
1002
+ background: #e2e8f0;
1003
+ }
1004
+
1005
+ .poc-tool-select {
1006
+ border: 0.5px solid #d4d9df;
1007
+ background: #fff;
1008
+ cursor: pointer;
1009
+ }
1010
+
1011
+ .poc-editor-root :deep(.ProseMirror) {
1012
+ outline: none;
1013
+ }
1014
+
1015
+ .poc-editor-root :deep(td),
1016
+ .poc-editor-root :deep(th) {
1017
+ border: 0.5px solid #d4d9df;
1018
+ }
1019
+
1020
+ /* no CDB os tokens de heading vencem os overrides text-24px/text-20px — replica */
1021
+ .poc-editor-root :deep(h2) {
1022
+ font-size: 14px;
1023
+ line-height: 16px;
1024
+ }
1025
+
1026
+ .poc-editor-root :deep(h3) {
1027
+ font-size: 12px;
1028
+ line-height: 14px;
1029
+ }
1030
+
1031
+ /* popover de nota de rodapé (ancorado na chamada, estilo comentário) */
1032
+ .poc-footnote-pop {
1033
+ position: absolute;
1034
+ z-index: 40;
1035
+ width: 364px;
1036
+ background: #fff;
1037
+ border: 0.5px solid #d4d9df;
1038
+ border-radius: 10px;
1039
+ padding: 10px 12px;
1040
+ box-shadow: 0 10px 25px -5px rgb(0 0 0 / 0.18);
1041
+ }
1042
+
1043
+ .poc-footnote-pop__head {
1044
+ font-size: 11px;
1045
+ font-weight: 650;
1046
+ letter-spacing: 0.04em;
1047
+ text-transform: uppercase;
1048
+ color: #6b7280;
1049
+ margin-bottom: 6px;
1050
+ }
1051
+
1052
+ .poc-footnote-pop__toolbar {
1053
+ display: flex;
1054
+ align-items: center;
1055
+ gap: 4px;
1056
+ margin-bottom: 6px;
1057
+ }
1058
+
1059
+ .poc-footnote-pop__tool {
1060
+ width: 24px;
1061
+ height: 24px;
1062
+ border: none;
1063
+ border-radius: 6px;
1064
+ background: transparent;
1065
+ color: #374151;
1066
+ font-size: 12px;
1067
+ cursor: pointer;
1068
+ }
1069
+
1070
+ .poc-footnote-pop__tool:hover {
1071
+ background: rgba(28, 36, 45, 0.06);
1072
+ }
1073
+
1074
+ .poc-footnote-pop__sep {
1075
+ width: 1px;
1076
+ height: 16px;
1077
+ background: #e5e7eb;
1078
+ margin: 0 2px;
1079
+ }
1080
+
1081
+ .poc-footnote-pop__select {
1082
+ height: 24px;
1083
+ border: 1px solid #e5e7eb;
1084
+ border-radius: 6px;
1085
+ background: #fff;
1086
+ font-size: 11px;
1087
+ color: #374151;
1088
+ max-width: 108px;
1089
+ outline: none;
1090
+ }
1091
+
1092
+ .poc-footnote-pop__select--narrow {
1093
+ max-width: 58px;
1094
+ }
1095
+
1096
+ .poc-footnote-pop__swatch {
1097
+ width: 14px;
1098
+ height: 14px;
1099
+ border: 1px solid rgb(0 0 0 / 0.15);
1100
+ border-radius: 4px;
1101
+ padding: 0;
1102
+ cursor: pointer;
1103
+ }
1104
+
1105
+ .poc-footnote-pop__input {
1106
+ width: 100%;
1107
+ min-height: 58px;
1108
+ max-height: 160px;
1109
+ overflow-y: auto;
1110
+ border: 1px solid #e5e7eb;
1111
+ border-radius: 8px;
1112
+ padding: 6px 8px;
1113
+ font-size: 12.5px;
1114
+ line-height: 1.5;
1115
+ color: #111827;
1116
+ outline: none;
1117
+ }
1118
+
1119
+ .poc-footnote-pop__input:focus {
1120
+ border-color: #a5b4fc;
1121
+ }
1122
+
1123
+ .poc-footnote-pop__input:empty::before {
1124
+ content: attr(data-placeholder);
1125
+ color: #9ca3af;
1126
+ }
1127
+
1128
+ .poc-footnote-pop__actions {
1129
+ display: flex;
1130
+ align-items: center;
1131
+ gap: 8px;
1132
+ margin-top: 8px;
1133
+ }
1134
+
1135
+ .poc-footnote-pop__spacer {
1136
+ flex: 1;
1137
+ }
1138
+
1139
+ .poc-footnote-pop__actions button {
1140
+ border: none;
1141
+ border-radius: 8px;
1142
+ padding: 4px 12px;
1143
+ font-size: 12px;
1144
+ font-weight: 500;
1145
+ cursor: pointer;
1146
+ background: transparent;
1147
+ color: #565d69;
1148
+ }
1149
+
1150
+ .poc-footnote-pop__remove {
1151
+ color: #b91c1c !important;
1152
+ }
1153
+
1154
+ .poc-footnote-pop__save {
1155
+ background: rgba(0, 0, 0, 0.88) !important;
1156
+ color: #fff !important;
1157
+ }
1158
+ </style>