<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { api } from '../api'
import ProjectColorDot from './ProjectColorDot.vue'
// Метка проекта (0.65): цвет-кружок и фавикон сайта одним блоком — в шапке
// страницы проекта и на карточке списка. Крупнее, чем одиночный кружок
// (ProjectColorDot в бейджах остался прежним): по метке проект узнаётся
// раньше, чем прочитано имя.
//
// Иконку тянет бэкенд-прокси (0.63), поэтому 404 — штатный ответ: у проекта
// нет site_url, у сайта нет иконки (или она SVG). Тогда остаётся один кружок,
// без битой картинки.
const props = defineProps<{
color: string | null
projectId: number
siteUrl: string | null
// Ссылку отдаёт только шапка страницы проекта: карточка списка целиком —
// RouterLink, вложенная ссылка невалидна
href?: string | null
// Подсказка и aria-label ссылки-фавикона
label?: string
}>()
const broken = ref(false)
// Смена проекта/сайта — снова пробуем иконку (иначе флаг «сломан» залипнет)
watch(
() => [props.projectId, props.siteUrl],
() => {
broken.value = false
},
)
const src = computed(() => api.projectFaviconUrl(props.projectId))
const showIcon = computed(() => Boolean(props.siteUrl) && !broken.value)
// Нет ни цвета, ни иконки — метки нет вовсе: пустой спан в ряду с gap
// (карточка, шапка проекта) давал фантомный отступ перед именем
const visible = computed(() => Boolean(props.color) || showIcon.value)
</script>
<template>
<span v-if="visible" class="proj-mark">
<ProjectColorDot :color="color" />
<a
v-if="showIcon && href"
class="proj-fav-link"
:href="href"
target="_blank"
rel="noopener noreferrer"
:title="label"
:aria-label="label"
>
<img class="proj-fav" :src="src" alt="" @error="broken = true" />
</a>
<img v-else-if="showIcon" class="proj-fav" :src="src" alt="" @error="broken = true" />
</span>
</template>
<style scoped>
/* Размеры в em от базового кегля метки: в карточке списка база — тайтл 16px
(кружок 16px, фавикон 18px), в шапке страницы базу задаёт сама шапка. */
.proj-mark {
display: inline-flex;
align-items: center;
gap: 0.3em;
flex: none;
}
/* крупнее одиночного кружка, и без своего отступа справа (0.73): зазор здесь
задаёт flex-gap метки, а margin самого кружка дал бы двойной интервал —
селектор потомка бьёт правило ProjectColorDot */
.proj-mark .proj-dot {
width: 1em;
height: 1em;
margin-right: 0;
}
.proj-fav {
display: block;
width: 1.15em;
height: 1.15em;
border-radius: 0.18em;
object-fit: contain;
}
.proj-fav-link {
display: inline-flex;
flex: none;
border-radius: 0.18em;
}
.proj-fav-link:hover .proj-fav,
.proj-fav-link:focus-visible .proj-fav {
box-shadow: 0 0 0 1px var(--accent, #7aa2f7);
}
.proj-fav-link:focus-visible {
outline: var(--gn-focus-outline-width, 2px) solid var(--gn-focus-outline-color, #e0af68);
outline-offset: var(--gn-focus-outline-offset, 3px);
}
</style>