@meistrari/minuta-nuxt 1.0.0 → 1.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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/minuta-nuxt",
3
3
  "configKey": "minutaNuxt",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
@@ -15,6 +15,11 @@ const props = defineProps<{
15
15
  analysis: MinutaAnalysis
16
16
  /** container com scroll do documento (para o scroll suave ao clicar) */
17
17
  scrollContainer: HTMLElement | null
18
+ /**
19
+ * Modo lista: em tela estreita não há margem onde ancorar os cards, então
20
+ * eles viram uma coluna em fluxo normal (dentro de um painel lateral).
21
+ */
22
+ compact?: boolean
18
23
  }>()
19
24
 
20
25
  const emit = defineEmits<{
@@ -306,7 +311,7 @@ watch(() => props.editor, (editor, old) => {
306
311
  }, { immediate: true })
307
312
 
308
313
  onMounted(() => {
309
- const sheet = rootRef.value?.parentElement
314
+ const sheet = props.compact ? null : rootRef.value?.parentElement
310
315
  if (sheet) {
311
316
  // como o Google Docs: com os comentários abertos, a folha desloca pra
312
317
  // esquerda pra dar lugar à coluna (a classe vive enquanto o gutter vive)
@@ -331,7 +336,7 @@ onUnmounted(() => {
331
336
  </script>
332
337
 
333
338
  <template>
334
- <div ref="rootRef" class="minuta-gutter">
339
+ <div ref="rootRef" class="minuta-gutter" :class="{ 'minuta-gutter--compact': props.compact }">
335
340
  <!-- análise sem âncora: sem `trecho` não há onde grudar o card; diz isso
336
341
  em vez de empilhar tudo no topo com clique morto -->
337
342
  <div v-if="items.length && orphanCount === items.length" ref="noticeRef" class="minuta-gutter-notice">
@@ -347,7 +352,7 @@ onUnmounted(() => {
347
352
  'minuta-gutter-card--active': activeId === item.id,
348
353
  'minuta-gutter-card--orphan': !item.range,
349
354
  }"
350
- :style="{ transform: `translateY(${item.finalY}px)` }"
355
+ :style="props.compact ? undefined : { transform: `translateY(${item.finalY}px)` }"
351
356
  :title="item.range ? undefined : 'Sem trecho de referência: não dá para localizar no texto'"
352
357
  @click="onCardClick(item)"
353
358
  >
@@ -384,7 +389,7 @@ onUnmounted(() => {
384
389
  </template>
385
390
 
386
391
  <style scoped>
387
- .minuta-gutter{height:100%;left:calc(100% + 16px);pointer-events:none;position:absolute;top:0;width:264px;z-index:20}.minuta-gutter-card{background:#fff;border:.5px solid #dee3e7;border-radius:10px;cursor:pointer;left:0;padding:8px 12px;pointer-events:auto;position:absolute;right:0;transition:border-color .15s ease,background .15s ease,transform .2s ease}.minuta-gutter-card:hover{border-color:#b6bcc6}.minuta-gutter-card--active{background:#fafbfc;border-color:#565d69}.minuta-gutter-card--orphan{border-style:dashed;cursor:default;opacity:.7}.minuta-gutter-notice{background:#fafbfc;border:.5px solid #e5e7eb;border-radius:8px;color:#6b7280;font-size:11.5px;line-height:1.45;margin-bottom:8px;padding:8px 10px;position:relative}.minuta-gutter-card__source{align-items:baseline;border-top:.5px solid #eceef1;display:flex;gap:8px;justify-content:space-between;margin-top:5px;padding-top:5px}.minuta-gutter-card__source-text{color:#9ca3af;font-size:10px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.minuta-gutter-card__source-btn{background:none;border:none;color:#565d69;cursor:pointer;flex-shrink:0;font-size:10px;font-weight:500;padding:0}.minuta-gutter-card__source-btn:hover{color:#111827;text-decoration:underline}.minuta-gutter-card__badge{align-items:center;border-radius:999px;display:inline-flex;flex-shrink:0;font-size:9.5px;font-weight:650;letter-spacing:.05em;line-height:1.5;padding:1px 7px;text-transform:uppercase}.minuta-gutter-card__kind{align-items:center;display:flex;gap:6px;margin-bottom:4px;min-width:0}.minuta-gutter-card__onde{color:#b6bcc6;font-size:10px;font-weight:400;letter-spacing:normal;min-width:0;overflow:hidden;text-overflow:ellipsis;text-transform:none;white-space:nowrap}.minuta-gutter-card__title{color:#111827;font-size:12px;font-weight:590;line-height:1.35;margin:0}.minuta-gutter-card__body{color:#6b7280;display:-webkit-box;font-size:11px;-webkit-line-clamp:3;line-height:1.45;margin:3px 0 0;-webkit-box-orient:vertical;overflow:hidden}
392
+ .minuta-gutter{height:100%;left:calc(100% + 16px);pointer-events:none;position:absolute;top:0;width:264px;z-index:20}.minuta-gutter--compact{display:flex;flex-direction:column;gap:8px;position:static;width:100%}.minuta-gutter--compact .minuta-gutter-card{position:static;transform:none!important}.minuta-gutter-card{background:#fff;border:.5px solid #dee3e7;border-radius:10px;cursor:pointer;left:0;padding:8px 12px;pointer-events:auto;position:absolute;right:0;transition:border-color .15s ease,background .15s ease,transform .2s ease}.minuta-gutter-card:hover{border-color:#b6bcc6}.minuta-gutter-card--active{background:#fafbfc;border-color:#565d69}.minuta-gutter-card--orphan{border-style:dashed;cursor:default;opacity:.7}.minuta-gutter-notice{background:#fafbfc;border:.5px solid #e5e7eb;border-radius:8px;color:#6b7280;font-size:11.5px;line-height:1.45;margin-bottom:8px;padding:8px 10px;position:relative}.minuta-gutter-card__source{align-items:baseline;border-top:.5px solid #eceef1;display:flex;gap:8px;justify-content:space-between;margin-top:5px;padding-top:5px}.minuta-gutter-card__source-text{color:#9ca3af;font-size:10px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.minuta-gutter-card__source-btn{background:none;border:none;color:#565d69;cursor:pointer;flex-shrink:0;font-size:10px;font-weight:500;padding:0}.minuta-gutter-card__source-btn:hover{color:#111827;text-decoration:underline}.minuta-gutter-card__badge{align-items:center;border-radius:999px;display:inline-flex;flex-shrink:0;font-size:9.5px;font-weight:650;letter-spacing:.05em;line-height:1.5;padding:1px 7px;text-transform:uppercase}.minuta-gutter-card__kind{align-items:center;display:flex;gap:6px;margin-bottom:4px;min-width:0}.minuta-gutter-card__onde{color:#b6bcc6;font-size:10px;font-weight:400;letter-spacing:normal;min-width:0;overflow:hidden;text-overflow:ellipsis;text-transform:none;white-space:nowrap}.minuta-gutter-card__title{color:#111827;font-size:12px;font-weight:590;line-height:1.35;margin:0}.minuta-gutter-card__body{color:#6b7280;display:-webkit-box;font-size:11px;-webkit-line-clamp:3;line-height:1.45;margin:3px 0 0;-webkit-box-orient:vertical;overflow:hidden}
388
393
  </style>
389
394
 
390
395
  <style>
@@ -1,203 +1,101 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeUnmount, ref, watch } from 'vue'
2
+ import { computed, ref, watch } from 'vue'
3
3
  import type { MinutaFile } from '../composables/useMinutaApi'
4
4
  import { useMinutaApi } from '../composables/useMinutaApi'
5
5
 
6
6
  /**
7
- * Overlay de visualização dos documentos do caso, aberto pelos cards de fato
8
- * extraído da margem ou pelo botão Documentos.
7
+ * Visualização dos documentos do caso, aberta pelos cards de fato extraído da
8
+ * margem ou pelo botão Documentos.
9
9
  *
10
- * O documento é baixado com `fetch` e exibido a partir de uma `blob:` URL, e
11
- * não apontando o `<iframe>` direto para a URL presignada do Vault.
10
+ * O corpo é o `TelaPreview` do Tela Build — o mesmo visualizador que o resto
11
+ * do produto usa. Ele resolve o que a implementação anterior (um `<iframe>`
12
+ * apontando para uma `blob:` URL) não resolvia:
12
13
  *
13
- * O sintoma que isso corrige é o "modal em branco que começa a baixar a peça",
14
- * e ele tem duas causas possíveis que produzem exatamente a mesma tela:
15
- *
16
- * 1. a URL presignada responde `Content-Disposition: attachment` — o iframe
17
- * navega, o browser trata a navegação como download, e vale até para PDF;
18
- * 2. o arquivo é de um formato que o browser não renderiza (docx, xlsx), e aí
19
- * o iframe baixa mesmo sem disposition nenhum.
20
- *
21
- * A `blob:` URL resolve (1): o disposition da origem não a acompanha, e o tipo
22
- * do blob é o que dissermos que é. A classificação por tipo resolve (2): o que
23
- * o browser não abre ganha um cartão com botão de baixar, em vez de cair no
24
- * mesmo `else` do PDF e baixar sozinho sem nunca ter chance de aparecer.
14
+ * - **destaque do trecho.** O card de fato manda `highlight`, e antes esse
15
+ * campo era recebido e descartado: o visualizador nativo do Chrome não tem
16
+ * API de destaque. O TelaPreview aceita `highlightText`/`highlightPage`.
17
+ * - zoom, navegação por página, modo arrasto e tela cheia, que o `<iframe>`
18
+ * delegava à barra cinza do browser.
19
+ * - `vault://` resolvido por `resolveFileUrl`, e o cartão de download para
20
+ * formato que o navegador não abre.
25
21
  */
26
22
 
27
23
  const props = defineProps<{
28
24
  files: MinutaFile[]
29
25
  initialUrl?: string | null
30
- /** página inicial (PDF via #page=N) */
26
+ /** página para onde rolar e destacar */
31
27
  initialPage?: number | null
28
+ /** trecho a destacar na página (vem do card de fato extraído) */
29
+ highlight?: string | null
32
30
  }>()
33
31
 
34
32
  const emit = defineEmits<{ close: [], error: [message: string] }>()
35
33
 
36
34
  const { getDownloadUrl } = useMinutaApi()
37
-
38
- type ViewerKind = 'image' | 'text' | 'pdf' | 'binary'
39
- type ViewerContent = { kind: ViewerKind, src: string, text?: string, sizeLabel?: string }
35
+ const { pdfLoader } = usePdfLoader()
40
36
 
41
37
  const selected = ref(props.initialUrl || props.files[0]?.url || '')
42
- const content = ref<ViewerContent | null>(null)
43
- const loading = ref(false)
44
- const loadError = ref<string | null>(null)
45
-
46
- const currentName = computed(() => props.files.find(f => f.url === selected.value)?.name ?? '')
47
-
48
- const IMAGE_RE = /\.(?:png|jpe?g|gif|webp|avif|bmp|svg)$/i
49
- const TEXT_RE = /\.(?:txt|md|json|csv|tsv|log|xml|ya?ml)$/i
50
- const PDF_RE = /\.pdf$/i
51
-
52
- /**
53
- * O nome do arquivo manda; o `Content-Type` do blob é a reserva.
54
- *
55
- * Referência do Vault costuma trazer o nome original (extensão junto), mas
56
- * upload sem extensão existe — e aí adivinhar pelo tipo é melhor do que mandar
57
- * tudo para o cartão de download.
58
- */
59
- function classify(name: string, mimeType: string): ViewerKind {
60
- if (IMAGE_RE.test(name))
61
- return 'image'
62
- if (PDF_RE.test(name))
63
- return 'pdf'
64
- if (TEXT_RE.test(name))
65
- return 'text'
66
- if (mimeType.startsWith('image/'))
67
- return 'image'
68
- if (mimeType === 'application/pdf')
69
- return 'pdf'
70
- if (mimeType.startsWith('text/') || mimeType === 'application/json')
71
- return 'text'
72
- return 'binary'
73
- }
38
+ watch(() => props.initialUrl, (url) => {
39
+ if (url)
40
+ selected.value = url
41
+ })
74
42
 
75
- function formatSize(bytes: number): string {
76
- if (bytes < 1024)
77
- return `${bytes} B`
78
- if (bytes < 1024 * 1024)
79
- return `${Math.round(bytes / 1024)} KB`
80
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
43
+ const EXT_MIME: Record<string, string> = {
44
+ pdf: 'application/pdf',
45
+ png: 'image/png',
46
+ jpg: 'image/jpeg',
47
+ jpeg: 'image/jpeg',
48
+ gif: 'image/gif',
49
+ webp: 'image/webp',
50
+ svg: 'image/svg+xml',
51
+ txt: 'text/plain',
52
+ md: 'text/markdown',
53
+ csv: 'text/csv',
54
+ json: 'application/json',
81
55
  }
82
56
 
83
- /** só uma `blob:` viva por vez — sem isto, abrir 10 documentos segura os 10 na memória */
84
- let objectUrl: string | null = null
85
- function releaseObjectUrl() {
86
- if (objectUrl) {
87
- URL.revokeObjectURL(objectUrl)
88
- objectUrl = null
89
- }
57
+ function mimeOf(name: string): string {
58
+ const ext = name.split('.').pop()?.toLowerCase() ?? ''
59
+ return EXT_MIME[ext] ?? 'application/octet-stream'
90
60
  }
91
61
 
92
- let loadToken = 0
62
+ /** o seletor do TelaPreview agrupa por "variável"; aqui só há uma categoria */
63
+ const fileOptions = computed(() => props.files.map(file => ({
64
+ variable: 'Documentos do caso',
65
+ file: file.name,
66
+ value: file.url,
67
+ mimeType: mimeOf(file.name),
68
+ })))
93
69
 
94
- /**
95
- * Uma carga só pode escrever no estado se ainda for a carga corrente.
96
- *
97
- * Vale depois de CADA await, não só do primeiro: `blob.text()` de um arquivo
98
- * grande termina depois de o usuário já ter trocado de documento, e escreveria
99
- * o conteúdo antigo sob o nome novo.
100
- *
101
- * O desmonte também invalida (incrementa o token), e não só revoga a `blob:`
102
- * atual: sem isso, uma carga em voo criaria uma `blob:` DEPOIS da limpeza —
103
- * URL que ninguém mais revoga, segurando os bytes do arquivo pelo resto da vida
104
- * da página.
105
- */
106
- function isStale(token: number): boolean {
107
- return token !== loadToken
108
- }
109
-
110
- async function load() {
111
- const file = props.files.find(f => f.url === selected.value)
70
+ const currentFile = computed(() => {
71
+ const file = props.files.find(f => f.url === selected.value) ?? props.files[0]
112
72
  if (!file)
113
- return
73
+ return null
74
+ return {
75
+ id: file.url,
76
+ fileName: file.name,
77
+ fileUrl: file.url,
78
+ fileType: mimeOf(file.name),
79
+ }
80
+ })
114
81
 
115
- const token = ++loadToken
116
- loading.value = true
117
- loadError.value = null
118
- content.value = null
82
+ /** o destaque só vale para o documento que o card apontou */
83
+ const highlightForCurrent = computed(() => (
84
+ selected.value === props.initialUrl ? props.highlight ?? null : null
85
+ ))
86
+ const pageForCurrent = computed(() => (
87
+ selected.value === props.initialUrl ? props.initialPage ?? null : null
88
+ ))
119
89
 
90
+ async function resolveFileUrl(url: string): Promise<string> {
120
91
  try {
121
- const downloadUrl = await getDownloadUrl(file.url)
122
- const response = await fetch(downloadUrl)
123
- if (!response.ok)
124
- throw new Error(`HTTP ${response.status}`)
125
- const blob = await response.blob()
126
- if (isStale(token))
127
- return
128
-
129
- const kind = classify(file.name, blob.type || '')
130
-
131
- if (kind === 'text') {
132
- const text = await blob.text()
133
- if (isStale(token))
134
- return
135
- releaseObjectUrl()
136
- content.value = { kind, src: '', text }
137
- return
138
- }
139
-
140
- // daqui até o fim é síncrono: a checagem acima ainda vale, e nada é
141
- // criado depois de a carga ter sido invalidada
142
-
143
- // o tipo do blob manda no que o browser faz com ele: forçar
144
- // application/pdf faz o visualizador nativo abrir mesmo quando o Vault
145
- // devolveu octet-stream
146
- const typed = kind === 'pdf' && blob.type !== 'application/pdf'
147
- ? new Blob([blob], { type: 'application/pdf' })
148
- : blob
149
-
150
- releaseObjectUrl()
151
- objectUrl = URL.createObjectURL(typed)
152
- const page = kind === 'pdf' && props.initialPage && file.url === props.initialUrl
153
- ? `#page=${props.initialPage}`
154
- : ''
155
- content.value = {
156
- kind,
157
- src: objectUrl + page,
158
- sizeLabel: formatSize(blob.size),
159
- }
92
+ return await getDownloadUrl(url)
160
93
  }
161
- catch {
162
- if (isStale(token))
163
- return
164
- loadError.value = 'Não consegui carregar o documento.'
165
- emit('error', 'Não consegui carregar o documento')
94
+ catch (error: any) {
95
+ emit('error', error?.message || 'Não foi possível abrir o documento')
96
+ return ''
166
97
  }
167
- finally {
168
- if (!isStale(token))
169
- loading.value = false
170
- }
171
- }
172
-
173
- /**
174
- * Baixar de propósito. O `download` de uma `blob:` URL não depende do
175
- * disposition da origem, então funciona igual para PDF e para docx.
176
- */
177
- function downloadCurrent() {
178
- const current = content.value
179
- if (!current)
180
- return
181
- const link = document.createElement('a')
182
- link.download = currentName.value || 'documento'
183
- if (current.kind === 'text') {
184
- const href = URL.createObjectURL(new Blob([current.text ?? ''], { type: 'text/plain' }))
185
- link.href = href
186
- link.click()
187
- URL.revokeObjectURL(href)
188
- return
189
- }
190
- // sem o fragmento `#page=`: ele é do visualizador, não do arquivo
191
- link.href = current.src.split('#')[0]!
192
- link.click()
193
98
  }
194
-
195
- watch(selected, load, { immediate: true })
196
- onBeforeUnmount(() => {
197
- // invalida antes de revogar: ver `isStale`
198
- loadToken++
199
- releaseObjectUrl()
200
- })
201
99
  </script>
202
100
 
203
101
  <template>
@@ -205,44 +103,40 @@ onBeforeUnmount(() => {
205
103
  <div class="minuta-viewer__overlay" @click.self="emit('close')">
206
104
  <div class="minuta-viewer__modal">
207
105
  <div class="minuta-viewer__head">
208
- <select v-if="files.length > 1" v-model="selected">
209
- <option v-for="file in files" :key="file.url" :value="file.url">
210
- {{ file.name }}
211
- </option>
212
- </select>
213
- <span v-else>{{ currentName }}</span>
214
- <div class="minuta-viewer__actions">
215
- <button v-if="content" type="button" class="minuta-viewer__download" @click="downloadCurrent">
216
- Baixar
217
- </button>
218
- <button type="button" class="minuta-viewer__close" @click="emit('close')">
219
- ×
220
- </button>
221
- </div>
106
+ <span class="minuta-viewer__title">Documentos do caso</span>
107
+ <TelaButton variant="ghost" size="sm" icon="i-ph-x" @click="emit('close')" />
222
108
  </div>
223
109
  <div class="minuta-viewer__body">
224
- <p v-if="loading" class="minuta-viewer__loading">
225
- Carregando…
226
- </p>
227
- <p v-else-if="loadError" class="minuta-viewer__loading">
228
- {{ loadError }}
229
- </p>
230
- <img v-else-if="content?.kind === 'image'" :src="content.src" :alt="currentName">
231
- <pre v-else-if="content?.kind === 'text'">{{ content.text }}</pre>
232
- <iframe v-else-if="content?.kind === 'pdf'" :src="content.src" :title="currentName" />
233
- <!-- formato que o browser não abre: cartão explícito, e não
234
- um iframe em branco que dispara download sozinho -->
235
- <div v-else-if="content" class="minuta-viewer__binary">
236
- <p class="minuta-viewer__binary-name">
237
- {{ currentName }}
238
- </p>
239
- <p class="minuta-viewer__binary-hint">
240
- Este formato não abre no navegador{{ content.sizeLabel ? ` — ${content.sizeLabel}` : '' }}.
241
- </p>
242
- <button type="button" class="minuta-viewer__download" @click="downloadCurrent">
243
- Baixar arquivo
244
- </button>
245
- </div>
110
+ <TelaPreview
111
+ class="h-full"
112
+ :model-value="selected"
113
+ :file-options="fileOptions"
114
+ :current-file="currentFile"
115
+ :resolve-file-url="resolveFileUrl"
116
+ :pdf-loader="pdfLoader"
117
+ :highlight-text="highlightForCurrent"
118
+ :highlight-page="pageForCurrent"
119
+ :labels="{ selectVariable: 'Selecionar documento', file: 'Documento' }"
120
+ :select-variable-labels="{
121
+ placeholder: 'Selecionar documento',
122
+ searchPlaceholder: 'Buscar documento...',
123
+ noVariablesFound: 'Nenhum documento encontrado',
124
+ variablesLabel: 'Categoria',
125
+ filesLabel: 'documentos',
126
+ }"
127
+ :content-labels="{
128
+ failedToLoadFile: 'Não foi possível carregar o documento',
129
+ page: 'Página',
130
+ download: 'Baixar',
131
+ unableToPreview: 'Este formato não abre no navegador',
132
+ zoomOut: 'Diminuir',
133
+ zoomIn: 'Aumentar',
134
+ handMode: 'Modo arrasto',
135
+ fullscreen: 'Tela cheia',
136
+ pageOf: 'de',
137
+ }"
138
+ @update:model-value="selected = $event"
139
+ />
246
140
  </div>
247
141
  </div>
248
142
  </div>
@@ -250,5 +144,5 @@ onBeforeUnmount(() => {
250
144
  </template>
251
145
 
252
146
  <style scoped>
253
- .minuta-viewer__overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:24px;position:fixed;z-index:100}.minuta-viewer__modal{background:#fff;border-radius:12px;box-shadow:0 20px 25px -5px rgba(0,0,0,.2);display:flex;flex-direction:column;height:100%;max-height:90vh;max-width:960px;overflow:hidden;width:100%}.minuta-viewer__head{align-items:center;border-bottom:1px solid #e5e7eb;color:#111827;display:flex;font-size:14px;font-weight:600;gap:12px;justify-content:space-between;padding:10px 16px}.minuta-viewer__head select{background:#fff;border:.5px solid #d4d9df;border-radius:6px;font-size:13px;max-width:70%;padding:4px 8px}.minuta-viewer__actions{align-items:center;display:flex;gap:8px}.minuta-viewer__close{background:none;border:none;color:#6b7280;cursor:pointer;font-size:18px;line-height:1}.minuta-viewer__download{background:#fff;border:.5px solid #d4d9df;border-radius:6px;color:#374151;cursor:pointer;font-family:inherit;font-size:12px;font-weight:500;padding:4px 10px}.minuta-viewer__download:hover{background:#f9fafb}.minuta-viewer__binary{align-items:center;display:flex;flex-direction:column;gap:8px;height:100%;justify-content:center;padding:24px;text-align:center}.minuta-viewer__binary-name{color:#111827;font-size:14px;font-weight:500;margin:0;word-break:break-all}.minuta-viewer__binary-hint{color:#6b7280;font-size:13px;margin:0}.minuta-viewer__body{background:#f9fafb;flex:1;min-height:0;overflow-y:auto}.minuta-viewer__loading{color:#6b7280;font-size:13px;padding:24px}.minuta-viewer__body img{height:100%;-o-object-fit:contain;object-fit:contain;width:100%}.minuta-viewer__body pre{color:#1f2937;font-size:13px;line-height:1.6;margin:0;padding:20px;white-space:pre-wrap}.minuta-viewer__body iframe{border:0;height:100%;width:100%}
147
+ .minuta-viewer__overlay{align-items:center;background:rgba(15,23,42,.45);display:flex;inset:0;justify-content:center;padding:24px;position:fixed;z-index:60}.minuta-viewer__modal{background:#fff;border-radius:12px;box-shadow:0 24px 60px rgba(15,23,42,.28);display:flex;flex-direction:column;height:min(88vh,100%);overflow:hidden;width:min(1100px,100%)}.minuta-viewer__head{align-items:center;border-bottom:1px solid rgb(0 0 0/8%);display:flex;gap:12px;justify-content:space-between;padding:10px 12px 10px 16px}.minuta-viewer__title{color:#171717;font-size:13px;font-weight:540}.minuta-viewer__body{background:#fff;flex:1;min-height:0}@media (max-width:640px){.minuta-viewer__overlay{padding:0}.minuta-viewer__modal{border-radius:0;height:100%;width:100%}}
254
148
  </style>
@@ -202,7 +202,10 @@ watch(containerRef, (el) => {
202
202
  if (!el)
203
203
  return
204
204
  resizeObserver = new ResizeObserver(() => {
205
- zoom.value = Math.min(1, (el.clientWidth - 48) / A4_WIDTH_PX)
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)
206
209
  })
207
210
  resizeObserver.observe(el)
208
211
  }, { immediate: true })
@@ -1137,7 +1140,7 @@ defineExpose({ editor, schedulePaginate, refreshChrome, containerRef })
1137
1140
  </template>
1138
1141
 
1139
1142
  <style scoped>
1140
- :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;text-align:justify}: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;margin-bottom:16px;margin-left:48px}:deep(ul)>li{list-style-type:disc}:deep(ol)>li{list-style-type:decimal}: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-tplvar){background:rgba(251,191,36,.16);border-radius:3px;box-shadow:inset 0 0 0 1px rgba(180,83,9,.35);color:#78350f;display:inline;padding:0 2px;white-space:pre-wrap}:deep(.poc-tplvar--unknown){background:rgba(244,63,94,.12);box-shadow:inset 0 0 0 1px rgba(190,18,60,.45);color:#881337;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}:deep(.ProseMirror-selectednode.poc-tplvar){box-shadow:inset 0 0 0 2px rgba(180,83,9,.75)}: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}
1143
+ :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{align-items:center;background:#fafbfc;border-bottom:.5px solid #dee3e7;display:flex;flex-wrap:wrap;gap:2px;padding:4px 12px}@media (max-width:640px){.poc-toolbar{flex-wrap:nowrap;overflow-x:auto;padding:4px 8px;scrollbar-width:none}.poc-toolbar::-webkit-scrollbar{display:none}.poc-toolbar>*{flex:none}}.poc-tool{align-items:center;border-radius:6px;display:flex;font-size:13px;height:28px;justify-content:center;min-width:28px;padding-left:6px;padding-right:6px;position:relative;transition:all .15s;transition-timing-function:ease}.poc-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){display:block;pointer-events:none;text-align:initial;text-indent:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}: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:24px;font-weight:640;letter-spacing:-.3px;line-height:32px;margin:0 0 20px}:deep(h2){color:#0f172a;font-size:19px;font-weight:620;letter-spacing:-.2px;line-height:26px;margin:28px 0 14px}:deep(h3){color:#0f172a;font-size:17px;font-weight:600;letter-spacing:-.1px;line-height:24px;margin:24px 0 12px}:deep(h1):first-child,:deep(h2):first-child,:deep(h3):first-child{margin-top:0}:deep(strong){font-weight:540}:deep(p){color:#000;font-size:18px;font-weight:400;letter-spacing:-.4px;line-height:28px;line-height:1.7em;margin-bottom:.75em;text-align:justify}: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;margin-bottom:16px;margin-left:48px}:deep(ul)>li{list-style-type:disc}:deep(ol)>li{list-style-type:decimal}: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-tplvar){background:rgba(251,191,36,.16);border-radius:3px;box-shadow:inset 0 0 0 1px rgba(180,83,9,.35);color:#78350f;display:inline;padding:0 2px;white-space:pre-wrap}:deep(.poc-tplvar--unknown){background:rgba(244,63,94,.12);box-shadow:inset 0 0 0 1px rgba(190,18,60,.45);color:#881337;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}:deep(.ProseMirror-selectednode.poc-tplvar){box-shadow:inset 0 0 0 2px rgba(180,83,9,.75)}: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}
1141
1144
  </style>
1142
1145
 
1143
1146
  <style scoped>
@@ -1221,17 +1224,6 @@ defineExpose({ editor, schedulePaginate, refreshChrome, containerRef })
1221
1224
  border: 0.5px solid #d4d9df;
1222
1225
  }
1223
1226
 
1224
- /* no CDB os tokens de heading vencem os overrides text-24px/text-20px — replica */
1225
- .poc-editor-root :deep(h2) {
1226
- font-size: 14px;
1227
- line-height: 16px;
1228
- }
1229
-
1230
- .poc-editor-root :deep(h3) {
1231
- font-size: 12px;
1232
- line-height: 14px;
1233
- }
1234
-
1235
1227
  /* popover de nota de rodapé (ancorado na chamada, estilo comentário) */
1236
1228
  .poc-footnote-pop {
1237
1229
  position: absolute;
@@ -251,6 +251,12 @@ async function exportDocx() {
251
251
  // ==========================================
252
252
 
253
253
  const historyOpen = ref(false)
254
+ /**
255
+ * Painel de análise em tela estreita. Declarado aqui, e não junto do resto do
256
+ * layout responsivo, porque `toggleHistory` precisa fechá-lo: os dois painéis
257
+ * são mutuamente exclusivos.
258
+ */
259
+ const showAnalysisPanel = ref(false)
254
260
  const versions = ref<MinutaVersion[]>([])
255
261
  const loadingVersions = ref(false)
256
262
  const savingCheckpoint = ref(false)
@@ -296,6 +302,8 @@ function toggleHistory() {
296
302
  closeHistory()
297
303
  return
298
304
  }
305
+ // só cabe um painel: dois de 320px lado a lado esmagam o documento
306
+ showAnalysisPanel.value = false
299
307
  historyOpen.value = true
300
308
  void loadVersions()
301
309
  }
@@ -446,6 +454,44 @@ async function applyChrome({ html, scope, showPageNumber }: { html: string, scop
446
454
  }
447
455
  }
448
456
 
457
+ /**
458
+ * Comentários de margem só cabem quando há folha (≈794px) + coluna (264px) +
459
+ * respiro. Abaixo disso a margem fica fora da tela — então os cards viram um
460
+ * painel lateral que o usuário abre e fecha.
461
+ */
462
+ const GUTTER_MIN_WIDTH = 1120
463
+ const bodyRef = ref<HTMLElement | null>(null)
464
+ const bodyWidth = ref(0)
465
+ let bodyResizeObserver: ResizeObserver | null = null
466
+
467
+ const narrowLayout = computed(() => bodyWidth.value > 0 && bodyWidth.value < GUTTER_MIN_WIDTH)
468
+
469
+ function openAnalysisPanel() {
470
+ // mutuamente exclusivo com o histórico (ver toggleHistory)
471
+ closeHistory()
472
+ showAnalysisPanel.value = true
473
+ }
474
+ const analysisCount = computed(() => {
475
+ const a = minuta.value?.analysis
476
+ if (!a)
477
+ return 0
478
+ return (a.teses?.length ?? 0) + (a.jurisprudencias?.length ?? 0) + (a.citacoes?.length ?? 0) + (a.fatos?.length ?? 0)
479
+ })
480
+
481
+ watch(bodyRef, (el) => {
482
+ bodyResizeObserver?.disconnect()
483
+ bodyResizeObserver = null
484
+ if (!el || typeof ResizeObserver === 'undefined')
485
+ return
486
+ bodyWidth.value = el.clientWidth
487
+ bodyResizeObserver = new ResizeObserver(() => {
488
+ bodyWidth.value = el.clientWidth
489
+ })
490
+ bodyResizeObserver.observe(el)
491
+ }, { immediate: true })
492
+
493
+ onUnmounted(() => bodyResizeObserver?.disconnect())
494
+
449
495
  onMounted(() => window.addEventListener('poc:edit-chrome', onEditChrome))
450
496
  onUnmounted(() => window.removeEventListener('poc:edit-chrome', onEditChrome))
451
497
 
@@ -523,10 +569,22 @@ const editorCompRef = ref<any>(null)
523
569
  const docPreviewOpen = ref(false)
524
570
  const docPreviewUrl = ref<string | null>(null)
525
571
  const docPreviewPage = ref<number | null>(null)
526
-
527
- function openDocPreview(url?: string | null) {
572
+ /** trecho que o card de fato pediu para destacar no documento-fonte */
573
+ const docPreviewHighlight = ref<string | null>(null)
574
+
575
+ /**
576
+ * `fromCard` separa os dois chamadores: o botão Documentos abre a lista limpa,
577
+ * e o card de fato extraído mantém página e destaque mesmo quando o documento
578
+ * que ele nomeia não casa com nenhum anexo (aí abre o primeiro arquivo, mas a
579
+ * localização que o card pediu continua valendo).
580
+ */
581
+ function openDocPreview(url?: string | null, fromCard = false) {
528
582
  if (!minuta.value?.files.length)
529
583
  return
584
+ if (!fromCard) {
585
+ docPreviewPage.value = null
586
+ docPreviewHighlight.value = null
587
+ }
530
588
  docPreviewUrl.value = url ?? minuta.value.files[0].url
531
589
  docPreviewOpen.value = true
532
590
  }
@@ -555,7 +613,8 @@ function onOpenDocument(payload: { documento: string, pagina?: number, highlight
555
613
  const file = minuta.value?.files.find(f => f.name === payload.documento)
556
614
  ?? minuta.value?.files.find(f => f.name.includes(payload.documento) || payload.documento.includes(f.name))
557
615
  docPreviewPage.value = payload.pagina ?? null
558
- openDocPreview(file?.url)
616
+ docPreviewHighlight.value = payload.highlight ?? null
617
+ openDocPreview(file?.url, true)
559
618
  }
560
619
 
561
620
  const hasAnalysis = computed(() => {
@@ -592,32 +651,41 @@ defineExpose({ saveNow, exportDocx, editor })
592
651
  >
593
652
  <span v-else class="minuta-embed__name">{{ minuta.name }}</span>
594
653
  <span class="minuta-embed__save-state">{{ SAVE_LABEL[saveState] }}</span>
595
- <button
654
+ <TelaButton
596
655
  v-if="minuta.status === 'READY' && minuta.files.length"
597
- class="minuta-embed__ghost-btn"
598
- type="button"
656
+ variant="ghost"
657
+ size="sm"
658
+ icon="i-ph-files"
659
+ leading
660
+ class="minuta-embed__toolbar-btn"
599
661
  @click="openDocPreview()"
600
662
  >
601
- Documentos
602
- </button>
603
- <button
663
+ <span class="minuta-embed__btn-label">Documentos</span>
664
+ </TelaButton>
665
+ <TelaButton
604
666
  v-if="minuta.status === 'READY'"
605
- class="minuta-embed__ghost-btn"
606
- :class="{ 'minuta-embed__ghost-btn--on': historyOpen }"
607
- type="button"
667
+ :variant="historyOpen ? 'secondary' : 'ghost'"
668
+ size="sm"
669
+ icon="i-ph-clock-counter-clockwise"
670
+ leading
671
+ class="minuta-embed__toolbar-btn"
608
672
  @click="toggleHistory"
609
673
  >
610
- Histórico
611
- </button>
612
- <button
674
+ <span class="minuta-embed__btn-label">Histórico</span>
675
+ </TelaButton>
676
+ <TelaButton
613
677
  v-if="minuta.status === 'READY'"
614
- class="minuta-embed__export"
678
+ variant="primary"
679
+ size="sm"
680
+ icon="i-ph-download-simple"
681
+ leading
682
+ :loading="isExporting"
615
683
  :disabled="isExporting || !!versionPreview"
616
- type="button"
684
+ class="minuta-embed__toolbar-btn"
617
685
  @click="exportDocx"
618
686
  >
619
- {{ isExporting ? 'Exportando…' : 'Baixar DOCX' }}
620
- </button>
687
+ <span class="minuta-embed__btn-label">{{ isExporting ? 'Exportando…' : 'Baixar DOCX' }}</span>
688
+ </TelaButton>
621
689
  </div>
622
690
 
623
691
  <div v-if="isLoading" class="minuta-embed__status">
@@ -636,7 +704,7 @@ defineExpose({ saveNow, exportDocx, editor })
636
704
  A geração falhou: {{ minuta.error || 'erro desconhecido' }}
637
705
  </div>
638
706
 
639
- <div v-else-if="minuta" class="minuta-embed__body">
707
+ <div v-else-if="minuta" ref="bodyRef" class="minuta-embed__body">
640
708
  <div class="minuta-embed__document">
641
709
  <MinutaDocxEditor
642
710
  ref="editorCompRef"
@@ -652,7 +720,7 @@ defineExpose({ saveNow, exportDocx, editor })
652
720
  >
653
721
  <template #gutter>
654
722
  <MinutaAnnotationsGutter
655
- v-if="hasAnalysis && minuta.analysis"
723
+ v-if="hasAnalysis && minuta.analysis && !narrowLayout"
656
724
  :editor="editor"
657
725
  :analysis="minuta.analysis"
658
726
  :scroll-container="editorCompRef?.containerRef ?? null"
@@ -662,6 +730,17 @@ defineExpose({ saveNow, exportDocx, editor })
662
730
  </template>
663
731
  </MinutaDocxEditor>
664
732
 
733
+ <!-- tela estreita: os comentários viram painel lateral -->
734
+ <button
735
+ v-if="hasAnalysis && narrowLayout && !showAnalysisPanel"
736
+ type="button"
737
+ class="minuta-embed__analysis-toggle"
738
+ @click="openAnalysisPanel"
739
+ >
740
+ Análise
741
+ <span class="minuta-embed__analysis-count">{{ analysisCount }}</span>
742
+ </button>
743
+
665
744
  <!-- edição de cabeçalho/rodapé (canvas estilo Word) -->
666
745
  <div v-if="chromeEditor" class="minuta-embed__chrome-overlay">
667
746
  <MinutaChromeEditor
@@ -703,6 +782,26 @@ defineExpose({ saveNow, exportDocx, editor })
703
782
  </div>
704
783
  </div>
705
784
 
785
+ <!-- painel: análise (tela estreita — sem margem para ancorar os cards) -->
786
+ <aside v-if="hasAnalysis && narrowLayout && showAnalysisPanel && minuta.analysis" class="minuta-embed__panel minuta-embed__panel--analysis">
787
+ <div class="minuta-embed__panel-head">
788
+ <span>Análise da peça</span>
789
+ <button type="button" @click="showAnalysisPanel = false">
790
+ ×
791
+ </button>
792
+ </div>
793
+ <div class="minuta-embed__panel-scroll">
794
+ <MinutaAnnotationsGutter
795
+ :editor="editor"
796
+ :analysis="minuta.analysis"
797
+ :scroll-container="editorCompRef?.containerRef ?? null"
798
+ compact
799
+ @open-document="onOpenDocument"
800
+ @update-analysis="onAnalysisUpdate"
801
+ />
802
+ </div>
803
+ </aside>
804
+
706
805
  <!-- painel: histórico -->
707
806
  <aside v-if="historyOpen" class="minuta-embed__panel">
708
807
  <div class="minuta-embed__panel-head">
@@ -771,6 +870,7 @@ defineExpose({ saveNow, exportDocx, editor })
771
870
  :files="minuta.files"
772
871
  :initial-url="docPreviewUrl"
773
872
  :initial-page="docPreviewPage"
873
+ :highlight="docPreviewHighlight"
774
874
  @close="docPreviewOpen = false"
775
875
  @error="msg => emit('error', msg)"
776
876
  />
@@ -779,5 +879,5 @@ defineExpose({ saveNow, exportDocx, editor })
779
879
  </template>
780
880
 
781
881
  <style scoped>
782
- .minuta-embed{background:#f7f7f8;display:flex;flex-direction:column;height:100%}.minuta-embed__toolbar{align-items:center;background:#fff;border-bottom:1px solid #e5e7eb;display:flex;gap:8px;padding:8px 16px}.minuta-embed__name--input{background:transparent;border:0;font-family:inherit;outline:none}.minuta-embed__name--input:focus{text-decoration:underline}.minuta-embed__name{color:#111827;flex:1;font-size:15px;font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.minuta-embed__save-state{color:#9ca3af;font-size:12px;margin-right:4px}.minuta-embed__ghost-btn{background:#fff;border:1px solid #e5e7eb;border-radius:6px;color:#374151;cursor:pointer;font-size:13px;padding:6px 12px}.minuta-embed__ghost-btn--on{background:#f3f4f6}.minuta-embed__export{background:#111827;border:none;border-radius:6px;color:#fff;cursor:pointer;font-size:13px;padding:6px 12px}.minuta-embed__export:disabled{cursor:default;opacity:.6}.minuta-embed__chrome-overlay{bottom:16px;position:absolute;right:16px;z-index:30}.minuta-embed__status{color:#6b7280;font-size:14px;padding:48px 24px;text-align:center}.minuta-embed__status--error{color:#b91c1c}.minuta-embed__body,.minuta-embed__document{display:flex;flex:1;min-height:0}.minuta-embed__document{min-width:0;position:relative}.minuta-embed__banner{align-items:center;background:#fffbeb;border:1px solid #fcd34d;border-radius:16px;bottom:16px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1);display:flex;flex-wrap:wrap;gap:8px 12px;justify-content:center;left:50%;max-width:calc(100% - 32px);padding:8px 16px;position:absolute;transform:translateX(-50%);width:-moz-max-content;width:max-content;z-index:30}.minuta-embed__banner-text{color:#92400e;font-size:12px;font-weight:500}.minuta-embed__banner-actions{display:flex;gap:8px}.minuta-embed__banner-actions button{background:#fff;border:1px solid #fcd34d;border-radius:6px;color:#92400e;cursor:pointer;font-size:12px;padding:4px 10px;white-space:nowrap}.minuta-embed__banner-actions button:disabled{cursor:default;opacity:.6}.minuta-embed__panel{background:#fff;border-left:1px solid #e5e7eb;display:flex;flex-direction:column;width:320px}.minuta-embed__panel-head{align-items:center;border-bottom:1px solid #e5e7eb;color:#111827;display:flex;font-size:14px;font-weight:600;justify-content:space-between;padding:10px 16px}.minuta-embed__panel-head button{background:none;border:none;color:#6b7280;cursor:pointer;font-size:18px;line-height:1}.minuta-embed__checkpoint{border-bottom:1px solid #e5e7eb;display:flex;flex-direction:column;gap:8px;padding:12px}.minuta-embed__checkpoint input{border:1px solid #e5e7eb;border-radius:6px;font-size:13px;padding:6px 10px}.minuta-embed__checkpoint button{background:#fff;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:13px;padding:6px 12px}.minuta-embed__versions{flex:1;min-height:0;overflow-y:auto;padding:8px}.minuta-embed__versions-empty{color:#6b7280;font-size:12px;margin:0;padding:8px 12px}.minuta-embed__version{background:#fff;border:1px solid #e5e7eb;border-radius:10px;cursor:pointer;display:block;margin-bottom:6px;padding:10px;text-align:left;width:100%}.minuta-embed__version:hover{background:#faf5ff;border-color:#c4b5fd}.minuta-embed__version--active{background:#f5f3ff;border-color:#a78bfa}.minuta-embed__version:disabled{cursor:wait;opacity:.7}.minuta-embed__version-row{align-items:center;color:#111827;display:flex;font-size:12px;gap:6px}.minuta-embed__version-type{background:#f3f4f6;border-radius:999px;color:#4b5563;font-size:11px;font-style:normal;padding:1px 8px}.minuta-embed__version-type[data-type=auto]{background:#f5f3ff;color:#6d28d9}.minuta-embed__version-type[data-type=manual]{background:#eff6ff;color:#1d4ed8}.minuta-embed__version-date{color:#9ca3af;font-size:11px;margin-left:auto}.minuta-embed__version-comment{color:#111827;display:block;font-size:12px;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.minuta-embed__version-author{color:#9ca3af;display:block;font-size:11px;margin-top:2px}
882
+ .minuta-embed{background:#f7f7f8;display:flex;flex-direction:column;height:100%}.minuta-embed__toolbar{align-items:center;background:#fff;border-bottom:1px solid #e5e7eb;display:flex;gap:8px;padding:8px 16px}@media (max-width:640px){.minuta-embed__toolbar{gap:4px;padding:6px 10px}.minuta-embed__btn-label{clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}.minuta-embed__save-state{display:none}}.minuta-embed__name--input{background:transparent;border:0;font-family:inherit;outline:none}.minuta-embed__name--input:focus{text-decoration:underline}.minuta-embed__name{color:#111827;flex:1;font-size:15px;font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.minuta-embed__save-state{color:#9ca3af;font-size:12px;margin-right:4px}.minuta-embed__chrome-overlay{bottom:16px;position:absolute;right:16px;z-index:30}.minuta-embed__status{color:#6b7280;font-size:14px;padding:48px 24px;text-align:center}.minuta-embed__status--error{color:#b91c1c}.minuta-embed__body,.minuta-embed__document{display:flex;flex:1;min-height:0}.minuta-embed__document{min-width:0;position:relative}.minuta-embed__banner{align-items:center;background:#fffbeb;border:1px solid #fcd34d;border-radius:16px;bottom:16px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1);display:flex;flex-wrap:wrap;gap:8px 12px;justify-content:center;left:50%;max-width:calc(100% - 32px);padding:8px 16px;position:absolute;transform:translateX(-50%);width:-moz-max-content;width:max-content;z-index:30}.minuta-embed__banner-text{color:#92400e;font-size:12px;font-weight:500}.minuta-embed__banner-actions{display:flex;gap:8px}.minuta-embed__banner-actions button{background:#fff;border:1px solid #fcd34d;border-radius:6px;color:#92400e;cursor:pointer;font-size:12px;padding:4px 10px;white-space:nowrap}.minuta-embed__banner-actions button:disabled{cursor:default;opacity:.6}.minuta-embed__analysis-toggle{align-items:center;background:#fff;border:1px solid #d4d9df;border-radius:999px;bottom:16px;box-shadow:0 2px 10px rgba(15,23,42,.12);color:#334155;cursor:pointer;display:inline-flex;font-size:13px;font-weight:540;gap:8px;padding:8px 14px;position:absolute;right:16px;z-index:20}.minuta-embed__analysis-toggle:hover{border-color:#b8bfc9;color:#0f172a}.minuta-embed__analysis-count{background:#eef1f5;border-radius:999px;color:#64748b;font-size:11px;font-variant-numeric:tabular-nums;padding:1px 7px}.minuta-embed__panel--analysis .minuta-embed__panel-scroll{flex:1;min-height:0;overflow-y:auto;padding:12px}.minuta-embed__panel{background:#fff;border-left:1px solid #e5e7eb;display:flex;flex-direction:column;flex-shrink:0;max-width:55%;width:320px}.minuta-embed__panel-head{align-items:center;border-bottom:1px solid #e5e7eb;color:#111827;display:flex;font-size:14px;font-weight:600;justify-content:space-between;padding:10px 16px}.minuta-embed__panel-head button{background:none;border:none;color:#6b7280;cursor:pointer;font-size:18px;line-height:1}.minuta-embed__checkpoint{border-bottom:1px solid #e5e7eb;display:flex;flex-direction:column;gap:8px;padding:12px}.minuta-embed__checkpoint input{border:1px solid #e5e7eb;border-radius:6px;font-size:13px;padding:6px 10px}.minuta-embed__checkpoint button{background:#fff;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:13px;padding:6px 12px}.minuta-embed__versions{flex:1;min-height:0;overflow-y:auto;padding:8px}.minuta-embed__versions-empty{color:#6b7280;font-size:12px;margin:0;padding:8px 12px}.minuta-embed__version{background:#fff;border:1px solid #e5e7eb;border-radius:10px;cursor:pointer;display:block;margin-bottom:6px;padding:10px;text-align:left;width:100%}.minuta-embed__version:hover{background:#faf5ff;border-color:#c4b5fd}.minuta-embed__version--active{background:#f5f3ff;border-color:#a78bfa}.minuta-embed__version:disabled{cursor:wait;opacity:.7}.minuta-embed__version-row{align-items:center;color:#111827;display:flex;font-size:12px;gap:6px}.minuta-embed__version-type{background:#f3f4f6;border-radius:999px;color:#4b5563;font-size:11px;font-style:normal;padding:1px 8px}.minuta-embed__version-type[data-type=auto]{background:#f5f3ff;color:#6d28d9}.minuta-embed__version-type[data-type=manual]{background:#eff6ff;color:#1d4ed8}.minuta-embed__version-date{color:#9ca3af;font-size:11px;margin-left:auto}.minuta-embed__version-comment{color:#111827;display:block;font-size:12px;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.minuta-embed__version-author{color:#9ca3af;display:block;font-size:11px;margin-top:2px}
783
883
  </style>
@@ -1219,7 +1219,11 @@ export async function pocExportDocx(editor, margins, chrome, options = {}) {
1219
1219
  const doc = editor.state.doc;
1220
1220
  const ctx = {
1221
1221
  view: editor.view,
1222
- breakSet: new Set((options.breaks ?? []).map((b) => b.pos)),
1222
+ // quebras `soft` (dentro de um parágrafo) não viram pageBreakBefore:
1223
+ // partir o parágrafo em dois no Word entregaria ao advogado um arquivo
1224
+ // que não reflui ao editar. Ali o Word quebra sozinho, no mesmo lugar
1225
+ // ou a uma linha de distância.
1226
+ breakSet: new Set((options.breaks ?? []).filter((b) => !b.soft).map((b) => b.pos)),
1223
1227
  pending: { breakBefore: false },
1224
1228
  lastAfterTw: 0,
1225
1229
  meta: /* @__PURE__ */ new WeakMap(),
@@ -54,11 +54,24 @@ export interface PocPageNote {
54
54
  text: string;
55
55
  /** corpo declarado no template, em pontos (default: 8pt, como o Word) */
56
56
  size?: number | null;
57
+ /**
58
+ * posição do MARCADOR no documento. Só é usada para repartir as notas
59
+ * quando a quebra cai no meio do parágrafo: a nota mora na página da
60
+ * chamada, então as que ficam depois do corte viajam para a página nova.
61
+ */
62
+ at?: number;
57
63
  }
58
64
  export interface PocPageBreak {
59
65
  pos: number;
60
66
  filler: number;
61
67
  pageNo: number;
68
+ /**
69
+ * quebra no MEIO de um parágrafo (entre linhas). O DOCX não recebe
70
+ * pageBreakBefore nessas: partir o parágrafo em dois no Word entregaria
71
+ * ao advogado um arquivo que não reflui quando ele edita o texto. Aqui o
72
+ * Word quebra naturalmente, e o preview só antecipa onde.
73
+ */
74
+ soft?: boolean;
62
75
  /** notas cujos marcadores estão nesta página — desenhadas acima do rodapé */
63
76
  notes?: PocPageNote[];
64
77
  /** altura reservada pelas notas (já descontada do filler) */
@@ -120,6 +120,54 @@ function absTop(el, sheet) {
120
120
  return top;
121
121
  }
122
122
  const HEADING_TAGS = /* @__PURE__ */ new Set(["H1", "H2", "H3", "H4"]);
123
+ const SPLITTABLE_TAGS = /* @__PURE__ */ new Set(["P"]);
124
+ const MIN_LINES_PER_SIDE = 2;
125
+ function findInlineSplit(view, dom, availablePx, fromLine = 0, minHead = MIN_LINES_PER_SIDE, minTail = MIN_LINES_PER_SIDE) {
126
+ if (!SPLITTABLE_TAGS.has(dom.tagName))
127
+ return null;
128
+ let rects;
129
+ try {
130
+ const range = document.createRange();
131
+ range.selectNodeContents(dom);
132
+ rects = Array.from(range.getClientRects()).filter((r) => r.height > 0 && r.width > 0);
133
+ } catch {
134
+ return null;
135
+ }
136
+ if (rects.length < minHead + minTail)
137
+ return null;
138
+ const lines = [];
139
+ for (const r of rects) {
140
+ const last2 = lines[lines.length - 1];
141
+ if (last2 && r.top < last2.bottom - 1) {
142
+ last2.bottom = Math.max(last2.bottom, r.bottom);
143
+ last2.left = Math.min(last2.left, r.left);
144
+ } else {
145
+ lines.push({ top: r.top, bottom: r.bottom, left: r.left });
146
+ }
147
+ }
148
+ if (lines.length < minHead + minTail)
149
+ return null;
150
+ const scale = dom.offsetHeight > 0 ? dom.getBoundingClientRect().height / dom.offsetHeight : 1;
151
+ if (!Number.isFinite(scale) || scale <= 0)
152
+ return null;
153
+ const originTop = lines[0].top;
154
+ const first = fromLine + minHead;
155
+ const last = lines.length - minTail;
156
+ let cut = -1;
157
+ for (let i = first; i <= last; i++) {
158
+ if ((lines[i].bottom - originTop) / scale > availablePx) {
159
+ cut = i;
160
+ break;
161
+ }
162
+ }
163
+ if (cut < first || cut > last)
164
+ return null;
165
+ const line = lines[cut];
166
+ const found = view.posAtCoords({ left: line.left + 1, top: (line.top + line.bottom) / 2 });
167
+ if (!found)
168
+ return null;
169
+ return { pos: found.pos, usedPx: (lines[cut - 1].bottom - originTop) / scale, line: cut };
170
+ }
123
171
  function pageSafetyPx(contentHeight) {
124
172
  return Math.max(32, Math.round(contentHeight * 0.08));
125
173
  }
@@ -180,10 +228,10 @@ export function computePocBreaks(view, cfg, sheet) {
180
228
  return;
181
229
  const top = absTop(dom, sheet) - baseY - chromeAbove(absTop(dom, sheet));
182
230
  const notes = [];
183
- child.descendants((inner) => {
231
+ child.descendants((inner, innerPos) => {
184
232
  if (inner.type.name === "footnoteRef" && inner.attrs.text) {
185
233
  noteCounter += 1;
186
- notes.push({ n: noteCounter, text: String(inner.attrs.text), size: inner.attrs.size ?? null });
234
+ notes.push({ n: noteCounter, text: String(inner.attrs.text), size: inner.attrs.size ?? null, at: pos + 1 + innerPos });
187
235
  }
188
236
  return true;
189
237
  });
@@ -193,6 +241,7 @@ export function computePocBreaks(view, cfg, sheet) {
193
241
  top,
194
242
  height: dom.offsetHeight,
195
243
  notes,
244
+ dom,
196
245
  // margem superior conta como parte da página que o item abre —
197
246
  // senão a "página" do preview fica mais alta que um A4 real e
198
247
  // o DOCX (página física) divergiria
@@ -217,9 +266,9 @@ export function computePocBreaks(view, cfg, sheet) {
217
266
  let forcedBreakPending = false;
218
267
  let pageNotes = [];
219
268
  const notesH = (notes) => measurer && notes.length ? measurer.height(notes) : 0;
220
- const closePage = (pos, filler) => {
269
+ const closePage = (pos, filler, soft = false) => {
221
270
  const height = notesH(pageNotes);
222
- breaks.push({ pos, filler: Math.max(0, filler - height), pageNo, notes: pageNotes, notesH: height });
271
+ breaks.push({ pos, filler: Math.max(0, filler - height), pageNo, notes: pageNotes, notesH: height, soft });
223
272
  pageNo++;
224
273
  pageNotes = [];
225
274
  };
@@ -244,6 +293,41 @@ export function computePocBreaks(view, cfg, sheet) {
244
293
  pageStart = startWithMargin;
245
294
  forcedBreakPending = false;
246
295
  } else if (item.top > pageStart && itemEnd - pageStart > capacity) {
296
+ let split = findInlineSplit(view, item.dom, capacity - (item.top - pageStart));
297
+ if (split) {
298
+ let pending = [...item.notes];
299
+ let lastLine = split.line;
300
+ while (split) {
301
+ lastLine = split.line;
302
+ const usedEnd = item.top + split.usedPx;
303
+ const cutAt = split.pos;
304
+ const stay = pending.filter((n) => n.at === void 0 || n.at < cutAt);
305
+ pending = pending.filter((n) => !stay.includes(n));
306
+ pageNotes.push(...stay);
307
+ closePage(cutAt, fullH - (usedEnd - pageStart), true);
308
+ pageStart = usedEnd;
309
+ if (itemEnd - pageStart <= contentH - notesH(pending))
310
+ break;
311
+ const fromTop = pageStart - item.top;
312
+ split = findInlineSplit(
313
+ view,
314
+ item.dom,
315
+ fromTop + contentH - notesH(pending),
316
+ split.line,
317
+ 1,
318
+ 1
319
+ );
320
+ if (!split)
321
+ split = findInlineSplit(view, item.dom, fromTop + contentH, lastLine, 1, 1);
322
+ }
323
+ pageNotes.push(...pending);
324
+ forcedBreakPending = false;
325
+ while (item.top + item.height - pageStart > contentH) {
326
+ pageStart += contentH;
327
+ pageNo++;
328
+ }
329
+ continue;
330
+ }
247
331
  let breakIndex = i;
248
332
  if (shouldMoveBreakToHeading({ heading: items[i - 1], item, pageStart, capacity }))
249
333
  breakIndex = i - 1;
package/package.json CHANGED
@@ -1,100 +1,101 @@
1
1
  {
2
- "name": "@meistrari/minuta-nuxt",
3
- "version": "1.0.0",
4
- "description": "Embed do editor de minuta (docx paginado A4) do defesa.ai para apps Nuxt — componente + proxy Nitro, sem segredos no browser",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/meistrari/case-defense-builder.git",
8
- "directory": "packages/minuta-nuxt"
9
- },
10
- "license": "UNLICENSED",
11
- "type": "module",
12
- "exports": {
13
- ".": {
14
- "types": "./dist/types.d.mts",
15
- "import": "./dist/module.mjs"
2
+ "name": "@meistrari/minuta-nuxt",
3
+ "version": "1.1.0",
4
+ "description": "Embed do editor de minuta (docx paginado A4) do defesa.ai para apps Nuxt — componente + proxy Nitro, sem segredos no browser",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/meistrari/case-defense-builder.git",
8
+ "directory": "packages/minuta-nuxt"
16
9
  },
17
- "./runtime/*": {
18
- "types": "./dist/runtime/*.d.ts",
19
- "import": "./dist/runtime/*.js"
20
- }
21
- },
22
- "main": "./dist/module.mjs",
23
- "typesVersions": {
24
- "*": {
25
- ".": [
26
- "./dist/types.d.mts"
27
- ]
28
- }
29
- },
30
- "files": [
31
- "dist"
32
- ],
33
- "scripts": {
34
- "prepack": "nuxt-module-build build",
35
- "build": "nuxt-module-build build",
36
- "dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground",
37
- "dev": "nuxi dev playground"
38
- },
39
- "dependencies": {
40
- "@fontsource-variable/inter": "5.3.0",
41
- "@nuxt/kit": "^3.17.7",
42
- "@phosphor-icons/vue": "2.2.1",
43
- "@tiptap/core": "2.11.9",
44
- "@tiptap/extension-table": "2.11.9",
45
- "@tiptap/extension-table-cell": "2.11.9",
46
- "@tiptap/extension-table-header": "2.11.9",
47
- "@tiptap/extension-table-row": "2.11.9",
48
- "@tiptap/pm": "2.11.9",
49
- "@tiptap/starter-kit": "2.11.9",
50
- "@tiptap/vue-3": "2.11.9",
51
- "defu": "^6.1.4",
52
- "docx": "9.7.1",
53
- "h3": "^1.15.0",
54
- "ofetch": "^1.4.1"
55
- },
56
- "overrides": {
57
- "@tiptap/core": "2.11.9",
58
- "@tiptap/pm": "2.11.9",
59
- "@tiptap/extension-blockquote": "2.11.9",
60
- "@tiptap/extension-bold": "2.11.9",
61
- "@tiptap/extension-bullet-list": "2.11.9",
62
- "@tiptap/extension-code": "2.11.9",
63
- "@tiptap/extension-code-block": "2.11.9",
64
- "@tiptap/extension-document": "2.11.9",
65
- "@tiptap/extension-dropcursor": "2.11.9",
66
- "@tiptap/extension-gapcursor": "2.11.9",
67
- "@tiptap/extension-hard-break": "2.11.9",
68
- "@tiptap/extension-heading": "2.11.9",
69
- "@tiptap/extension-history": "2.11.9",
70
- "@tiptap/extension-horizontal-rule": "2.11.9",
71
- "@tiptap/extension-italic": "2.11.9",
72
- "@tiptap/extension-list-item": "2.11.9",
73
- "@tiptap/extension-ordered-list": "2.11.9",
74
- "@tiptap/extension-paragraph": "2.11.9",
75
- "@tiptap/extension-strike": "2.11.9",
76
- "@tiptap/extension-text": "2.11.9",
77
- "@tiptap/extension-text-style": "2.11.9",
78
- "@tiptap/extension-bubble-menu": "2.11.9",
79
- "@tiptap/extension-floating-menu": "2.11.9"
80
- },
81
- "devDependencies": {
82
- "@nuxt/module-builder": "^0.8.4",
83
- "nuxt": "~3.17.7",
84
- "pino-pretty": "^13.1.3",
85
- "typescript": "^5.8.2",
86
- "vue": "^3.5.13",
87
- "@meistrari/chat-nuxt": "3.14.0-rc.2"
88
- },
89
- "publishConfig": {
90
- "access": "public"
91
- },
92
- "peerDependencies": {
93
- "@meistrari/chat-nuxt": ">=3.13.0"
94
- },
95
- "peerDependenciesMeta": {
96
- "@meistrari/chat-nuxt": {
97
- "optional": true
10
+ "license": "UNLICENSED",
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/types.d.mts",
15
+ "import": "./dist/module.mjs"
16
+ },
17
+ "./runtime/*": {
18
+ "types": "./dist/runtime/*.d.ts",
19
+ "import": "./dist/runtime/*.js"
20
+ }
21
+ },
22
+ "main": "./dist/module.mjs",
23
+ "typesVersions": {
24
+ "*": {
25
+ ".": [
26
+ "./dist/types.d.mts"
27
+ ]
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "prepack": "nuxt-module-build build",
35
+ "build": "nuxt-module-build build",
36
+ "dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground",
37
+ "dev": "nuxi dev playground"
38
+ },
39
+ "dependencies": {
40
+ "@fontsource-variable/inter": "5.3.0",
41
+ "@nuxt/kit": "^3.17.7",
42
+ "@phosphor-icons/vue": "2.2.1",
43
+ "@tiptap/core": "2.11.9",
44
+ "@tiptap/extension-table": "2.11.9",
45
+ "@tiptap/extension-table-cell": "2.11.9",
46
+ "@tiptap/extension-table-header": "2.11.9",
47
+ "@tiptap/extension-table-row": "2.11.9",
48
+ "@tiptap/pm": "2.11.9",
49
+ "@tiptap/starter-kit": "2.11.9",
50
+ "@tiptap/vue-3": "2.11.9",
51
+ "defu": "^6.1.4",
52
+ "docx": "9.7.1",
53
+ "h3": "^1.15.0",
54
+ "ofetch": "^1.4.1"
55
+ },
56
+ "overrides": {
57
+ "@tiptap/core": "2.11.9",
58
+ "@tiptap/pm": "2.11.9",
59
+ "@tiptap/extension-blockquote": "2.11.9",
60
+ "@tiptap/extension-bold": "2.11.9",
61
+ "@tiptap/extension-bullet-list": "2.11.9",
62
+ "@tiptap/extension-code": "2.11.9",
63
+ "@tiptap/extension-code-block": "2.11.9",
64
+ "@tiptap/extension-document": "2.11.9",
65
+ "@tiptap/extension-dropcursor": "2.11.9",
66
+ "@tiptap/extension-gapcursor": "2.11.9",
67
+ "@tiptap/extension-hard-break": "2.11.9",
68
+ "@tiptap/extension-heading": "2.11.9",
69
+ "@tiptap/extension-history": "2.11.9",
70
+ "@tiptap/extension-horizontal-rule": "2.11.9",
71
+ "@tiptap/extension-italic": "2.11.9",
72
+ "@tiptap/extension-list-item": "2.11.9",
73
+ "@tiptap/extension-ordered-list": "2.11.9",
74
+ "@tiptap/extension-paragraph": "2.11.9",
75
+ "@tiptap/extension-strike": "2.11.9",
76
+ "@tiptap/extension-text": "2.11.9",
77
+ "@tiptap/extension-text-style": "2.11.9",
78
+ "@tiptap/extension-bubble-menu": "2.11.9",
79
+ "@tiptap/extension-floating-menu": "2.11.9"
80
+ },
81
+ "devDependencies": {
82
+ "@nuxt/module-builder": "^0.8.4",
83
+ "nuxt": "~3.17.7",
84
+ "pino-pretty": "^13.1.3",
85
+ "typescript": "^5.8.2",
86
+ "vue": "^3.5.13",
87
+ "@meistrari/chat-nuxt": "3.14.0-rc.2"
88
+ },
89
+ "publishConfig": {
90
+ "access": "public"
91
+ },
92
+ "peerDependencies": {
93
+ "@meistrari/chat-nuxt": ">=3.13.0",
94
+ "@meistrari/tela-build": ">=1.63.0"
95
+ },
96
+ "peerDependenciesMeta": {
97
+ "@meistrari/chat-nuxt": {
98
+ "optional": true
99
+ }
98
100
  }
99
- }
100
101
  }