This commit is contained in:
parent
4f5bbac05e
commit
a502a50176
46 changed files with 2561 additions and 91 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
33
src/components/details/details.module.css
Normal file
33
src/components/details/details.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
35
src/components/details/details.tsx
Normal file
35
src/components/details/details.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
3
src/components/details/index.ts
Normal file
3
src/components/details/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
export { Details } from './details';
|
||||
96
src/components/dropdown/dropdown.module.css
Normal file
96
src/components/dropdown/dropdown.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
52
src/components/dropdown/dropdown.tsx
Normal file
52
src/components/dropdown/dropdown.tsx
Normal 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>;
|
||||
}
|
||||
4
src/components/dropdown/index.ts
Normal file
4
src/components/dropdown/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
|
||||
export type { DropdownApi } from './dropdown';
|
||||
export { Dropdown } from './dropdown';
|
||||
174
src/components/hero/hero.module.css
Normal file
174
src/components/hero/hero.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
61
src/components/hero/hero.tsx
Normal file
61
src/components/hero/hero.tsx
Normal 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) => (
|
||||
<>
|
||||
•
|
||||
<a href={source().url.toString()} target="_blank">
|
||||
{source().rating.score} {source().label}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</Index>
|
||||
</span>
|
||||
|
||||
<p class={css.summary}>{props.entry.overview}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
src/components/hero/index.ts
Normal file
1
src/components/hero/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Hero } from "./hero";
|
||||
1
src/components/list/index.ts
Normal file
1
src/components/list/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { List } from "./list";
|
||||
92
src/components/list/list.module.css
Normal file
92
src/components/list/list.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
27
src/components/list/list.tsx
Normal file
27
src/components/list/list.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
3
src/components/select/index.ts
Normal file
3
src/components/select/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
export { Select } from './select';
|
||||
176
src/components/select/select.module.css
Normal file
176
src/components/select/select.module.css
Normal 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);
|
||||
}
|
||||
} */
|
||||
67
src/components/select/select.tsx
Normal file
67
src/components/select/select.tsx
Normal 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>
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue