@meistrari/minuta-nuxt 1.1.1 → 1.2.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/dist/module.json +1 -1
- package/dist/runtime/components/MinutaChromeCanvas.vue +1 -1
- package/dist/runtime/components/MinutaChromeEditor.vue +1 -1
- package/dist/runtime/components/MinutaDocxEditor.vue +64 -1302
- package/dist/runtime/components/MinutaEmbed.vue +2 -2
- package/dist/runtime/components/MinutaTemplateEditor.vue +2 -2
- package/dist/runtime/composables/useMinutaApi.d.ts +2 -2
- package/dist/runtime/composables/useMinutaApi.js +1 -1
- package/dist/runtime/composables/vaultImages.d.ts +1 -5
- package/dist/runtime/composables/vaultImages.js +2 -12
- package/package.json +2 -1
- package/dist/runtime/components/MinutaVariablePicker.vue +0 -149
- package/dist/runtime/composables/docxExport.d.ts +0 -24
- package/dist/runtime/composables/docxExport.js +0 -1304
- package/dist/runtime/composables/editorExtensions.d.ts +0 -93
- package/dist/runtime/composables/editorExtensions.js +0 -548
- package/dist/runtime/composables/paginationPlugin.d.ts +0 -139
- package/dist/runtime/composables/paginationPlugin.js +0 -505
- package/dist/runtime/utils/footnoteHtml.d.ts +0 -26
- package/dist/runtime/utils/footnoteHtml.js +0 -117
- package/dist/runtime/utils/pocKeepWithNext.d.ts +0 -44
- package/dist/runtime/utils/pocKeepWithNext.js +0 -9
- package/dist/runtime/utils/sanitizeRichHtml.d.ts +0 -10
- package/dist/runtime/utils/sanitizeRichHtml.js +0 -73
- package/dist/runtime/utils/templateVariables.d.ts +0 -49
- package/dist/runtime/utils/templateVariables.js +0 -36
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
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 MinutaVariablePicker from './MinutaVariablePicker.vue'
|
|
9
|
-
import type { MinutaVariableDef } from '../utils/templateVariables'
|
|
2
|
+
import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
|
10
3
|
import type { Editor } from '@tiptap/core'
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
import { createMinutaEditor } from '@meistrari/minuta-editor'
|
|
5
|
+
import type { MinutaEditor, MinutaVariableDef } from '@meistrari/minuta-editor'
|
|
6
|
+
import '@meistrari/minuta-editor/style.css'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Casca Vue do editor. O editor em si — folha A4, paginação, toolbar,
|
|
10
|
+
* popovers, placeholders — mora em `@meistrari/minuta-editor`, sem framework;
|
|
11
|
+
* este componente só traduz props/emits/expose para a API dele.
|
|
12
|
+
*
|
|
13
|
+
* A API pública (props, eventos, `defineExpose`, slot `gutter`) é a mesma de
|
|
14
|
+
* antes da extração: quem consome `<MinutaDocxEditor>` — o embed, o editor de
|
|
15
|
+
* template, o client do defesa.ai — não muda uma linha.
|
|
16
|
+
*/
|
|
19
17
|
|
|
20
18
|
const props = defineProps<{
|
|
21
19
|
initialHtml: string
|
|
@@ -33,9 +31,7 @@ const props = defineProps<{
|
|
|
33
31
|
uploadImage?: ((file: File) => Promise<{ src: string, vaultSrc: string } | null>) | null
|
|
34
32
|
/**
|
|
35
33
|
* Catálogo de variáveis do app hospedeiro. Com ele o editor vira editor de
|
|
36
|
-
* TEMPLATE: `/` e o botão da toolbar inserem placeholders.
|
|
37
|
-
* nada muda — placeholders já existentes no documento continuam sendo
|
|
38
|
-
* renderizados, só não há como inserir novos.
|
|
34
|
+
* TEMPLATE: `/` e o botão da toolbar inserem placeholders.
|
|
39
35
|
*/
|
|
40
36
|
variables?: MinutaVariableDef[]
|
|
41
37
|
}>()
|
|
@@ -46,1309 +42,75 @@ const emit = defineEmits<{
|
|
|
46
42
|
blockClick: [blockId: string]
|
|
47
43
|
}>()
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (props.editable)
|
|
52
|
-
return
|
|
53
|
-
const target = event.target as HTMLElement
|
|
54
|
-
if (target.closest('.poc-varchip') || target.closest('.poc-page-headerarea')
|
|
55
|
-
|| target.closest('.poc-page-footerarea') || target.closest('.poc-first-header')
|
|
56
|
-
|| target.closest('.poc-last-footer')) {
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
const block = target.closest('[data-block-id]')
|
|
60
|
-
if (block)
|
|
61
|
-
emit('blockClick', block.getAttribute('data-block-id')!)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const A4_WIDTH_PX = 794
|
|
65
|
-
const A4_HEIGHT_PX = 1123
|
|
66
|
-
const PAGE_GAP_PX = 32
|
|
67
|
-
|
|
68
|
-
const pageConfig: PocPageConfig = {
|
|
69
|
-
pageHeight: A4_HEIGHT_PX,
|
|
70
|
-
margins: props.margins,
|
|
71
|
-
gap: PAGE_GAP_PX,
|
|
72
|
-
headerLogoUrl: props.header.logoUrl,
|
|
73
|
-
headerPosition: props.header.position,
|
|
74
|
-
headerText: props.chromeText.header,
|
|
75
|
-
footerText: props.chromeText.footer,
|
|
76
|
-
headerScope: props.chromeText.headerScope ?? 'all',
|
|
77
|
-
footerScope: props.chromeText.footerScope ?? 'all',
|
|
78
|
-
showPageNumber: props.chromeText.showPageNumber !== false,
|
|
79
|
-
version: 0,
|
|
80
|
-
}
|
|
81
|
-
|
|
45
|
+
const host = ref<HTMLElement | null>(null)
|
|
46
|
+
/** a instância do Tiptap — o que os consumidores sempre receberam em `editor` */
|
|
82
47
|
const editor = shallowRef<Editor | null>(null)
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const uiTick = ref(0)
|
|
88
|
-
/** notas de rodapé cujas chamadas caem na ÚLTIMA página (sem chrome próprio) */
|
|
89
|
-
const lastPageNotes = ref<PocPageNote[]>([])
|
|
90
|
-
|
|
91
|
-
/** Reconstrói o chrome (cabeçalhos/rodapés) com os textos atuais, sem repaginar. */
|
|
92
|
-
function refreshChrome() {
|
|
93
|
-
const ed = editor.value
|
|
94
|
-
if (!ed)
|
|
95
|
-
return
|
|
96
|
-
pageConfig.headerLogoUrl = props.header.logoUrl
|
|
97
|
-
pageConfig.headerPosition = props.header.position
|
|
98
|
-
pageConfig.headerText = props.chromeText.header
|
|
99
|
-
pageConfig.footerText = props.chromeText.footer
|
|
100
|
-
pageConfig.headerScope = props.chromeText.headerScope ?? 'all'
|
|
101
|
-
pageConfig.footerScope = props.chromeText.footerScope ?? 'all'
|
|
102
|
-
pageConfig.showPageNumber = props.chromeText.showPageNumber !== false
|
|
103
|
-
pageConfig.version = (pageConfig.version ?? 0) + 1
|
|
104
|
-
const current = pocPageKey.getState(ed.state)
|
|
105
|
-
if (current)
|
|
106
|
-
ed.view.dispatch(ed.state.tr.setMeta(PAGINATE_META, { ...current, breaks: [...current.breaks] }))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
watch(
|
|
110
|
-
() => [props.chromeText.header, props.chromeText.footer, props.chromeText.headerScope, props.chromeText.footerScope, props.chromeText.showPageNumber, props.header.logoUrl, props.header.position],
|
|
111
|
-
() => refreshChrome(),
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
// --- paginação: agenda a medição fora do ciclo do PM (rAF) e só despacha se os breaks mudaram ---
|
|
115
|
-
// (lone rows — linha com uma única célula preenchida — agora são node
|
|
116
|
-
// decorations do plugin de paginação: sobrevivem a qualquer redraw do PM)
|
|
117
|
-
let paginateScheduled = false
|
|
118
|
-
function schedulePaginate() {
|
|
119
|
-
if (paginateScheduled || !editor.value || !sheetRef.value)
|
|
120
|
-
return
|
|
121
|
-
paginateScheduled = true
|
|
122
|
-
requestAnimationFrame(() => {
|
|
123
|
-
paginateScheduled = false
|
|
124
|
-
const ed = editor.value
|
|
125
|
-
const sheet = sheetRef.value
|
|
126
|
-
if (!ed || ed.isDestroyed || !sheet)
|
|
127
|
-
return
|
|
128
|
-
const { breaks, total, markers, lastNotes } = computePocBreaks(ed.view, pageConfig, sheet)
|
|
129
|
-
const notesChanged = lastNotes.length !== lastPageNotes.value.length
|
|
130
|
-
if (notesChanged)
|
|
131
|
-
lastPageNotes.value = lastNotes
|
|
132
|
-
const current = pocPageKey.getState(ed.state)
|
|
133
|
-
if (current && breaksEqual(current.breaks, breaks) && current.total === total
|
|
134
|
-
&& current.markers.length === markers.length && !notesChanged) {
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
totalPages.value = total
|
|
138
|
-
ed.view.dispatch(ed.state.tr.setMeta(PAGINATE_META, { breaks, total, markers }))
|
|
139
|
-
})
|
|
140
|
-
}
|
|
48
|
+
/** container com scroll: o embed ancora o gutter de anotações nele */
|
|
49
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
50
|
+
/** a folha: alvo do Teleport do slot `gutter` */
|
|
51
|
+
const sheetEl = ref<HTMLElement | null>(null)
|
|
141
52
|
|
|
142
|
-
let
|
|
53
|
+
let instance: MinutaEditor | null = null
|
|
143
54
|
|
|
144
55
|
onMounted(() => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
contentObserver.observe(pmEl)
|
|
164
|
-
})
|
|
165
|
-
},
|
|
166
|
-
onTransaction: ({ transaction }) => {
|
|
167
|
-
uiTick.value++
|
|
168
|
-
syncVariableTrigger(transaction.docChanged)
|
|
169
|
-
if (transaction.docChanged) {
|
|
170
|
-
// troca de imagem fria (load progressivo) não é edição do
|
|
171
|
-
// advogado: repagina, mas não marca o doc como sujo — senão
|
|
172
|
-
// o autosave dispararia sozinho durante o carregamento
|
|
173
|
-
if (!transaction.getMeta(POC_ASSET_SWAP))
|
|
174
|
-
emit('changed')
|
|
175
|
-
schedulePaginate()
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
}) as unknown as Editor
|
|
179
|
-
}
|
|
180
|
-
catch (error: any) {
|
|
181
|
-
createError.value = error?.message ?? String(error)
|
|
182
|
-
console.error('[poc] falha ao criar editor:', error)
|
|
183
|
-
}
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
onBeforeUnmount(() => {
|
|
187
|
-
contentObserver?.disconnect()
|
|
188
|
-
editor.value?.destroy()
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
watch(() => props.editable, (value) => {
|
|
192
|
-
editor.value?.setEditable(value)
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
// --- zoom (mesma abordagem do preview: CSS zoom quando o painel é mais estreito que A4) ---
|
|
196
|
-
const containerRef = ref<HTMLElement | null>(null)
|
|
197
|
-
const zoom = ref(1)
|
|
198
|
-
let resizeObserver: ResizeObserver | null = null
|
|
199
|
-
watch(containerRef, (el) => {
|
|
200
|
-
resizeObserver?.disconnect()
|
|
201
|
-
resizeObserver = null
|
|
202
|
-
if (!el)
|
|
203
|
-
return
|
|
204
|
-
resizeObserver = new ResizeObserver(() => {
|
|
205
|
-
// respiro lateral da folha: 48px dá moldura no desktop, mas em tela de
|
|
206
|
-
// celular custa ~13% do zoom e o texto fica ilegível
|
|
207
|
-
const breathing = el.clientWidth < 640 ? 12 : 48
|
|
208
|
-
zoom.value = Math.min(1, (el.clientWidth - breathing) / A4_WIDTH_PX)
|
|
56
|
+
if (!host.value)
|
|
57
|
+
return
|
|
58
|
+
instance = createMinutaEditor(host.value, {
|
|
59
|
+
html: props.initialHtml,
|
|
60
|
+
editable: props.editable,
|
|
61
|
+
margins: props.margins,
|
|
62
|
+
header: props.header,
|
|
63
|
+
chrome: props.chromeText,
|
|
64
|
+
// getter: o catálogo pode chegar depois do mount (host que carrega async)
|
|
65
|
+
variables: () => props.variables ?? [],
|
|
66
|
+
uploadImage: props.uploadImage ?? undefined,
|
|
67
|
+
showBlockOrigin: props.showBlockOrigin,
|
|
68
|
+
onReady: (tiptap) => {
|
|
69
|
+
editor.value = tiptap
|
|
70
|
+
emit('ready', tiptap)
|
|
71
|
+
},
|
|
72
|
+
onChange: () => emit('changed'),
|
|
73
|
+
onBlockClick: blockId => emit('blockClick', blockId),
|
|
209
74
|
})
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
onUnmounted(() => {
|
|
213
|
-
window.removeEventListener('poc:edit-footnote', onEditFootnoteEvent)
|
|
214
|
-
resizeObserver?.disconnect()
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
const sheetStyle = computed(() => ({
|
|
218
|
-
width: `${A4_WIDTH_PX}px`,
|
|
219
|
-
minHeight: `${A4_HEIGHT_PX}px`,
|
|
220
|
-
padding: `${props.margins.top}px ${props.margins.right}px ${props.margins.bottom}px ${props.margins.left}px`,
|
|
221
|
-
boxSizing: 'border-box' as const,
|
|
222
|
-
zoom: zoom.value,
|
|
223
|
-
}))
|
|
224
|
-
|
|
225
|
-
/** chrome em formato canvas (editor estilo Word): coordenadas no frame da página */
|
|
226
|
-
function isChromeCanvas(html: string | undefined) {
|
|
227
|
-
return !!html && html.includes('data-el=')
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const chromeCanvasStyle = computed(() => ({
|
|
231
|
-
position: 'absolute' as const,
|
|
232
|
-
top: '0',
|
|
233
|
-
left: `${-props.margins.left}px`,
|
|
234
|
-
width: '794px',
|
|
235
|
-
height: '100%',
|
|
236
|
-
pointerEvents: 'none' as const,
|
|
237
|
-
}))
|
|
238
|
-
|
|
239
|
-
const firstHeaderStyle = computed(() => ({
|
|
240
|
-
height: `${props.margins.top}px`,
|
|
241
|
-
left: `${props.margins.left}px`,
|
|
242
|
-
right: `${props.margins.right}px`,
|
|
243
|
-
textAlign: props.header.position,
|
|
244
|
-
lineHeight: `${props.margins.top}px`,
|
|
245
|
-
}))
|
|
246
|
-
|
|
247
|
-
const lastFooterStyle = computed(() => ({
|
|
248
|
-
height: `${props.margins.bottom}px`,
|
|
249
|
-
left: `${props.margins.left}px`,
|
|
250
|
-
right: `${props.margins.right}px`,
|
|
251
|
-
}))
|
|
252
|
-
|
|
253
|
-
/** as notas da última página encostam no rodapé, dentro da área útil */
|
|
254
|
-
const lastNotesStyle = computed(() => ({
|
|
255
|
-
position: 'absolute' as const,
|
|
256
|
-
bottom: `${props.margins.bottom}px`,
|
|
257
|
-
left: `${props.margins.left}px`,
|
|
258
|
-
right: `${props.margins.right}px`,
|
|
259
|
-
}))
|
|
260
|
-
|
|
261
|
-
function editChrome(part: 'header' | 'footer') {
|
|
262
|
-
window.dispatchEvent(new CustomEvent('poc:edit-chrome', { detail: { part } }))
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// --- toolbar estilo Google Docs (etapa de edição) ---
|
|
266
|
-
const PALETTE = [
|
|
267
|
-
'#000000',
|
|
268
|
-
'#434343',
|
|
269
|
-
'#666666',
|
|
270
|
-
'#999999',
|
|
271
|
-
'#B7B7B7',
|
|
272
|
-
'#CCCCCC',
|
|
273
|
-
'#D9D9D9',
|
|
274
|
-
'#EFEFEF',
|
|
275
|
-
'#F3F3F3',
|
|
276
|
-
'#FFFFFF',
|
|
277
|
-
'#980000',
|
|
278
|
-
'#FF0000',
|
|
279
|
-
'#FF9900',
|
|
280
|
-
'#FFFF00',
|
|
281
|
-
'#00FF00',
|
|
282
|
-
'#00FFFF',
|
|
283
|
-
'#4A86E8',
|
|
284
|
-
'#0000FF',
|
|
285
|
-
'#9900FF',
|
|
286
|
-
'#FF00FF',
|
|
287
|
-
'#E6B8AF',
|
|
288
|
-
'#F4CCCC',
|
|
289
|
-
'#FCE5CD',
|
|
290
|
-
'#FFF2CC',
|
|
291
|
-
'#D9EAD3',
|
|
292
|
-
'#D0E0E3',
|
|
293
|
-
'#C9DAF8',
|
|
294
|
-
'#CFE2F3',
|
|
295
|
-
'#D9D2E9',
|
|
296
|
-
'#EAD1DC',
|
|
297
|
-
'#CC4125',
|
|
298
|
-
'#E06666',
|
|
299
|
-
'#F6B26B',
|
|
300
|
-
'#FFD966',
|
|
301
|
-
'#93C47D',
|
|
302
|
-
'#76A5AF',
|
|
303
|
-
'#6D9EEB',
|
|
304
|
-
'#6FA8DC',
|
|
305
|
-
'#8E7CC3',
|
|
306
|
-
'#C27BA0',
|
|
307
|
-
'#A61C00',
|
|
308
|
-
'#CC0000',
|
|
309
|
-
'#E69138',
|
|
310
|
-
'#F1C232',
|
|
311
|
-
'#6AA84F',
|
|
312
|
-
'#45818E',
|
|
313
|
-
'#3C78D8',
|
|
314
|
-
'#3D85C6',
|
|
315
|
-
'#674EA7',
|
|
316
|
-
'#A64D79',
|
|
317
|
-
]
|
|
318
|
-
|
|
319
|
-
const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 24, 30, 36]
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* tamanho (pt) efetivo da seleção: mark inlineStyle manda; senão o computed
|
|
323
|
-
* do bloco onde o cursor está (o pipeline inlina font-size nos parágrafos)
|
|
324
|
-
*/
|
|
325
|
-
const currentFontSizePt = computed<number | null>(() => {
|
|
326
|
-
void uiTick.value
|
|
327
|
-
const ed = editor.value
|
|
328
|
-
if (!ed)
|
|
329
|
-
return null
|
|
330
|
-
const markStyle: string | undefined = ed.getAttributes('inlineStyle')?.style
|
|
331
|
-
const fromMark = markStyle?.match(/font-size:\s*([\d.]+)\s*(pt|px)/i)
|
|
332
|
-
if (fromMark) {
|
|
333
|
-
const value = Number.parseFloat(fromMark[1])
|
|
334
|
-
return Math.round(fromMark[2].toLowerCase() === 'pt' ? value : value / (96 / 72))
|
|
335
|
-
}
|
|
336
|
-
try {
|
|
337
|
-
const { node, offset } = ed.view.domAtPos(ed.state.selection.from)
|
|
338
|
-
const el = node.nodeType === Node.TEXT_NODE
|
|
339
|
-
? node.parentElement
|
|
340
|
-
: (node.childNodes[offset] instanceof HTMLElement ? node.childNodes[offset] as HTMLElement : node as HTMLElement)
|
|
341
|
-
if (el instanceof HTMLElement)
|
|
342
|
-
return Math.round(Number.parseFloat(getComputedStyle(el).fontSize) / (96 / 72))
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
// posição sem DOM (ex.: seleção de nó atômico)
|
|
346
|
-
}
|
|
347
|
-
return null
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
const openPalette = ref<'color' | 'highlight' | null>(null)
|
|
351
|
-
|
|
352
|
-
const active = computed(() => {
|
|
353
|
-
void uiTick.value
|
|
354
|
-
const ed = editor.value
|
|
355
|
-
if (!ed)
|
|
356
|
-
return { bold: false, italic: false, underline: false, strike: false, bullet: false, ordered: false, heading: 0 }
|
|
357
|
-
return {
|
|
358
|
-
bold: ed.isActive('bold'),
|
|
359
|
-
italic: ed.isActive('italic'),
|
|
360
|
-
underline: ed.isActive('underline'),
|
|
361
|
-
strike: ed.isActive('strike'),
|
|
362
|
-
bullet: ed.isActive('bulletList'),
|
|
363
|
-
ordered: ed.isActive('orderedList'),
|
|
364
|
-
heading: ed.isActive('heading', { level: 1 }) ? 1 : ed.isActive('heading', { level: 2 }) ? 2 : ed.isActive('heading', { level: 3 }) ? 3 : 0,
|
|
365
|
-
}
|
|
75
|
+
containerRef.value = instance.container
|
|
76
|
+
sheetEl.value = instance.sheet
|
|
366
77
|
})
|
|
367
78
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return
|
|
373
|
-
const current: string = ed.getAttributes('inlineStyle')?.style || ''
|
|
374
|
-
const cleaned = current.replace(new RegExp(`${prop}:\\s*[^;]+;?`, 'gi'), '').trim()
|
|
375
|
-
const style = `${cleaned}${cleaned && !cleaned.endsWith(';') ? ';' : ''} ${prop}: ${value};`.trim()
|
|
376
|
-
ed.chain().focus().setMark('inlineStyle', { style }).run()
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function setTextColor(color: string) {
|
|
380
|
-
mergeInlineStyle('color', color)
|
|
381
|
-
openPalette.value = null
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function setHighlight(color: string) {
|
|
385
|
-
mergeInlineStyle('background-color', color)
|
|
386
|
-
openPalette.value = null
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function setFontSize(event: Event) {
|
|
390
|
-
const value = (event.target as HTMLSelectElement).value
|
|
391
|
-
if (value)
|
|
392
|
-
mergeInlineStyle('font-size', `${value}pt`)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** mesmas fontes do canvas de cabeçalho/rodapé — o export lê a fonte por run */
|
|
396
|
-
const DOC_FONTS = ['Arial', 'Times New Roman', 'Calibri', 'Cambria', 'Georgia', 'Garamond', 'Verdana', 'Courier New']
|
|
397
|
-
|
|
398
|
-
const currentFontFamily = computed<string | null>(() => {
|
|
399
|
-
void uiTick.value
|
|
400
|
-
const ed = editor.value
|
|
401
|
-
if (!ed)
|
|
402
|
-
return null
|
|
403
|
-
const markStyle: string | undefined = ed.getAttributes('inlineStyle')?.style
|
|
404
|
-
const fromMark = markStyle?.match(/font-family:\s*([^;]+)/i)
|
|
405
|
-
if (fromMark)
|
|
406
|
-
return fromMark[1].split(',')[0].trim().replace(/["']/g, '')
|
|
407
|
-
try {
|
|
408
|
-
const { node } = ed.view.domAtPos(ed.state.selection.from)
|
|
409
|
-
const el = node.nodeType === Node.TEXT_NODE ? node.parentElement : node as HTMLElement
|
|
410
|
-
const family = el ? getComputedStyle(el).fontFamily : ''
|
|
411
|
-
return family.split(',')[0]?.trim().replace(/["']/g, '') || null
|
|
412
|
-
}
|
|
413
|
-
catch {
|
|
414
|
-
return null
|
|
415
|
-
}
|
|
79
|
+
onBeforeUnmount(() => {
|
|
80
|
+
instance?.destroy()
|
|
81
|
+
instance = null
|
|
82
|
+
editor.value = null
|
|
416
83
|
})
|
|
417
84
|
|
|
418
|
-
|
|
419
|
-
const value = (event.target as HTMLSelectElement).value
|
|
420
|
-
if (value)
|
|
421
|
-
mergeInlineStyle('font-family', value)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// --- notas de rodapé: popover ancorado no documento (estilo Google Docs) ---
|
|
425
|
-
const footnoteEditor = ref<{ pos: number, text: string, top: number, left: number, isNew: boolean } | null>(null)
|
|
426
|
-
const footnoteInputRef = ref<HTMLDivElement | null>(null)
|
|
427
|
-
|
|
428
|
-
/** personalização da nota: mesmos tamanhos/cores acessíveis num clique */
|
|
429
|
-
const FOOTNOTE_SIZES = [7, 8, 9, 10, 11, 12]
|
|
430
|
-
const FOOTNOTE_COLORS = ['#000000', '#6b7280', '#b91c1c', '#1d4ed8', '#047857']
|
|
431
|
-
|
|
432
|
-
function openFootnoteEditor(pos: number, isNew: boolean) {
|
|
433
|
-
const ed = editor.value
|
|
434
|
-
const sheet = sheetRef.value
|
|
435
|
-
if (!ed || ed.isDestroyed || !sheet)
|
|
436
|
-
return
|
|
437
|
-
const node = ed.state.doc.nodeAt(pos)
|
|
438
|
-
if (!node || node.type.name !== 'footnoteRef')
|
|
439
|
-
return
|
|
440
|
-
const coords = ed.view.coordsAtPos(pos)
|
|
441
|
-
const sheetRect = sheet.getBoundingClientRect()
|
|
442
|
-
footnoteEditor.value = {
|
|
443
|
-
pos,
|
|
444
|
-
text: node.attrs.text,
|
|
445
|
-
top: coords.bottom - sheetRect.top + 6,
|
|
446
|
-
left: Math.max(8, Math.min(coords.left - sheetRect.left, sheetRect.width - 372)),
|
|
447
|
-
isNew,
|
|
448
|
-
}
|
|
449
|
-
nextTick(() => {
|
|
450
|
-
const el = footnoteInputRef.value
|
|
451
|
-
if (!el)
|
|
452
|
-
return
|
|
453
|
-
el.innerHTML = sanitizeFootnoteHtml(footnoteEditor.value?.text ?? '')
|
|
454
|
-
el.focus()
|
|
455
|
-
// caret no fim do conteúdo
|
|
456
|
-
const sel = window.getSelection()
|
|
457
|
-
if (sel) {
|
|
458
|
-
const range = document.createRange()
|
|
459
|
-
range.selectNodeContents(el)
|
|
460
|
-
range.collapse(false)
|
|
461
|
-
sel.removeAllRanges()
|
|
462
|
-
sel.addRange(range)
|
|
463
|
-
}
|
|
464
|
-
})
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/** B/I/U no trecho selecionado dentro do popover */
|
|
468
|
-
function footnoteExec(command: 'bold' | 'italic' | 'underline') {
|
|
469
|
-
footnoteInputRef.value?.focus()
|
|
470
|
-
document.execCommand(command)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/** fonte/tamanho/cor no trecho selecionado (span com estilo permitido) */
|
|
474
|
-
function applyFootnoteStyle(prop: 'font-family' | 'font-size' | 'color', value: string) {
|
|
475
|
-
const el = footnoteInputRef.value
|
|
476
|
-
const sel = window.getSelection()
|
|
477
|
-
if (!el || !sel || sel.rangeCount === 0)
|
|
478
|
-
return
|
|
479
|
-
const range = sel.getRangeAt(0)
|
|
480
|
-
if (!el.contains(range.commonAncestorContainer) || range.collapsed)
|
|
481
|
-
return
|
|
482
|
-
const span = document.createElement('span')
|
|
483
|
-
span.style.setProperty(prop, value)
|
|
484
|
-
span.appendChild(range.extractContents())
|
|
485
|
-
range.insertNode(span)
|
|
486
|
-
sel.removeAllRanges()
|
|
487
|
-
const next = document.createRange()
|
|
488
|
-
next.selectNodeContents(span)
|
|
489
|
-
sel.addRange(next)
|
|
490
|
-
el.focus()
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function onFootnoteFontChange(event: Event) {
|
|
494
|
-
const value = (event.target as HTMLSelectElement).value
|
|
495
|
-
if (value)
|
|
496
|
-
applyFootnoteStyle('font-family', value)
|
|
497
|
-
;(event.target as HTMLSelectElement).value = ''
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function onFootnoteSizeChange(event: Event) {
|
|
501
|
-
const value = (event.target as HTMLSelectElement).value
|
|
502
|
-
if (value)
|
|
503
|
-
applyFootnoteStyle('font-size', `${value}pt`)
|
|
504
|
-
;(event.target as HTMLSelectElement).value = ''
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/** insere a chamada no cursor e abre o popover pra digitar a nota */
|
|
508
|
-
function insertFootnote() {
|
|
509
|
-
const ed = editor.value
|
|
510
|
-
if (!ed)
|
|
511
|
-
return
|
|
512
|
-
// colapsa a seleção (inserir com texto selecionado o substituiria)
|
|
513
|
-
const to = ed.state.selection.to
|
|
514
|
-
ed.chain().focus().setTextSelection(to).insertContent({ type: 'footnoteRef', attrs: { number: '?', text: '' } }).run()
|
|
515
|
-
renumberPocFootnotes(ed)
|
|
516
|
-
const pos = ed.state.selection.from - 1
|
|
517
|
-
openFootnoteEditor(pos, true)
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function saveFootnote() {
|
|
521
|
-
const state = footnoteEditor.value
|
|
522
|
-
const ed = editor.value
|
|
523
|
-
// lê o conteúdo formatado ANTES de fechar o popover
|
|
524
|
-
const html = footnoteInputRef.value ? sanitizeFootnoteHtml(footnoteInputRef.value.innerHTML) : state?.text ?? ''
|
|
525
|
-
footnoteEditor.value = null
|
|
526
|
-
if (!state || !ed || ed.isDestroyed)
|
|
527
|
-
return
|
|
528
|
-
const node = ed.state.doc.nodeAt(state.pos)
|
|
529
|
-
if (!node || node.type.name !== 'footnoteRef')
|
|
530
|
-
return
|
|
531
|
-
const text = html.trim()
|
|
532
|
-
if (!text)
|
|
533
|
-
ed.chain().focus().deleteRange({ from: state.pos, to: state.pos + node.nodeSize }).run()
|
|
534
|
-
else
|
|
535
|
-
ed.view.dispatch(ed.state.tr.setNodeMarkup(state.pos, undefined, { ...node.attrs, text }))
|
|
536
|
-
renumberPocFootnotes(ed)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function cancelFootnote() {
|
|
540
|
-
const state = footnoteEditor.value
|
|
541
|
-
const ed = editor.value
|
|
542
|
-
footnoteEditor.value = null
|
|
543
|
-
// cancelar uma nota recém-criada (ainda vazia) remove a chamada
|
|
544
|
-
if (state?.isNew && ed && !ed.isDestroyed) {
|
|
545
|
-
const node = ed.state.doc.nodeAt(state.pos)
|
|
546
|
-
if (node?.type.name === 'footnoteRef' && !node.attrs.text)
|
|
547
|
-
ed.chain().focus().deleteRange({ from: state.pos, to: state.pos + node.nodeSize }).run()
|
|
548
|
-
renumberPocFootnotes(ed)
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function removeFootnote() {
|
|
553
|
-
if (footnoteInputRef.value)
|
|
554
|
-
footnoteInputRef.value.innerHTML = ''
|
|
555
|
-
saveFootnote()
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function onEditFootnoteEvent(event: Event) {
|
|
559
|
-
if (!props.editable)
|
|
560
|
-
return
|
|
561
|
-
const { pos } = (event as CustomEvent).detail || {}
|
|
562
|
-
if (typeof pos === 'number')
|
|
563
|
-
openFootnoteEditor(pos, false)
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/** clicar na PRÓPRIA nota no pé da página também abre a edição */
|
|
567
|
-
function onNotesClick(event: MouseEvent) {
|
|
568
|
-
if (!props.editable)
|
|
569
|
-
return
|
|
570
|
-
const line = (event.target as HTMLElement).closest?.('.poc-page-note')
|
|
571
|
-
if (!line)
|
|
572
|
-
return
|
|
573
|
-
const number = line.querySelector('sup')?.textContent?.trim()
|
|
574
|
-
const ed = editor.value
|
|
575
|
-
if (!number || !ed)
|
|
576
|
-
return
|
|
577
|
-
let target: number | null = null
|
|
578
|
-
ed.state.doc.descendants((node, pos) => {
|
|
579
|
-
if (target != null)
|
|
580
|
-
return false
|
|
581
|
-
if (node.type.name === 'footnoteRef' && node.attrs.number === number) {
|
|
582
|
-
target = pos
|
|
583
|
-
return false
|
|
584
|
-
}
|
|
585
|
-
return true
|
|
586
|
-
})
|
|
587
|
-
if (target != null)
|
|
588
|
-
openFootnoteEditor(target, false)
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/** alinhamento por bloco: mescla text-align no style dos parágrafos/títulos selecionados */
|
|
592
|
-
function setAlignment(align: 'left' | 'center' | 'right' | 'justify') {
|
|
593
|
-
const ed = editor.value
|
|
594
|
-
if (!ed)
|
|
595
|
-
return
|
|
596
|
-
const { state, view } = ed
|
|
597
|
-
const { from, to } = state.selection
|
|
598
|
-
const tr = state.tr
|
|
599
|
-
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
600
|
-
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
|
601
|
-
const style = (node.attrs.style || '').replace(/text-align:\s*[^;]+;?/gi, '').trim()
|
|
602
|
-
const merged = `${style}${style && !style.endsWith(';') ? ';' : ''} text-align: ${align};`.trim()
|
|
603
|
-
tr.setNodeMarkup(pos, undefined, { ...node.attrs, style: merged })
|
|
604
|
-
}
|
|
605
|
-
})
|
|
606
|
-
view.dispatch(tr)
|
|
607
|
-
ed.commands.focus()
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// inserir imagem do computador (vira ilha: arrastável e redimensionável)
|
|
611
|
-
const imageInput = ref<HTMLInputElement | null>(null)
|
|
612
|
-
|
|
613
|
-
function pickImage() {
|
|
614
|
-
imageInput.value?.click()
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function insertDataUriImage(file: File) {
|
|
618
|
-
const reader = new FileReader()
|
|
619
|
-
reader.onload = () => {
|
|
620
|
-
editor.value?.chain().focus().insertContent(
|
|
621
|
-
`<img src="${reader.result}" style="width: 420px; height: auto; max-width: 100%;">`,
|
|
622
|
-
).run()
|
|
623
|
-
}
|
|
624
|
-
reader.readAsDataURL(file)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function onImageFile(event: Event) {
|
|
628
|
-
const file = (event.target as HTMLInputElement).files?.[0]
|
|
629
|
-
if (!file || !file.type.startsWith('image/'))
|
|
630
|
-
return
|
|
631
|
-
if (props.uploadImage) {
|
|
632
|
-
void (async () => {
|
|
633
|
-
try {
|
|
634
|
-
const uploaded = await props.uploadImage!(file)
|
|
635
|
-
if (uploaded) {
|
|
636
|
-
editor.value?.chain().focus().insertContent(vaultImageHtml(uploaded.src, uploaded.vaultSrc)).run()
|
|
637
|
-
return
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
catch (error) {
|
|
641
|
-
console.error('[minuta-nuxt] upload de imagem falhou, embutindo como data URI', error)
|
|
642
|
-
}
|
|
643
|
-
insertDataUriImage(file)
|
|
644
|
-
})()
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
insertDataUriImage(file)
|
|
648
|
-
}
|
|
649
|
-
;(event.target as HTMLInputElement).value = ''
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/** insere a quebra de página manual do template (o engine e o DOCX respeitam) */
|
|
653
|
-
function insertPageBreak() {
|
|
654
|
-
editor.value?.chain().focus().insertContent('<p>PAGEBREAK</p>').run()
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function clearFormatting() {
|
|
658
|
-
editor.value?.chain().focus().unsetAllMarks().run()
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
const TEXT_COLORS = [
|
|
662
|
-
{ label: 'Preto', value: '#1C242D' },
|
|
663
|
-
{ label: 'Vermelho', value: '#DC2626' },
|
|
664
|
-
{ label: 'Azul', value: '#2563EB' },
|
|
665
|
-
{ label: 'Verde', value: '#16A34A' },
|
|
666
|
-
{ label: 'Laranja', value: '#EA580C' },
|
|
667
|
-
]
|
|
668
|
-
|
|
669
|
-
function applyColor(color: string) {
|
|
670
|
-
editor.value?.chain().focus().setMark('inlineStyle', { style: `color: ${color}` }).run()
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function clearColor() {
|
|
674
|
-
editor.value?.chain().focus().unsetMark('inlineStyle').run()
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// --- variáveis de template: gatilho `/`, botão da toolbar e seletor ---
|
|
678
|
-
|
|
679
|
-
const hasVariables = computed(() => (props.variables?.length ?? 0) > 0)
|
|
680
|
-
const variablePickerRef = ref<any>(null)
|
|
681
|
-
/**
|
|
682
|
-
* `from` = posição do `/` que abriu o seletor (apagado na inserção); `null`
|
|
683
|
-
* quando veio do botão da toolbar, que não digitou nada no documento.
|
|
684
|
-
* `query === undefined` faz o popover usar o próprio campo de busca.
|
|
685
|
-
*/
|
|
686
|
-
const variablePicker = ref<{ top: number, left: number, from: number | null, query: string | undefined } | null>(null)
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Coordenada do documento → coordenada dentro da folha.
|
|
690
|
-
*
|
|
691
|
-
* A folha tem `zoom` CSS: `coordsAtPos` devolve px de TELA e o `top`/`left` do
|
|
692
|
-
* popover é interpretado em px de LAYOUT lá dentro. Sem dividir pelo zoom o
|
|
693
|
-
* seletor desgruda do cursor em qualquer zoom diferente de 100%.
|
|
694
|
-
*/
|
|
695
|
-
function sheetCoords(pos: number): { top: number, left: number } | null {
|
|
696
|
-
const ed = editor.value
|
|
697
|
-
const sheet = sheetRef.value
|
|
698
|
-
if (!ed || ed.isDestroyed || !sheet)
|
|
699
|
-
return null
|
|
700
|
-
let coords
|
|
701
|
-
try {
|
|
702
|
-
coords = ed.view.coordsAtPos(pos)
|
|
703
|
-
}
|
|
704
|
-
catch {
|
|
705
|
-
return null
|
|
706
|
-
}
|
|
707
|
-
const rect = sheet.getBoundingClientRect()
|
|
708
|
-
const z = zoom.value || 1
|
|
709
|
-
return {
|
|
710
|
-
top: (coords.bottom - rect.top) / z + 6,
|
|
711
|
-
left: Math.max(8, Math.min((coords.left - rect.left) / z, A4_WIDTH_PX - 330)),
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* `/` no início de palavra abre o seletor; o que vem depois filtra.
|
|
717
|
-
*
|
|
718
|
-
* Exige delimitador à esquerda para não disparar dentro de uma data ("12/03")
|
|
719
|
-
* nem de uma referência de artigo ("art. 5º, II/CF") — onde a barra é conteúdo,
|
|
720
|
-
* não comando.
|
|
721
|
-
*/
|
|
722
|
-
const VARIABLE_TRIGGER_RE = /(?:^|[\s(\[«"'])\/([\p{L}\d_]*)$/u
|
|
723
|
-
/** acima disto não é mais busca, é texto — o gatilho desiste */
|
|
724
|
-
const VARIABLE_TRIGGER_MAX_QUERY = 24
|
|
725
|
-
|
|
726
|
-
function closeVariablePicker() {
|
|
727
|
-
variablePicker.value = null
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Reavalia o gatilho a cada transação. Só ABRE quando o documento mudou (foi
|
|
732
|
-
* alguém digitando); mover o cursor para perto de uma barra que já estava no
|
|
733
|
-
* texto não abre nada, mas fecha o que estiver aberto.
|
|
734
|
-
*/
|
|
735
|
-
function syncVariableTrigger(docChanged: boolean) {
|
|
736
|
-
const ed = editor.value
|
|
737
|
-
if (!ed || ed.isDestroyed || !hasVariables.value || !props.editable) {
|
|
738
|
-
if (variablePicker.value)
|
|
739
|
-
closeVariablePicker()
|
|
740
|
-
return
|
|
741
|
-
}
|
|
742
|
-
// seletor aberto pela toolbar não tem gatilho no texto para reavaliar
|
|
743
|
-
if (variablePicker.value && variablePicker.value.from === null)
|
|
744
|
-
return
|
|
745
|
-
|
|
746
|
-
const { from, empty } = ed.state.selection
|
|
747
|
-
if (!empty) {
|
|
748
|
-
closeVariablePicker()
|
|
749
|
-
return
|
|
750
|
-
}
|
|
85
|
+
watch(() => props.editable, value => instance?.setEditable(value))
|
|
751
86
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
if (!match || (match[1]?.length ?? 0) > VARIABLE_TRIGGER_MAX_QUERY) {
|
|
759
|
-
closeVariablePicker()
|
|
760
|
-
return
|
|
761
|
-
}
|
|
762
|
-
if (!variablePicker.value && !docChanged)
|
|
763
|
-
return
|
|
764
|
-
|
|
765
|
-
const slashPos = from - match[0].length + match[0].indexOf('/')
|
|
766
|
-
const coords = sheetCoords(slashPos)
|
|
767
|
-
if (!coords) {
|
|
768
|
-
closeVariablePicker()
|
|
769
|
-
return
|
|
770
|
-
}
|
|
771
|
-
variablePicker.value = { ...coords, from: slashPos, query: match[1] ?? '' }
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* Botão da toolbar: mesmo seletor, ancorado no cursor, com campo de busca
|
|
776
|
-
* próprio.
|
|
777
|
-
*
|
|
778
|
-
* NÃO devolve o foco ao editor aqui. O `focus()` do Tiptap é adiado por um
|
|
779
|
-
* `requestAnimationFrame`, então ele chegava DEPOIS do popover montar e roubava
|
|
780
|
-
* o foco do campo de busca — o que a pessoa digitava virava texto do documento
|
|
781
|
-
* e o Enter inseria o primeiro item de uma lista que nunca chegou a ser
|
|
782
|
-
* filtrada. A posição do cursor não depende do foco: `state.selection`
|
|
783
|
-
* continua onde estava (ou no começo, se ninguém nunca clicou no documento).
|
|
784
|
-
*/
|
|
785
|
-
function openVariablePicker() {
|
|
786
|
-
const ed = editor.value
|
|
787
|
-
if (!ed || ed.isDestroyed || !hasVariables.value)
|
|
788
|
-
return
|
|
789
|
-
const coords = sheetCoords(ed.state.selection.from)
|
|
790
|
-
if (!coords)
|
|
791
|
-
return
|
|
792
|
-
variablePicker.value = { ...coords, from: null, query: undefined }
|
|
793
|
-
}
|
|
87
|
+
watch(
|
|
88
|
+
() => [props.chromeText.header, props.chromeText.footer, props.chromeText.headerScope, props.chromeText.footerScope, props.chromeText.showPageNumber],
|
|
89
|
+
() => instance?.setChrome({ ...props.chromeText }),
|
|
90
|
+
)
|
|
91
|
+
watch(() => [props.header.logoUrl, props.header.position], () => instance?.setHeader({ ...props.header }))
|
|
92
|
+
watch(() => props.variables, () => instance?.setVariables(() => props.variables ?? []))
|
|
794
93
|
|
|
795
|
-
function
|
|
796
|
-
|
|
797
|
-
if (!ed || ed.isDestroyed)
|
|
798
|
-
return
|
|
799
|
-
const trigger = variablePicker.value
|
|
800
|
-
const chain = ed.chain().focus()
|
|
801
|
-
// some com o `/consulta` que abriu o seletor — o placeholder toma o lugar dele
|
|
802
|
-
if (trigger?.from != null)
|
|
803
|
-
chain.deleteRange({ from: trigger.from, to: ed.state.selection.from })
|
|
804
|
-
chain.insertContent({
|
|
805
|
-
type: 'templateVariable',
|
|
806
|
-
attrs: { varKey: variable.key, label: variable.label },
|
|
807
|
-
}).run()
|
|
808
|
-
closeVariablePicker()
|
|
94
|
+
function schedulePaginate() {
|
|
95
|
+
instance?.schedulePaginate()
|
|
809
96
|
}
|
|
810
97
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
* texto do filtro), então as setas e o Enter têm que ser interceptados aqui —
|
|
814
|
-
* senão andam o cursor e quebram o parágrafo.
|
|
815
|
-
*/
|
|
816
|
-
function onEditorKeyDown(_view: unknown, event: KeyboardEvent): boolean {
|
|
817
|
-
const picker = variablePickerRef.value
|
|
818
|
-
if (!variablePicker.value || !picker)
|
|
819
|
-
return false
|
|
820
|
-
if (event.key === 'ArrowDown') {
|
|
821
|
-
picker.move(1)
|
|
822
|
-
return true
|
|
823
|
-
}
|
|
824
|
-
if (event.key === 'ArrowUp') {
|
|
825
|
-
picker.move(-1)
|
|
826
|
-
return true
|
|
827
|
-
}
|
|
828
|
-
if (event.key === 'Escape') {
|
|
829
|
-
closeVariablePicker()
|
|
830
|
-
return true
|
|
831
|
-
}
|
|
832
|
-
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
833
|
-
// sem resultado o Enter é Enter: quebrar o parágrafo é o que a pessoa quis
|
|
834
|
-
if (!picker.hasResults())
|
|
835
|
-
return false
|
|
836
|
-
picker.choose()
|
|
837
|
-
return true
|
|
838
|
-
}
|
|
839
|
-
return false
|
|
98
|
+
function refreshChrome() {
|
|
99
|
+
instance?.refreshChrome()
|
|
840
100
|
}
|
|
841
101
|
|
|
842
|
-
// catálogo esvaziado (ou editor virou read-only) com o seletor aberto
|
|
843
|
-
watch(() => [hasVariables.value, props.editable], () => {
|
|
844
|
-
if (variablePicker.value && (!hasVariables.value || !props.editable))
|
|
845
|
-
closeVariablePicker()
|
|
846
|
-
})
|
|
847
|
-
|
|
848
102
|
defineExpose({ editor, schedulePaginate, refreshChrome, containerRef })
|
|
849
103
|
</script>
|
|
850
104
|
|
|
851
105
|
<template>
|
|
852
|
-
<div
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
<button class="poc-tool" title="Refazer" @click="editor?.chain().focus().redo().run()">
|
|
859
|
-
<MinutaIcon name="i-ph-arrow-u-up-right" />
|
|
860
|
-
</button>
|
|
861
|
-
<div class="poc-tool-sep" />
|
|
862
|
-
<select class="poc-tool-select w-118px" title="Fonte" :value="currentFontFamily ?? ''" @change="setFontFamily">
|
|
863
|
-
<option value="" disabled>
|
|
864
|
-
Fonte
|
|
865
|
-
</option>
|
|
866
|
-
<option v-if="currentFontFamily && !DOC_FONTS.includes(currentFontFamily)" :value="currentFontFamily">
|
|
867
|
-
{{ currentFontFamily }}
|
|
868
|
-
</option>
|
|
869
|
-
<option v-for="font in DOC_FONTS" :key="font" :value="font" :style="{ fontFamily: font }">
|
|
870
|
-
{{ font }}
|
|
871
|
-
</option>
|
|
872
|
-
</select>
|
|
873
|
-
|
|
874
|
-
<select class="poc-tool-select w-64px" title="Tamanho da fonte" :value="currentFontSizePt ?? ''" @change="setFontSize">
|
|
875
|
-
<option value="" disabled>
|
|
876
|
-
Tam.
|
|
877
|
-
</option>
|
|
878
|
-
<option v-if="currentFontSizePt != null && !FONT_SIZES.includes(currentFontSizePt)" :value="currentFontSizePt">
|
|
879
|
-
{{ currentFontSizePt }}
|
|
880
|
-
</option>
|
|
881
|
-
<option v-for="size in FONT_SIZES" :key="size" :value="size">
|
|
882
|
-
{{ size }}
|
|
883
|
-
</option>
|
|
884
|
-
</select>
|
|
885
|
-
<div class="poc-tool-sep" />
|
|
886
|
-
<button class="poc-tool font-bold" :class="{ 'poc-tool--on': active.bold }" title="Negrito" @click="editor?.chain().focus().toggleBold().run()">
|
|
887
|
-
B
|
|
888
|
-
</button>
|
|
889
|
-
<button class="poc-tool italic" :class="{ 'poc-tool--on': active.italic }" title="Itálico" @click="editor?.chain().focus().toggleItalic().run()">
|
|
890
|
-
I
|
|
891
|
-
</button>
|
|
892
|
-
<button class="poc-tool underline" :class="{ 'poc-tool--on': active.underline }" title="Sublinhado" @click="editor?.chain().focus().toggleMark('underline').run()">
|
|
893
|
-
U
|
|
894
|
-
</button>
|
|
895
|
-
<button class="poc-tool line-through" :class="{ 'poc-tool--on': active.strike }" title="Tachado" @click="editor?.chain().focus().toggleStrike().run()">
|
|
896
|
-
S
|
|
897
|
-
</button>
|
|
898
|
-
<div class="poc-tool-sep" />
|
|
899
|
-
<div class="relative">
|
|
900
|
-
<button class="poc-tool" title="Cor do texto" @click="openPalette = openPalette === 'color' ? null : 'color'">
|
|
901
|
-
<span class="font-semibold">A</span>
|
|
902
|
-
<span class="poc-color-underline" />
|
|
903
|
-
</button>
|
|
904
|
-
<div v-if="openPalette === 'color'" class="poc-palette">
|
|
905
|
-
<button v-for="c in PALETTE" :key="c" class="poc-palette-swatch" :style="{ background: c }" :title="c" @click="setTextColor(c)" />
|
|
906
|
-
<label class="poc-palette-custom">
|
|
907
|
-
Personalizada
|
|
908
|
-
<input type="color" @change="setTextColor(($event.target as HTMLInputElement).value)">
|
|
909
|
-
</label>
|
|
910
|
-
</div>
|
|
911
|
-
</div>
|
|
912
|
-
<div class="relative">
|
|
913
|
-
<button class="poc-tool" title="Cor de destaque" @click="openPalette = openPalette === 'highlight' ? null : 'highlight'">
|
|
914
|
-
<MinutaIcon name="i-ph-highlighter" />
|
|
915
|
-
</button>
|
|
916
|
-
<div v-if="openPalette === 'highlight'" class="poc-palette">
|
|
917
|
-
<button v-for="c in PALETTE" :key="c" class="poc-palette-swatch" :style="{ background: c }" :title="c" @click="setHighlight(c)" />
|
|
918
|
-
<label class="poc-palette-custom">
|
|
919
|
-
Personalizada
|
|
920
|
-
<input type="color" @change="setHighlight(($event.target as HTMLInputElement).value)">
|
|
921
|
-
</label>
|
|
922
|
-
</div>
|
|
923
|
-
</div>
|
|
924
|
-
<div class="poc-tool-sep" />
|
|
925
|
-
<button class="poc-tool" title="Alinhar à esquerda" @click="setAlignment('left')">
|
|
926
|
-
<MinutaIcon name="i-ph-text-align-left" />
|
|
927
|
-
</button>
|
|
928
|
-
<button class="poc-tool" title="Centralizar" @click="setAlignment('center')">
|
|
929
|
-
<MinutaIcon name="i-ph-text-align-center" />
|
|
930
|
-
</button>
|
|
931
|
-
<button class="poc-tool" title="Alinhar à direita" @click="setAlignment('right')">
|
|
932
|
-
<MinutaIcon name="i-ph-text-align-right" />
|
|
933
|
-
</button>
|
|
934
|
-
<button class="poc-tool" title="Justificar" @click="setAlignment('justify')">
|
|
935
|
-
<MinutaIcon name="i-ph-text-align-justify" />
|
|
936
|
-
</button>
|
|
937
|
-
<div class="poc-tool-sep" />
|
|
938
|
-
<button class="poc-tool" :class="{ 'poc-tool--on': active.bullet }" title="Lista com marcadores" @click="editor?.chain().focus().toggleBulletList().run()">
|
|
939
|
-
<MinutaIcon name="i-ph-list-bullets" />
|
|
940
|
-
</button>
|
|
941
|
-
<button class="poc-tool" :class="{ 'poc-tool--on': active.ordered }" title="Lista numerada" @click="editor?.chain().focus().toggleOrderedList().run()">
|
|
942
|
-
<MinutaIcon name="i-ph-list-numbers" />
|
|
943
|
-
</button>
|
|
944
|
-
<div class="poc-tool-sep" />
|
|
945
|
-
<button class="poc-tool" title="Inserir imagem" @click="pickImage">
|
|
946
|
-
<MinutaIcon name="i-ph-image" />
|
|
947
|
-
</button>
|
|
948
|
-
<input ref="imageInput" type="file" accept="image/*" class="hidden" @change="onImageFile">
|
|
949
|
-
<button
|
|
950
|
-
v-if="hasVariables"
|
|
951
|
-
class="poc-tool"
|
|
952
|
-
:class="{ 'poc-tool--on': !!variablePicker }"
|
|
953
|
-
title="Inserir variável (ou digite / no texto)"
|
|
954
|
-
@click="openVariablePicker"
|
|
955
|
-
>
|
|
956
|
-
<MinutaIcon name="i-ph-brackets-curly" />
|
|
957
|
-
</button>
|
|
958
|
-
<button class="poc-tool" title="Inserir nota de rodapé" @click="insertFootnote">
|
|
959
|
-
<MinutaIcon name="i-ph-note" />
|
|
960
|
-
</button>
|
|
961
|
-
<button class="poc-tool" title="Inserir quebra de página" @click="insertPageBreak">
|
|
962
|
-
<MinutaIcon name="i-ph-arrows-out-line-vertical" />
|
|
963
|
-
</button>
|
|
964
|
-
<button class="poc-tool" title="Limpar formatação" @click="clearFormatting">
|
|
965
|
-
<MinutaIcon name="i-ph-eraser" />
|
|
966
|
-
</button>
|
|
967
|
-
</div>
|
|
968
|
-
|
|
969
|
-
<div ref="containerRef" class="relative min-h-0 w-full flex-1 overflow-y-auto bg-gray-120 py-24px" @click="openPalette = null; closeVariablePicker()">
|
|
970
|
-
<div ref="sheetRef" class="poc-sheet relative mx-auto bg-white shadow-md" :style="sheetStyle" @click="onNotesClick">
|
|
971
|
-
<!-- cabeçalho da 1ª página (na margem superior da folha) -->
|
|
972
|
-
<div class="poc-first-header" :style="firstHeaderStyle" title="Clique para editar o cabeçalho" @click="editChrome('header')">
|
|
973
|
-
<img
|
|
974
|
-
v-if="header.logoUrl"
|
|
975
|
-
:src="header.logoUrl"
|
|
976
|
-
:style="{ maxHeight: `${Math.max(24, margins.top - 36)}px`, maxWidth: '50%' }"
|
|
977
|
-
class="inline-block align-middle"
|
|
978
|
-
>
|
|
979
|
-
<!-- HTML persistido → sanitizado (stored XSS) -->
|
|
980
|
-
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
981
|
-
<div v-if="isChromeCanvas(chromeText.header)" class="poc-chrome-canvas" :style="chromeCanvasStyle" v-html="sanitizeRichHtml(chromeText.header)" />
|
|
982
|
-
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
983
|
-
<div v-else class="poc-chrome-text" v-html="sanitizeRichHtml(chromeText.header)" />
|
|
984
|
-
</div>
|
|
985
|
-
|
|
986
|
-
<p v-if="createError" class="body-13-regular text-red-600">
|
|
987
|
-
Falha ao montar o editor: {{ createError }}
|
|
988
|
-
</p>
|
|
989
|
-
<div ref="editorHost" :class="{ 'poc-review-cursor': !editable }" @click="onSheetClick" />
|
|
990
|
-
|
|
991
|
-
<!-- notas cujas chamadas caem na última página (as demais moram no chrome) -->
|
|
992
|
-
<div v-if="lastPageNotes.length" class="poc-page-notes poc-last-notes" :style="lastNotesStyle">
|
|
993
|
-
<!-- conteúdo da nota já sai sanitizado do render; re-sanitiza porque
|
|
994
|
-
também pode vir de edição salva no servidor -->
|
|
995
|
-
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
996
|
-
<p
|
|
997
|
-
v-for="note in lastPageNotes"
|
|
998
|
-
:key="note.n"
|
|
999
|
-
class="poc-page-note"
|
|
1000
|
-
:style="note.size ? { fontSize: `${note.size}pt` } : undefined"
|
|
1001
|
-
v-html="`<sup>${note.n}</sup> ${sanitizeFootnoteHtml(note.text)}`"
|
|
1002
|
-
/>
|
|
1003
|
-
</div>
|
|
1004
|
-
|
|
1005
|
-
<!-- rodapé da última página (na margem inferior da folha) -->
|
|
1006
|
-
<div class="poc-last-footer" :style="lastFooterStyle" title="Clique para editar o rodapé" @click="editChrome('footer')">
|
|
1007
|
-
<template v-if="!(chromeText.footerScope === 'first' && totalPages > 1)">
|
|
1008
|
-
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
1009
|
-
<div v-if="isChromeCanvas(chromeText.footer)" class="poc-chrome-canvas" :style="chromeCanvasStyle" v-html="sanitizeRichHtml(chromeText.footer)" />
|
|
1010
|
-
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
1011
|
-
<div v-else class="poc-chrome-text" v-html="sanitizeRichHtml(chromeText.footer)" />
|
|
1012
|
-
</template>
|
|
1013
|
-
<div v-else class="poc-chrome-text" />
|
|
1014
|
-
<span v-if="chromeText.showPageNumber !== false" class="ml-auto">Página {{ totalPages }} de {{ totalPages }}</span>
|
|
1015
|
-
</div>
|
|
1016
|
-
<MinutaVariablePicker
|
|
1017
|
-
v-if="variablePicker"
|
|
1018
|
-
ref="variablePickerRef"
|
|
1019
|
-
:variables="props.variables ?? []"
|
|
1020
|
-
:top="variablePicker.top"
|
|
1021
|
-
:left="variablePicker.left"
|
|
1022
|
-
:query="variablePicker.query"
|
|
1023
|
-
@select="insertVariable"
|
|
1024
|
-
@close="closeVariablePicker"
|
|
1025
|
-
/>
|
|
1026
|
-
|
|
1027
|
-
<slot name="gutter" />
|
|
1028
|
-
|
|
1029
|
-
<!-- popover de edição da nota de rodapé (ancorado na chamada) -->
|
|
1030
|
-
<div
|
|
1031
|
-
v-if="footnoteEditor"
|
|
1032
|
-
class="poc-footnote-pop"
|
|
1033
|
-
:style="{ top: `${footnoteEditor.top}px`, left: `${footnoteEditor.left}px` }"
|
|
1034
|
-
@click.stop
|
|
1035
|
-
>
|
|
1036
|
-
<div class="poc-footnote-pop__head">
|
|
1037
|
-
Nota de rodapé
|
|
1038
|
-
</div>
|
|
1039
|
-
<div class="poc-footnote-pop__toolbar">
|
|
1040
|
-
<button class="poc-footnote-pop__tool font-bold" type="button" title="Negrito" @mousedown.prevent @click="footnoteExec('bold')">
|
|
1041
|
-
B
|
|
1042
|
-
</button>
|
|
1043
|
-
<button class="poc-footnote-pop__tool italic" type="button" title="Itálico" @mousedown.prevent @click="footnoteExec('italic')">
|
|
1044
|
-
I
|
|
1045
|
-
</button>
|
|
1046
|
-
<button class="poc-footnote-pop__tool underline" type="button" title="Sublinhado" @mousedown.prevent @click="footnoteExec('underline')">
|
|
1047
|
-
U
|
|
1048
|
-
</button>
|
|
1049
|
-
<span class="poc-footnote-pop__sep" />
|
|
1050
|
-
<select class="poc-footnote-pop__select" title="Fonte do trecho selecionado" @mousedown.stop @change="onFootnoteFontChange">
|
|
1051
|
-
<option value="">
|
|
1052
|
-
Fonte
|
|
1053
|
-
</option>
|
|
1054
|
-
<option v-for="font in DOC_FONTS" :key="font" :value="font" :style="{ fontFamily: font }">
|
|
1055
|
-
{{ font }}
|
|
1056
|
-
</option>
|
|
1057
|
-
</select>
|
|
1058
|
-
<select class="poc-footnote-pop__select poc-footnote-pop__select--narrow" title="Tamanho do trecho selecionado" @mousedown.stop @change="onFootnoteSizeChange">
|
|
1059
|
-
<option value="">
|
|
1060
|
-
Tam.
|
|
1061
|
-
</option>
|
|
1062
|
-
<option v-for="size in FOOTNOTE_SIZES" :key="size" :value="size">
|
|
1063
|
-
{{ size }}
|
|
1064
|
-
</option>
|
|
1065
|
-
</select>
|
|
1066
|
-
<span class="poc-footnote-pop__sep" />
|
|
1067
|
-
<button
|
|
1068
|
-
v-for="color in FOOTNOTE_COLORS"
|
|
1069
|
-
:key="color"
|
|
1070
|
-
class="poc-footnote-pop__swatch"
|
|
1071
|
-
type="button"
|
|
1072
|
-
:style="{ background: color }"
|
|
1073
|
-
:title="`Cor ${color}`"
|
|
1074
|
-
@mousedown.prevent
|
|
1075
|
-
@click="applyFootnoteStyle('color', color)"
|
|
1076
|
-
/>
|
|
1077
|
-
</div>
|
|
1078
|
-
<div
|
|
1079
|
-
ref="footnoteInputRef"
|
|
1080
|
-
class="poc-footnote-pop__input"
|
|
1081
|
-
contenteditable="true"
|
|
1082
|
-
data-placeholder="Escreva a nota…"
|
|
1083
|
-
@keydown.enter.exact.prevent="saveFootnote"
|
|
1084
|
-
@keydown.esc.prevent="cancelFootnote"
|
|
1085
|
-
/>
|
|
1086
|
-
<div class="poc-footnote-pop__actions">
|
|
1087
|
-
<button v-if="!footnoteEditor.isNew" class="poc-footnote-pop__remove" type="button" @click="removeFootnote">
|
|
1088
|
-
Remover
|
|
1089
|
-
</button>
|
|
1090
|
-
<span class="poc-footnote-pop__spacer" />
|
|
1091
|
-
<button class="poc-footnote-pop__cancel" type="button" @click="cancelFootnote">
|
|
1092
|
-
Cancelar
|
|
1093
|
-
</button>
|
|
1094
|
-
<button class="poc-footnote-pop__save" type="button" @click="saveFootnote">
|
|
1095
|
-
Salvar
|
|
1096
|
-
</button>
|
|
1097
|
-
</div>
|
|
1098
|
-
</div>
|
|
1099
|
-
</div>
|
|
1100
|
-
|
|
1101
|
-
<TiptapBubbleMenu v-if="editor && editable" :editor="editor" :tippy-options="{ duration: 100 }">
|
|
1102
|
-
<div class="flex items-center gap-2px rounded-8px border border-gray-200 bg-white p-2px shadow-lg">
|
|
1103
|
-
<button
|
|
1104
|
-
class="poc-bubble-btn font-bold"
|
|
1105
|
-
:class="{ 'poc-bubble-btn--active': editor.isActive('bold') }"
|
|
1106
|
-
@click="editor.chain().focus().toggleBold().run()"
|
|
1107
|
-
>
|
|
1108
|
-
B
|
|
1109
|
-
</button>
|
|
1110
|
-
<button
|
|
1111
|
-
class="poc-bubble-btn italic"
|
|
1112
|
-
:class="{ 'poc-bubble-btn--active': editor.isActive('italic') }"
|
|
1113
|
-
@click="editor.chain().focus().toggleItalic().run()"
|
|
1114
|
-
>
|
|
1115
|
-
I
|
|
1116
|
-
</button>
|
|
1117
|
-
<button
|
|
1118
|
-
class="poc-bubble-btn underline"
|
|
1119
|
-
:class="{ 'poc-bubble-btn--active': editor.isActive('underline') }"
|
|
1120
|
-
@click="editor.chain().focus().toggleMark('underline').run()"
|
|
1121
|
-
>
|
|
1122
|
-
U
|
|
1123
|
-
</button>
|
|
1124
|
-
<div class="mx-2px h-16px w-1px bg-gray-200" />
|
|
1125
|
-
<button
|
|
1126
|
-
v-for="color in TEXT_COLORS"
|
|
1127
|
-
:key="color.value"
|
|
1128
|
-
class="poc-color-swatch"
|
|
1129
|
-
:style="{ background: color.value }"
|
|
1130
|
-
:title="color.label"
|
|
1131
|
-
@click="applyColor(color.value)"
|
|
1132
|
-
/>
|
|
1133
|
-
<button class="poc-bubble-btn" title="Remover cor" @click="clearColor">
|
|
1134
|
-
<MinutaIcon name="i-ph-eraser" />
|
|
1135
|
-
</button>
|
|
1136
|
-
</div>
|
|
1137
|
-
</TiptapBubbleMenu>
|
|
1138
|
-
</div>
|
|
1139
|
-
</div>
|
|
106
|
+
<div ref="host" class="minuta-docx-editor" />
|
|
107
|
+
<!-- o slot continua sendo renderizado DENTRO da folha, como antes: o gutter
|
|
108
|
+
se posiciona em relação a ela -->
|
|
109
|
+
<Teleport v-if="sheetEl" :to="sheetEl">
|
|
110
|
+
<slot name="gutter" />
|
|
111
|
+
</Teleport>
|
|
1140
112
|
</template>
|
|
1141
113
|
|
|
1142
114
|
<style scoped>
|
|
1143
|
-
|
|
1144
|
-
</style>
|
|
1145
|
-
|
|
1146
|
-
<style scoped>
|
|
1147
|
-
/* utilities que no CDB vêm do UnoCSS — aqui o package se basta */
|
|
1148
|
-
.h-full { height: 100%; }
|
|
1149
|
-
.w-full { width: 100%; }
|
|
1150
|
-
.w-1px { width: 1px; }
|
|
1151
|
-
.w-64px { width: 64px; }
|
|
1152
|
-
.h-16px { height: 16px; }
|
|
1153
|
-
.flex { display: flex; }
|
|
1154
|
-
.flex-col { flex-direction: column; }
|
|
1155
|
-
.flex-1 { flex: 1 1 0%; }
|
|
1156
|
-
.items-center { align-items: center; }
|
|
1157
|
-
.gap-2px { gap: 2px; }
|
|
1158
|
-
.min-h-0 { min-height: 0; }
|
|
1159
|
-
.overflow-y-auto { overflow-y: auto; }
|
|
1160
|
-
.relative { position: relative; }
|
|
1161
|
-
.hidden { display: none; }
|
|
1162
|
-
.inline-block { display: inline-block; }
|
|
1163
|
-
.align-middle { vertical-align: middle; }
|
|
1164
|
-
.ml-auto { margin-left: auto; }
|
|
1165
|
-
.mx-auto { margin-left: auto; margin-right: auto; }
|
|
1166
|
-
.mx-2px { margin-left: 2px; margin-right: 2px; }
|
|
1167
|
-
.p-2px { padding: 2px; }
|
|
1168
|
-
.py-24px { padding-top: 24px; padding-bottom: 24px; }
|
|
1169
|
-
.bg-white { background: #fff; }
|
|
1170
|
-
.bg-gray-120 { background: #ebedf0; }
|
|
1171
|
-
.bg-gray-200 { background: #e5e7eb; }
|
|
1172
|
-
.border { border-width: 1px; border-style: solid; }
|
|
1173
|
-
.border-gray-200 { border-color: #e5e7eb; }
|
|
1174
|
-
.rounded-8px { border-radius: 8px; }
|
|
1175
|
-
.shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); }
|
|
1176
|
-
.shadow-lg { box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); }
|
|
1177
|
-
.font-bold { font-weight: 700; }
|
|
1178
|
-
.font-semibold { font-weight: 600; }
|
|
1179
|
-
.italic { font-style: italic; }
|
|
1180
|
-
.underline { text-decoration: underline; }
|
|
1181
|
-
.line-through { text-decoration: line-through; }
|
|
1182
|
-
.text-red-600 { color: #dc2626; }
|
|
1183
|
-
.body-13-regular { font-size: 13px; font-weight: 400; }
|
|
1184
|
-
</style>
|
|
1185
|
-
|
|
1186
|
-
<style scoped>
|
|
1187
|
-
/* acabamento de paridade com o defesa.ai (hover states + fonte + detalhes
|
|
1188
|
-
que o conversor de utilities não cobre) */
|
|
1189
|
-
.poc-editor-root {
|
|
1190
|
-
font-family: 'Inter Variable', Inter, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
.poc-toolbar {
|
|
1194
|
-
background: #fafbfc;
|
|
1195
|
-
border-bottom: 0.5px solid #dee3e7;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
.poc-tool {
|
|
1199
|
-
border: none;
|
|
1200
|
-
background: transparent;
|
|
1201
|
-
cursor: pointer;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
.poc-tool:hover {
|
|
1205
|
-
background: #ebedf0;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
.poc-tool--on {
|
|
1209
|
-
background: #e2e8f0;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
.poc-tool-select {
|
|
1213
|
-
border: 0.5px solid #d4d9df;
|
|
1214
|
-
background: #fff;
|
|
1215
|
-
cursor: pointer;
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
.poc-editor-root :deep(.ProseMirror) {
|
|
1219
|
-
outline: none;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
.poc-editor-root :deep(td),
|
|
1223
|
-
.poc-editor-root :deep(th) {
|
|
1224
|
-
border: 0.5px solid #d4d9df;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
/* popover de nota de rodapé (ancorado na chamada, estilo comentário) */
|
|
1228
|
-
.poc-footnote-pop {
|
|
1229
|
-
position: absolute;
|
|
1230
|
-
z-index: 40;
|
|
1231
|
-
width: 364px;
|
|
1232
|
-
background: #fff;
|
|
1233
|
-
border: 0.5px solid #d4d9df;
|
|
1234
|
-
border-radius: 10px;
|
|
1235
|
-
padding: 10px 12px;
|
|
1236
|
-
box-shadow: 0 10px 25px -5px rgb(0 0 0 / 0.18);
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
.poc-footnote-pop__head {
|
|
1240
|
-
font-size: 11px;
|
|
1241
|
-
font-weight: 650;
|
|
1242
|
-
letter-spacing: 0.04em;
|
|
1243
|
-
text-transform: uppercase;
|
|
1244
|
-
color: #6b7280;
|
|
1245
|
-
margin-bottom: 6px;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
.poc-footnote-pop__toolbar {
|
|
1249
|
-
display: flex;
|
|
1250
|
-
align-items: center;
|
|
1251
|
-
gap: 4px;
|
|
1252
|
-
margin-bottom: 6px;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
.poc-footnote-pop__tool {
|
|
1256
|
-
width: 24px;
|
|
1257
|
-
height: 24px;
|
|
1258
|
-
border: none;
|
|
1259
|
-
border-radius: 6px;
|
|
1260
|
-
background: transparent;
|
|
1261
|
-
color: #374151;
|
|
1262
|
-
font-size: 12px;
|
|
1263
|
-
cursor: pointer;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
.poc-footnote-pop__tool:hover {
|
|
1267
|
-
background: rgba(28, 36, 45, 0.06);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
.poc-footnote-pop__sep {
|
|
1271
|
-
width: 1px;
|
|
1272
|
-
height: 16px;
|
|
1273
|
-
background: #e5e7eb;
|
|
1274
|
-
margin: 0 2px;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
.poc-footnote-pop__select {
|
|
1278
|
-
height: 24px;
|
|
1279
|
-
border: 1px solid #e5e7eb;
|
|
1280
|
-
border-radius: 6px;
|
|
1281
|
-
background: #fff;
|
|
1282
|
-
font-size: 11px;
|
|
1283
|
-
color: #374151;
|
|
1284
|
-
max-width: 108px;
|
|
1285
|
-
outline: none;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
.poc-footnote-pop__select--narrow {
|
|
1289
|
-
max-width: 58px;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
.poc-footnote-pop__swatch {
|
|
1293
|
-
width: 14px;
|
|
1294
|
-
height: 14px;
|
|
1295
|
-
border: 1px solid rgb(0 0 0 / 0.15);
|
|
1296
|
-
border-radius: 4px;
|
|
1297
|
-
padding: 0;
|
|
1298
|
-
cursor: pointer;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
.poc-footnote-pop__input {
|
|
1302
|
-
width: 100%;
|
|
1303
|
-
min-height: 58px;
|
|
1304
|
-
max-height: 160px;
|
|
1305
|
-
overflow-y: auto;
|
|
1306
|
-
border: 1px solid #e5e7eb;
|
|
1307
|
-
border-radius: 8px;
|
|
1308
|
-
padding: 6px 8px;
|
|
1309
|
-
font-size: 12.5px;
|
|
1310
|
-
line-height: 1.5;
|
|
1311
|
-
color: #111827;
|
|
1312
|
-
outline: none;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
.poc-footnote-pop__input:focus {
|
|
1316
|
-
border-color: #a5b4fc;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
.poc-footnote-pop__input:empty::before {
|
|
1320
|
-
content: attr(data-placeholder);
|
|
1321
|
-
color: #9ca3af;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
.poc-footnote-pop__actions {
|
|
1325
|
-
display: flex;
|
|
1326
|
-
align-items: center;
|
|
1327
|
-
gap: 8px;
|
|
1328
|
-
margin-top: 8px;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
.poc-footnote-pop__spacer {
|
|
1332
|
-
flex: 1;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
.poc-footnote-pop__actions button {
|
|
1336
|
-
border: none;
|
|
1337
|
-
border-radius: 8px;
|
|
1338
|
-
padding: 4px 12px;
|
|
1339
|
-
font-size: 12px;
|
|
1340
|
-
font-weight: 500;
|
|
1341
|
-
cursor: pointer;
|
|
1342
|
-
background: transparent;
|
|
1343
|
-
color: #565d69;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
.poc-footnote-pop__remove {
|
|
1347
|
-
color: #b91c1c !important;
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
.poc-footnote-pop__save {
|
|
1351
|
-
background: rgba(0, 0, 0, 0.88) !important;
|
|
1352
|
-
color: #fff !important;
|
|
1353
|
-
}
|
|
115
|
+
.minuta-docx-editor{display:flex;flex-direction:column;height:100%;min-height:0;width:100%}
|
|
1354
116
|
</style>
|