.
All checks were successful
Test action / Print hello world (push) Successful in 6m38s

This commit is contained in:
Chris Kruining 2025-09-22 15:33:18 +02:00
parent 4f5bbac05e
commit a502a50176
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
46 changed files with 2561 additions and 91 deletions

View file

@ -1,21 +0,0 @@
.increment {
font-family: inherit;
font-size: inherit;
padding: 1em 2em;
color: #335d92;
background-color: rgba(68, 107, 158, 0.1);
border-radius: 2em;
border: 2px solid rgba(68, 107, 158, 0);
outline: none;
width: 200px;
font-variant-numeric: tabular-nums;
cursor: pointer;
}
.increment:focus {
border: 2px solid #335d92;
}
.increment:active {
background-color: rgba(68, 107, 158, 0.2);
}

View file

@ -1,11 +0,0 @@
import { createSignal } from "solid-js";
import "./Counter.css";
export default function Counter() {
const [count, setCount] = createSignal(0);
return (
<button class="increment" onClick={() => setCount(count() + 1)} type="button">
Clicks: {count()}
</button>
);
}

View file

@ -0,0 +1,33 @@
.container {
isolation: isolate;
display: block grid;
container-type: inline-size;
}
.header {
position: relative;
block-size: 80cqb;
&::after {
content: "";
position: absolute;
inset: 0;
display: block;
background: linear-gradient(
atan(var(--ratio, .2)),
var(--surface-2) 20em,
transparent 90%
);
}
& > .background {
position: absolute;
inset: 0;
block-size: 100%;
inline-size: 100%;
object-fit: cover;
object-position: center;
z-index: 0;
}
}

View file

@ -0,0 +1,35 @@
import { Component, createSignal, onCleanup, onMount } from "solid-js";
import { Entry } from "~/features/content";
import css from "./details.module.css";
interface DetailsProps {
entry: Entry;
}
export const Details: Component<DetailsProps> = (props) => {
const [header, setHeader] = createSignal<HTMLElement>();
onMount(() => {
const observer = new ResizeObserver(([entry]) => {
const { inlineSize, blockSize } = entry.contentBoxSize[0];
(entry.target as HTMLElement).style.setProperty(
"--ratio",
String((blockSize * 0.2) / inlineSize)
);
});
observer.observe(header()!);
onCleanup(() => observer.disconnect());
});
return (
<div class={css.container}>
<header ref={setHeader} class={css.header}>
<img class={css.background} src={props.entry.image} />
<h1>{props.entry.title}</h1>
</header>
</div>
);
};

View file

@ -0,0 +1,3 @@
export { Details } from './details';

View file

@ -0,0 +1,96 @@
.box {
display: contents;
&:has(> :popover-open) > .button {
background-color: var(--surface-500);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
.button {
position: relative;
display: grid;
grid-template-columns: inherit;
place-items: center start;
/* Make sure the height of the button does not collapse when it is empty */
block-size: 1em;
box-sizing: content-box;
padding: var(--size-2);
background-color: transparent;
border: none;
border-radius: var(--radius-2);
font-size: 1rem;
line-height: 1;
cursor: pointer;
&:hover {
background-color: var(--surface-4);
}
&:has(> .caret) {
padding-inline-end: calc(1em + (2 * var(--size-2)));
}
& > .caret {
position: absolute;
inset-inline-end: var(--size-2);
inset-block-start: 50%;
translate: 0 -50%;
inline-size: 1em;
}
}
.dialog {
display: none;
position: relative;
grid-template-columns: inherit;
inset-inline-start: anchor(start);
inset-block-start: anchor(end);
position-try-fallbacks: flip-block, flip-inline;
/* inline-size: anchor-size(self-inline); */
background-color: var(--surface-4);
padding: var(--size-2);
border: none;
box-shadow: var(--shadow-2);
&:popover-open {
display: grid;
}
& > header {
display: grid;
grid-column: 1 / -1;
gap: var(--size-1);
}
& > main {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
row-gap: var(--size-1);
}
}
.option {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
place-items: center start;
border-radius: var(--radius-2);
padding: var(--size-1);
margin-inline: calc(-1 * var(--size-1));
cursor: pointer;
&.selected {
background-color: color(from var(--cyan-4) srgb r g b / .1);
}
}

View file

@ -0,0 +1,52 @@
import { createSignal, JSX, createEffect, Show } from "solid-js";
import { FaSolidAngleDown } from "solid-icons/fa";
import css from './dropdown.module.css';
export interface DropdownApi {
show(): void;
hide(): void;
}
interface DropdownProps {
api?: (api: DropdownApi) => any,
id: string;
class?: string;
open?: boolean;
showCaret?: boolean;
text: JSX.Element;
children: JSX.Element;
}
export function Dropdown(props: DropdownProps) {
const [dialog, setDialog] = createSignal<HTMLDialogElement>();
const [open, setOpen] = createSignal<boolean>(props.open ?? false);
createEffect(() => {
dialog()?.[open() ? 'showPopover' : 'hidePopover']();
});
createEffect(() => {
props.api?.({
show() {
dialog()?.showPopover();
},
hide() {
dialog()?.hidePopover();
},
});
});
return <section class={`${css.box} ${props.class}`}>
<button id={`${props.id}_button`} popoverTarget={`${props.id}_dialog`} class={css.button}>
{props.text}
<Show when={props.showCaret}>
<FaSolidAngleDown class={css.caret} />
</Show>
</button>
<dialog ref={setDialog} id={`${props.id}_dialog`} anchor={`${props.id}_button`} popover class={css.dialog} onToggle={e => setOpen(e.newState === 'open')}>
{props.children}
</dialog>
</section>;
}

View file

@ -0,0 +1,4 @@
export type { DropdownApi } from './dropdown';
export { Dropdown } from './dropdown';

View file

@ -0,0 +1,174 @@
@property --thumb-image {
syntax: "<image>";
inherits: true;
}
.container {
isolation: isolate;
display: block grid;
grid-auto-flow: column;
grid-auto-columns: 100%;
container-type: inline-size;
overflow: hidden visible;
scroll-snap-type: inline mandatory;
overscroll-behavior-inline: contain;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
scroll-marker-group: after;
&::scroll-marker-group {
display: block grid;
grid-auto-flow: column;
grid-auto-columns: 5em;
gap: 1rem;
justify-content: start;
padding-inline: var(--size-6);
inline-size: 100%;
block-size: 8.333333em;
z-index: 1;
}
}
.page {
--__i: var(--sibling-index);
--__c: var(--sibling-count);
scroll-snap-align: center;
position: relative;
display: grid;
grid: repeat(3, auto) / 15em 1fr;
grid-template-areas:
"thumbnail . ."
"thumbnail title cta"
"thumbnail detail detail"
"thumbnail summary summary";
align-content: end;
align-items: center;
gap: 1rem;
padding: var(--size-6);
block-size: 80vh;
overflow: clip;
container-type: scroll-state;
animation:
animate-in linear forwards,
animate-out linear forwards;
animation-timeline: view(inline);
animation-range: entry, exit;
color: var(--gray-0);
&::after {
content: "";
position: absolute;
inset: 0;
display: block;
background: linear-gradient(182.5deg, transparent 20%, var(--surface-2) 90%),
linear-gradient(transparent 50%, #0007 75%);
}
&::scroll-marker {
display: block;
content: " ";
inline-size: 5rem;
aspect-ratio: 3 / 5;
background: var(--thumb-image) center / cover no-repeat;
background-color: cornflowerblue;
border-radius: var(--radius-2);
transform: scale(1);
transform-origin: top left;
transition: 0.3s;
}
&::scroll-marker:target-current {
/* outline: 1px solid white; */
transform: translate(calc(-0cqi - (6rem * (var(--__i) - 1))), -29rem)
scale(3);
}
}
.title {
grid-area: title;
font-size: 2.5em;
z-index: 1;
filter: contrast(9);
}
.cta {
grid-area: cta;
z-index: 1;
border-radius: var(--radius-2);
background-color: var(--gray-2);
color: var(--gray-8);
text-decoration-color: var(--gray-8);
padding: var(--size-3);
font-weight: var(--font-weight-9);
outline-offset: var(--size-1);
&:focus-visible {
outline: 1px solid var(--gray-2);
}
}
.thumbnail {
grid-area: thumbnail;
inline-size: 15em;
aspect-ratio: 3 / 5;
border-radius: var(--radius-3);
object-fit: cover;
object-position: center;
z-index: 1;
opacity: 0 !important;
}
.background {
position: absolute;
inset: 0;
block-size: 100%;
inline-size: 100%;
object-fit: cover;
object-position: center;
z-index: 0;
}
.detail {
grid-area: detail;
z-index: 1;
}
.summary {
grid-area: summary;
text-wrap: balance;
z-index: 1;
}
@keyframes animate-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes animate-out {
0% {
opacity: 1;
}
20% {
opacity: 0;
}
100% {
opacity: 0;
}
}

View file

@ -0,0 +1,61 @@
import { Component, createEffect, createMemo, For, Index } from "solid-js";
import { createSlug, Entry } from "~/features/content";
import css from "./hero.module.css";
type HeroProps = {
entries: Entry[];
class?: string;
};
export function Hero(props: HeroProps) {
return (
<div class={`${css.container} ${props.class ?? ""}`}>
<For each={props.entries}>{(entry) => <Page entry={entry} />}</For>
</div>
);
}
const Page: Component<{ entry: Entry }> = (props) => {
const slug = createMemo(() => createSlug(props.entry));
return (
<div
class={`${css.page}`}
style={{ "--thumb-image": `url(${props.entry.thumbnail})` }}
>
<h2 class={css.title}>{props.entry.title}</h2>
<a class={css.cta} href={`/play/${slug()}`}>
Continue
</a>
<img src={props.entry.thumbnail} class={css.thumbnail} />
{/* <img src={props.entry.image} class={css.background} /> */}
<video
class={css.background}
src={props.entry.trailer}
poster={props.entry.image}
muted
autoplay
/>
<span class={css.detail}>
{props.entry.releaseDate}
<Index each={props.entry.sources ?? []}>
{(source) => (
<>
&nbsp;&nbsp;
<a href={source().url.toString()} target="_blank">
{source().rating.score} {source().label}
</a>
</>
)}
</Index>
</span>
<p class={css.summary}>{props.entry.overview}</p>
</div>
);
};

View file

@ -0,0 +1 @@
export { Hero } from "./hero";

View file

@ -0,0 +1 @@
export { List } from "./list";

View file

@ -0,0 +1,92 @@
.container {
--_space: var(--size-6);
display: grid;
grid: auto auto / auto auto;
grid-template-areas:
"heading metadata"
"list list";
justify-content: space-between;
inline-size: 100%;
padding-inline: var(--_space);
}
.heading {
grid-area: heading;
font-size: var(--size-7);
color: var(--text-1);
padding-inline: var(--_space);
}
.metadata {
grid-area: metadata;
color: var(--text-2);
}
.list {
grid-area: list;
list-style-type: none;
container-type: inline-size;
display: grid;
grid-auto-flow: column;
gap: var(--_space);
padding: calc(8 * var(--_space)) calc(2 * var(--_space)) calc(2.5 * var(--_space));
scroll-padding: calc(2 * var(--_space));
margin: calc(-7 * var(--_space)) calc(-1 * var(--_space)) 0em;
overflow: visible auto;
scroll-snap-type: inline mandatory;
overscroll-behavior-inline: contain;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
/* the before and afters have unsnappable elements that create bouncy edges to the scroll */
&::before,
&::after {
content: "";
display: block;
}
&::before {
inline-size: 15cqi;
}
&::after {
inline-size: 100cqi;
}
& > li {
scroll-snap-align: start;
container-type: scroll-state;
padding: 0;
position: relative;
isolation: isolate;
z-index: calc(var(--sibling-count) - var(--sibling-index));
&:has(> :hover, > :focus-within) {
z-index: calc(var(--sibling-count) + 1);
}
& > * {
@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

@ -0,0 +1,27 @@
import { Accessor, Index, JSX } from "solid-js";
import css from "./list.module.css";
interface ListProps<T> {
label: string;
items: T[];
class?: string;
children: (item: Accessor<T>) => JSX.Element;
}
export function List<T>(props: ListProps<T>) {
return (
<section class={`${css.container} ${props.class ?? ""}`}>
<b role="heading" class={css.heading}>
{props.label}
</b>
<sub class={css.metadata}>{props.items.length} result(s)</sub>
<ul class={css.list}>
<Index each={props.items}>
{(item) => <li>{props.children(item)}</li>}
</Index>
</ul>
</section>
);
}

View file

@ -0,0 +1,3 @@
export { Select } from './select';

View file

@ -0,0 +1,176 @@
.box {
appearance: none;
display: block grid;
place-items: center start;
padding: var(--size-2);
background-color: transparent;
border: none;
border-radius: var(--radius-2);
font-size: 1rem;
&:hover {
background-color: var(--surface-700);
}
@supports (appearance: base-select) {
&,
&::picker(select) {
appearance: base-select;
}
&::picker(select) {
/* display: block grid;
row-gap: var(--size-2); */
background-color: var(--surface-3);
padding: var(--size-2) 0;
border: none;
box-shadow: var(--shadow-2);
opacity: 0;
block-size: 0;
overflow: clip;
transition:
height 0.5s ease-out,
opacity 0.5s ease-out,
overlay 0.5s,
display 0.5s,
overflow 0.5s;
transition-behavior: allow-discrete;
}
&:open {
background-color: var(--surface-3);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
&::picker(select) {
opacity: 1;
block-size: calc-size(auto, size);
overflow: auto;
@starting-style {
opacity: 0;
block-size: 0;
}
}
}
& > option {
display: block grid;
grid-auto-flow: column;
place-items: center start;
border-radius: var(--radius-2);
padding: var(--size-2);
cursor: pointer;
&:checked {
background-color: var(--surface-4);
}
&::checkmark {
display: none;
}
}
}
}
/* .box {
display: contents;
&:has(> :popover-open) > .button {
background-color: var(--surface-500);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
.button {
position: relative;
display: grid;
grid-template-columns: inherit;
place-items: center start;
block-size: 1em;
box-sizing: content-box;
padding: var(--size-2);
background-color: transparent;
border: none;
border-radius: var(--radius-2);
font-size: 1rem;
cursor: pointer;
&:hover {
background-color: var(--surface-700);
}
&:has(> .caret) {
padding-inline-end: calc(1em + (2 * var(--size-2)));
}
& > .caret {
position: absolute;
inset-inline-end: var(--size-2);
inset-block-start: 50%;
translate: 0 -50%;
inline-size: 1em;
}
}
.dialog {
display: none;
position: relative;
grid-template-columns: inherit;
inset-inline-start: anchor(start);
inset-block-start: anchor(end);
position-try-fallbacks: flip-block, flip-inline;
background-color: var(--surface-3);
padding: var(--size-2);
border: none;
box-shadow: var(--shadow-2);
&:popover-open {
display: grid;
}
& > header {
display: grid;
grid-column: 1 / -1;
gap: var(--padding-s);
}
& > main {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
row-gap: var(--padding-s);
}
}
.option {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
place-items: center start;
border-radius: var(--radii-m);
padding: var(--padding-s);
margin-inline: calc(-1 * var(--padding-s));
cursor: pointer;
&.selected {
background-color: oklch(from var(--info) l c h / .1);
}
} */

View file

@ -0,0 +1,67 @@
import { createMemo, createSignal, For, JSX, Setter, createEffect, Show } from "solid-js";
import { Dropdown, DropdownApi } from "../dropdown";
import css from './select.module.css';
interface SelectProps<T, K extends string> {
id: string;
class?: string;
value: K;
setValue?: Setter<K>;
values: Record<K, T>;
open?: boolean;
showCaret?: boolean;
children: (key: K, value: T) => JSX.Element;
filter?: (query: string, key: K, value: T) => boolean;
}
export function Select<T, K extends string>(props: SelectProps<T, K>) {
const [dropdown, setDropdown] = createSignal<DropdownApi>();
const [key, setKey] = createSignal<K>(props.value);
const [query, setQuery] = createSignal<string>('');
const showCaret = createMemo(() => props.showCaret ?? true);
const values = createMemo(() => {
let entries = Object.entries<T>(props.values) as [K, T][];
const filter = props.filter;
const q = query();
if (filter) {
entries = entries.filter(([k, v]) => filter(q, k, v));
}
return entries;
});
createEffect(() => {
props.setValue?.(() => key());
});
const text = <Show when={key()}>{
key => {
const value = createMemo(() => props.values[key()]);
return <>{props.children(key(), value())}</>;
}
}</Show>
return <Dropdown api={setDropdown} id={props.id} class={`${css.box} ${props.class}`} showCaret={showCaret()} open={props.open} text={text}>
<Show when={props.filter !== undefined}>
<header>
<input value={query()} onInput={e => setQuery(e.target.value)} />
</header>
</Show>
<main>
<For each={values()}>{
([k, v]) => {
const selected = createMemo(() => key() === k);
return <span class={`${css.option} ${selected() ? css.selected : ''}`} onpointerdown={() => {
setKey(() => k);
dropdown()?.hide();
}}>{props.children(k, v)}</span>;
}
}</For>
</main>
</Dropdown>
}