Files
immich/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
T
Min Idzelis c9e251c78c feat(web): highlight active person thumbnail in detail panel and edit faces panel (#27401)
- Dim non-hovered person thumbnails to 40% opacity when any face is active
- Add ring highlight on the active person's thumbnail
- Add focus-visible outline styling for keyboard navigation
- Apply same treatment to both detail panel people section and edit faces side panel

Change-Id: I4ac10fe4568b95f3e0e8d9104133180f6a6a6964

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-04-01 10:49:09 -05:00

96 lines
2.5 KiB
Svelte

<script lang="ts">
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
import Image from '$lib/components/Image.svelte';
import { Icon } from '@immich/ui';
import { mdiEyeOffOutline } from '@mdi/js';
import type { ClassValue } from 'svelte/elements';
interface Props {
url: string;
altText: string | undefined;
title?: string | null;
heightStyle?: string | undefined;
widthStyle: string;
curve?: boolean;
shadow?: boolean;
circle?: boolean;
hidden?: boolean;
border?: boolean;
highlighted?: boolean;
hiddenIconClass?: string;
class?: ClassValue;
brokenAssetClass?: ClassValue;
preload?: boolean;
onComplete?: ((errored: boolean) => void) | undefined;
}
let {
url,
altText,
title = null,
heightStyle = undefined,
widthStyle,
curve = false,
shadow = false,
circle = false,
hidden = false,
border = false,
highlighted = false,
hiddenIconClass = 'text-white',
onComplete = undefined,
class: imageClass = '',
brokenAssetClass = '',
preload = true,
}: Props = $props();
let loaded = $state(false);
let errored = $state(false);
const setLoaded = () => {
loaded = true;
onComplete?.(false);
};
const setErrored = () => {
errored = true;
onComplete?.(true);
};
let sharedClasses = $derived([
curve && 'rounded-xl',
circle && 'rounded-full',
shadow && 'shadow-lg',
(circle || !heightStyle) && 'aspect-square',
border && 'border-3 border-immich-dark-primary/80 hover:border-immich-primary',
'transition-shadow duration-150',
highlighted && 'ring-4 ring-immich-primary dark:ring-immich-dark-primary',
]);
let style = $derived(
`width: ${widthStyle}; height: ${heightStyle ?? ''}; filter: ${hidden ? 'grayscale(50%)' : 'none'}; opacity: ${hidden ? '0.5' : '1'};`,
);
</script>
{#if errored}
<BrokenAsset class={[sharedClasses, brokenAssetClass]} width={widthStyle} height={heightStyle} />
{:else}
<Image
src={url}
onLoad={setLoaded}
onError={setErrored}
class={['object-cover bg-gray-300 dark:bg-gray-700', sharedClasses, imageClass]}
{style}
alt={loaded || errored ? altText : ''}
draggable={false}
title={title ?? undefined}
loading={preload ? 'eager' : 'lazy'}
/>
{/if}
{#if hidden}
<div class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
<!-- TODO fix `title` type -->
<Icon title={title ?? undefined} icon={mdiEyeOffOutline} size="2em" class={hiddenIconClass} />
</div>
{/if}