diff options
| author | Volpeon <git@volpeon.ink> | 2026-07-22 19:40:59 +0200 |
|---|---|---|
| committer | Volpeon <git@volpeon.ink> | 2026-07-22 19:40:59 +0200 |
| commit | c1e28224aa2e1450031669d9a7e359b63e04687d (patch) | |
| tree | 68f3d8905c763a91f68001d868197effa92adafa /src/ui/components | |
| download | corvos-website-c1e28224aa2e1450031669d9a7e359b63e04687d.tar.gz corvos-website-c1e28224aa2e1450031669d9a7e359b63e04687d.tar.bz2 corvos-website-c1e28224aa2e1450031669d9a7e359b63e04687d.zip | |
Init
Diffstat (limited to 'src/ui/components')
| -rw-r--r-- | src/ui/components/Logo.tsx | 34 | ||||
| -rw-r--r-- | src/ui/components/ReleaseCard.tsx | 47 | ||||
| -rw-r--r-- | src/ui/components/SiteFooter.tsx | 39 | ||||
| -rw-r--r-- | src/ui/components/SiteHeader.tsx | 121 | ||||
| -rw-r--r-- | src/ui/components/SiteNav.tsx | 38 |
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 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | import site from "@/corvos.config.ts"; | ||
| 4 | |||
| 5 | export type Props = HTMLAttributes<"a"> & { | ||
| 6 | size?: "sm" | "lg"; | ||
| 7 | variant?: "static-black" | "static-white"; | ||
| 8 | }; | ||
| 9 | |||
| 10 | export 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 @@ | |||
| 1 | import type { Select } from "corvos"; | ||
| 2 | |||
| 3 | import type site from "@/corvos.config.ts"; | ||
| 4 | |||
| 5 | import { CardBody } from "$/ui/objects/CardBody.tsx"; | ||
| 6 | import { Heading } from "$/ui/objects/Heading.tsx"; | ||
| 7 | |||
| 8 | import { Card, type Props as CardProps } from "../objects/Card.tsx"; | ||
| 9 | |||
| 10 | export type Props = CardProps<"a"> & { | ||
| 11 | "heading-level": "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; | ||
| 12 | "page": Select<typeof site, ".html", { type: "release" }>; | ||
| 13 | }; | ||
| 14 | |||
| 15 | export 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 @@ | |||
| 1 | import site from "@/corvos.config.ts"; | ||
| 2 | |||
| 3 | import { Container } from "../layouts/Container.tsx"; | ||
| 4 | import { Divider } from "../objects/Divider.tsx"; | ||
| 5 | import { Logo } from "./Logo.tsx"; | ||
| 6 | |||
| 7 | export 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 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | import { Icon } from "$/ui/objects/Icon.tsx"; | ||
| 4 | |||
| 5 | import { nav } from "../../data/nav.ts"; | ||
| 6 | import { ActionButton } from "../objects/ActionButton.tsx"; | ||
| 7 | import { Navbar } from "../objects/Navbar.tsx"; | ||
| 8 | import { NavbarItem } from "../objects/NavbarItem.tsx"; | ||
| 9 | import { Logo } from "./Logo.tsx"; | ||
| 10 | |||
| 11 | export 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 | |||
| 21 | export 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 @@ | |||
| 1 | import { Icon } from "$/ui/objects/Icon.tsx"; | ||
| 2 | |||
| 3 | import { nav } from "../../data/nav.ts"; | ||
| 4 | import { Navbar } from "../objects/Navbar.tsx"; | ||
| 5 | import { NavbarItem } from "../objects/NavbarItem.tsx"; | ||
| 6 | |||
| 7 | export interface Props { | ||
| 8 | url: string; | ||
| 9 | } | ||
| 10 | |||
| 11 | export 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 | }; | ||
