@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.
- package/README.md +106 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +18 -0
- package/dist/module.d.ts +18 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +45 -0
- package/dist/runtime/components/MinutaAnnotationsGutter.vue +425 -0
- package/dist/runtime/components/MinutaChatEditCard.vue +124 -0
- package/dist/runtime/components/MinutaChromeCanvas.vue +549 -0
- package/dist/runtime/components/MinutaChromeEditor.vue +100 -0
- package/dist/runtime/components/MinutaDocumentViewer.vue +90 -0
- package/dist/runtime/components/MinutaDocxEditor.vue +1158 -0
- package/dist/runtime/components/MinutaEmbed.vue +783 -0
- package/dist/runtime/components/MinutaIcon.vue +53 -0
- package/dist/runtime/composables/annotationAnchors.d.ts +73 -0
- package/dist/runtime/composables/annotationAnchors.js +243 -0
- package/dist/runtime/composables/chatEditOps.d.ts +37 -0
- package/dist/runtime/composables/chatEditOps.js +61 -0
- package/dist/runtime/composables/docxExport.d.ts +24 -0
- package/dist/runtime/composables/docxExport.js +1292 -0
- package/dist/runtime/composables/editorExtensions.d.ts +56 -0
- package/dist/runtime/composables/editorExtensions.js +466 -0
- package/dist/runtime/composables/paginationPlugin.d.ts +126 -0
- package/dist/runtime/composables/paginationPlugin.js +421 -0
- package/dist/runtime/composables/useMinutaApi.d.ts +112 -0
- package/dist/runtime/composables/useMinutaApi.js +68 -0
- package/dist/runtime/composables/vaultImages.d.ts +12 -0
- package/dist/runtime/composables/vaultImages.js +85 -0
- package/dist/runtime/composables/versionDiff.d.ts +34 -0
- package/dist/runtime/composables/versionDiff.js +62 -0
- package/dist/runtime/server/api/minuta/[...path].d.ts +2 -0
- package/dist/runtime/server/api/minuta/[...path].js +6 -0
- package/dist/runtime/server/api/minuta/index.d.ts +2 -0
- package/dist/runtime/server/api/minuta/index.js +3 -0
- package/dist/runtime/server/utils/minuta-proxy.d.ts +10 -0
- package/dist/runtime/server/utils/minuta-proxy.js +63 -0
- package/dist/runtime/utils/footnoteHtml.d.ts +26 -0
- package/dist/runtime/utils/footnoteHtml.js +117 -0
- package/dist/runtime/utils/pocKeepWithNext.d.ts +44 -0
- package/dist/runtime/utils/pocKeepWithNext.js +9 -0
- package/dist/runtime/utils/sanitizeRichHtml.d.ts +10 -0
- package/dist/runtime/utils/sanitizeRichHtml.js +73 -0
- package/dist/types.d.mts +7 -0
- package/dist/types.d.ts +7 -0
- 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>
|