75 lines
1.8 KiB
Vue
75 lines
1.8 KiB
Vue
<script setup lang="ts">
|
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
|
|
|
interface ResultPanelAction {
|
|
label: string
|
|
color?: 'primary' | 'neutral'
|
|
variant?: 'solid' | 'soft' | 'outline' | 'ghost'
|
|
to?: string
|
|
disabled?: boolean
|
|
onClick?: () => void
|
|
}
|
|
|
|
const props = defineProps<{
|
|
tone: 'success' | 'error' | 'info'
|
|
icon: string
|
|
title: string
|
|
description: string
|
|
actions: ResultPanelAction[]
|
|
}>()
|
|
|
|
const role = computed(() => (props.tone === 'error' ? 'alert' : 'status'))
|
|
|
|
const toneTileClass: Record<'success' | 'error' | 'info', string> = {
|
|
success: 'bg-(--rc-success-soft) text-success',
|
|
error: 'bg-(--rc-error-soft) text-error',
|
|
info: 'bg-(--rc-info-soft) text-info',
|
|
}
|
|
|
|
const tileClass = computed(() => toneTileClass[props.tone])
|
|
|
|
const titleEl = ref<HTMLHeadingElement | null>(null)
|
|
|
|
onMounted(async () => {
|
|
await nextTick()
|
|
titleEl.value?.focus()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div :role="role" class="flex flex-col gap-4">
|
|
<div :class="['flex size-11 items-center justify-center', tileClass]">
|
|
<UIcon :name="icon" class="size-5.5" />
|
|
</div>
|
|
<div class="flex flex-col gap-1.5">
|
|
<h1
|
|
ref="titleEl"
|
|
tabindex="-1"
|
|
class="text-xl/7 font-semibold text-highlighted outline-none"
|
|
>
|
|
{{ title }}
|
|
</h1>
|
|
<p class="text-base text-pretty text-toned">{{ description }}</p>
|
|
<slot />
|
|
</div>
|
|
<div v-if="actions.length" class="flex w-full flex-col gap-2">
|
|
<UButton
|
|
v-for="action in actions"
|
|
:key="action.label"
|
|
block
|
|
:color="action.color ?? 'primary'"
|
|
:variant="action.variant ?? 'solid'"
|
|
:class="[
|
|
'h-11 justify-center text-base sm:h-10',
|
|
action.variant === 'outline' ? 'text-highlighted' : '',
|
|
]"
|
|
:to="action.to"
|
|
:disabled="action.disabled"
|
|
@click="action.onClick"
|
|
>
|
|
{{ action.label }}
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</template>
|