This commit is contained in:
David Ascanio 2026-05-12 12:29:31 -03:00
commit b22c58cc5a
4 changed files with 161 additions and 183 deletions

View File

@ -41,25 +41,26 @@
/* Client-side highlights inside the detail body. */
mark.search-match {
background-color: #fde68a;
color: #78350f;
padding: 0 2px;
background-color: #fdff32;
color: #000;
padding: 2px;
border-radius: 2px;
font-weight: 600;
box-shadow: 0 0 0 1px #f59e0b inset;
box-shadow: 0 0 0 1px #e9ff32 inset;
}
.dark mark.search-match {
background-color: #78350f;
color: #fde68a;
box-shadow: 0 0 0 1px #f59e0b inset;
background-color: #fdff32;
color: #000;
box-shadow: 0 0 0 1px #e9ff32 inset;
}
/* The currently-focused match (the one the user navigated to). */
mark.search-match.is-current {
background-color: #f97316;
color: #ffffff;
box-shadow: 0 0 0 2px #c2410c inset;
background-color: #8cff32;
padding: 2px;
color: #000;
box-shadow: 0 0 0 2px #8cff32 inset;
animation: search-match-pulse 0.9s ease-out 1;
}
@ -70,7 +71,7 @@ mark.search-match.is-current {
}
@keyframes search-match-pulse {
0% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.75), 0 0 0 2px #c2410c inset; }
70% { box-shadow: 0 0 0 14px rgba(249, 115, 22, 0), 0 0 0 2px #c2410c inset; }
100% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0), 0 0 0 2px #c2410c inset; }
0% { box-shadow: 0 0 0 0 #e9ff32, 0 0 0 2px #8cff32 inset; }
70% { box-shadow: 0 0 0 14px rgba(249, 115, 22, 0), 0 0 0 2px #8cff32 inset; }
100% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0), 0 0 0 2px #8cff32 inset; }
}

View File

@ -384,30 +384,90 @@ function onInputKey(e: KeyboardEvent) {
<!-- El botón de favorito ya no vive aquí: ahora es un FAB flotante
en la esquina inferior derecha del panel para que siga visible
mientras se lee. Ver más abajo en el scroll container. -->
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
<UButton
v-if="matchUrl"
:to="matchUrl"
target="_blank"
icon="i-lucide-external-link"
label="Ver en sitio"
color="primary"
variant="solid"
size="sm"
:icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
:color="isFav ? 'primary' : 'neutral'"
variant="ghost"
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"
@click="onToggleFavorite"
/>
</UTooltip>
</template>
</UDashboardNavbar>
<div class="flex flex-col sm:flex-row justify-between gap-2 p-4 sm:px-6 border-b border-default">
<div class="min-w-0">
<p class="font-semibold text-sm text-highlighted">
<div class="flex flex-col sm:flex-row justify-between gap-2 p-4 sm:px-6 border-b border-default shadow-lg z-10">
<div class="min-w-0 flex gap-1 sm:gap-4 sm:items-center flex-col sm:flex-row">
<p class=" text-sm text-highlighted flex items-center gap-1 mb-0.5">
<UIcon name="ph:calendar" class="size-5 text-green-600" />
{{ safeDate(activity) }}
</p>
<p class="text-muted text-sm truncate">
<p class="text-sm truncate flex items-center gap-1">
<UIcon name="ph:map-pin" class="size-5 text-green-600" />
{{ formatLocation(activity) }}
</p>
</div>
<ul v-if="fileLinks.length" class="flex flex-wrap gap-3 text-xs text-muted">
<div
v-if="activity?.body"
class="flex items-center gap-2"
>
<UFieldGroup>
<UInput
v-model="localQuery"
icon="i-lucide-search"
placeholder="Buscar frase exacta en este documento..."
class="w-full"
:ui="{ trailing: 'pe-1' }"
@keydown="onInputKey"
>
<template #trailing>
<UButton
v-if="localQuery"
icon="i-lucide-x"
variant="link"
aria-label="Limpiar"
@click="clearLocalQuery"
/>
</template>
</UInput>
<UBadge v-if="localQuery" class="bg-gray-400 text-white">
<span
class="text-xs tabular-nums whitespace-nowrap min-w-[3.5rem] text-center text-white"
:class="totalMatches ? 'text-toned font-medium' : 'text-dimmed'"
>
<template v-if="localQuery">
{{ totalMatches ? `${currentIdx + 1} / ${totalMatches}` : '0 / 0' }}
</template>
</span>
</UBadge>
<UTooltip text="Anterior (Shift+Enter)">
<UButton
icon="i-lucide-chevron-up"
:disabled="!totalMatches"
aria-label="Coincidencia anterior"
class="bg-gray-400"
color="neutral"
@click="prevMatch"
/>
</UTooltip>
<UTooltip text="Siguiente (Enter)">
<UButton
icon="i-lucide-chevron-down"
:disabled="!totalMatches"
aria-label="Coincidencia siguiente"
class="bg-gray-400"
color="neutral"
@click="nextMatch"
/>
</UTooltip>
</UFieldGroup>
</div>
<!-- <ul v-if="fileLinks.length" class="flex flex-wrap gap-3 text-xs text-muted">
<li
v-for="(f, idx) in fileLinks"
:key="idx"
@ -422,77 +482,15 @@ function onInputKey(e: KeyboardEvent) {
<span>{{ f.label }}</span>
</ULink>
</li>
</ul>
</div>
<div ref="scrollContainer" class="flex-1 overflow-y-auto relative">
<!-- Find-in-document toolbar: sticky at the top of the scroll area so
the user can always reach it while reading. -->
<div
v-if="activity?.body"
class="sticky top-0 z-10 bg-default/95 backdrop-blur-sm border-b border-default px-4 sm:px-6 py-2 flex items-center gap-2"
>
<div class="relative flex-1 min-w-0 max-w-md">
<UInput
v-model="localQuery"
icon="i-lucide-search"
placeholder="Buscar frase exacta en este documento..."
size="sm"
class="w-full"
:ui="{ trailing: 'pe-1' }"
@keydown="onInputKey"
>
<template #trailing>
<UButton
v-if="localQuery"
icon="i-lucide-x"
color="neutral"
variant="link"
size="xs"
aria-label="Limpiar"
@click="clearLocalQuery"
/>
</template>
</UInput>
</div>
<span
class="text-xs tabular-nums whitespace-nowrap min-w-[3.5rem] text-center"
:class="totalMatches ? 'text-toned font-medium' : 'text-dimmed'"
>
<template v-if="localQuery">
{{ totalMatches ? `${currentIdx + 1} / ${totalMatches}` : '0 / 0' }}
</template>
</span>
<UTooltip text="Anterior (Shift+Enter)">
<UButton
icon="i-lucide-chevron-up"
color="neutral"
variant="ghost"
size="sm"
:disabled="!totalMatches"
aria-label="Coincidencia anterior"
@click="prevMatch"
/>
</UTooltip>
<UTooltip text="Siguiente (Enter)">
<UButton
icon="i-lucide-chevron-down"
color="neutral"
variant="ghost"
size="sm"
:disabled="!totalMatches"
aria-label="Coincidencia siguiente"
@click="nextMatch"
/>
</UTooltip>
</ul> -->
</div>
<div ref="scrollContainer" class="flex-1 overflow-y-auto relative bg-gray-100">
<div class="p-1 sm:p-4 bg-white rounded-lg shadow-md max-w-4xl m-4 sm:mx-auto sm:my-6">
<article
v-if="activity?.body"
ref="bodyContainer"
class="prose prose-sm max-w-none dark:prose-invert p-4 sm:p-6 pb-24"
class="prose prose-sm max-w-none dark:prose-invert p-4 sm:p-6 pb-24 prose-p:mb-2 text-base/7 prose:text-base/7"
/>
<p v-else class="p-4 sm:p-6 text-sm text-muted">
No hay contenido disponible para esta coincidencia.
@ -501,34 +499,6 @@ function onInputKey(e: KeyboardEvent) {
<ULink :to="matchUrl" target="_blank" class="text-primary">verla en el sitio</ULink>.
</span>
</p>
<!--
Botón flotante (FAB) para guardar/quitar de favoritos.
- Vive dentro del contenedor de scroll para anclarse al panel y no
interferir con otros layouts (e.g. layouts multi-panel).
- Truco: el wrapper es `sticky bottom-0 h-0` con `pointer-events-none`,
de forma que ocupa cero altura en el flujo del documento pero
mantiene al hijo absoluto pegado al borde inferior visible mientras
el usuario hace scroll. Así el botón siempre está a un toque,
igual en escritorio y en móvil.
- El padding-bottom extra del article (`pb-24`) evita que el FAB
tape el final del texto cuando se llega al final del documento.
-->
<div
v-if="collection"
class="sticky bottom-0 inset-x-0 h-0 z-20 pointer-events-none"
>
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
<UButton
:icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
:color="isFav ? 'primary' : 'neutral'"
variant="solid"
size="xl"
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"
class="absolute bottom-4 end-4 sm:bottom-6 sm:end-6 rounded-full shadow-lg shadow-black/15 dark:shadow-black/40 ring-1 ring-default pointer-events-auto transition-transform hover:scale-105 active:scale-95"
@click="onToggleFavorite"
/>
</UTooltip>
</div>
</div>
</UDashboardPanel>

View File

@ -618,7 +618,9 @@ const rows = computed<RowVm[]>(() => {
body: bodyResult?.html ?? null,
bodyApproximate: bodyResult?.approximate ?? false,
extraSnippets,
extraFields
extraFields,
matchesPosition: activity._matchesPosition,
bodyMatchCount: activity._matchesPosition?.body?.length
}
})
@ -674,7 +676,7 @@ useIntersectionObserver(
:ref="(el) => { activitiesRefs[row.activity._id] = el as Element | null }"
>
<div
class="p-4 sm:px-6 text-sm cursor-pointer border-l-2 transition-colors"
class="bg-white p-6 sm:px-6 text-sm cursor-pointer border-l-2 transition-colors"
:class="[
row.activity.unread ? 'text-highlighted' : 'text-toned',
selectedActivity && selectedActivity._id === row.activity._id
@ -683,10 +685,14 @@ useIntersectionObserver(
]"
@click="selectedActivity = row.activity"
>
<div class="">
<div class="flex items-start justify-between gap-2 mb-1 ">
<div class="text-sm font-semibold line-clamp-2" v-html="row.title" />
<div class="flex items-center gap-1 shrink-0">
<UChip v-if="row.activity.unread" />
<UTooltip text="Cantidad aproximada de resultados">
<UBadge v-if="row.bodyMatchCount>0" :label="row.bodyMatchCount" size="sm" variant="soft" />
</UTooltip>
<UTooltip
v-if="collection"
:text="favorites.isFavorite(collection, row.activity._id) ? 'Quitar de mi lista' : 'Guardar en mi lista'"
@ -703,22 +709,21 @@ useIntersectionObserver(
</div>
</div>
<p class="flex items-center gap-2 text-xs text-muted mb-1">
<span v-if="hasDate(row.activity)">{{ safeDate(row.activity) }}</span>
<USeparator v-if="hasDate(row.activity)" orientation="vertical" class="h-3" />
<span class="truncate">{{ formatLocation(row.activity) }}</span>
<p class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-sm sm:text-xs mb-1 justify-between ">
<span v-if="hasDate(row.activity)" class="flex items-center gap-1"><UIcon name="ph:calendar" class="size-4 text-green-600" />{{ safeDate(row.activity) }}</span>
<span class="truncate flex items-center"><UIcon name="ph:map-pin" class="size-4 text-green-600" />{{ formatLocation(row.activity) }}</span>
</p>
<div class="bg-gray-100 p-1 rounded-xl !text-sm" v-if="row.body">
<div class="mt-2 !text-sm" v-if="row.body">
<div
v-if="row.bodyApproximate"
class="text-[11px] text-warning flex items-center gap-1 mb-0.5"
class="text-[11px] text-warning flex items-center gap-1 mb-0.5 bg-white p-2 rounded-xl"
>
<UIcon name="i-lucide-info" class="size-3" />
<span>Coincidencia en el documento (abre para ubicarla con la búsqueda interna)</span>
</div>
<div
class="snippet-html text-dimmed line-clamp-3 text-xs"
class="mt-1 snippet-html text-dimmed text-sm sm:text-xs bg-white sm:p-0 rounded-lg"
v-html="row.body"
/>
</div>
@ -750,6 +755,7 @@ useIntersectionObserver(
</div>
</div>
</div>
</div>
<div ref="sentinel" class="h-12 -mt-12 pointer-events-none" aria-hidden="true" />

View File

@ -441,8 +441,9 @@ const mobileActions = computed<DropdownMenuItem[][]>(() => [[
:label="labelFor(it.collection)"
size="xs"
variant="subtle"
color="neutral"
class="mb-1 capitalize"
class="mb-1 uppercase font-semibold"
:color="it.collection=='activities'?'success':'info'"
/>
<div class="text-sm font-semibold line-clamp-2">
{{ it.hit?.title || 'Sin título' }}