Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4a62f80
feat(Image): add headless Image component and useImage composable
claude Apr 16, 2026
160bda7
refactor(Image): split examples into multi-file learning events
claude Apr 16, 2026
d59fac0
fix(Image): restore class propagation, remove v-show, clean up context
johnleider Apr 16, 2026
bdfd9db
docs(useImage): rename handleRetry to onRetry in retry example
johnleider Apr 16, 2026
ecb15bc
feat(AspectRatio): add primitive with CSS aspect-ratio binding
johnleider Apr 16, 2026
66257fd
docs(Image): wrap bare markup in image.md vue code fences with <templ…
johnleider Apr 17, 2026
01d3bb2
docs(Image): center the basic usage example
johnleider Apr 16, 2026
8983620
docs: expand prose under Examples headers
johnleider Apr 16, 2026
7432ad2
docs: add mermaid diagrams to strong-fit examples
johnleider Apr 16, 2026
7435256
docs(Image): add info callout explaining why src lives on Root
johnleider Apr 16, 2026
8092f47
docs(Image): recast src callout as INFO and rephrase opener
johnleider Apr 16, 2026
d63b5f6
feat(Image): expose threshold and root observer options on Image.Root
johnleider Apr 16, 2026
6d8684c
feat(Image): emit loadstart on Image.Img
johnleider Apr 16, 2026
ea7e099
feat(Image): add Image.Presence for source-transition crossfading
johnleider Apr 16, 2026
3f43b67
fix(Image): defer Presence leaving flip by a frame so cached srcs cro…
johnleider Apr 16, 2026
57b6733
docs(Image): add simulated load delay to the Presence gallery example
johnleider Apr 17, 2026
0bca9aa
fix(Image): set immediate=false on Image.Presence's internal Presence
johnleider Apr 17, 2026
f5a8bf5
refactor(Image): rename Image.Presence to Image.Swap
johnleider Apr 17, 2026
76aecd4
docs(Image): remove simulated network delay from gallery example
johnleider Apr 17, 2026
5effe9f
fix(Image): hold Image.Swap's current img at opacity 1 during swaps
johnleider Apr 17, 2026
641796a
refactor(Image): strip styling from Image.Swap, keep it headless
johnleider Apr 17, 2026
278030f
fix(Image): close Image.Swap contract gaps from code review
johnleider Apr 17, 2026
7dbb4a5
refactor(Image): rename Image.Swap slot props to source / previousSource
johnleider Apr 17, 2026
269fd78
docs(Image): drop alt-text eslint-disable comments from example files
johnleider Apr 17, 2026
e9e47d3
refactor(Image): name the useLogger instance before calling warn
johnleider Apr 17, 2026
b783ac4
refactor(Image): split Image.Swap into a sibling overlay of Image.Img
johnleider Apr 17, 2026
382d0eb
refactor(Image): remove Image.Swap — not ready, abandon for now
johnleider Apr 17, 2026
278c438
test(Image): close remaining coverage gaps on Root and Img
johnleider Apr 17, 2026
6c763d0
docs(useImage): show Usage as a full .vue SFC instead of a ts fence
johnleider Apr 17, 2026
fca806b
fix(useImage): force browser to refetch on retry by cycling through idle
johnleider Apr 17, 2026
2ee1969
docs(useImage): move basic example under Examples with substantive prose
johnleider Apr 17, 2026
2bedadb
docs(useImage): simulate flaky network in the retry example
johnleider Apr 17, 2026
86e9176
docs(Image): note retry in loadstart JSDoc and cover with a test
johnleider Apr 17, 2026
06a3dd1
docs(useImage): replace 'Failed to load' text with a broken-image icon
johnleider Apr 17, 2026
3696932
docs(useImage): use a neutral broken-image icon in both error states
johnleider Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/docs/src/examples/components/aspect-ratio/ResponsiveImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { AspectRatio, Image } from '@vuetify/v0'

defineProps<{
src: string
alt: string
ratio: number | string
}>()
</script>

<template>
<AspectRatio
class="w-full bg-surface-tint rounded overflow-hidden relative"
:ratio
>
<Image.Root :src>
<Image.Img
:alt
class="w-full h-full object-cover"
/>

<Image.Placeholder class="absolute inset-0 flex items-center justify-center">
<div class="w-full h-full bg-surface-tint animate-pulse" />
</Image.Placeholder>

<Image.Fallback class="absolute inset-0 flex items-center justify-center bg-error-container text-on-error-container text-sm">
Failed to load
</Image.Fallback>
</Image.Root>
</AspectRatio>
</template>
38 changes: 38 additions & 0 deletions apps/docs/src/examples/components/aspect-ratio/basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { AspectRatio } from '@vuetify/v0'
import { shallowRef } from 'vue'

const ratios = [
{ label: '1 / 1', value: '1 / 1' },
{ label: '4 / 3', value: '4 / 3' },
{ label: '16 / 9', value: '16 / 9' },
{ label: '21 / 9', value: '21 / 9' },
]

const active = shallowRef('16 / 9')
</script>

<template>
<div class="space-y-4">
<div class="flex gap-2">
<button
v-for="r in ratios"
:key="r.value"
class="px-3 py-1 border rounded text-sm"
:class="active === r.value ? 'bg-primary text-on-primary' : 'bg-surface'"
@click="active = r.value"
>
{{ r.label }}
</button>
</div>

<AspectRatio
class="w-full bg-surface-tint rounded overflow-hidden"
:ratio="active"
>
<div class="w-full h-full flex items-center justify-center text-on-surface-variant text-sm">
{{ active }}
</div>
</AspectRatio>
</div>
</template>
18 changes: 18 additions & 0 deletions apps/docs/src/examples/components/aspect-ratio/responsive.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
import ResponsiveImage from './ResponsiveImage.vue'
</script>

<template>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<ResponsiveImage
alt="Landscape photo"
:ratio="16 / 9"
src="https://picsum.photos/seed/ar-1/800/450"
/>
<ResponsiveImage
alt="Square photo"
:ratio="1"
src="https://picsum.photos/seed/ar-2/400/400"
/>
</div>
</template>
34 changes: 34 additions & 0 deletions apps/docs/src/examples/components/image/BlurUpImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { Image } from '@vuetify/v0'

defineProps<{
src: string
placeholder: string
alt: string
}>()
</script>

<template>
<Image.Root
class="w-full aspect-4/3 bg-surface-tint rounded overflow-hidden relative"
lazy
root-margin="200px"
:src
>
<Image.Img
:alt
class="w-full h-full object-cover opacity-0 transition-opacity duration-500 data-[state=loaded]:opacity-100"
height="300"
width="400"
/>

<Image.Placeholder class="absolute inset-0">
<img
:alt
aria-hidden="true"
class="w-full h-full object-cover scale-105 blur-xl"
:src="placeholder"
>
</Image.Placeholder>
</Image.Root>
</template>
49 changes: 49 additions & 0 deletions apps/docs/src/examples/components/image/PictureImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { Image } from '@vuetify/v0'

defineProps<{
src: string
sources: { srcset: string, type: string }[]
alt: string
width: number
height: number
}>()
</script>

<template>
<Image.Root
renderless
:src
>
<picture class="block w-full aspect-4/3 bg-surface-tint rounded overflow-hidden relative">
<source
v-for="source in sources"
:key="source.type"
:srcset="source.srcset"
:type="source.type"
>

<Image.Img
v-slot="{ attrs }"
:alt
:height
renderless
:width
>
<img
v-bind="attrs"
:alt="attrs.alt"
class="w-full h-full object-cover"
>
</Image.Img>

<Image.Placeholder class="absolute inset-0 flex items-center justify-center bg-surface-tint">
<span class="text-on-surface-variant text-sm">Loading...</span>
</Image.Placeholder>

<Image.Fallback class="absolute inset-0 flex items-center justify-center bg-error-container text-on-error-container text-sm">
Image failed to load
</Image.Fallback>
</picture>
</Image.Root>
</template>
28 changes: 28 additions & 0 deletions apps/docs/src/examples/components/image/basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { Image } from '@vuetify/v0'
</script>

<template>
<div class="flex items-center justify-center">
<Image.Root
class="w-100 h-75 bg-surface-tint rounded overflow-hidden relative"
src="https://picsum.photos/seed/v0-image/400/300"
>
<Image.Img
alt="Random photo"
class="w-full h-full object-cover"
height="300"
loading="lazy"
width="400"
/>

<Image.Placeholder class="absolute inset-0 flex items-center justify-center">
<div class="w-full h-full bg-surface-tint animate-pulse" />
</Image.Placeholder>

<Image.Fallback class="absolute inset-0 flex items-center justify-center bg-error-container text-on-error-container text-sm">
Image failed to load
</Image.Fallback>
</Image.Root>
</div>
</template>
43 changes: 43 additions & 0 deletions apps/docs/src/examples/components/image/observer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
import BlurUpImage from './BlurUpImage.vue'

const photos = [
{
id: 1,
src: 'https://picsum.photos/seed/blur-1/400/300',
placeholder: 'https://picsum.photos/seed/blur-1/20/15',
alt: 'Photo 1',
},
{
id: 2,
src: 'https://picsum.photos/seed/blur-2/400/300',
placeholder: 'https://picsum.photos/seed/blur-2/20/15',
alt: 'Photo 2',
},
{
id: 3,
src: 'https://picsum.photos/seed/blur-3/400/300',
placeholder: 'https://picsum.photos/seed/blur-3/20/15',
alt: 'Photo 3',
},
]
</script>

<template>
<div class="h-100 overflow-auto border border-divider rounded p-4">
<p class="text-sm text-on-surface-variant mb-4">
A low-quality placeholder shows first; the full image fades in once it
loads. Scroll to trigger the next image.
</p>

<div class="flex flex-col gap-100">
<BlurUpImage
v-for="photo in photos"
:key="photo.id"
:alt="photo.alt"
:placeholder="photo.placeholder"
:src="photo.src"
/>
</div>
</div>
</template>
15 changes: 15 additions & 0 deletions apps/docs/src/examples/components/image/picture.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import PictureImage from './PictureImage.vue'
</script>

<template>
<PictureImage
alt="Format-negotiated photo"
:height="300"
:sources="[
{ srcset: 'https://picsum.photos/seed/picture/400/300.webp', type: 'image/webp' },
]"
src="https://picsum.photos/seed/picture/400/300"
:width="400"
/>
</template>
33 changes: 33 additions & 0 deletions apps/docs/src/examples/composables/use-image/LazyImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { toRef } from 'vue'
import { useLazyImage } from './useLazyImage'

const props = defineProps<{
src: string
alt: string
}>()

const { target, source, isLoaded, status, onLoad, onError } = useLazyImage(
toRef(() => props.src),
)
</script>

<template>
<div ref="target" class="w-full aspect-4/3 bg-surface-tint rounded overflow-hidden relative">
<img
v-show="isLoaded"
:alt
class="w-full h-full object-cover"
:src="source"
@error="onError"
@load="onLoad"
>

<span
v-if="!isLoaded"
class="absolute inset-0 flex items-center justify-center text-on-surface-variant text-sm"
>
{{ status }}
</span>
</div>
</template>
68 changes: 68 additions & 0 deletions apps/docs/src/examples/composables/use-image/RetryableImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script setup lang="ts">
import { useImage } from '@vuetify/v0'
import { mdiImageBrokenVariant } from '@mdi/js'
import { shallowRef, toRef } from 'vue'

const props = defineProps<{
src: string
alt: string
}>()

const BROKEN = 'https://invalid.example/missing.jpg'
const attempts = shallowRef(0)
const currentSrc = shallowRef(BROKEN)

const { source, isLoaded, isError, onLoad, onError, retry } = useImage({
src: toRef(() => currentSrc.value),
})

function onRetry () {
attempts.value++
// Simulate a flaky network: each retry has a 25% chance of succeeding.
// On success, swap the broken URL for the real one — the src change
// triggers a fresh fetch. Otherwise call retry() to re-attempt the
// existing (broken) URL.
if (Math.random() < 0.25) {
currentSrc.value = props.src
} else {
retry()
}
}
</script>

<template>
<div class="w-full aspect-4/3 bg-surface-tint rounded overflow-hidden relative flex flex-col items-center justify-center gap-2">
<img
v-show="isLoaded"
:alt
class="w-full h-full object-cover"
:src="source"
@error="onError"
@load="onLoad"
>

<template v-if="isError">
<svg
aria-label="Image failed to load"
class="size-24 text-on-surface-variant"
role="img"
viewBox="0 0 24 24"
>
<path :d="mdiImageBrokenVariant" fill="currentColor" />
</svg>
<span v-if="attempts > 0" class="text-xs text-on-surface-variant">
Attempt {{ attempts + 1 }}
</span>
<button
class="px-3 py-1 bg-primary text-on-primary rounded text-sm"
@click="onRetry"
>
Retry
</button>
</template>

<span v-else-if="!isLoaded" class="text-on-surface-variant text-sm">
Loading...
</span>
</div>
</template>
Loading
Loading