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. */ /* Client-side highlights inside the detail body. */
mark.search-match { mark.search-match {
background-color: #fde68a; background-color: #fdff32;
color: #78350f; color: #000;
padding: 0 2px; padding: 2px;
border-radius: 2px; border-radius: 2px;
font-weight: 600; font-weight: 600;
box-shadow: 0 0 0 1px #f59e0b inset; box-shadow: 0 0 0 1px #e9ff32 inset;
} }
.dark mark.search-match { .dark mark.search-match {
background-color: #78350f; background-color: #fdff32;
color: #fde68a; color: #000;
box-shadow: 0 0 0 1px #f59e0b inset; box-shadow: 0 0 0 1px #e9ff32 inset;
} }
/* The currently-focused match (the one the user navigated to). */ /* The currently-focused match (the one the user navigated to). */
mark.search-match.is-current { mark.search-match.is-current {
background-color: #f97316; background-color: #8cff32;
color: #ffffff; padding: 2px;
box-shadow: 0 0 0 2px #c2410c inset; color: #000;
box-shadow: 0 0 0 2px #8cff32 inset;
animation: search-match-pulse 0.9s ease-out 1; animation: search-match-pulse 0.9s ease-out 1;
} }
@ -70,7 +71,7 @@ mark.search-match.is-current {
} }
@keyframes search-match-pulse { @keyframes search-match-pulse {
0% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.75), 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 #c2410c 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 #c2410c 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 <!-- 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 en la esquina inferior derecha del panel para que siga visible
mientras se lee. Ver más abajo en el scroll container. --> mientras se lee. Ver más abajo en el scroll container. -->
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
<UButton <UButton
v-if="matchUrl" :icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
:to="matchUrl" :color="isFav ? 'primary' : 'neutral'"
target="_blank" variant="ghost"
icon="i-lucide-external-link" :aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"
label="Ver en sitio" @click="onToggleFavorite"
color="primary"
variant="solid"
size="sm"
/> />
</UTooltip>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<div class="flex flex-col sm:flex-row justify-between gap-2 p-4 sm:px-6 border-b border-default"> <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"> <div class="min-w-0 flex gap-1 sm:gap-4 sm:items-center flex-col sm:flex-row">
<p class="font-semibold text-sm text-highlighted"> <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) }} {{ safeDate(activity) }}
</p> </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) }} {{ formatLocation(activity) }}
</p> </p>
</div> </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 <li
v-for="(f, idx) in fileLinks" v-for="(f, idx) in fileLinks"
:key="idx" :key="idx"
@ -422,77 +482,15 @@ function onInputKey(e: KeyboardEvent) {
<span>{{ f.label }}</span> <span>{{ f.label }}</span>
</ULink> </ULink>
</li> </li>
</ul> </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>
</div> </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 <article
v-if="activity?.body" v-if="activity?.body"
ref="bodyContainer" 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"> <p v-else class="p-4 sm:p-6 text-sm text-muted">
No hay contenido disponible para esta coincidencia. 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>. <ULink :to="matchUrl" target="_blank" class="text-primary">verla en el sitio</ULink>.
</span> </span>
</p> </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>
</div> </div>
</UDashboardPanel> </UDashboardPanel>

View File

@ -618,7 +618,9 @@ const rows = computed<RowVm[]>(() => {
body: bodyResult?.html ?? null, body: bodyResult?.html ?? null,
bodyApproximate: bodyResult?.approximate ?? false, bodyApproximate: bodyResult?.approximate ?? false,
extraSnippets, 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 }" :ref="(el) => { activitiesRefs[row.activity._id] = el as Element | null }"
> >
<div <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="[ :class="[
row.activity.unread ? 'text-highlighted' : 'text-toned', row.activity.unread ? 'text-highlighted' : 'text-toned',
selectedActivity && selectedActivity._id === row.activity._id selectedActivity && selectedActivity._id === row.activity._id
@ -683,10 +685,14 @@ useIntersectionObserver(
]" ]"
@click="selectedActivity = row.activity" @click="selectedActivity = row.activity"
> >
<div class="flex items-start justify-between gap-2 mb-1"> <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="text-sm font-semibold line-clamp-2" v-html="row.title" />
<div class="flex items-center gap-1 shrink-0"> <div class="flex items-center gap-1 shrink-0">
<UChip v-if="row.activity.unread" /> <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 <UTooltip
v-if="collection" v-if="collection"
:text="favorites.isFavorite(collection, row.activity._id) ? 'Quitar de mi lista' : 'Guardar en mi lista'" :text="favorites.isFavorite(collection, row.activity._id) ? 'Quitar de mi lista' : 'Guardar en mi lista'"
@ -703,22 +709,21 @@ useIntersectionObserver(
</div> </div>
</div> </div>
<p class="flex items-center gap-2 text-xs text-muted mb-1"> <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)">{{ safeDate(row.activity) }}</span> <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>
<USeparator v-if="hasDate(row.activity)" orientation="vertical" class="h-3" /> <span class="truncate flex items-center"><UIcon name="ph:map-pin" class="size-4 text-green-600" />{{ formatLocation(row.activity) }}</span>
<span class="truncate">{{ formatLocation(row.activity) }}</span>
</p> </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 <div
v-if="row.bodyApproximate" 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" /> <UIcon name="i-lucide-info" class="size-3" />
<span>Coincidencia en el documento (abre para ubicarla con la búsqueda interna)</span> <span>Coincidencia en el documento (abre para ubicarla con la búsqueda interna)</span>
</div> </div>
<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" v-html="row.body"
/> />
</div> </div>
@ -740,7 +745,7 @@ useIntersectionObserver(
<div <div
v-if="!row.extraSnippets.length && row.extraFields.length" v-if="!row.extraSnippets.length && row.extraFields.length"
class="mt-1.5 text-xs text-muted flex items-center gap-1.5" class="mt-1.5 text-xs text-muted flex items-center gap-1.5 "
> >
<UIcon name="i-lucide-info" class="size-3.5" /> <UIcon name="i-lucide-info" class="size-3.5" />
<span> <span>
@ -750,6 +755,7 @@ useIntersectionObserver(
</div> </div>
</div> </div>
</div> </div>
</div>
<div ref="sentinel" class="h-12 -mt-12 pointer-events-none" aria-hidden="true" /> <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)" :label="labelFor(it.collection)"
size="xs" size="xs"
variant="subtle" 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"> <div class="text-sm font-semibold line-clamp-2">
{{ it.hit?.title || 'Sin título' }} {{ it.hit?.title || 'Sin título' }}