feat: article editor core
This commit is contained in:
1 parent
d493a181e1
commit
a4734b2c91
55 files changed
+4186
-496
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user