feat: activity screen in the SPA, route /activity

This commit is contained in:
fsylvain committed 2026-10-07 01:48:21 +02:00
1 parent e37a46a027
commit 35db442cd3
47 files changed
+7582 -11

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
<script setup lang="ts">
import { t } from '@/i18n'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { bodyDeltaLabel, changeValue, fieldLabel, type ActivityChange } from '@/lib/activity'
const props = defineProps<{
changes: ActivityChange[]
isDesktop: boolean
}>()
const expanded = ref<Record<number, boolean>>({})
const overflowing = ref<Record<number, boolean>>({})
const valueBoxes = new Map<number, HTMLElement[]>()
function register(index: number, element: unknown) {
if (!(element instanceof HTMLElement)) return
const boxes = valueBoxes.get(index) ?? []
if (!boxes.includes(element)) valueBoxes.set(index, [...boxes, element])
}
function measure() {
const next: Record<number, boolean> = { ...overflowing.value }
for (const [index, boxes] of valueBoxes) {
if (expanded.value[index]) continue
next[index] = boxes.some((box) => box.scrollHeight > box.clientHeight)
}
overflowing.value = next
}
function toggle(index: number) {
expanded.value = { ...expanded.value, [index]: !expanded.value[index] }
}
watch(
() => props.changes,
() => {
expanded.value = {}
overflowing.value = {}
valueBoxes.clear()
nextTick(measure)
},
)
onMounted(() => {
nextTick(measure)
window.addEventListener('resize', measure)
})
onBeforeUnmount(() => window.removeEventListener('resize', measure))
</script>
<template>
<div data-testid="activity-diff" class="flex flex-col border border-default">
<div
v-for="(change, index) in changes"
:key="index"
data-testid="activity-change"
class="flex flex-col gap-2 border-t border-muted p-3 first:border-t-0"
>
<span class="font-medium text-highlighted">{{ fieldLabel(change.field) }}</span>
<span
v-if="change.kind === 'body'"
data-testid="activity-body-change"
class="flex items-center gap-1.5 text-toned"
>
<UIcon name="i-lucide-file-pen-line" class="size-3.5 shrink-0" />
{{ bodyDeltaLabel(change.delta) }}
</span>
<template v-else>
<div
class="grid items-start gap-1.5 md:grid-cols-[minmax(0,1fr)_16px_minmax(0,1fr)] md:gap-2"
>
<div class="flex min-w-0 flex-col gap-0.5">
<span class="text-2xs text-muted">{{ t('Before') }}</span>
<span
:ref="(element) => register(index, element)"
data-testid="activity-before"
class="border border-default bg-elevated px-2.5 py-2 wrap-anywhere text-toned"
:class="expanded[index] ? '' : 'line-clamp-2'"
>{{ changeValue(change.field, change.before) }}</span
>
</div>
<UIcon
:name="isDesktop ? 'i-lucide-arrow-right' : 'i-lucide-arrow-down'"
class="size-3.5 shrink-0 justify-self-center text-muted md:mt-7.5"
/>
<div class="flex min-w-0 flex-col gap-0.5">
<span class="text-2xs text-muted">{{ t('After') }}</span>
<span
:ref="(element) => register(index, element)"
data-testid="activity-after"
class="border border-accented bg-default px-2.5 py-2 wrap-anywhere text-highlighted"
:class="expanded[index] ? '' : 'line-clamp-2'"
>{{ changeValue(change.field, change.after) }}</span
>
</div>
</div>
<UButton
v-if="overflowing[index]"
color="neutral"
variant="link"
class="min-h-8 self-start px-0 text-xs font-medium text-highlighted underline underline-offset-3 md:min-h-4"
@click="toggle(index)"
>
{{ expanded[index] ? t('Show less') : t('Show more') }}
</UButton>
</template>
</div>
</div>
</template>