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/)
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -14,7 +14,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="pictureFrame" />
|
||||
<script type="module" crossorigin src="/build/assets/index-DHJU4XGZ.js"></script>
|
||||
<script type="module" crossorigin src="/build/assets/index-RZqoO867.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/build/assets/_plugin-vue_export-helper-BNDVmFr7.js">
|
||||
<link rel="stylesheet" crossorigin href="/build/assets/index-BlLBHR1q.css">
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user