layout + non looping video

This commit is contained in:
nicwands 2026-06-24 11:16:12 -04:00
parent dbb93ba802
commit 1894e37605
10 changed files with 111 additions and 21 deletions

2
src/components.d.ts vendored
View file

@ -24,8 +24,10 @@ declare module 'vue' {
SiteHeader: typeof import('./components/site/Header.vue')['default'] SiteHeader: typeof import('./components/site/Header.vue')['default']
StrapiContent: typeof import('./components/strapi/Content.vue')['default'] StrapiContent: typeof import('./components/strapi/Content.vue')['default']
StrapiCopy: typeof import('./components/strapi/Copy.vue')['default'] StrapiCopy: typeof import('./components/strapi/Copy.vue')['default']
StrapiDiptych: typeof import('./components/strapi/Diptych.vue')['default']
StrapiLink: typeof import('./components/strapi/Link.vue')['default'] StrapiLink: typeof import('./components/strapi/Link.vue')['default']
StrapiMap: typeof import('./components/strapi/Map.vue')['default'] StrapiMap: typeof import('./components/strapi/Map.vue')['default']
StrapiMedia: typeof import('./components/strapi/Media.vue')['default'] StrapiMedia: typeof import('./components/strapi/Media.vue')['default']
SvgPlay: typeof import('./components/svg/Play.vue')['default']
} }
} }

View file

@ -14,10 +14,13 @@ const props = defineProps({ row: Object })
.dl-row { .dl-row {
grid-template-columns: desktop-vw(40px) 1fr; grid-template-columns: desktop-vw(40px) 1fr;
display: grid; display: grid;
align-items: center; align-items: flex-start;
padding-top: desktop-vw(8px); padding-top: desktop-vw(8px);
border-top: 1px dashed currentColor; border-top: 1px dashed currentColor;
.label {
padding-top: 0.4em;
}
.content { .content {
p, p,
a { a {

View file

@ -1,6 +1,6 @@
<template> <template>
<div v-if="global" class="layout"> <div v-if="global" class="layout" :style="{ '--header-height': headerHeight + 'px' }">
<site-header v-show="showHeader" :copy="headerCopy" :dl="headerDl" /> <site-header v-show="showHeader" :copy="headerCopy" :dl="headerDl" ref="header" />
<client-only> <client-only>
<mosaic-scroll :items="mosaicItems" /> <mosaic-scroll :items="mosaicItems" />
@ -17,20 +17,26 @@
</template async> </template async>
<script setup> <script setup>
import { computed} from 'vue'; import { computed, ref} from 'vue';
import { useSeoMeta } from '@unhead/vue' import { useSeoMeta } from '@unhead/vue'
import { useStrapiGlobal } from '@/composables/useStrapi' import { useStrapiGlobal } from '@/composables/useStrapi'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import useBreakpoints from '@/composables/useBreakpoints'; import useBreakpoints from '@/composables/useBreakpoints';
import { useElementSize } from '@vueuse/core';
const header = ref()
const route = useRoute() const route = useRoute()
const { data: global } = await useStrapiGlobal() const { data: global } = await useStrapiGlobal()
const {isMobile} = useBreakpoints() const { isMobile } = useBreakpoints()
const { height: headerHeight } = useElementSize(header)
const headerCopy = computed(() => global.value?.header_copy) const headerCopy = computed(() => global.value?.header_copy)
const headerDl = computed(() => global.value?.header_dl) const headerDl = computed(() => global.value?.header_dl)
const mosaicItems = computed(() => global.value?.mosaic_items || []) const mosaicItems = computed(() => global.value?.mosaic_items || [])
const showHeader = computed(() => !(isMobile && route.name === 'mosaic-item')) const showHeader = computed(() => !(route.name === 'mosaic-item' && isMobile.value))
// SEO // SEO
const seo = computed(() => global.value?.seo) const seo = computed(() => global.value?.seo)

View file

@ -47,7 +47,9 @@ const onClose = () => router.push('/')
padding-top: desktop-vw(27px); padding-top: desktop-vw(27px);
.meta { .meta {
grid-column: 1 / span 2; grid-column: 2 / span 2;
margin-top: var(--header-height);
padding-top: var(--layout-column-gap);
} }
.window { .window {
@ -78,7 +80,7 @@ const onClose = () => router.push('/')
} }
} }
&.content { &.content {
grid-column: 3 / -2; grid-column: 4 / -2;
.lenis { .lenis {
max-height: desktop-vh(950px); max-height: desktop-vh(950px);
@ -118,6 +120,8 @@ const onClose = () => router.push('/')
display: grid; display: grid;
gap: var(--layout-column-gap); gap: var(--layout-column-gap);
align-items: flex-start; align-items: flex-start;
margin: 0;
padding: 0;
} }
.window { .window {
border-radius: mobile-vw(10px); border-radius: mobile-vw(10px);

View file

@ -14,7 +14,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue' import { nextTick, ref, watch } from 'vue'
import _shuffle from 'lodash/shuffle' import _shuffle from 'lodash/shuffle'
import { useMosaicLayout } from '@/composables/useMosaicLayout' import { useMosaicLayout } from '@/composables/useMosaicLayout'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
@ -69,8 +69,11 @@ const generateMosaicContent = async (index) => {
// Start/stop scroll when page is open // Start/stop scroll when page is open
watch( watch(
() => route.name, () => route.name,
() => { async () => {
if (route.name === 'mosaic-item') { if (route.name === 'mosaic-item') {
if (!scrollContainer.value) {
await nextTick()
}
scrollContainer.value?.stop?.() scrollContainer.value?.stop?.()
} else { } else {
scrollContainer.value?.start?.() scrollContainer.value?.start?.()

View file

@ -24,7 +24,7 @@ const props = defineProps({
left: 0; left: 0;
right: 0; right: 0;
pointer-events: none; pointer-events: none;
padding: desktop-vw(27px) var(--layout-margin); padding: desktop-vw(27px) var(--layout-margin) 0;
align-items: flex-start; align-items: flex-start;
> * { > * {

View file

@ -0,0 +1,21 @@
<template>
<section class="strapi-diptych">
<strapi-media v-bind="props.left_media" />
<strapi-media v-bind="props.right_media" />
</section>
</template>
<script setup>
const props = defineProps({
left_media: Object,
right_media: Object,
})
</script>
<style lang="scss">
.strapi-diptych {
grid-template-columns: 1fr 1fr;
display: grid;
gap: var(--layout-column-gap);
}
</style>

View file

@ -12,11 +12,13 @@
<script setup> <script setup>
import Copy from './Copy.vue' import Copy from './Copy.vue'
import Diptych from './Diptych.vue'
import Media from './Media.vue' import Media from './Media.vue'
const MAP = { const MAP = {
'global.copy': Copy, 'global.copy': Copy,
'global.media': Media, 'global.media': Media,
'global.dipytch': Diptych,
} }
const props = defineProps({ const props = defineProps({

View file

@ -1,7 +1,11 @@
<template> <template>
<div <div
v-if="image" v-if="image"
:class="['strapi-media', { 'fill-space': fillSpace }, `fit-${fit}`]" :class="[
'strapi-media',
{ 'fill-space': fillSpace, 'can-play': canPlay },
`fit-${fit}`,
]"
:style="styles" :style="styles"
> >
<div class="image-sizer"> <div class="image-sizer">
@ -16,13 +20,19 @@
decoding="async" decoding="async"
/> />
<button v-if="!shouldLoop" class="play-button" @click="onPlayClick">
<svg-play />
</button>
<video <video
v-if="video?.url" v-if="video?.url"
:src="video.url" :src="video.url"
muted :muted="shouldLoop"
autoplay :autoplay="shouldLoop"
playsinline :playsinline="shouldLoop"
loop :loop="shouldLoop"
:controls="!shouldLoop"
ref="videoRef"
/> />
</div> </div>
</div> </div>
@ -30,11 +40,15 @@
<script setup> <script setup>
import { breakpoints } from '@/libs/theme' import { breakpoints } from '@/libs/theme'
import { computed } from 'vue' import { computed, ref } from 'vue'
const props = defineProps({ const props = defineProps({
image: Object, image: Object,
video: Object, video: Object,
looping: {
type: Boolean,
default: () => true,
},
aspect: { aspect: {
type: [String, Number], type: [String, Number],
default: () => -1, default: () => -1,
@ -57,6 +71,10 @@ const props = defineProps({
}, },
}) })
const videoRef = ref()
const canPlay = ref(props.looping !== false)
const shouldLoop = computed(() => props.looping !== false)
const formats = computed(() => props.image?.formats || {}) const formats = computed(() => props.image?.formats || {})
const ratio = computed(() => { const ratio = computed(() => {
if (!props.image) return 1 if (!props.image) return 1
@ -82,6 +100,11 @@ const sizes = computed(
() => () =>
`(max-width: ${breakpoints.mobile}) ${props.mobileSize}, ${props.desktopSize}`, `(max-width: ${breakpoints.mobile}) ${props.mobileSize}, ${props.desktopSize}`,
) )
const onPlayClick = () => {
canPlay.value = true
videoRef.value?.play()
}
</script> </script>
<style lang="scss"> <style lang="scss">
@ -94,7 +117,8 @@ const sizes = computed(
overflow: hidden; overflow: hidden;
padding-bottom: var(--aspect); padding-bottom: var(--aspect);
& > * { img,
video {
display: block; display: block;
position: absolute; position: absolute;
height: 100%; height: 100%;
@ -102,9 +126,17 @@ const sizes = computed(
left: 0; left: 0;
top: 0; top: 0;
} }
.svg { .play-button {
background-size: cover; position: absolute;
background-repeat: no-repeat; top: 50%;
left: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
svg {
width: 4rem;
height: auto;
}
} }
} }
@ -126,6 +158,11 @@ const sizes = computed(
} }
} }
&:not(.can-play) video {
opacity: 0;
pointer-events: none;
}
// fits // fits
&.fit-cover .image-sizer > * { &.fit-cover .image-sizer > * {
object-fit: cover; object-fit: cover;

View file

@ -0,0 +1,12 @@
<template>
<svg
class="svg-play"
width="15"
height="16"
viewBox="0 0 15 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0 16V0L14.607 8L0 16Z" fill="currentColor" />
</svg>
</template>