applied the cool new carousel css feature!

This commit is contained in:
Chris Kruining 2025-04-17 00:03:37 +02:00
parent 6a0c1cb377
commit 3142ac6185
Signed by: chris
SSH key fingerprint: SHA256:nG82MUfuVdRVyCKKWqhY+pCrbz9nbX6uzUns4RKa1Pg
8 changed files with 233 additions and 75 deletions

View file

@ -9,6 +9,9 @@
} }
.list { .list {
list-style-type: none;
container-type: inline-size;
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
@ -18,14 +21,53 @@
margin: -10em -4em 0em; margin: -10em -4em 0em;
overflow: visible auto; overflow: visible auto;
scroll-snap-type: inline proximity; scroll-snap-type: inline mandatory;
overscroll-behavior-inline: contain;
@media (hover: none) { @media (prefers-reduced-motion: no-preference) {
padding: 5em; scroll-behavior: smooth;
margin: 0; }
/* the before and afters have unsnappable elements that create bouncy edges to the scroll */
&::before,
&::after {
content: "";
display: block;
}
&::before {
order: 0;
inline-size: 15cqi;
}
&::after {
order: 11;
inline-size: 50cqi;
}
& > li {
scroll-snap-align: start;
container-type: scroll-state;
padding: 0;
position: relative;
order: calc(var(--sibling-count) - var(--sibling-index));
z-index: var(--sibling-index);
& > * { & > * {
scroll-snap-align: start; @supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
animation: slide-in linear both;
animation-timeline: view(inline);
animation-range: cover -100cqi contain 15cqi;
}
}
} }
} }
} }
@keyframes slide-in {
from {
transform: translateX(-100cqi) scale(0.5);
}
}

View file

@ -10,13 +10,15 @@ interface ListProps<T> {
export function List<T>(props: ListProps<T>) { export function List<T>(props: ListProps<T>) {
return ( return (
<section class={`${css.container} ${props.class ?? ''}`}> <section class={`${css.container} ${props.class ?? ""}`}>
<b role="heading" class={css.heading}> <b role="heading" class={css.heading}>
{props.label} {props.label}
</b> </b>
<ul class={css.list}> <ul class={css.list}>
<Index each={props.items}>{(item) => props.children(item)}</Index> <Index each={props.items}>
{(item) => <li>{props.children(item)}</li>}
</Index>
</ul> </ul>
</section> </section>
); );

View file

@ -13,8 +13,7 @@ const client = createClient<paths>({
export const listUsers = query(async () => { export const listUsers = query(async () => {
const { data, error } = await client.GET("/Users", { const { data, error } = await client.GET("/Users", {
params: { params: {},
},
}); });
return data ?? []; return data ?? [];
@ -62,7 +61,7 @@ export const getContinueWatching = query(
const items = (data?.Items ?? []).map(({ Id, Name }) => ({ const items = (data?.Items ?? []).map(({ Id, Name }) => ({
id: Id, id: Id,
title: Name, title: Name,
thumbnail: `${baseUrl}Items/${Id}/Images/Primary`, thumbnail: `${baseUrl}/Items/${Id}/Images/Primary`,
})); }));
return items; return items;

View file

@ -1,11 +1,15 @@
import type { Category, Entry } from "./types"; import type { Category, Entry } from "./types";
import { query } from "@solidjs/router"; import { query } from "@solidjs/router";
import { entries } from "./data"; import { entries } from "./data";
import { getContinueWatching } from "./apis/jellyfin";
export const listCategories = query(async (): Promise<Category[]> => { export const listCategories = query(async (): Promise<Category[]> => {
"use server"; "use server";
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
return [ return [
{ label: "Continue", entries: await getContinueWatching(jellyfinUserId) },
{ {
label: "Popular", label: "Popular",
entries: [ entries: [

View file

@ -25,15 +25,18 @@
/* Dot */ /* Dot */
radial-gradient(circle at 25% 30% #7772 #7774 1em transparent 1em), radial-gradient(circle at 25% 30% #7772 #7774 1em transparent 1em),
/* Dot */ /* Dot */
radial-gradient(circle at 85% 15% #7772 #7774 1em transparent 1em), radial-gradient(circle at 85% 15% #7772 #7774 1em transparent 1em),
/* Bottom fade */ /* Bottom fade */ linear-gradient(165deg transparent 60% #555 60% #333),
linear-gradient(165deg transparent 60% #555 60% #333),
/* wave dark part */ /* wave dark part */
radial-gradient(ellipse 5em 2.25em at 0.5em calc(50% - 1em) #333 100% transparent 100%), radial-gradient(
ellipse 5em 2.25em at 0.5em calc(50% - 1em) #333 100% transparent 100%
),
/* wave light part */ /* wave light part */
radial-gradient(ellipse 5em 2.25em at calc(100% - 0.5em) calc(50% + 1em) #555 100% transparent 100%), radial-gradient(
/* Base */ ellipse 5em 2.25em at calc(100% - 0.5em) calc(50% + 1em) #555 100%
linear-gradient(to bottom #333 50% #555 50%); transparent 100%
),
/* Base */ linear-gradient(to bottom #333 50% #555 50%);
transform-origin: 50% 0; transform-origin: 50% 0;
transform: scale(1.1) translateY(calc(-4 * var(--padding))); transform: scale(1.1) translateY(calc(-4 * var(--padding)));
@ -41,7 +44,7 @@
user-select: none; user-select: none;
} }
& > main { & > figcaption {
--offset: calc(1.5 * var(--padding)); --offset: calc(1.5 * var(--padding));
grid-area: 1/ 1; grid-area: 1/ 1;
display: grid; display: grid;
@ -98,11 +101,10 @@
will-change: transform; will-change: transform;
} }
& > main { & > figcaption {
clip-path: inset(40%); clip-path: inset(40%);
} }
} }
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
& { & {
transition: transform var(--duration-moderate-1) linear; transition: transform var(--duration-moderate-1) linear;
@ -112,7 +114,7 @@
transition: transform var(--duration-moderate-1) ease-in-out; transition: transform var(--duration-moderate-1) ease-in-out;
} }
& > main { & > figcaption {
transition: clip-path var(--duration-moderate-1) ease-in-out; transition: clip-path var(--duration-moderate-1) ease-in-out;
} }
@ -126,4 +128,4 @@
} }
} }
} }
} }

View file

@ -7,14 +7,14 @@ export const ListItem: Component<{ entry: Entry }> = (props) => {
const slug = createMemo(() => createSlug(props.entry)); const slug = createMemo(() => createSlug(props.entry));
return ( return (
<div class={css.listItem}> <figure class={css.listItem}>
<img src={props.entry.thumbnail} /> <img src={props.entry.thumbnail} alt={props.entry.title} />
<main> <figcaption>
<strong>{props.entry.title}</strong> <strong>{props.entry.title}</strong>
<a href={`/watch/${slug()}`}>Watch now</a> <a href={`/watch/${slug()}`}>Watch now</a>
</main> </figcaption>
</div> </figure>
); );
}; };

View file

@ -1,39 +1,121 @@
.list { .carousel {
display: block grid;
grid: auto 1fr / 100%;
& > header {
anchor-name: --carousel; anchor-name: --carousel;
overflow: auto; padding-inline: 3rem;
scroll-snap-type: inline mandatory; font-size: 1.75rem;
font-weight: 900;
}
& > ul {
list-style-type: none;
container-type: size;
inline-size: 100%;
block-size: min(60svh, 720px);
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
inline-size: 80%; overflow: visible auto;
scroll-snap-type: inline mandatory;
overscroll-behavior-inline: contain;
justify-self: center; justify-self: center;
& > li { gap: 1em;
inline-size: 30vw; padding-inline: 2em;
list-style: none; scroll-padding-inline: 2em;
scroll-snap-align: start; padding-block: 2em 4em;
margin-block-end: 5em;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
} }
&::scroll-button(inline-start), /* the before and afters have unsnappable elements that create bouncy edges to the scroll */
&::scroll-button(inline-end) { &::before,
position: fixed; &::after {
position-anchor: --carousel; content: "";
display: block;
}
&::before {
order: 0;
inline-size: 15cqi;
}
&::after {
order: 11;
inline-size: 50cqi;
}
&::scroll-button(*) {
z-index: 20;
background: oklch(from var(--surface-1) l c h / 50%);
backdrop-filter: blur(10px);
} }
&::scroll-button(inline-start) { &::scroll-button(inline-start) {
--_inner: center span-inline-end; position-area: center span-inline-start;
--_outer: inline-start center; content: "◄" / "Previous";
position-area: var(--_outer);
content: 'arrow_back' / 'Previous';
} }
&::scroll-button(inline-end) { &::scroll-button(inline-end) {
--_inner: center span-inline-start; position-area: center span-inline-end;
--_outer: inline-end center; content: "►" / "Next";
position-area: var(--_outer);
content: 'arrow_forward' / 'Next';
} }
}
& > li {
scroll-snap-align: start;
container-type: scroll-state;
padding: 0;
position: relative;
order: calc(var(--sibling-count) - var(--sibling-index));
z-index: var(--sibling-index);
& > figure {
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
animation: slide-in linear both;
animation-timeline: view(inline);
animation-range: cover -100cqi contain 25cqi;
}
}
@container scroll-state(snapped: inline) {
outline: 1px solid var(--gray-1);
outline-offset: 10px;
}
flex-shrink: 0;
block-size: 100cqb;
aspect-ratio: 9/16;
background: light-dark(#ccc, #444);
box-shadow: var(--shadow-5);
border-radius: 20px;
overflow: clip;
display: flex;
@container (width < 480px) {
block-size: 50cqb;
}
& > img {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}
}
}
}
}
@keyframes slide-in {
from {
transform: translateX(-100cqi) scale(0.75);
}
}

View file

@ -7,47 +7,74 @@ import {
getContinueWatching, getContinueWatching,
} from "~/features/content"; } from "~/features/content";
import { Show } from "solid-js"; import { Show } from "solid-js";
import { List } from "~/components/list"; import css from "./index.module.css";
import { ListItem } from "~/features/overview/list-item";
import css from './index.module.css';
export const route = { export const route = {
preload: async () => ({ preload: async () => ({
highlight: await getEntry("14"), highlight: await getEntry("14"),
categories: await listCategories(), categories: await listCategories(),
continue: await getContinueWatching("a9c51af84bf54578a99ab4dd0ebf0763"),
}), }),
}; };
export default function Home() { export default function Home() {
const highlight = createAsync(() => getEntry("14")); const highlight = createAsync(() => getEntry("14"));
const categories = createAsync(() => listCategories()); const categories = createAsync(() => listCategories());
const continueWatching = createAsync(() =>
getContinueWatching("a9c51af84bf54578a99ab4dd0ebf0763"),
);
return ( return (
<> <>
<Title>Home</Title> <Title>Home</Title>
<ul class={css.list}> {/* <div class={css.carousel}>
<li>Item 0</li> <header>some category</header>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
<li>Item 6</li>
<li>Item 7</li>
<li>Item 8</li>
<li>Item 9</li>
</ul>
{/* <Show when={continueWatching()}>{ <ul>
entries => <List label="Continue watching" items={entries()}> <li>
{(item) => <ListItem entry={item()} />} <figure>
</List> <img src="https://assets.codepen.io/2585/1.jpg" alt="Item 1" />
}</Show> */} </figure>
</li>
<li>
<figure>
<img src="https://assets.codepen.io/2585/2.avif" alt="Item 2" />
</figure>
</li>
<li>
<figure>
<img src="https://assets.codepen.io/2585/3.avif" alt="Item 3" />
</figure>
</li>
<li>
<figure>
<img src="https://assets.codepen.io/2585/4.avif" alt="Item 4" />
</figure>
</li>
<li>
<figure>
<img src="https://assets.codepen.io/2585/5.avif" alt="Item 5" />
</figure>
</li>
<li>
<figure>
<img src="https://assets.codepen.io/2585/6.avif" alt="Item 6" />
</figure>
</li>
<li>
<figure>
<img src="https://assets.codepen.io/2585/7.avif" alt="Item 7" />
</figure>
</li>
<li>
<figure>
<img src="https://assets.codepen.io/2585/8.avif" alt="Item 8" />
</figure>
</li>
<li>
<figure>
<img src="https://assets.codepen.io/2585/9.avif" alt="Item 9" />
</figure>
</li>
</ul>
</div> */}
<Show when={highlight() && categories()}> <Show when={highlight() && categories()}>
<Overview highlight={highlight()!} categories={categories()!} /> <Overview highlight={highlight()!} categories={categories()!} />