fix(home): preview locks aspect to panel dims + object-fit so it never overflows
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
The large FrameCard preview let the <img> drive height (`flex: 0 0 auto`, `width: auto`, `max-*: 100%`). On wide-container layouts and on the new V2 1200×1600 dimensions the image's intrinsic size leaked past the card, and the max-width/max-height combo can drop aspect ratio in some browsers. Now: the preview container locks its `aspect-ratio` to `panelDims(model, orientation)` — same source of truth that drives the empty-placeholder shape — and the <img> fills the container with `object-fit: contain`. Container shape is stable whether or not the thumbnail has loaded; image always scales to fit, portrait or landscape device, narrow or wide phone column. emptyAspectStyle no longer needs to carry aspect (parent already has it); empty-preview placeholder fills 100% of the parent now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,18 +89,22 @@ const props = defineProps<{
|
||||
|
||||
defineEmits<{ 'add-photo': [deviceId: number]; edit: [deviceId: number] }>()
|
||||
|
||||
// On large cards we let the <img> drive the preview height (its intrinsic
|
||||
// aspect ratio is the device's actual aspect). Until the image loads — or
|
||||
// when no thumbnail exists yet — we want the empty placeholder to reserve
|
||||
// roughly the same shape so the layout doesn't jump.
|
||||
const previewStyle = computed(() => ({}))
|
||||
|
||||
const emptyAspectStyle = computed(() => {
|
||||
// Large cards lock the preview's aspect to the panel's actual dimensions,
|
||||
// independent of whether the image has loaded. That way the layout never
|
||||
// jumps when the thumbnail arrives, the <img> can use object-fit: contain
|
||||
// to scale down inside the container without overflow, and both portrait
|
||||
// and landscape devices fit cleanly inside whatever their parent column
|
||||
// allows.
|
||||
const previewStyle = computed(() => {
|
||||
if (props.size !== 'large') return {}
|
||||
const { width, height } = panelDims(props.model ?? 'v1', props.orientation)
|
||||
return { aspectRatio: `${width} / ${height}` }
|
||||
})
|
||||
|
||||
// Empty-preview placeholder fills its parent (which already has aspect set
|
||||
// by previewStyle), so nothing extra needed here.
|
||||
const emptyAspectStyle = computed(() => ({}))
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'ok': return 'Online'
|
||||
@@ -190,26 +194,27 @@ const statusText = computed(() => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Cap the preview height so portrait frames (3:5) don't blow past half
|
||||
// the viewport. Landscape frames (5:3) at full card width are short enough
|
||||
// that the cap never engages. The photo is centered within the preview;
|
||||
// portrait shots get a thin grey bar on each side instead of stretching.
|
||||
// Preview locks to the device's actual aspect via inline aspect-ratio set
|
||||
// by previewStyle. Caps height at 40dvh so a portrait frame's tall aspect
|
||||
// doesn't push everything else below the fold; `min-width: 0` lets flex
|
||||
// shrink it on narrow containers.
|
||||
&--large &__preview {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-height: 40dvh;
|
||||
background: var(--color-surface-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
max-height: 40dvh;
|
||||
}
|
||||
|
||||
&--large &__img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&--large &__empty-preview {
|
||||
@@ -219,6 +224,7 @@ const statusText = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--large &__body {
|
||||
|
||||
@@ -146,35 +146,34 @@ describe('FrameCard', () => {
|
||||
expect(wrapper.emitted('edit')![0]).toEqual([1])
|
||||
})
|
||||
|
||||
it('reserves a V1 landscape (1600 × 960) placeholder when no thumbnail is present', () => {
|
||||
// Aspect ratio is set on the preview container itself (not the placeholder)
|
||||
// so the box shape is stable whether or not the thumbnail has loaded —
|
||||
// image inside scales with object-fit: contain.
|
||||
it('locks preview aspect to V1 landscape (1600 × 960)', () => {
|
||||
const wrapper = mount(FrameCard, {
|
||||
props: { ...defaultProps, size: 'large', orientation: 'landscape', model: 'v1' },
|
||||
})
|
||||
const empty = wrapper.find('.frame-card__empty-preview')
|
||||
expect(empty.attributes('style')).toMatch(/aspect-ratio:\s*1600\s*\/\s*960/)
|
||||
expect(wrapper.find('.frame-card__preview').attributes('style')).toMatch(/aspect-ratio:\s*1600\s*\/\s*960/)
|
||||
})
|
||||
|
||||
it('reserves a V1 portrait (960 × 1600) placeholder when no thumbnail is present', () => {
|
||||
it('locks preview aspect to V1 portrait (960 × 1600)', () => {
|
||||
const wrapper = mount(FrameCard, {
|
||||
props: { ...defaultProps, size: 'large', orientation: 'portrait', model: 'v1' },
|
||||
})
|
||||
const empty = wrapper.find('.frame-card__empty-preview')
|
||||
expect(empty.attributes('style')).toMatch(/aspect-ratio:\s*960\s*\/\s*1600/)
|
||||
expect(wrapper.find('.frame-card__preview').attributes('style')).toMatch(/aspect-ratio:\s*960\s*\/\s*1600/)
|
||||
})
|
||||
|
||||
it('reserves a V2 portrait (1200 × 1600) placeholder when no thumbnail is present', () => {
|
||||
it('locks preview aspect to V2 portrait (1200 × 1600)', () => {
|
||||
const wrapper = mount(FrameCard, {
|
||||
props: { ...defaultProps, size: 'large', orientation: 'portrait', model: 'v2' },
|
||||
})
|
||||
const empty = wrapper.find('.frame-card__empty-preview')
|
||||
expect(empty.attributes('style')).toMatch(/aspect-ratio:\s*1200\s*\/\s*1600/)
|
||||
expect(wrapper.find('.frame-card__preview').attributes('style')).toMatch(/aspect-ratio:\s*1200\s*\/\s*1600/)
|
||||
})
|
||||
|
||||
it('reserves a V2 landscape (1600 × 1200) placeholder when no thumbnail is present', () => {
|
||||
it('locks preview aspect to V2 landscape (1600 × 1200)', () => {
|
||||
const wrapper = mount(FrameCard, {
|
||||
props: { ...defaultProps, size: 'large', orientation: 'landscape', model: 'v2' },
|
||||
})
|
||||
const empty = wrapper.find('.frame-card__empty-preview')
|
||||
expect(empty.attributes('style')).toMatch(/aspect-ratio:\s*1600\s*\/\s*1200/)
|
||||
expect(wrapper.find('.frame-card__preview').attributes('style')).toMatch(/aspect-ratio:\s*1600\s*\/\s*1200/)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user