fix(vibe-stories): finalize laptop viewport sizing
This commit is contained in:
@@ -72,39 +72,50 @@ const tStories = computed(() => [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const defaultScreenViewport = Object.freeze({
|
||||||
|
left: 19,
|
||||||
|
top: 1.75,
|
||||||
|
width: 62.75,
|
||||||
|
height: 71.5,
|
||||||
|
radius: 12
|
||||||
|
})
|
||||||
|
|
||||||
const currentIndex = ref(0)
|
const currentIndex = ref(0)
|
||||||
let autoplayTimer = null
|
let autoplayTimer = null
|
||||||
const isPaginating = ref(false)
|
const isPaginating = ref(false)
|
||||||
const containerRef = ref(null)
|
const containerRef = ref(null)
|
||||||
|
const laptopRef = ref(null)
|
||||||
let wheelHandler = null
|
let wheelHandler = null
|
||||||
|
let resizeObserver = null
|
||||||
|
|
||||||
const screenRegion = ref({
|
const LAPTOP_ASPECT_RATIO = 2675 / 4608
|
||||||
centerX: 50,
|
const laptopHeightPx = ref(null)
|
||||||
centerY: 45.75,
|
|
||||||
width: 80,
|
// Visible image container geometry relative to `.laptop-container`.
|
||||||
height: 90,
|
// Adjust these five values directly to control the screen viewport.
|
||||||
radius: 4
|
const screenViewport = ref({ ...defaultScreenViewport })
|
||||||
})
|
|
||||||
|
|
||||||
const formatPercent = (value) => `${value}%`
|
const formatPercent = (value) => `${value}%`
|
||||||
const formatPixels = (value) => `${value}px`
|
const formatPixels = (value) => `${value}px`
|
||||||
|
|
||||||
const screenContentStyle = computed(() => ({
|
// The percentages below are always resolved against `.laptop-container`.
|
||||||
top: formatPercent(screenRegion.value.centerY - screenRegion.value.height / 2),
|
const screenViewportStyle = computed(() => ({
|
||||||
left: formatPercent(screenRegion.value.centerX - screenRegion.value.width / 2),
|
'--screen-left': formatPercent(screenViewport.value.left),
|
||||||
width: formatPercent(screenRegion.value.width),
|
'--screen-top': formatPercent(screenViewport.value.top),
|
||||||
height: formatPercent(screenRegion.value.height),
|
'--screen-width': formatPercent(screenViewport.value.width),
|
||||||
borderRadius: formatPixels(screenRegion.value.radius)
|
'--screen-height': formatPercent(screenViewport.value.height),
|
||||||
|
'--screen-radius': formatPixels(screenViewport.value.radius)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const currentStory = computed(() => tStories.value[currentIndex.value] ?? tStories.value[0])
|
const currentStory = computed(() => tStories.value[currentIndex.value] ?? tStories.value[0])
|
||||||
|
|
||||||
// Keep crop settings explicit in source so they survive reload/HMR.
|
const currentImageStyle = computed(() => currentStory.value?.imageStyle || {})
|
||||||
const currentImageStyle = computed(() => ({
|
|
||||||
objectFit: 'cover',
|
const laptopContainerStyle = computed(() => (
|
||||||
objectPosition: 'center center',
|
laptopHeightPx.value
|
||||||
...(currentStory.value?.imageStyle || {})
|
? { height: `${laptopHeightPx.value}px` }
|
||||||
}))
|
: {}
|
||||||
|
))
|
||||||
|
|
||||||
const transitionName = ref('slide-left')
|
const transitionName = ref('slide-left')
|
||||||
|
|
||||||
@@ -149,9 +160,18 @@ const stopAutoplay = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateLaptopHeight = () => {
|
||||||
|
const laptop = laptopRef.value
|
||||||
|
if (!laptop) return
|
||||||
|
|
||||||
|
const nextHeight = laptop.clientWidth * LAPTOP_ASPECT_RATIO
|
||||||
|
laptopHeightPx.value = nextHeight > 0 ? nextHeight : null
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startAutoplay()
|
startAutoplay()
|
||||||
const container = containerRef.value
|
const container = containerRef.value
|
||||||
|
const laptop = laptopRef.value
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
wheelHandler = (e) => {
|
wheelHandler = (e) => {
|
||||||
@@ -166,6 +186,17 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.addEventListener('wheel', wheelHandler, { passive: false })
|
container.addEventListener('wheel', wheelHandler, { passive: false })
|
||||||
|
|
||||||
|
updateLaptopHeight()
|
||||||
|
|
||||||
|
if (typeof ResizeObserver !== 'undefined' && laptop) {
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateLaptopHeight()
|
||||||
|
})
|
||||||
|
resizeObserver.observe(laptop)
|
||||||
|
} else if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('resize', updateLaptopHeight)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -174,6 +205,13 @@ onUnmounted(() => {
|
|||||||
if (container && wheelHandler) {
|
if (container && wheelHandler) {
|
||||||
container.removeEventListener('wheel', wheelHandler)
|
container.removeEventListener('wheel', wheelHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
resizeObserver = null
|
||||||
|
} else if (typeof window !== 'undefined') {
|
||||||
|
window.removeEventListener('resize', updateLaptopHeight)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -185,7 +223,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="laptop-wrapper" @mouseenter="stopAutoplay" @mouseleave="startAutoplay">
|
<div class="laptop-wrapper" @mouseenter="stopAutoplay" @mouseleave="startAutoplay">
|
||||||
<div class="laptop-container">
|
<div ref="laptopRef" class="laptop-container" :style="laptopContainerStyle">
|
||||||
<!-- Navigation Controls -->
|
<!-- Navigation Controls -->
|
||||||
<button class="nav-btn prev" :aria-label="t.stories?.ui?.prevLabel || 'Previous story'" @click="prev">
|
<button class="nav-btn prev" :aria-label="t.stories?.ui?.prevLabel || 'Previous story'" @click="prev">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6" /></svg>
|
||||||
@@ -194,7 +232,7 @@ onUnmounted(() => {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="screen-content" :style="screenContentStyle">
|
<div class="screen-content" :style="screenViewportStyle">
|
||||||
<a :href="withBase(currentStory.link)" class="screen-link">
|
<a :href="withBase(currentStory.link)" class="screen-link">
|
||||||
<transition :name="transitionName">
|
<transition :name="transitionName">
|
||||||
<div :key="currentStory.id" class="screen-image-wrapper">
|
<div :key="currentStory.id" class="screen-image-wrapper">
|
||||||
@@ -304,13 +342,15 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
aspect-ratio: 4608 / 2675;
|
||||||
}
|
}
|
||||||
|
|
||||||
.laptop-frame {
|
.laptop-frame {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));
|
filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));
|
||||||
}
|
}
|
||||||
@@ -322,6 +362,11 @@ onUnmounted(() => {
|
|||||||
.screen-content {
|
.screen-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
top: var(--screen-top);
|
||||||
|
left: var(--screen-left);
|
||||||
|
width: var(--screen-width);
|
||||||
|
height: var(--screen-height);
|
||||||
|
border-radius: var(--screen-radius);
|
||||||
background: #0b0b0f;
|
background: #0b0b0f;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
perspective: 1000px;
|
perspective: 1000px;
|
||||||
|
|||||||
Reference in New Issue
Block a user