summaryrefslogtreecommitdiffstats
path: root/src/ui/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/components')
-rw-r--r--src/ui/components/Logo.tsx34
-rw-r--r--src/ui/components/ReleaseCard.tsx47
-rw-r--r--src/ui/components/SiteFooter.tsx39
-rw-r--r--src/ui/components/SiteHeader.tsx121
-rw-r--r--src/ui/components/SiteNav.tsx38
5 files changed, 279 insertions, 0 deletions
diff --git a/src/ui/components/Logo.tsx b/src/ui/components/Logo.tsx
new file mode 100644
index 0000000..a9ec9be
--- /dev/null
+++ b/src/ui/components/Logo.tsx
@@ -0,0 +1,34 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3import site from "@/corvos.config.ts";
4
5export type Props = HTMLAttributes<"a"> & {
6 size?: "sm" | "lg";
7 variant?: "static-black" | "static-white";
8};
9
10export const Logo = async ({ size, variant, class: cls, ...props }: Props) => {
11 const symbols = await site.load("assets/symbols.svg");
12
13 return (
14 <a
15 aria-label="Home"
16 class:list={[
17 "c-logo",
18 {
19 [`c-logo--${size}`]: size,
20 [`c-logo--${variant}`]: variant,
21 },
22 cls,
23 ]}
24 href={site.$.url()}
25 rel="home"
26 {...props}
27 >
28 <svg class="c-logo__img">
29 <title>Logo</title>
30 <use href={`${symbols.url}#logo${size === "sm" ? "-sm" : ""}`} />
31 </svg>
32 </a>
33 );
34};
diff --git a/src/ui/components/ReleaseCard.tsx b/src/ui/components/ReleaseCard.tsx
new file mode 100644
index 0000000..bffe256
--- /dev/null
+++ b/src/ui/components/ReleaseCard.tsx
@@ -0,0 +1,47 @@
1import type { Select } from "corvos";
2
3import type site from "@/corvos.config.ts";
4
5import { CardBody } from "$/ui/objects/CardBody.tsx";
6import { Heading } from "$/ui/objects/Heading.tsx";
7
8import { Card, type Props as CardProps } from "../objects/Card.tsx";
9
10export type Props = CardProps<"a"> & {
11 "heading-level": "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
12 "page": Select<typeof site, ".html", { type: "release" }>;
13};
14
15export const ReleaseCard = ({ "heading-level": headingLevel, page, ...props }: Props) => {
16 return (
17 <Card
18 as="a"
19 class="c-with-visited-indicator h-entry u-url"
20 horizontal
21 href={page.url}
22 {...props}
23 >
24 <CardBody class="c-with-visited-indicator__indicator u-pie-0" />
25
26 <CardBody main>
27 <Heading as={headingLevel} class="p-name">
28 <span class="u-mie-100">{page.data.title}</span>
29 </Heading>
30 </CardBody>
31
32 <CardBody as="small" class="u-c-mute u-d-none@sm-lo">
33 <ul class="l-hlist l-hlist--separated">
34 <li class="l-hlist__item">
35 <time class="dt-published" datetime={page.data.date.toISOString()}>
36 {page.data.date.toLocaleDateString(undefined, {
37 day: "numeric",
38 month: "short",
39 year: "numeric",
40 })}
41 </time>
42 </li>
43 </ul>
44 </CardBody>
45 </Card>
46 );
47};
diff --git a/src/ui/components/SiteFooter.tsx b/src/ui/components/SiteFooter.tsx
new file mode 100644
index 0000000..8bada2e
--- /dev/null
+++ b/src/ui/components/SiteFooter.tsx
@@ -0,0 +1,39 @@
1import site from "@/corvos.config.ts";
2
3import { Container } from "../layouts/Container.tsx";
4import { Divider } from "../objects/Divider.tsx";
5import { Logo } from "./Logo.tsx";
6
7export const SiteFooter = async () => {
8 const symbols = await site.load("assets/symbols.svg");
9
10 return (
11 <footer class="c-site-footer">
12 <div class="c-site-footer__banner">
13 <svg class="c-site-footer__banner-img">
14 <use href={`${symbols.url}#site-footer`} />
15 </svg>
16
17 <svg class="c-site-footer__banner-bg">
18 <use href={`${symbols.url}#site-footer-bg`} />
19 </svg>
20 </div>
21
22 <div class="c-site-footer__content-wrap t-static-white">
23 <Container class="c-site-footer__content" fixed="150" pad-block pad-inline>
24 <div class="c-site-footer__sections">
25 <section class="c-site-footer__section s-links s-links--mark-external s-links--static-white">
26 Developed by <a href="https://volpeon.ink/">Volpeon</a>
27 </section>
28 </div>
29
30 <Divider class="c-site-footer__divider" level="quiet" variant="static-white" />
31
32 <section class="c-site-footer__logo">
33 <Logo size="lg" variant="static-white" />
34 </section>
35 </Container>
36 </div>
37 </footer>
38 );
39};
diff --git a/src/ui/components/SiteHeader.tsx b/src/ui/components/SiteHeader.tsx
new file mode 100644
index 0000000..17831a1
--- /dev/null
+++ b/src/ui/components/SiteHeader.tsx
@@ -0,0 +1,121 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3import { Icon } from "$/ui/objects/Icon.tsx";
4
5import { nav } from "../../data/nav.ts";
6import { ActionButton } from "../objects/ActionButton.tsx";
7import { Navbar } from "../objects/Navbar.tsx";
8import { NavbarItem } from "../objects/NavbarItem.tsx";
9import { Logo } from "./Logo.tsx";
10
11export type Props = HTMLAttributes<"header"> & {
12 "url": string;
13 "mode"?: "compact" | "none";
14 "at-end"?: boolean;
15 "hidden"?: boolean;
16 "sm"?: JSX.Element;
17 "end"?: JSX.Element;
18 "children"?: JSX.Element;
19};
20
21export const SiteHeader = ({
22 children,
23 sm,
24 url,
25 mode,
26 "at-end": atEnd,
27 hidden,
28 end,
29 class: cls,
30 ...props
31}: Props) => {
32 const navItems = nav.map((item) => ({
33 ...item,
34 selected: new RegExp(`^${item.url.replace("/#", "/")}`).test(url),
35 }));
36
37 const parent = navItems
38 .filter((item) => item.url !== url)
39 .reduce(
40 (parent: (typeof navItems)[number] | undefined, item) =>
41 item.selected && item.url.length > (parent?.url.length ?? 0) ? item : parent,
42 undefined,
43 );
44 const selected = navItems.reduce(
45 (sel, item) => (item.selected && item.url.length > sel.url.length ? item : sel),
46 {
47 icon: "",
48 label: "Back",
49 selected: true,
50 url: "/",
51 },
52 );
53
54 const variant = atEnd ? "static-white" : undefined;
55
56 return (
57 <header
58 class:list={[
59 "c-site-header",
60 "l-media",
61 { "c-site-header--end": atEnd, "c-site-header--hidden": hidden },
62 cls,
63 ]}
64 {...props}
65 >
66 <Logo class="c-site-header__logo l-media__block" variant={variant} />
67
68 {mode !== "none" && (
69 <>
70 <Navbar aria-label="Mainmenu" class="c-site-header__nav l-media__block" variant={variant}>
71 {mode === "compact" ? (
72 <NavbarItem
73 href={selected.url}
74 pre={<Icon icon="arrow-left" variant="bold" />}
75 selected
76 >
77 {selected.label}
78 </NavbarItem>
79 ) : (
80 navItems
81 .filter((item) => item.url !== "/" && !item.hidden)
82 .map((item) => (
83 <NavbarItem as="a" href={item.url} selected={item.selected}>
84 {item.label}
85 </NavbarItem>
86 ))
87 )}
88 </Navbar>
89
90 {parent && (
91 <ActionButton
92 align-block
93 class="c-site-header__back l-media__block"
94 href={parent.url}
95 icon={<Icon icon="arrow-left" variant="bold" />}
96 level="quiet"
97 pill
98 />
99 )}
100 </>
101 )}
102
103 {children}
104
105 {sm && <div class="c-site-header__slot c-site-header__slot--sm">{sm}</div>}
106
107 {end && (
108 <>
109 <div class="c-site-header__spacer" />
110 <div class="c-site-header__slot c-site-header__slot--end">{end}</div>
111 </>
112 )}
113
114 <Logo
115 class="c-site-header__logo c-site-header__logo--sm l-media__block"
116 size="sm"
117 variant={variant}
118 />
119 </header>
120 );
121};
diff --git a/src/ui/components/SiteNav.tsx b/src/ui/components/SiteNav.tsx
new file mode 100644
index 0000000..7518e3c
--- /dev/null
+++ b/src/ui/components/SiteNav.tsx
@@ -0,0 +1,38 @@
1import { Icon } from "$/ui/objects/Icon.tsx";
2
3import { nav } from "../../data/nav.ts";
4import { Navbar } from "../objects/Navbar.tsx";
5import { NavbarItem } from "../objects/NavbarItem.tsx";
6
7export interface Props {
8 url: string;
9}
10
11export const SiteNav = ({ url }: Props) => {
12 const navItems = nav
13 .filter((item) => !item.hidden)
14 .map((item) => ({
15 ...item,
16 selected: item.url === "/" ? item.url === url : new RegExp(`^${item.url}`).test(url),
17 }));
18
19 return (
20 <>
21 <div class="c-site-nav-placeholder"></div>
22
23 <footer class="c-site-nav">
24 <Navbar aria-label="Mainmenu" quiet variant="static-white">
25 {navItems.map((item) => (
26 <NavbarItem
27 href={item.url}
28 icon={<Icon icon={item.icon} variant={item.selected ? "fill" : "regular"} />}
29 selected={item.selected}
30 >
31 {item.label}
32 </NavbarItem>
33 ))}
34 </Navbar>
35 </footer>
36 </>
37 );
38};