Files
ComfyUI_frontend/src/components/templates/thumbnails/LogoOverlay.vue
Christian Byrne 1bbbcfedf0 feat: add provider logo overlays to workflow template thumbnails (#8365)
## Summary

Add support for overlaying provider logos on workflow template
thumbnails at runtime.

## Changes

- **What**: 
  - Add `LogoInfo` interface and `logos` field to `TemplateInfo` type
  - Create `LogoOverlay.vue` component for rendering positioned logos
  - Fetch logo index from `templates/index_logo.json` in store
  - Add `getLogoUrl` helper to `useTemplateWorkflows` composable
  - Integrate `LogoOverlay` into `WorkflowTemplateSelectorDialog`

## Review Focus

- Logo positioning uses Tailwind classes (e.g. `absolute bottom-2
right-2`)
- Supports multiple logos per template with configurable size/opacity
- Gracefully handles missing logos (returns empty string, renders
nothing)
- Templates must explicitly declare logos - no magic inference from
models

## Dependencies

Requires separate PR in workflow_templates repo to:
1. Update `index.schema.json` with logos definition
2. Add `logos` field to templates in `index.json`

## Screenshots (if applicable)

<img width="869" height="719" alt="image"
src="https://github.com/user-attachments/assets/65ed1ee4-fbb4-42c9-95d4-7e37813b3655"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8365-feat-add-provider-logo-overlays-to-workflow-template-thumbnails-2f66d73d365081309236c6b991cb6f7b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-31 19:18:13 -08:00

118 lines
3.1 KiB
Vue

<template>
<div
v-for="logo in validLogos"
:key="logo.key"
:class="
cn('pointer-events-none absolute z-10', logo.position ?? defaultPosition)
"
>
<div
v-show="!hasAllFailed(logo.providers)"
data-testid="logo-pill"
class="flex items-center gap-1.5 rounded-full bg-black/20 py-1 pr-2"
:style="{ opacity: logo.opacity ?? 0.85 }"
>
<div class="ml-0.5 flex items-center">
<img
v-for="(provider, providerIndex) in logo.providers"
:key="provider"
data-testid="logo-img"
:src="logo.urls[providerIndex]"
:alt="provider"
class="h-6 w-6 rounded-full border-2 border-white object-cover"
:class="{ relative: providerIndex > 0 }"
:style="
providerIndex > 0 ? { marginLeft: `${logo.gap ?? -6}px` } : {}
"
draggable="false"
@error="onImageError(provider)"
/>
</div>
<span class="text-sm font-medium text-white">
{{ logo.label }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { LogoInfo } from '@/platform/workflow/templates/types/template'
import { cn } from '@/utils/tailwindUtil'
const { t, locale } = useI18n()
function formatProviderList(providers: string[]): string {
const localeValue = String(locale.value)
try {
return new Intl.ListFormat(localeValue, {
style: 'long',
type: 'conjunction'
}).format(providers)
} catch {
return providers.join(t('templates.logoProviderSeparator'))
}
}
const {
logos,
getLogoUrl,
defaultPosition = 'top-2 left-2'
} = defineProps<{
logos: LogoInfo[]
getLogoUrl: (provider: string) => string
defaultPosition?: string
}>()
const failedLogos = ref(new Set<string>())
function onImageError(provider: string) {
failedLogos.value = new Set([...failedLogos.value, provider])
}
function hasAllFailed(providers: string[]): boolean {
return providers.every((p) => failedLogos.value.has(p))
}
interface ValidatedLogo {
key: string
providers: string[]
urls: string[]
label: string
position: string | undefined
opacity: number | undefined
gap: number | undefined
}
const validLogos = computed<ValidatedLogo[]>(() => {
const result: ValidatedLogo[] = []
logos.forEach((logo, index) => {
const providers = Array.isArray(logo.provider)
? logo.provider
: [logo.provider]
const urls = providers.map((p) => getLogoUrl(p))
const validProviders = providers.filter((_, i) => urls[i])
const validUrls = urls.filter((url) => url)
if (validProviders.length === 0) return
const providerKey = validProviders.join('-')
const layoutKey = `${logo.position ?? ''}-${logo.opacity ?? ''}-${logo.gap ?? ''}`
result.push({
key: providerKey ? `${providerKey}-${layoutKey}` : `logo-${index}`,
providers: validProviders,
urls: validUrls,
label: logo.label ?? formatProviderList(validProviders),
position: logo.position,
opacity: logo.opacity,
gap: logo.gap
})
})
return result
})
</script>