too lazy to think of a message, so enjoy this pointless text. Good luck future me...
All checks were successful
Test action / Print hello world (push) Successful in 6m12s

This commit is contained in:
Chris Kruining 2025-09-22 14:25:07 +00:00
parent a502a50176
commit ff31c28d38
46 changed files with 1403 additions and 1397 deletions

2
src/features/shell/index.tsx Normal file → Executable file
View file

@ -1 +1 @@
export { Shell } from './shell';
export { Shell } from './shell';

336
src/features/shell/nav.module.css Normal file → Executable file
View file

@ -1,168 +1,168 @@
.nav {
grid-area: 2 / 1 / 3 / 2;
display: block grid;
grid-auto-flow: row;
justify-content: space-between;
inline-size: 5em;
block-size: 100%;
padding: 1em;
background: inherit;
z-index: 0;
transition: z-index 0.3s step-end;
& > ul {
position: relative;
display: block grid;
grid-template-columns: 2.5rem auto;
align-content: center;
inline-size: 4rem;
gap: 1rem;
transform-origin: left center;
padding: 0;
padding-inline-start: 0.5rem;
margin: 0;
&::before {
content: "";
position: absolute;
inset-inline-start: 100%;
inset-block: -1em;
inline-size: 40vw;
background-image: linear-gradient(to right, rgb(from var(--surface-1) r g b / .9) 50%, transparent);
mask: radial-gradient(
ellipse 40vw 100% at left center,
black 25%,
transparent
);
backdrop-filter: blur(5px);
opacity: 0;
transition: opacity 0.3s var(--ease-3);
}
& > a {
position: relative;
grid-column: span 2;
display: block grid;
grid-template-columns: subgrid;
align-items: center;
text-decoration: none;
transform-origin: center left;
transition:
transform 2s var(--ease-spring-5),
opacity 0.3s var(--ease-3);
color: var(--text-2);
font-size: 2rem;
line-height: 1.5;
& > span {
opacity: 0;
transition: opacity 0.3s var(--ease-3);
text-shadow: 0 0 .5em var(--surface-1);
}
& > svg {
fill: var(--text-2);
inline-size: 2.5rem;
block-size: 2.5rem;
}
&.active {
color: var(--yellow-5);
list-style: disc;
&::before {
content: "•";
position: absolute;
inset-inline-start: -1rem;
}
& > svg {
fill: var(--yellow-5);
}
}
}
&:has(a:is(:hover, :focus)) {
&::before {
opacity: 1;
}
& > a {
transform: scale(max(1, calc(1.5 - (0.2 * abs(var(--target) - var(--sibling-index))))));
& > span {
opacity: 1;
}
}
}
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
opacity: 0.25;
}
&:has(a:is(:hover, :focus):nth-child(1)) {
--target: 1;
}
&:has(a:is(:hover, :focus):nth-child(2)) {
--target: 2;
}
&:has(a:is(:hover, :focus):nth-child(3)) {
--target: 3;
}
&:has(a:is(:hover, :focus):nth-child(4)) {
--target: 4;
}
&:has(a:is(:hover, :focus):nth-child(5)) {
--target: 5;
}
&:has(a:is(:hover, :focus):nth-child(6)) {
--target: 6;
}
&:has(a:is(:hover, :focus):nth-child(7)) {
--target: 7;
}
&:has(a:is(:hover, :focus):nth-child(8)) {
--target: 8;
}
&:has(a:is(:hover, :focus):nth-child(9)) {
--target: 9;
}
&:has(a:is(:hover, :focus):nth-child(10)) {
--target: 10;
}
&:has(a:is(:hover, :focus):nth-child(11)) {
--target: 11;
}
&:has(a:is(:hover, :focus):nth-child(12)) {
--target: 12;
}
&:has(a:is(:hover, :focus):nth-child(13)) {
--target: 13;
}
&:has(a:is(:hover, :focus):nth-child(14)) {
--target: 14;
}
&:has(a:is(:hover, :focus):nth-child(15)) {
--target: 15;
}
}
&:has(a:hover, :focus-within) {
z-index: 1;
transition: z-index 0.3s step-start;
}
}
.nav {
grid-area: 2 / 1 / 3 / 2;
display: block grid;
grid-auto-flow: row;
justify-content: space-between;
inline-size: 5em;
block-size: 100%;
padding: 1em;
background: inherit;
z-index: 0;
transition: z-index 0.3s step-end;
& > ul {
position: relative;
display: block grid;
grid-template-columns: 2.5rem auto;
align-content: center;
inline-size: 4rem;
gap: 1rem;
transform-origin: left center;
padding: 0;
padding-inline-start: 0.5rem;
margin: 0;
&::before {
content: "";
position: absolute;
inset-inline-start: 100%;
inset-block: -1em;
inline-size: 40vw;
background-image: linear-gradient(to right, rgb(from var(--surface-1) r g b / .9) 50%, transparent);
mask: radial-gradient(
ellipse 40vw 100% at left center,
black 25%,
transparent
);
backdrop-filter: blur(5px);
opacity: 0;
transition: opacity 0.3s var(--ease-3);
}
& > a {
position: relative;
grid-column: span 2;
display: block grid;
grid-template-columns: subgrid;
align-items: center;
text-decoration: none;
transform-origin: center left;
transition:
transform 2s var(--ease-spring-5),
opacity 0.3s var(--ease-3);
color: var(--text-2);
font-size: 2rem;
line-height: 1.5;
& > span {
opacity: 0;
transition: opacity 0.3s var(--ease-3);
text-shadow: 0 0 .5em var(--surface-1);
}
& > svg {
fill: var(--text-2);
inline-size: 2.5rem;
block-size: 2.5rem;
}
&.active {
color: var(--yellow-5);
list-style: disc;
&::before {
content: "•";
position: absolute;
inset-inline-start: -1rem;
}
& > svg {
fill: var(--yellow-5);
}
}
}
&:has(a:is(:hover, :focus)) {
&::before {
opacity: 1;
}
& > a {
transform: scale(max(1, calc(1.5 - (0.2 * abs(var(--target) - var(--sibling-index))))));
& > span {
opacity: 1;
}
}
}
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
opacity: 0.25;
}
&:has(a:is(:hover, :focus):nth-child(1)) {
--target: 1;
}
&:has(a:is(:hover, :focus):nth-child(2)) {
--target: 2;
}
&:has(a:is(:hover, :focus):nth-child(3)) {
--target: 3;
}
&:has(a:is(:hover, :focus):nth-child(4)) {
--target: 4;
}
&:has(a:is(:hover, :focus):nth-child(5)) {
--target: 5;
}
&:has(a:is(:hover, :focus):nth-child(6)) {
--target: 6;
}
&:has(a:is(:hover, :focus):nth-child(7)) {
--target: 7;
}
&:has(a:is(:hover, :focus):nth-child(8)) {
--target: 8;
}
&:has(a:is(:hover, :focus):nth-child(9)) {
--target: 9;
}
&:has(a:is(:hover, :focus):nth-child(10)) {
--target: 10;
}
&:has(a:is(:hover, :focus):nth-child(11)) {
--target: 11;
}
&:has(a:is(:hover, :focus):nth-child(12)) {
--target: 12;
}
&:has(a:is(:hover, :focus):nth-child(13)) {
--target: 13;
}
&:has(a:is(:hover, :focus):nth-child(14)) {
--target: 14;
}
&:has(a:is(:hover, :focus):nth-child(15)) {
--target: 15;
}
}
&:has(a:hover, :focus-within) {
z-index: 1;
transition: z-index 0.3s step-start;
}
}

50
src/features/shell/nav.tsx Normal file → Executable file
View file

@ -1,25 +1,25 @@
import { A } from "@solidjs/router";
import { AiOutlineHome, AiOutlineStar, AiOutlineSearch } from "solid-icons/ai";
import { Component } from "solid-js";
import css from "./nav.module.css";
export const Nav: Component = (props) => {
return (
<nav class={css.nav}>
<ul>
<A href="/" activeClass={css.active} end={true}>
<AiOutlineHome />
<span>Home</span>
</A>
<A href="/library" activeClass={css.active}>
<AiOutlineStar />
<span>Library</span>
</A>
<A href="/search" activeClass={css.active}>
<AiOutlineSearch />
<span>Search</span>
</A>
</ul>
</nav>
);
};
import { A } from "@solidjs/router";
import { AiOutlineHome, AiOutlineStar, AiOutlineSearch } from "solid-icons/ai";
import { Component } from "solid-js";
import css from "./nav.module.css";
export const Nav: Component = (props) => {
return (
<nav class={css.nav}>
<ul>
<A href="/" activeClass={css.active} end={true}>
<AiOutlineHome />
<span>Home</span>
</A>
<A href="/library" activeClass={css.active}>
<AiOutlineStar />
<span>Library</span>
</A>
<A href="/search" activeClass={css.active}>
<AiOutlineSearch />
<span>Search</span>
</A>
</ul>
</nav>
);
};

102
src/features/shell/shell.module.css Normal file → Executable file
View file

@ -1,52 +1,52 @@
.container {
position: relative;
display: block grid;
grid: auto 1fr / 5em 1fr;
grid-template-areas:
"top top"
"nav content";
inline-size: 100%;
block-size: 100%;
z-index: 0;
overflow: clip;
container-type: inline-size;
background-color: var(--surface-1);
contain: layout style paint;
&::after {
content: '';
grid-area: content;
display: block;
position: absolute;
inset-inline-start: 0;
inset-block-start: 0;
inline-size: var(--radius-4);
block-size: var(--radius-4);
background: radial-gradient(circle at bottom right, transparent var(--radius-4), var(--surface-1) var(--radius-4));
pointer-events: none;
}
}
.body {
grid-area: 2 / 1 / 3 / 3;
inline-size: 100%;
block-size: 100%;
background: linear-gradient(180deg,
transparent,
transparent 90vh,
var(--surface-500) 90vh,
var(--surface-500));
overflow: clip auto;
padding-inline-start: 5em;
transition: filter var(--duration-moderate-1) var(--ease-3);
container-type: size;
& > div {
background-color: var(--surface-2);
container-type: inline-size;
contain: layout style paint;
inline-size: 100%;
block-size: fit-content;
min-block-size: 100%;
}
.container {
position: relative;
display: block grid;
grid: auto 1fr / 5em 1fr;
grid-template-areas:
"top top"
"nav content";
inline-size: 100%;
block-size: 100%;
z-index: 0;
overflow: clip;
container-type: inline-size;
background-color: var(--surface-1);
contain: layout style paint;
&::after {
content: '';
grid-area: content;
display: block;
position: absolute;
inset-inline-start: 0;
inset-block-start: 0;
inline-size: var(--radius-4);
block-size: var(--radius-4);
background: radial-gradient(circle at bottom right, transparent var(--radius-4), var(--surface-1) var(--radius-4));
pointer-events: none;
}
}
.body {
grid-area: 2 / 1 / 3 / 3;
inline-size: 100%;
block-size: 100%;
background: linear-gradient(180deg,
transparent,
transparent 90vh,
var(--surface-500) 90vh,
var(--surface-500));
overflow: clip auto;
padding-inline-start: 5em;
transition: filter var(--duration-moderate-1) var(--ease-3);
container-type: size;
& > div {
background-color: var(--surface-2);
container-type: inline-size;
contain: layout style paint;
inline-size: 100%;
block-size: fit-content;
min-block-size: 100%;
}
}

44
src/features/shell/shell.tsx Normal file → Executable file
View file

@ -1,22 +1,22 @@
import { ParentComponent } from "solid-js";
import { Top } from "./top";
import { Nav } from "./nav";
import css from "./shell.module.css";
import { User } from "../user";
interface ShellProps {
user: User | undefined;
}
export const Shell: ParentComponent<ShellProps> = (props) => {
return (
<main class={css.container}>
<Top user={props.user} />
<Nav />
<div class={css.body}>
<div>{props.children}</div>
</div>
</main>
);
};
import { ParentComponent } from "solid-js";
import { Top } from "./top";
import { Nav } from "./nav";
import css from "./shell.module.css";
import { User } from "../user";
interface ShellProps {
user: User | undefined;
}
export const Shell: ParentComponent<ShellProps> = (props) => {
return (
<main class={css.container}>
<Top user={props.user} />
<Nav />
<div class={css.body}>
<div>{props.children}</div>
</div>
</main>
);
};

74
src/features/shell/top.module.css Normal file → Executable file
View file

@ -1,37 +1,37 @@
.top {
grid-area: 1 / 1 / 2 / 3;
display: block grid;
grid-auto-flow: column;
justify-content: end;
z-index: 1;
background-color: inherit;
padding: 0.5em;
}
.accountTrigger {
anchor-name: --account-trigger;
background: transparent;
padding: 0;
margin: 0;
border-radius: var(--radius-round);
}
.accountMenu {
position-anchor: --account-trigger;
position: absolute;
inset: auto;
inset-inline-end: anchor(end);
inset-block-start: anchor(start);
display: block grid;
grid-auto-flow: row;
gap: var(--size-3);
padding: var(--size-3);
background-color: light-dark(var(--gray-1), var(--gray-9));
border-radius: var(--radius-2);
box-shadow: var(--shadow-2);
&:not(:popover-open) {
display: none;
}
}
.top {
grid-area: 1 / 1 / 2 / 3;
display: block grid;
grid-auto-flow: column;
justify-content: end;
z-index: 1;
background-color: inherit;
padding: 0.5em;
}
.accountTrigger {
anchor-name: --account-trigger;
background: transparent;
padding: 0;
margin: 0;
border-radius: var(--radius-round);
}
.accountMenu {
position-anchor: --account-trigger;
position: absolute;
inset: auto;
inset-inline-end: anchor(end);
inset-block-start: anchor(start);
display: block grid;
grid-auto-flow: row;
gap: var(--size-3);
padding: var(--size-3);
background-color: light-dark(var(--gray-1), var(--gray-9));
border-radius: var(--radius-2);
box-shadow: var(--shadow-2);
&:not(:popover-open) {
display: none;
}
}

118
src/features/shell/top.tsx Normal file → Executable file
View file

@ -1,59 +1,59 @@
import { Component, Show } from "solid-js";
import { signIn, signOut, client } from "~/auth.client";
import { Avatar, Profile, User } from "../user";
import { ColorSchemePicker } from "../theme";
import css from "./top.module.css";
interface TopProps {
user: User | undefined;
}
export const Top: Component<TopProps> = (props) => {
const login = async (e: SubmitEvent) => {
e.preventDefault();
await signIn.oauth2({
providerId: "authelia",
callbackURL: "/",
});
};
const logout = async (e: SubmitEvent) => {
e.preventDefault();
await signOut();
};
return (
<aside class={css.top}>
<Show
when={props.user}
fallback={
<form method="post" onSubmit={login}>
<button type="submit">Sign in</button>
</form>
}
>
{(user) => (
<>
<button
class={css.accountTrigger}
id="account-menu-trigger"
popovertarget="account-menu-popover"
>
<Avatar user={user()} />
</button>
<div class={css.accountMenu} id="account-menu-popover" popover>
<Profile user={user()} />
<a href="/settings">Settings</a>
<form method="post" onSubmit={logout}>
<button type="submit">Log out</button>
</form>
</div>
</>
)}
</Show>
<ColorSchemePicker />
</aside>
);
};
import { Component, Show } from "solid-js";
import { signIn, signOut, client } from "~/auth.client";
import { Avatar, Profile, User } from "../user";
import { ColorSchemePicker } from "../theme";
import css from "./top.module.css";
interface TopProps {
user: User | undefined;
}
export const Top: Component<TopProps> = (props) => {
const login = async (e: SubmitEvent) => {
e.preventDefault();
await signIn.oauth2({
providerId: "authelia",
callbackURL: "/",
});
};
const logout = async (e: SubmitEvent) => {
e.preventDefault();
await signOut();
};
return (
<aside class={css.top}>
<Show
when={props.user}
fallback={
<form method="post" onSubmit={login}>
<button type="submit">Sign in</button>
</form>
}
>
{(user) => (
<>
<button
class={css.accountTrigger}
id="account-menu-trigger"
popovertarget="account-menu-popover"
>
<Avatar user={user()} />
</button>
<div class={css.accountMenu} id="account-menu-popover" popover>
<Profile user={user()} />
<a href="/settings">Settings</a>
<form method="post" onSubmit={logout}>
<button type="submit">Log out</button>
</form>
</div>
</>
)}
</Show>
<ColorSchemePicker />
</aside>
);
};

0
src/features/theme/context.ts Normal file → Executable file
View file

0
src/features/theme/index.ts Normal file → Executable file
View file

0
src/features/theme/picker.module.css Normal file → Executable file
View file

0
src/features/theme/picker.tsx Normal file → Executable file
View file

14
src/features/user/avatar.module.css Normal file → Executable file
View file

@ -1,7 +1,7 @@
.avatar {
inline-size: var(--size-8);
border-radius: var(--radius-round);
aspect-ratio: 1;
object-fit: cover;
object-position: center;
}
.avatar {
inline-size: var(--size-8);
border-radius: var(--radius-round);
aspect-ratio: 1;
object-fit: cover;
object-position: center;
}

54
src/features/user/avatar.tsx Normal file → Executable file
View file

@ -1,27 +1,27 @@
import { Component, createMemo, Show } from "solid-js";
import { User } from "./user";
import { hash } from "~/utilities";
import css from "./avatar.module.css";
interface AvatarProps {
user: User | undefined;
}
export const Avatar: Component<AvatarProps> = (props) => {
const hashedEmail = hash("SHA-256", () => props.user?.email);
const src = createMemo(() => {
const user = props.user;
if (user === undefined) {
return "";
}
if (user.image === null) {
return `https://www.gravatar.com/avatar/${hashedEmail()}`;
}
return user.image;
});
return <img src={src()} class={css.avatar} />;
};
import { Component, createMemo, Show } from "solid-js";
import { User } from "./user";
import { hash } from "~/utilities";
import css from "./avatar.module.css";
interface AvatarProps {
user: User | undefined;
}
export const Avatar: Component<AvatarProps> = (props) => {
const hashedEmail = hash("SHA-256", () => props.user?.email);
const src = createMemo(() => {
const user = props.user;
if (user === undefined) {
return "";
}
if (user.image === null) {
return `https://www.gravatar.com/avatar/${hashedEmail()}`;
}
return user.image;
});
return <img src={src()} class={css.avatar} />;
};

8
src/features/user/index.ts Normal file → Executable file
View file

@ -1,4 +1,4 @@
export type { User } from "./user";
export { Avatar } from "./avatar";
export { Profile } from "./profile";
export type { User } from "./user";
export { Avatar } from "./avatar";
export { Profile } from "./profile";

44
src/features/user/profile.module.css Normal file → Executable file
View file

@ -1,22 +1,22 @@
.profile {
display: block grid;
grid: auto 1fr / auto 1fr;
gap: var(--size-2);
place-content: start;
background-color: light-dark(var(--gray-1), var(--gray-9));
& > img {
grid-area: span 2 / 1;
}
& > strong {
font-size: var(--size-4);
line-height: 1;
color: light-dark(var(--gray-7), var(--gray-3));
}
& > span {
line-height: 1;
color: light-dark(var(--gray-4), var(--gray-6));
}
}
.profile {
display: block grid;
grid: auto 1fr / auto 1fr;
gap: var(--size-2);
place-content: start;
background-color: light-dark(var(--gray-1), var(--gray-9));
& > img {
grid-area: span 2 / 1;
}
& > strong {
font-size: var(--size-4);
line-height: 1;
color: light-dark(var(--gray-7), var(--gray-3));
}
& > span {
line-height: 1;
color: light-dark(var(--gray-4), var(--gray-6));
}
}

36
src/features/user/profile.tsx Normal file → Executable file
View file

@ -1,18 +1,18 @@
import { Component } from "solid-js";
import { User } from "./user";
import { Avatar } from "./avatar";
import css from "./profile.module.css";
interface ProfileProps {
user: User | undefined;
}
export const Profile: Component<ProfileProps> = (props) => {
return (
<div class={css.profile}>
<Avatar user={props.user} />
<strong>{props.user?.name ?? ""}</strong>
<span>{props.user?.email ?? ""}</span>
</div>
);
};
import { Component } from "solid-js";
import { User } from "./user";
import { Avatar } from "./avatar";
import css from "./profile.module.css";
interface ProfileProps {
user: User | undefined;
}
export const Profile: Component<ProfileProps> = (props) => {
return (
<div class={css.profile}>
<Avatar user={props.user} />
<strong>{props.user?.name ?? ""}</strong>
<span>{props.user?.email ?? ""}</span>
</div>
);
};

12
src/features/user/user.ts Normal file → Executable file
View file

@ -1,6 +1,6 @@
export interface User {
username: string;
name: string;
email: string;
image: string | null;
}
export interface User {
username: string;
name: string;
email: string;
image: string | null;
}