Newer
Older
gnexus-tasks / frontend / src / components / ProjectMark.vue
<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;
}
/* крупнее одиночного кружка: селектор потомка бьёт правило ProjectColorDot */
.proj-mark .proj-dot {
  width: 1em;
  height: 1em;
}
.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>