Files
homepage/src/lib/components/recipes/TitleImgParallax.svelte
Alexander Bocken 62515b95f0
All checks were successful
CI / update (push) Successful in 3m30s
fix: extend recipe hero images into status bar safe area
Account for env(safe-area-inset-top) in the hero negative margin-top
so images fill the status bar area on edge-to-edge Android instead of
leaving empty space.
2026-04-09 21:33:53 +02:00

166 lines
3.4 KiB
Svelte

<script>
import { onMount } from "svelte";
let { src, color = '', alt = "", transitionName = '', children } = $props();
let isredirected = $state(false);
onMount(() => {
fetch(src, { method: 'HEAD' })
.then(response => {
isredirected = response.redirected
})
})
function show_dialog_img(){
if(isredirected){
return
}
/** @type {HTMLDialogElement} */(document.querySelector("#img_carousel")).showModal();
}
function close_dialog_img(){
/** @type {HTMLDialogElement} */(document.querySelector("#img_carousel")).close();
}
import Cross from "$lib/assets/icons/Cross.svelte";
import "$lib/css/action_button.css";
import "$lib/css/shake.css";
import { do_on_key } from "$lib/components/recipes/do_on_key";
</script>
<style>
:root {
--scale: 0.3;
--space: 10vw;
--font-primary: 'Lato', sans-serif;
--font-heading: 'Playfair Display', serif;
}
@media (prefers-reduced-motion) {
:root {
--scale: 0;
}
}
* {
box-sizing: border-box;
}
.section {
margin-bottom: -20vh;
margin-top: calc(-3.5rem - 12px - env(safe-area-inset-top, 0px));
transform-origin: center top;
transform: scaleY(calc(1 - var(--scale)));
}
.section > * {
transform-origin: center top;
transform: scaleY(calc(1 / (1 - var(--scale))));
}
.content {
position: relative;
margin: 30vh auto 0;
}
.image-container {
position: sticky;
display: flex;
align-items: center;
justify-content: center;
top: 0;
height: max(55dvh, 540px);
z-index: -10;
margin: 0;
}
.image-wrap {
position: absolute;
top: 0;
left: 0;
right: 0;
margin-inline: auto;
width: min(calc(1000px + 2rem), 100dvw);
height: max(65dvh, 640px);
overflow: hidden;
}
.image{
display: block;
position: absolute;
top: 0;
width: min(calc(1000px + 2rem), 100dvw);
height: max(65dvh, 640px);
object-fit: cover;
object-position: 50% 20%;
}
.image-container::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 50%;
}
:global(h1){
width: 100%;
}
/* DIALOG */
dialog{
position: relative;
background-color: unset;
padding:0;
max-height: 90vh;
margin-inline: auto;
overflow: visible;
border: unset;
}
dialog img{
max-width: calc(95vmin - 2rem);
max-height: 95vmin; /* cannot use calc() for some reason */
}
dialog[open]::backdrop{
animation: show 200ms ease forwards;
}
@keyframes show{
from {
backdrop-filter: blur(0px);
}
to {
backdrop-filter: blur(10px);
}
}
dialog button{
position: absolute;
top: -2rem;
right: -2rem;
}
.zoom-in{
cursor: zoom-in;
}
</style>
<section class="section">
<figure class="image-container">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class:zoom-in={!isredirected} onclick={show_dialog_img}>
<div class="image-wrap" style:background-color={color}>
<img class="image" {src} {alt} style:view-transition-name={transitionName || null} fetchpriority="high"/>
</div>
<noscript>
<div class="image-wrap" style:background-color={color}>
<img class="image" {src} {alt}/>
</div>
</noscript>
</div>
</figure>
<div class=content>{@render children()}</div>
</section>
<dialog id=img_carousel>
<img {src} {alt}>
<button class=action_button onkeydown={(event) => do_on_key(event, 'Enter', false, close_dialog_img)} onclick={close_dialog_img}>
<Cross fill=white width=2rem height=2rem></Cross>
</button>
</dialog>