feat: article editor core

This commit is contained in:
fsylvain committed 2026-09-27 02:11:38 +02:00
1 parent d493a181e1
commit a4734b2c91
55 files changed
+4186 -496

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
const props = defineProps<{
state: 'clean' | 'dirty' | 'saving' | 'saved' | 'error' | 'offline'
savedAt?: Date | null
}>()
defineEmits<{ retry: [] }>()
const HOUR_MS = 60 * 60 * 1000
const TICK_MS = 15000
const now = ref(Date.now())
let ticker: ReturnType<typeof setInterval> | null = null
onMounted(() => {
ticker = setInterval(() => {
now.value = Date.now()
}, TICK_MS)
})
onUnmounted(() => {
if (ticker !== null) {
clearInterval(ticker)
}
})
function formatClock(date: Date) {
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
const savedText = computed(() => {
if (!props.savedAt) {
return 'Saved'
}
const diffMs = now.value - props.savedAt.getTime()
if (diffMs < HOUR_MS) {
return `Saved at ${formatClock(props.savedAt)}`
}
const minutes = Math.floor(diffMs / 60000)
return minutes === 1 ? 'Saved 1 min ago' : `Saved ${minutes} min ago`
})
</script>
<template>
<span
aria-live="polite"
class="inline-flex shrink-0 items-center gap-1.5 text-sm whitespace-nowrap"
:class="{
'text-warning': state === 'dirty',
'text-error': state === 'error',
'text-muted': state !== 'dirty' && state !== 'error',
}"
>
<template v-if="state === 'clean' || state === 'saved'">
<UIcon name="i-lucide-check" class="size-3.5" />
<span>{{ savedText }}</span>
</template>
<template v-else-if="state === 'dirty'">
<UIcon name="i-lucide-circle-dot" class="size-3.5" />
<span>Unsaved changes</span>
</template>
<template v-else-if="state === 'saving'">
<UIcon
name="i-lucide-loader-circle"
class="size-3.5 animate-spin"
/>
<span>Saving…</span>
</template>
<template v-else-if="state === 'error'">
<UIcon name="i-lucide-circle-alert" class="size-3.5" />
<span>Save failed</span>
<UButton
size="xs"
color="neutral"
variant="outline"
icon="i-lucide-rotate-cw"
class="h-6 gap-1 px-2 text-xs text-highlighted"
@click="$emit('retry')"
>
Retry
</UButton>
</template>
<template v-else-if="state === 'offline'">
<UIcon name="i-lucide-wifi-off" class="size-3.5" />
<span>Waiting for connection</span>
</template>
</span>
</template>