# Thumbnail

## Usage

`Thumbnail` is a 64px preview. With no `src` it shows a placeholder. `isLoading` without `src` is a skeleton. `isLoading` with `src` covers the image with a spinner. Listen for `click` to make the preview a button, and for `remove` to show the dismiss control.

`showRemoveOn` is `hover` by default. The control also appears on keyboard focus, and it stays visible for a coarse pointer. `always` keeps it visible. `isDisabled` fades the thumbnail and ignores both events.

`label` is the accessible name. It is not drawn on the thumbnail. It is used by the tooltip and by the remove button.

```vue
<script setup lang="ts">
import { Thumbnail } from '@astryx-vue/core/Thumbnail'
</script>

<template>
  <Thumbnail src="/photo.jpg" alt="Snowy mountain peaks" label="snowy-peaks.jpg" />
</template>
```

### Anatomy

- **Image**: The preview image, displayed as a square with cover fit.
- **Placeholder**: An image silhouette icon shown when no src is provided.
- **Remove button**: An overlaid close button in the top-right corner. Visibility follows showRemoveOn, which defaults to hover. Activating it emits remove. Sits on a fixed --color-overlay scrim with an --color-on-dark icon to stay visible on any image.
- **Upload overlay**: A semi-transparent overlay with a spinner, shown when isLoading is true and a src preview is available.
- **Skeleton**: A shimmer animation shown when isLoading is true and no src is set.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Give `alt` what the image shows, and `label` the file name a person would use to remove it. |
| Do | Use `showRemoveOn="always"` when the pointer may be coarse, or when removal is the main action. |
| Do | Keep a placeholder until a file has a preview, then pass `src`. |
| Don't | Put the file name in the visible square. The tooltip already carries `label`. |
| Don't | Attach `click` and expect it to fire while `isDisabled` or `isLoading` is set. |

## Examples

### Thumbnail — Disabled

Thumbnails in the disabled state with reduced opacity. The remove button and click handler are suppressed when disabled.

```vue
<script setup lang="ts">
import { Stack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { Thumbnail } from '@astryx-vue/core/Thumbnail'
import { GOLDEN_SUNSET } from './images'

function noop() {}
</script>

<template>
  <Stack direction="vertical" :gap="4">
    <Stack direction="vertical" :gap="1">
      <Text type="supporting" color="secondary">
        {{ 'Enabled' }}
      </Text>
      <Stack align="center" direction="horizontal" :gap="3">
        <Thumbnail
          :src="GOLDEN_SUNSET"
          alt="Golden sunset over mountains"
          label="golden-sunset.jpg"
          show-remove-on="always"
          @remove="noop"
        />
        <Thumbnail
          label="document.pdf"
          show-remove-on="always"
          @remove="noop"
        />
      </Stack>
    </Stack>
    <Stack direction="vertical" :gap="1">
      <Text type="supporting" color="secondary">
        {{ 'Disabled' }}
      </Text>
      <Stack align="center" direction="horizontal" :gap="3">
        <Thumbnail
          :src="GOLDEN_SUNSET"
          alt="Golden sunset over mountains"
          label="golden-sunset.jpg"
          is-disabled
          @remove="noop"
        />
        <Thumbnail label="document.pdf" is-disabled @remove="noop" />
      </Stack>
    </Stack>
  </Stack>
</template>
```

### Thumbnail — Gallery

A row of clickable thumbnails with labels that open a detail view. Use for image attachment lists where users need to preview and manage uploads.

```vue
<script setup lang="ts">
import type { ThumbnailScene } from './images'
import { Stack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { Thumbnail } from '@astryx-vue/core/Thumbnail'
import { ref } from 'vue'
import { thumbnailScenes } from './images'

const items = ref<ThumbnailScene[]>([...thumbnailScenes])
const selected = ref<string | null>(null)

function remove(id: number) {
  items.value = items.value.filter(item => item.id !== id)
}
</script>

<template>
  <Stack direction="vertical" :gap="3">
    <Text type="supporting" color="secondary">
      {{ 'Click to preview, dismiss to remove' }}
    </Text>
    <Stack align="center" direction="horizontal" :gap="2">
      <Thumbnail
        v-for="item in items"
        :key="item.id"
        :src="item.src"
        :alt="item.alt"
        :label="item.label"
        show-remove-on="always"
        @click="selected = item.label"
        @remove="remove(item.id)"
      />
    </Stack>
    <Text v-if="selected" type="supporting" color="secondary">
      Selected: {{ selected }}
    </Text>
  </Stack>
</template>
```

### Thumbnail — Removable

Thumbnails with a remove button overlay. The close button stays visible on both dark and light images.

```vue
<script setup lang="ts">
import type { ThumbnailScene } from './images'
import { Stack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { Thumbnail } from '@astryx-vue/core/Thumbnail'
import { ref } from 'vue'
import { thumbnailScenes } from './images'

const items = ref<ThumbnailScene[]>([...thumbnailScenes])

function remove(id: number) {
  items.value = items.value.filter(item => item.id !== id)
}
</script>

<template>
  <Stack direction="vertical" :gap="4">
    <Text type="supporting" color="secondary">
      {{ 'Remove button adapts contrast to image luminance' }}
    </Text>
    <Stack align="center" direction="horizontal" :gap="3">
      <Thumbnail
        v-for="item in items"
        :key="item.id"
        :src="item.src"
        :alt="item.alt"
        :label="item.label"
        show-remove-on="always"
        @remove="remove(item.id)"
      />
      <Text v-if="items.length === 0" type="supporting" color="secondary">
        {{ 'All removed.' }}
      </Text>
    </Stack>
  </Stack>
</template>
```

### Thumbnail — States

All visual states side by side: image loaded, placeholder, skeleton loading, and upload overlay.

```vue
<script setup lang="ts">
import { Stack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { Thumbnail } from '@astryx-vue/core/Thumbnail'
import { MISTY_VALLEY } from './images'
</script>

<template>
  <Stack direction="vertical" :gap="4">
    <Stack direction="vertical" :gap="1">
      <Text type="supporting" color="secondary">
        {{ 'Lifecycle: empty → uploading → processing → loaded' }}
      </Text>
      <Stack align="end" direction="horizontal" :gap="3">
        <Stack direction="vertical" :gap="1" h-align="center">
          <Thumbnail label="report.pdf" />
          <Text type="supporting" color="secondary">
            {{ 'Placeholder' }}
          </Text>
        </Stack>
        <Stack direction="vertical" :gap="1" h-align="center">
          <Thumbnail is-loading label="uploading.jpg" />
          <Text type="supporting" color="secondary">
            {{ 'Skeleton' }}
          </Text>
        </Stack>
        <Stack direction="vertical" :gap="1" h-align="center">
          <Thumbnail
            :src="MISTY_VALLEY"
            alt="Misty mountain valley"
            is-loading
            label="misty-valley.jpg"
          />
          <Text type="supporting" color="secondary">
            {{ 'Uploading' }}
          </Text>
        </Stack>
        <Stack direction="vertical" :gap="1" h-align="center">
          <Thumbnail
            :src="MISTY_VALLEY"
            alt="Misty mountain valley"
            label="misty-valley.jpg"
          />
          <Text type="supporting" color="secondary">
            {{ 'Loaded' }}
          </Text>
        </Stack>
      </Stack>
    </Stack>
  </Stack>
</template>
```

## Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `src` | `string` | - | Image URL. A failed load falls back to the placeholder for that `src`. |
| `alt` | `string` | - | Alternative text for the image. |
| `label` | `string` | - | Accessible name. Not drawn. Used by the tooltip and the remove button. |
| `isLoading` | `boolean` | `false` | Skeleton when there is no `src`. Spinner over the image when there is one. |
| `isDisabled` | `boolean` | `false` | Fades the thumbnail and ignores click and remove. |
| `showRemoveOn` | `'always' \| 'hover'` | `'hover'` | When the remove button is visible. `hover` also reveals it on keyboard focus, and keeps it visible for a coarse pointer. |

## Events

| Name | Payload | When |
| --- | --- | --- |
| <code>click</code> | <code>MouseEvent</code> | The preview is activated and the thumbnail is neither disabled nor loading. |
| <code>remove</code> | <code>MouseEvent</code> | The remove button is used. The button is only rendered when this listener is present. |

## Theming

The square is 64px with the element radius and a neutral fill. A 1px border is inset. The placeholder glyph is 24px. The remove button is 20×20, inset by one spacing step from the top and end edges, with an overlay fill and an on-dark icon. A coarse pointer expands the hit area by 2px. Disabled opacity is 0.5.
