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 | |
| download | corvos-website-c1e28224aa2e1450031669d9a7e359b63e04687d.tar.gz corvos-website-c1e28224aa2e1450031669d9a7e359b63e04687d.tar.bz2 corvos-website-c1e28224aa2e1450031669d9a7e359b63e04687d.zip | |
Init
Diffstat (limited to 'src/ui')
44 files changed, 1687 insertions, 0 deletions
diff --git a/src/ui/base/BaseLayout.tsx b/src/ui/base/BaseLayout.tsx new file mode 100644 index 0000000..c57d41e --- /dev/null +++ b/src/ui/base/BaseLayout.tsx | |||
| @@ -0,0 +1,134 @@ | |||
| 1 | // @ts-types="corvos/mod.ts" | ||
| 2 | import { getGenerator, type Select } from "corvos/mod.ts"; | ||
| 3 | |||
| 4 | import site from "@/corvos.config.ts"; | ||
| 5 | |||
| 6 | import { SiteFooter } from "../components/SiteFooter.tsx"; | ||
| 7 | import { SiteNav } from "../components/SiteNav.tsx"; | ||
| 8 | import { Lightbox } from "../objects/Lightbox.tsx"; | ||
| 9 | |||
| 10 | export interface Props { | ||
| 11 | page: Select<typeof site, ".html">; | ||
| 12 | image?: string; | ||
| 13 | theme?: "l1" | "l2" | null; | ||
| 14 | children?: JSX.Element; | ||
| 15 | class?: unknown; | ||
| 16 | } | ||
| 17 | |||
| 18 | export const BaseLayout = site.$.component( | ||
| 19 | async ({ page, theme = "l1", image: imageUrl, children, class: cls }: Props, pages) => { | ||
| 20 | const query = site.$.query(pages); | ||
| 21 | |||
| 22 | const stylesheet = await site.load("styles/style.scss"); | ||
| 23 | const script = await site.load("scripts/script.ts"); | ||
| 24 | const fonts = await site.glob("assets/**.woff2"); | ||
| 25 | const favicon = await site.load("assets/favicon.svg"); | ||
| 26 | const image = await site.load( | ||
| 27 | imageUrl ?? "https://strapi.volpeon.ink/uploads/icon_7999cf625e.png", | ||
| 28 | ); | ||
| 29 | |||
| 30 | const feeds = query.type(".rss").many(); | ||
| 31 | |||
| 32 | return ( | ||
| 33 | <> | ||
| 34 | {{ __html: "<!DOCTYPE html>" }} | ||
| 35 | <html lang="en"> | ||
| 36 | <head> | ||
| 37 | <meta charset="utf-8" /> | ||
| 38 | <meta content="light dark" name="color-scheme" /> | ||
| 39 | <meta content="width=device-width, initial-scale=1" name="viewport" /> | ||
| 40 | <meta content="scale" name="text-scale" /> | ||
| 41 | <link href={site.$.url("sitemap.xml")} rel="sitemap" /> | ||
| 42 | <link href={favicon.url} rel="icon" type="image/svg+xml" /> | ||
| 43 | {fonts.map((font) => ( | ||
| 44 | <link as="font" href={font.url} rel="preload" type="font/woff2" /> | ||
| 45 | ))} | ||
| 46 | |||
| 47 | <style>@layer scope, theme, object, layout, component, utility;</style> | ||
| 48 | <link rel="stylesheet" href={stylesheet.url} /> | ||
| 49 | <script src={script.url} defer /> | ||
| 50 | {feeds.map((feed) => ( | ||
| 51 | <link | ||
| 52 | rel="alternate" | ||
| 53 | href={feed.url} | ||
| 54 | title={feed.data.title} | ||
| 55 | type="application/rss+xml" | ||
| 56 | /> | ||
| 57 | ))} | ||
| 58 | <title> | ||
| 59 | {page.data.title} | ||
| 60 | {page.data.type === "home" ? "" : " ยท Corvos"} | ||
| 61 | </title> | ||
| 62 | |||
| 63 | <meta property="og:type" content="website" /> | ||
| 64 | <meta property="og:site_name" content="Corvos" /> | ||
| 65 | <meta property="og:locale" content="en" /> | ||
| 66 | {"description" in page.data && page.data.description && ( | ||
| 67 | <> | ||
| 68 | <meta property="og:description" content={page.data.description} /> | ||
| 69 | <meta name="description" content={page.data.description} /> | ||
| 70 | </> | ||
| 71 | )} | ||
| 72 | <meta property="og:url" content={site.$.url("", true)} /> | ||
| 73 | <meta property="og:image" content={image.url} /> | ||
| 74 | <meta name="twitter:card" content={imageUrl ? "summary_large_image" : "summary"} /> | ||
| 75 | <meta name="generator" content={getGenerator()} /> | ||
| 76 | </head> | ||
| 77 | <body class:list={["t-no-js", { [`t-${theme}`]: theme }, cls]}> | ||
| 78 | {children} | ||
| 79 | |||
| 80 | <SiteFooter /> | ||
| 81 | <SiteNav url={page.url} /> | ||
| 82 | <Lightbox /> | ||
| 83 | <svg | ||
| 84 | version="1.1" | ||
| 85 | xmlns="http://www.w3.org/2000/svg" | ||
| 86 | xmlns:xlink="http://www.w3.org/1999/xlink" | ||
| 87 | class="u-d-none" | ||
| 88 | > | ||
| 89 | <title>Loading indicator</title> | ||
| 90 | <symbol id="loading-indicator" viewBox="0 0 50 50"> | ||
| 91 | <circle | ||
| 92 | cx="25" | ||
| 93 | cy="25" | ||
| 94 | r="20" | ||
| 95 | fill="none" | ||
| 96 | stroke="currentColor" | ||
| 97 | stroke-width=".4em" | ||
| 98 | stroke-linecap="round" | ||
| 99 | stroke-dasharray="1 200" | ||
| 100 | stroke-dashoffset="0" | ||
| 101 | stroke-miterlimit="10" | ||
| 102 | transform-origin="center center" | ||
| 103 | > | ||
| 104 | <animate | ||
| 105 | attributeName="stroke-dasharray" | ||
| 106 | keyTimes="0;0.5;1" | ||
| 107 | values="1 200;89 200;89 200" | ||
| 108 | dur="2s" | ||
| 109 | repeatCount="indefinite" | ||
| 110 | /> | ||
| 111 | <animate | ||
| 112 | attributeName="stroke-dashoffset" | ||
| 113 | keyTimes="0;0.5;1" | ||
| 114 | values="0;-35;-124" | ||
| 115 | dur="2s" | ||
| 116 | repeatCount="indefinite" | ||
| 117 | /> | ||
| 118 | <animateTransform | ||
| 119 | attributeName="transform" | ||
| 120 | type="rotate" | ||
| 121 | from="0" | ||
| 122 | to="360" | ||
| 123 | dur="1.3s" | ||
| 124 | repeatCount="indefinite" | ||
| 125 | /> | ||
| 126 | </circle> | ||
| 127 | </symbol> | ||
| 128 | </svg> | ||
| 129 | </body> | ||
| 130 | </html> | ||
| 131 | </> | ||
| 132 | ); | ||
| 133 | }, | ||
| 134 | ); | ||
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 | }; | ||
diff --git a/src/ui/layouts/CardList.tsx b/src/ui/layouts/CardList.tsx new file mode 100644 index 0000000..96ae642 --- /dev/null +++ b/src/ui/layouts/CardList.tsx | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"ul"> & { | ||
| 4 | "layout"?: | ||
| 5 | | "merge" | ||
| 6 | | "grid" | ||
| 7 | | "grid-sm" | ||
| 8 | | "grid-xs" | ||
| 9 | | "flex" | ||
| 10 | | "flex-sm" | ||
| 11 | | "flex-xs" | ||
| 12 | | "masonry" | ||
| 13 | | "masonry-h" | ||
| 14 | | "masonry-h-sm"; | ||
| 15 | "quiet"?: boolean; | ||
| 16 | "border"?: boolean; | ||
| 17 | "interactive"?: boolean; | ||
| 18 | "no-shadow"?: boolean; | ||
| 19 | "no-flush"?: boolean; | ||
| 20 | "keyboard-nav"?: boolean; | ||
| 21 | }; | ||
| 22 | |||
| 23 | export const CardList = ({ | ||
| 24 | layout, | ||
| 25 | quiet, | ||
| 26 | border, | ||
| 27 | interactive, | ||
| 28 | "no-shadow": noShadow, | ||
| 29 | "no-flush": noFlush, | ||
| 30 | "keyboard-nav": keyboardNav, | ||
| 31 | class: cls, | ||
| 32 | ...props | ||
| 33 | }: Props) => ( | ||
| 34 | <div | ||
| 35 | class:list={[ | ||
| 36 | "l-card-list", | ||
| 37 | { | ||
| 38 | [`l-card-list--${layout}`]: layout, | ||
| 39 | "l-card-list--interactive": interactive, | ||
| 40 | "l-card-list--no-flush": noFlush, | ||
| 41 | "l-card-list--quiet": quiet, | ||
| 42 | "l-card-list--borderless": !border, | ||
| 43 | "l-card-list--shadow": layout === "merge" && !noShadow, | ||
| 44 | "js-keyboard-nav": keyboardNav, | ||
| 45 | }, | ||
| 46 | cls, | ||
| 47 | ]} | ||
| 48 | data-keyboard-nav-item=".l-card-list__card" | ||
| 49 | data-keyboard-nav-mode="grid" | ||
| 50 | {...props} | ||
| 51 | /> | ||
| 52 | ); | ||
diff --git a/src/ui/layouts/Container.tsx b/src/ui/layouts/Container.tsx new file mode 100644 index 0000000..7ce7ed5 --- /dev/null +++ b/src/ui/layouts/Container.tsx | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | "as": Tag; | ||
| 5 | "pad-inline"?: boolean; | ||
| 6 | "pad-block"?: boolean; | ||
| 7 | "size"?: "sm"; | ||
| 8 | "fixed"?: boolean | "150" | "200"; | ||
| 9 | }>; | ||
| 10 | |||
| 11 | export const Container = <T extends HTMLTag = "div">({ | ||
| 12 | as: Tag = "div" as T, | ||
| 13 | class: cls, | ||
| 14 | "pad-inline": padInline, | ||
| 15 | "pad-block": padBlock, | ||
| 16 | fixed, | ||
| 17 | size, | ||
| 18 | ...props | ||
| 19 | }: Props<T>) => ( | ||
| 20 | <Tag | ||
| 21 | class:list={[ | ||
| 22 | "l-container", | ||
| 23 | { | ||
| 24 | "l-container--pad-i": padInline, | ||
| 25 | "l-container--pad-b": padBlock, | ||
| 26 | [`l-container--${size}`]: size, | ||
| 27 | [`l-container--fixed${typeof fixed === "boolean" ? "" : `-${fixed}`}`]: fixed, | ||
| 28 | }, | ||
| 29 | cls, | ||
| 30 | ]} | ||
| 31 | {...props} | ||
| 32 | /> | ||
| 33 | ); | ||
diff --git a/src/ui/layouts/EmojiGrid.tsx b/src/ui/layouts/EmojiGrid.tsx new file mode 100644 index 0000000..5dc9266 --- /dev/null +++ b/src/ui/layouts/EmojiGrid.tsx | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"ul"> & { | ||
| 4 | id: string; | ||
| 5 | sprite: { | ||
| 6 | variants: { size: number; space: number }[]; | ||
| 7 | wrap: number; | ||
| 8 | }; | ||
| 9 | count: number; | ||
| 10 | }; | ||
| 11 | |||
| 12 | export const EmojiGrid = ({ id, sprite, count, class: cls, ...props }: Props) => { | ||
| 13 | const size = sprite.variants[0]?.size ?? 48; | ||
| 14 | const space = sprite.variants[0]?.space ?? 2; | ||
| 15 | const { wrap } = sprite; | ||
| 16 | |||
| 17 | const src = `/emojis/${id}/sprite48.avif`; | ||
| 18 | const srcset = `/emojis/${id}/sprite48.avif 1x, /emojis/${id}/sprite96.avif 2x`; | ||
| 19 | |||
| 20 | const offsetX = (i: number) => -1 * (size + space) * (i % wrap); | ||
| 21 | const offsetY = (i: number) => -1 * (size + space) * Math.floor(i / wrap); | ||
| 22 | |||
| 23 | return ( | ||
| 24 | <ul class:list={["l-emoji-grid", cls]} style={{ "--emoji-size": `${size}px` }} {...props}> | ||
| 25 | {[...Array(count).keys()].map((i) => ( | ||
| 26 | <li> | ||
| 27 | <img | ||
| 28 | alt="" | ||
| 29 | class="l-emoji-grid__emoji" | ||
| 30 | src={src} | ||
| 31 | srcset={srcset} | ||
| 32 | width={size} | ||
| 33 | height={size} | ||
| 34 | style={{ "object-position": `${offsetX(i)}px ${offsetY(i)}px` }} | ||
| 35 | /> | ||
| 36 | </li> | ||
| 37 | ))} | ||
| 38 | </ul> | ||
| 39 | ); | ||
| 40 | }; | ||
diff --git a/src/ui/layouts/HList.tsx b/src/ui/layouts/HList.tsx new file mode 100644 index 0000000..c1d870a --- /dev/null +++ b/src/ui/layouts/HList.tsx | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | import type { Props as ActionButtonProps } from "../objects/ActionButton.tsx"; | ||
| 4 | |||
| 5 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 6 | "as"?: Tag; | ||
| 7 | "pill"?: boolean; | ||
| 8 | "icon"?: boolean; | ||
| 9 | "separated"?: boolean; | ||
| 10 | "size"?: "gapless" | "sm" | "lg" | "xl"; | ||
| 11 | "buttons-size"?: ActionButtonProps<Tag>["size"]; | ||
| 12 | "buttons"?: boolean; | ||
| 13 | }>; | ||
| 14 | |||
| 15 | export const HList = <T extends HTMLTag = "ul">({ | ||
| 16 | as: Tag = "ul" as T, | ||
| 17 | pill, | ||
| 18 | icon, | ||
| 19 | separated, | ||
| 20 | size, | ||
| 21 | "buttons-size": buttonsSize, | ||
| 22 | buttons, | ||
| 23 | class: cls, | ||
| 24 | ...props | ||
| 25 | }: Props<T>) => ( | ||
| 26 | <Tag | ||
| 27 | class:list={[ | ||
| 28 | "l-hlist", | ||
| 29 | { | ||
| 30 | "l-hlist--pill": pill, | ||
| 31 | "l-hlist--icon": icon, | ||
| 32 | "l-hlist--separated": separated, | ||
| 33 | [`l-hlist--${size}`]: size, | ||
| 34 | [`l-hlist--buttons-${buttonsSize}`]: buttonsSize, | ||
| 35 | "l-hlist--buttons": buttons, | ||
| 36 | }, | ||
| 37 | "js-keyboard-nav", | ||
| 38 | cls, | ||
| 39 | ]} | ||
| 40 | data-keyboard-nav-item="li > :first-child" | ||
| 41 | data-keyboard-nav-mode="nav" | ||
| 42 | {...props} | ||
| 43 | /> | ||
| 44 | ); | ||
diff --git a/src/ui/layouts/SplitView.tsx b/src/ui/layouts/SplitView.tsx new file mode 100644 index 0000000..81a55d1 --- /dev/null +++ b/src/ui/layouts/SplitView.tsx | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | "as": Tag; | ||
| 5 | "gapless"?: boolean; | ||
| 6 | "wrap-reverse"?: boolean; | ||
| 7 | }>; | ||
| 8 | |||
| 9 | export const SplitView = <T extends HTMLTag = "div">({ | ||
| 10 | as: Tag = "div" as T, | ||
| 11 | gapless, | ||
| 12 | "wrap-reverse": wrapReverse, | ||
| 13 | class: cls, | ||
| 14 | ...props | ||
| 15 | }: Props<T>) => ( | ||
| 16 | <Tag | ||
| 17 | class:list={[ | ||
| 18 | "l-split-view", | ||
| 19 | { | ||
| 20 | "l-split-view--gapless": gapless, | ||
| 21 | "l-split-view--wrap-reverse": wrapReverse, | ||
| 22 | }, | ||
| 23 | cls, | ||
| 24 | ]} | ||
| 25 | {...props} | ||
| 26 | /> | ||
| 27 | ); | ||
diff --git a/src/ui/layouts/SplitViewPanel.tsx b/src/ui/layouts/SplitViewPanel.tsx new file mode 100644 index 0000000..7f04b44 --- /dev/null +++ b/src/ui/layouts/SplitViewPanel.tsx | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | as: Tag; | ||
| 5 | sticky?: boolean; | ||
| 6 | side?: "25"; | ||
| 7 | min?: "50" | "100" | "200"; | ||
| 8 | fill?: boolean; | ||
| 9 | }>; | ||
| 10 | |||
| 11 | export const SplitViewPanel = <T extends HTMLTag = "div">({ | ||
| 12 | as: Tag = "div" as T, | ||
| 13 | sticky, | ||
| 14 | side, | ||
| 15 | min, | ||
| 16 | fill, | ||
| 17 | class: cls, | ||
| 18 | ...props | ||
| 19 | }: Props<T>) => ( | ||
| 20 | <Tag | ||
| 21 | class:list={[ | ||
| 22 | "l-split-view__panel", | ||
| 23 | { | ||
| 24 | "l-split-view__panel--sticky": sticky, | ||
| 25 | [`l-split-view__panel--side-${side}`]: side, | ||
| 26 | [`l-split-view__panel--min-${min}`]: min, | ||
| 27 | "l-split-view__panel--fill": fill, | ||
| 28 | }, | ||
| 29 | cls, | ||
| 30 | ]} | ||
| 31 | {...props} | ||
| 32 | /> | ||
| 33 | ); | ||
diff --git a/src/ui/objects/ActionButton.tsx b/src/ui/objects/ActionButton.tsx new file mode 100644 index 0000000..76f859f --- /dev/null +++ b/src/ui/objects/ActionButton.tsx | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | "as"?: Tag; | ||
| 5 | "label-as"?: string; | ||
| 6 | "pill"?: boolean; | ||
| 7 | "icon"?: JSX.Element; | ||
| 8 | "pre"?: JSX.Element; | ||
| 9 | "post"?: JSX.Element; | ||
| 10 | "highlighted"?: boolean; | ||
| 11 | "selected"?: boolean; | ||
| 12 | "wide"?: boolean; | ||
| 13 | "justify"?: boolean; | ||
| 14 | "align-block"?: boolean; | ||
| 15 | "level"?: "button" | "badge" | "quiet"; | ||
| 16 | "variant"?: "accent" | "positive" | "negative" | "warning" | "static-black" | "static-white"; | ||
| 17 | "size"?: "xs" | "sm" | "lg" | "xl"; | ||
| 18 | "children"?: JSX.Element; | ||
| 19 | }>; | ||
| 20 | |||
| 21 | export const ActionButton = <T extends HTMLTag = "a">({ | ||
| 22 | children, | ||
| 23 | as: Tag = "a" as T, | ||
| 24 | "label-as": Label = "span", | ||
| 25 | pill, | ||
| 26 | pre, | ||
| 27 | post, | ||
| 28 | wide, | ||
| 29 | justify, | ||
| 30 | icon, | ||
| 31 | "align-block": alignBlock, | ||
| 32 | highlighted, | ||
| 33 | selected, | ||
| 34 | level, | ||
| 35 | variant, | ||
| 36 | size, | ||
| 37 | class: cls, | ||
| 38 | ...props | ||
| 39 | }: Props<T>) => ( | ||
| 40 | <Tag | ||
| 41 | class:list={[ | ||
| 42 | "o-button", | ||
| 43 | { | ||
| 44 | "o-button--pill": pill, | ||
| 45 | "o-button--wide": wide, | ||
| 46 | "o-button--justify": justify, | ||
| 47 | "o-button--icon": icon, | ||
| 48 | "o-button--align-block": alignBlock, | ||
| 49 | "is-highlighted": highlighted, | ||
| 50 | "is-selected": selected, | ||
| 51 | [`o-button--${level}`]: level && level !== "button", | ||
| 52 | [`o-button--${variant}`]: variant, | ||
| 53 | [`o-button--${size}`]: size, | ||
| 54 | }, | ||
| 55 | cls, | ||
| 56 | ]} | ||
| 57 | {...props} | ||
| 58 | > | ||
| 59 | {pre} {icon ?? <Label class="o-button__label">{children}</Label>} {post} | ||
| 60 | </Tag> | ||
| 61 | ); | ||
diff --git a/src/ui/objects/Alert.tsx b/src/ui/objects/Alert.tsx new file mode 100644 index 0000000..f590e18 --- /dev/null +++ b/src/ui/objects/Alert.tsx | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | as?: Tag; | ||
| 5 | icon?: JSX.Element; | ||
| 6 | variant?: "accent" | "positive" | "negative" | "warning"; | ||
| 7 | }>; | ||
| 8 | |||
| 9 | export const Alert = <T extends HTMLTag = "div">({ | ||
| 10 | children, | ||
| 11 | as: Tag = "div" as T, | ||
| 12 | variant, | ||
| 13 | icon, | ||
| 14 | class: cls, | ||
| 15 | ...props | ||
| 16 | }: Props<T>) => ( | ||
| 17 | <Tag class:list={["o-alert", { [`o-alert--${variant}`]: variant }, cls]} {...props}> | ||
| 18 | {icon && <span class="u-mie-100">{icon}</span>} | ||
| 19 | {children} | ||
| 20 | </Tag> | ||
| 21 | ); | ||
diff --git a/src/ui/objects/Avatar.tsx b/src/ui/objects/Avatar.tsx new file mode 100644 index 0000000..459af12 --- /dev/null +++ b/src/ui/objects/Avatar.tsx | |||
| @@ -0,0 +1,69 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | import { StatusIndicator } from "$/ui/objects/StatusIndicator.tsx"; | ||
| 4 | |||
| 5 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 6 | "as"?: Tag; | ||
| 7 | "src"?: string; | ||
| 8 | "icon"?: JSX.Element; | ||
| 9 | "children"?: JSX.Element; | ||
| 10 | "class"?: string; | ||
| 11 | "circle"?: boolean; | ||
| 12 | "placeholder"?: boolean; | ||
| 13 | "color"?: `hsl(${number}, ${number}%, ${number}%)`; | ||
| 14 | "color2"?: `hsl(${number}, ${number}%, ${number}%)`; | ||
| 15 | "color-angle"?: number | `${number}`; | ||
| 16 | "size"?: "xs" | "sm" | "lg" | "xl" | "xxl" | "xxxl"; | ||
| 17 | "status"?: "accent" | "positive" | "negative" | "warning"; | ||
| 18 | }>; | ||
| 19 | |||
| 20 | const hslRegex = /^hsl\(([\d.]+), ([\d.]+%), ([\d.]+%)\)$/; | ||
| 21 | |||
| 22 | export const Avatar = <T extends HTMLTag = "div">({ | ||
| 23 | children, | ||
| 24 | as: Tag = "div" as T, | ||
| 25 | src, | ||
| 26 | icon, | ||
| 27 | circle, | ||
| 28 | placeholder, | ||
| 29 | color, | ||
| 30 | color2, | ||
| 31 | "color-angle": colorAngle, | ||
| 32 | size, | ||
| 33 | status, | ||
| 34 | class: cls, | ||
| 35 | ...props | ||
| 36 | }: Props<T>) => ( | ||
| 37 | <Tag | ||
| 38 | class:list={[ | ||
| 39 | "o-avatar", | ||
| 40 | { | ||
| 41 | "o-avatar--circle": circle, | ||
| 42 | "o-avatar--placeholder": placeholder, | ||
| 43 | "o-avatar--colored": color && !color2, | ||
| 44 | "o-avatar--colored-gradient": color && !!color2, | ||
| 45 | [`o-avatar--${size}`]: !!size, | ||
| 46 | }, | ||
| 47 | cls, | ||
| 48 | ]} | ||
| 49 | style={{ | ||
| 50 | "--o-avatar--bg-angle": colorAngle, | ||
| 51 | "--o-avatar--bg-color--h": color?.match(hslRegex)?.[1], | ||
| 52 | "--o-avatar--bg-color--l": color?.match(hslRegex)?.[3], | ||
| 53 | "--o-avatar--bg-color--s": color?.match(hslRegex)?.[2], | ||
| 54 | "--o-avatar--bg-color-2--h": color2?.match(hslRegex)?.[1], | ||
| 55 | "--o-avatar--bg-color-2--l": color2?.match(hslRegex)?.[3], | ||
| 56 | "--o-avatar--bg-color-2--s": color2?.match(hslRegex)?.[2], | ||
| 57 | }} | ||
| 58 | {...props} | ||
| 59 | > | ||
| 60 | {status && <StatusIndicator class="o-avatar__status" status={status} />} | ||
| 61 | {icon && <div class="o-avatar__icon u-ai-end u-jc-end">{icon}</div>} | ||
| 62 | |||
| 63 | {src ? ( | ||
| 64 | <img alt="" class="o-avatar__content" src={src} /> | ||
| 65 | ) : ( | ||
| 66 | <span class="o-avatar__content">{children}</span> | ||
| 67 | )} | ||
| 68 | </Tag> | ||
| 69 | ); | ||
diff --git a/src/ui/objects/Badge.tsx b/src/ui/objects/Badge.tsx new file mode 100644 index 0000000..e98ef0d --- /dev/null +++ b/src/ui/objects/Badge.tsx | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | import type { Props as ActionButtonProps } from "./ActionButton.tsx"; | ||
| 4 | |||
| 5 | import { ActionButton } from "./ActionButton.tsx"; | ||
| 6 | |||
| 7 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 8 | as?: Tag; | ||
| 9 | variant?: ActionButtonProps<Tag>["variant"]; | ||
| 10 | pill?: boolean; | ||
| 11 | selected?: boolean; | ||
| 12 | size?: ActionButtonProps<Tag>["size"]; | ||
| 13 | children?: JSX.Element; | ||
| 14 | }>; | ||
| 15 | |||
| 16 | export const Badge = <T extends HTMLTag = "span">({ as = "span" as T, ...props }: Props<T>) => ( | ||
| 17 | // deno-lint-ignore no-explicit-any | ||
| 18 | <ActionButton as={as} level="badge" {...(props as any)} /> | ||
| 19 | ); | ||
diff --git a/src/ui/objects/Body.tsx b/src/ui/objects/Body.tsx new file mode 100644 index 0000000..149662c --- /dev/null +++ b/src/ui/objects/Body.tsx | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"div">; | ||
| 4 | |||
| 5 | export const Body = ({ class: cls, ...props }: Props) => ( | ||
| 6 | <div | ||
| 7 | class:list={[ | ||
| 8 | "s-body", | ||
| 9 | "s-headings", | ||
| 10 | "s-headings--display", | ||
| 11 | "s-links", | ||
| 12 | "s-links--colored", | ||
| 13 | "s-code", | ||
| 14 | "s-figures", | ||
| 15 | "s-blockquotes", | ||
| 16 | "s-lists", | ||
| 17 | cls, | ||
| 18 | ]} | ||
| 19 | {...props} | ||
| 20 | /> | ||
| 21 | ); | ||
diff --git a/src/ui/objects/Button.tsx b/src/ui/objects/Button.tsx new file mode 100644 index 0000000..68e1776 --- /dev/null +++ b/src/ui/objects/Button.tsx | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | import type { Props as ActionButtonProps } from "./ActionButton.tsx"; | ||
| 4 | |||
| 5 | import { ActionButton } from "./ActionButton.tsx"; | ||
| 6 | |||
| 7 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 8 | as?: Tag; | ||
| 9 | variant?: "primary" | ActionButtonProps<Tag>["variant"]; | ||
| 10 | size?: ActionButtonProps<Tag>["size"]; | ||
| 11 | wide?: boolean; | ||
| 12 | justify?: boolean; | ||
| 13 | children?: JSX.Element; | ||
| 14 | }>; | ||
| 15 | |||
| 16 | export const Button = <T extends HTMLTag = "a">({ as = "a" as T, variant, ...props }: Props<T>) => ( | ||
| 17 | <ActionButton | ||
| 18 | as={as} | ||
| 19 | label-as={variant ? "strong" : "span"} | ||
| 20 | pill | ||
| 21 | selected={!!variant} | ||
| 22 | variant={variant === "primary" ? undefined : variant} | ||
| 23 | // deno-lint-ignore no-explicit-any | ||
| 24 | {...(props as any)} | ||
| 25 | /> | ||
| 26 | ); | ||
diff --git a/src/ui/objects/Card.tsx b/src/ui/objects/Card.tsx new file mode 100644 index 0000000..fd565e1 --- /dev/null +++ b/src/ui/objects/Card.tsx | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | "as"?: Tag; | ||
| 5 | "list"?: boolean; | ||
| 6 | "quiet"?: boolean; | ||
| 7 | "thumbnail"?: boolean; | ||
| 8 | "border"?: boolean; | ||
| 9 | "no-shadow"?: boolean; | ||
| 10 | "horizontal"?: boolean; | ||
| 11 | "interactive"?: boolean; | ||
| 12 | "highlight"?: boolean; | ||
| 13 | "children"?: JSX.Element; | ||
| 14 | }>; | ||
| 15 | |||
| 16 | export const Card = <T extends HTMLTag = "article">({ | ||
| 17 | as: Tag = "article" as T, | ||
| 18 | list, | ||
| 19 | quiet, | ||
| 20 | thumbnail, | ||
| 21 | border, | ||
| 22 | "no-shadow": noShadow, | ||
| 23 | horizontal, | ||
| 24 | interactive, | ||
| 25 | highlight, | ||
| 26 | class: cls, | ||
| 27 | ...props | ||
| 28 | }: Props<T>) => ( | ||
| 29 | <Tag | ||
| 30 | class:list={[ | ||
| 31 | "o-card", | ||
| 32 | { | ||
| 33 | "l-card-list__card": list, | ||
| 34 | "o-card--borderless": !border, | ||
| 35 | "o-card--horizontal": horizontal, | ||
| 36 | "o-card--interactive": interactive, | ||
| 37 | "o-card--highlight": highlight, | ||
| 38 | "o-card--quiet": quiet, | ||
| 39 | "o-card--collapsible": Tag === "details", | ||
| 40 | "o-card--shadow": !noShadow, | ||
| 41 | "o-card--thumbnail": thumbnail, | ||
| 42 | }, | ||
| 43 | cls, | ||
| 44 | ]} | ||
| 45 | {...props} | ||
| 46 | /> | ||
| 47 | ); | ||
diff --git a/src/ui/objects/CardBody.tsx b/src/ui/objects/CardBody.tsx new file mode 100644 index 0000000..aa5df3d --- /dev/null +++ b/src/ui/objects/CardBody.tsx | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | "as"?: Tag; | ||
| 5 | "list"?: boolean; | ||
| 6 | "main"?: boolean; | ||
| 7 | "no-mbs-neutralize"?: boolean; | ||
| 8 | "children"?: JSX.Element; | ||
| 9 | }>; | ||
| 10 | |||
| 11 | export const CardBody = <T extends HTMLTag = "div">({ | ||
| 12 | as: Tag = "div" as T, | ||
| 13 | list, | ||
| 14 | main, | ||
| 15 | "no-mbs-neutralize": noMbsNeutralize, | ||
| 16 | class: cls, | ||
| 17 | ...props | ||
| 18 | }: Props<T>) => ( | ||
| 19 | <Tag | ||
| 20 | class:list={[ | ||
| 21 | "o-card__body", | ||
| 22 | { | ||
| 23 | "o-card__body--main": main, | ||
| 24 | "o-card__header": Tag === "summary", | ||
| 25 | "l-card-list__card-body": list, | ||
| 26 | "o-card__body--no-mbs-neutralize": noMbsNeutralize, | ||
| 27 | }, | ||
| 28 | cls, | ||
| 29 | ]} | ||
| 30 | {...props} | ||
| 31 | /> | ||
| 32 | ); | ||
diff --git a/src/ui/objects/CardFooter.tsx b/src/ui/objects/CardFooter.tsx new file mode 100644 index 0000000..63d74d0 --- /dev/null +++ b/src/ui/objects/CardFooter.tsx | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | as?: Tag; | ||
| 5 | }>; | ||
| 6 | |||
| 7 | export const CardFooter = <T extends HTMLTag = "div">({ | ||
| 8 | as: Tag = "div" as T, | ||
| 9 | class: cls, | ||
| 10 | ...props | ||
| 11 | }: Props<T>) => <Tag class:list={["o-card__footer", cls]} {...props} />; | ||
diff --git a/src/ui/objects/CardImage.tsx b/src/ui/objects/CardImage.tsx new file mode 100644 index 0000000..12b9735 --- /dev/null +++ b/src/ui/objects/CardImage.tsx | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | import type { Props as ImageProps } from "corvos/plugins/images.ts"; | ||
| 2 | import type { CSSProperties } from "https://cdn.jsdelivr.net/gh/oscarotero/ssx@0.1.14/css.ts"; | ||
| 3 | |||
| 4 | import { Image } from "corvos/plugins/images.ts"; | ||
| 5 | |||
| 6 | export type Props = ImageProps & { | ||
| 7 | "list"?: boolean; | ||
| 8 | "img-class"?: unknown; | ||
| 9 | "img-style"?: CSSProperties; | ||
| 10 | "aspect"?: `${number} / ${number}`; | ||
| 11 | "content-class"?: unknown; | ||
| 12 | "children"?: JSX.Element; | ||
| 13 | }; | ||
| 14 | |||
| 15 | export const CardImage = ({ | ||
| 16 | children, | ||
| 17 | list, | ||
| 18 | "img-class": imgClass, | ||
| 19 | aspect, | ||
| 20 | "content-class": contentClass, | ||
| 21 | alt, | ||
| 22 | class: cls, | ||
| 23 | style, | ||
| 24 | "img-style": imgStyle, | ||
| 25 | ...props | ||
| 26 | }: Props) => ( | ||
| 27 | <div class:list={["o-card__image", cls]} style={style}> | ||
| 28 | <Image | ||
| 29 | class:list={["o-card__image-img", { "l-card-list__card-image": list }, imgClass]} | ||
| 30 | style={{ | ||
| 31 | "aspect-ratio": aspect, | ||
| 32 | "block-size": aspect ? "auto" : undefined, | ||
| 33 | ...imgStyle, | ||
| 34 | }} | ||
| 35 | alt={alt} | ||
| 36 | {...props} | ||
| 37 | /> | ||
| 38 | {children && <div class:list={["o-card__image-overlay", contentClass]}>{children}</div>} | ||
| 39 | </div> | ||
| 40 | ); | ||
diff --git a/src/ui/objects/CollapseCard.tsx b/src/ui/objects/CollapseCard.tsx new file mode 100644 index 0000000..27e1ce2 --- /dev/null +++ b/src/ui/objects/CollapseCard.tsx | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | import { Card, type Props as CardProps } from "./Card.tsx"; | ||
| 2 | import { CardBody } from "./CardBody.tsx"; | ||
| 3 | import { Heading } from "./Heading.tsx"; | ||
| 4 | |||
| 5 | export type Props = CardProps<"details"> & { | ||
| 6 | "heading-level": "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; | ||
| 7 | "title"?: JSX.Element; | ||
| 8 | "header"?: JSX.Element; | ||
| 9 | }; | ||
| 10 | |||
| 11 | export const CollapseCard = ({ | ||
| 12 | "heading-level": headingLevel, | ||
| 13 | children, | ||
| 14 | title, | ||
| 15 | header, | ||
| 16 | ...props | ||
| 17 | }: Props) => ( | ||
| 18 | <Card as="details" {...props}> | ||
| 19 | <CardBody as="summary"> | ||
| 20 | <Heading as={headingLevel} class="u-mbs-0" level="lg"> | ||
| 21 | {title} | ||
| 22 | </Heading> | ||
| 23 | {header} | ||
| 24 | </CardBody> | ||
| 25 | <CardBody>{children}</CardBody> | ||
| 26 | </Card> | ||
| 27 | ); | ||
diff --git a/src/ui/objects/ContentImage.tsx b/src/ui/objects/ContentImage.tsx new file mode 100644 index 0000000..ef45f89 --- /dev/null +++ b/src/ui/objects/ContentImage.tsx | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | import type { ClassValue } from "corvos/deps/clsx.ts"; | ||
| 2 | import type { Props as ImageProps } from "corvos/plugins/images.ts"; | ||
| 3 | |||
| 4 | import { Image } from "corvos/plugins/images.ts"; | ||
| 5 | |||
| 6 | export type Props = Omit<ImageProps, "src"> & { | ||
| 7 | "src"?: string; | ||
| 8 | "img-class"?: ClassValue; | ||
| 9 | }; | ||
| 10 | |||
| 11 | export const ContentImage = ({ src, "img-class": imgClass, ...props }: Props) => ( | ||
| 12 | <a href={src} data-lightbox {...props}> | ||
| 13 | <Image src={src} width="1400" class:list={imgClass} /> | ||
| 14 | </a> | ||
| 15 | ); | ||
diff --git a/src/ui/objects/Deflist.tsx b/src/ui/objects/Deflist.tsx new file mode 100644 index 0000000..53d60d6 --- /dev/null +++ b/src/ui/objects/Deflist.tsx | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"dl">; | ||
| 4 | |||
| 5 | export const Deflist = ({ class: cls, ...props }: Props) => ( | ||
| 6 | <dl class={`o-deflist ${cls ?? ""}`} {...props} /> | ||
| 7 | ); | ||
diff --git a/src/ui/objects/Dialog.tsx b/src/ui/objects/Dialog.tsx new file mode 100644 index 0000000..8ab6c4f --- /dev/null +++ b/src/ui/objects/Dialog.tsx | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | import { Icon } from "$/ui/objects/Icon.tsx"; | ||
| 4 | |||
| 5 | import { Container } from "../layouts/Container.tsx"; | ||
| 6 | import { ActionButton } from "./ActionButton.tsx"; | ||
| 7 | import { Divider } from "./Divider.tsx"; | ||
| 8 | import { Heading } from "./Heading.tsx"; | ||
| 9 | |||
| 10 | export type Props = HTMLAttributes<"dialog"> & { | ||
| 11 | title: string; | ||
| 12 | divider?: boolean; | ||
| 13 | children?: JSX.Element; | ||
| 14 | }; | ||
| 15 | |||
| 16 | export const Dialog = ({ children, title, divider, class: cls, ...props }: Props) => ( | ||
| 17 | <dialog popover="auto" class:list={["o-dialog is-modal", cls]} closedby="any" {...props}> | ||
| 18 | <Container fixed class="o-dialog__content"> | ||
| 19 | <header class="o-dialog__header"> | ||
| 20 | <Heading as="h1" class="u-mbs-0" level="xxl"> | ||
| 21 | {title} | ||
| 22 | </Heading> | ||
| 23 | |||
| 24 | <ActionButton | ||
| 25 | command="close" | ||
| 26 | commandfor={props.id} | ||
| 27 | as="button" | ||
| 28 | pill | ||
| 29 | size="lg" | ||
| 30 | level="quiet" | ||
| 31 | icon={<Icon icon="x" variant="bold" />} | ||
| 32 | class="u-mis-auto" | ||
| 33 | /> | ||
| 34 | </header> | ||
| 35 | |||
| 36 | {divider && <Divider level="faint" class="o-dialog__divider" />} | ||
| 37 | |||
| 38 | <div class="o-dialog__body">{children}</div> | ||
| 39 | </Container> | ||
| 40 | </dialog> | ||
| 41 | ); | ||
diff --git a/src/ui/objects/Divider.tsx b/src/ui/objects/Divider.tsx new file mode 100644 index 0000000..8f7efcd --- /dev/null +++ b/src/ui/objects/Divider.tsx | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | import type { HTMLAttributes, HTMLTag } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"div" | "hr"> & { | ||
| 4 | "level"?: "strong" | "medium" | "quiet" | "faint"; | ||
| 5 | "variant"?: "static-black" | "static-white"; | ||
| 6 | "vertical"?: boolean | "dot"; | ||
| 7 | "label-as"?: HTMLTag; | ||
| 8 | "children"?: JSX.Element; | ||
| 9 | }; | ||
| 10 | |||
| 11 | export const Divider = ({ | ||
| 12 | children, | ||
| 13 | level = "strong", | ||
| 14 | variant, | ||
| 15 | vertical, | ||
| 16 | "label-as": Label = "span", | ||
| 17 | class: cls, | ||
| 18 | ...props | ||
| 19 | }: Props) => | ||
| 20 | children ? ( | ||
| 21 | <div | ||
| 22 | class:list={[ | ||
| 23 | "o-divider", | ||
| 24 | "o-divider--labelled", | ||
| 25 | `o-divider--${level}`, | ||
| 26 | { | ||
| 27 | [`o-divider--${variant}`]: variant, | ||
| 28 | "o-divider--vertical": vertical === true, | ||
| 29 | "o-divider--dot": vertical === "dot", | ||
| 30 | }, | ||
| 31 | cls, | ||
| 32 | ]} | ||
| 33 | {...props} | ||
| 34 | > | ||
| 35 | <Label class="o-divider__label u-mbs-0">{children}</Label> | ||
| 36 | </div> | ||
| 37 | ) : ( | ||
| 38 | <hr | ||
| 39 | class:list={[ | ||
| 40 | "o-divider", | ||
| 41 | `o-divider--${level}`, | ||
| 42 | { | ||
| 43 | [`o-divider--${variant}`]: variant, | ||
| 44 | "o-divider--vertical": vertical === true, | ||
| 45 | "o-divider--dot": vertical === "dot", | ||
| 46 | }, | ||
| 47 | cls, | ||
| 48 | ]} | ||
| 49 | {...props} | ||
| 50 | /> | ||
| 51 | ); | ||
diff --git a/src/ui/objects/Emoji.tsx b/src/ui/objects/Emoji.tsx new file mode 100644 index 0000000..f04f776 --- /dev/null +++ b/src/ui/objects/Emoji.tsx | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | import site from "@/corvos.config.ts"; | ||
| 2 | |||
| 3 | export interface Props { | ||
| 4 | pack: string; | ||
| 5 | emoji: string; | ||
| 6 | size?: "125" | "150" | "200"; | ||
| 7 | class?: string; | ||
| 8 | } | ||
| 9 | |||
| 10 | export const Emoji = site.$.component( | ||
| 11 | ({ pack, emoji, size, class: cls, ...props }: Props, pages) => { | ||
| 12 | const query = site.$.query(pages); | ||
| 13 | const sprite = query.type(".svg").match({ emojiPack: pack }).one(); | ||
| 14 | |||
| 15 | return ( | ||
| 16 | <svg class:list={["o-emoji o-emoji--sprite", { [`o-emoji--${size}`]: size }, cls]} {...props}> | ||
| 17 | <use href={`${sprite.url}#${emoji}`} /> | ||
| 18 | </svg> | ||
| 19 | ); | ||
| 20 | }, | ||
| 21 | ); | ||
diff --git a/src/ui/objects/Figure.tsx b/src/ui/objects/Figure.tsx new file mode 100644 index 0000000..7f5e20a --- /dev/null +++ b/src/ui/objects/Figure.tsx | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"figure"> & { | ||
| 4 | children?: JSX.Element; | ||
| 5 | caption?: JSX.Element; | ||
| 6 | }; | ||
| 7 | |||
| 8 | export const Figure = ({ children, caption, class: cls, ...props }: Props) => ( | ||
| 9 | <figure class:list={["o-figure", cls]} {...props}> | ||
| 10 | {children} | ||
| 11 | {caption && <figcaption class="o-figure__caption">{caption}</figcaption>} | ||
| 12 | </figure> | ||
| 13 | ); | ||
diff --git a/src/ui/objects/FigureCaption.tsx b/src/ui/objects/FigureCaption.tsx new file mode 100644 index 0000000..4112170 --- /dev/null +++ b/src/ui/objects/FigureCaption.tsx | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"figcaption"> & { | ||
| 4 | children?: JSX.Element; | ||
| 5 | }; | ||
| 6 | |||
| 7 | export const FigureCaption = ({ children, class: cls, ...props }: Props) => ( | ||
| 8 | <figcaption class:list={["o-figure__caption", cls]} {...props}> | ||
| 9 | {children} | ||
| 10 | </figcaption> | ||
| 11 | ); | ||
diff --git a/src/ui/objects/Heading.tsx b/src/ui/objects/Heading.tsx new file mode 100644 index 0000000..089de3a --- /dev/null +++ b/src/ui/objects/Heading.tsx | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | as: Tag; | ||
| 5 | level?: "xxl" | "xl" | "lg" | "md" | "sm" | "xs"; | ||
| 6 | variant?: "static-black" | "static-white"; | ||
| 7 | display?: boolean; | ||
| 8 | highlight?: boolean; | ||
| 9 | children?: JSX.Element; | ||
| 10 | }>; | ||
| 11 | |||
| 12 | const levelTagMap = { | ||
| 13 | lg: "h3", | ||
| 14 | md: "h4", | ||
| 15 | sm: "h5", | ||
| 16 | xl: "h2", | ||
| 17 | xs: "h6", | ||
| 18 | xxl: "h1", | ||
| 19 | } as const; | ||
| 20 | |||
| 21 | export const Heading = <T extends HTMLTag = "div">({ | ||
| 22 | as, | ||
| 23 | children, | ||
| 24 | display, | ||
| 25 | level, | ||
| 26 | variant, | ||
| 27 | highlight, | ||
| 28 | class: cls, | ||
| 29 | ...props | ||
| 30 | }: Props<T>) => { | ||
| 31 | const Tag = as ?? (level ? levelTagMap[level] : "strong"); | ||
| 32 | return ( | ||
| 33 | <Tag | ||
| 34 | class:list={[ | ||
| 35 | "o-heading", | ||
| 36 | { | ||
| 37 | [`o-heading--${level}`]: level, | ||
| 38 | [`o-heading--${variant}`]: variant, | ||
| 39 | "o-heading--display": display, | ||
| 40 | "u-c-heading": Tag === "strong", | ||
| 41 | }, | ||
| 42 | cls, | ||
| 43 | ]} | ||
| 44 | {...props} | ||
| 45 | > | ||
| 46 | {highlight ? <span class="o-heading__highlight">{children}</span> : children} | ||
| 47 | </Tag> | ||
| 48 | ); | ||
| 49 | }; | ||
diff --git a/src/ui/objects/Icon.tsx b/src/ui/objects/Icon.tsx new file mode 100644 index 0000000..e8af718 --- /dev/null +++ b/src/ui/objects/Icon.tsx | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | import site from "@/corvos.config.ts"; | ||
| 4 | |||
| 5 | export type Props = HTMLAttributes<"svg"> & { | ||
| 6 | icon: Parameters<typeof site.$.icon>[0]; | ||
| 7 | variant?: Parameters<typeof site.$.icon>[1]; | ||
| 8 | }; | ||
| 9 | |||
| 10 | export const Icon = ({ icon, variant, class: cls, ...props }: Props) => ( | ||
| 11 | <svg | ||
| 12 | aria-hidden="true" | ||
| 13 | class:list={["o-icon", cls]} | ||
| 14 | height="1.142857143em" | ||
| 15 | width="1.142857143em" | ||
| 16 | {...props} | ||
| 17 | > | ||
| 18 | <use href={site.$.icon(icon, variant)} /> | ||
| 19 | </svg> | ||
| 20 | ); | ||
diff --git a/src/ui/objects/Lightbox.tsx b/src/ui/objects/Lightbox.tsx new file mode 100644 index 0000000..00268cc --- /dev/null +++ b/src/ui/objects/Lightbox.tsx | |||
| @@ -0,0 +1,122 @@ | |||
| 1 | /** Biome-ignore-all lint/a11y/useValidAnchor: <explanation> */ | ||
| 2 | /** Biome-ignore-all lint/a11y/useAltText: <explanation> */ | ||
| 3 | |||
| 4 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 5 | |||
| 6 | import { Icon } from "$/ui/objects/Icon.tsx"; | ||
| 7 | |||
| 8 | import { ActionButton } from "./ActionButton.tsx"; | ||
| 9 | import { LoadingIndicator } from "./LoadingIndicator.tsx"; | ||
| 10 | import { Thumbnail } from "./Thumbnail.tsx"; | ||
| 11 | |||
| 12 | export type Props = HTMLAttributes<"dialog">; | ||
| 13 | |||
| 14 | export const Lightbox = ({ class: cls, ...props }: Props) => ( | ||
| 15 | <dialog | ||
| 16 | class:list={["o-lightbox o-lightbox--static-black is-modal t-static-white", cls]} | ||
| 17 | closedby="any" | ||
| 18 | x-id="['lightbox']" | ||
| 19 | x-bind:id="$id('lightbox')" | ||
| 20 | x-data="lightbox" | ||
| 21 | x-on:close="handleClose" | ||
| 22 | x-on:touchstart="handleTouchStart" | ||
| 23 | x-on:touchend="handleTouchEnd" | ||
| 24 | {...{ | ||
| 25 | "x-on:keydown.document.capture": "handleKeyDown", | ||
| 26 | }} | ||
| 27 | {...props} | ||
| 28 | > | ||
| 29 | <div x-show="items.length > 1" class="o-lightbox__info"> | ||
| 30 | <small class="u-mis-100 u-mie-200 u-d-none@sm-lo"> | ||
| 31 | <strong x-text="current + 1"></strong> <span class="u-c-mute-more u-mi-50">⁄</span>{" "} | ||
| 32 | <strong x-text="items.length"></strong> | ||
| 33 | </small> | ||
| 34 | <ActionButton | ||
| 35 | x-on:click="prev" | ||
| 36 | as="button" | ||
| 37 | pill | ||
| 38 | level="quiet" | ||
| 39 | icon={<Icon icon="caret-left" variant="bold" />} | ||
| 40 | variant="static-white" | ||
| 41 | /> | ||
| 42 | <ActionButton | ||
| 43 | x-on:click="next" | ||
| 44 | as="button" | ||
| 45 | pill | ||
| 46 | level="quiet" | ||
| 47 | icon={<Icon icon="caret-right" variant="bold" />} | ||
| 48 | variant="static-white" | ||
| 49 | /> | ||
| 50 | </div> | ||
| 51 | |||
| 52 | <div class="o-lightbox__controls"> | ||
| 53 | <ActionButton | ||
| 54 | x-show="zoomedFull" | ||
| 55 | x-on:click="toggleZoom" | ||
| 56 | as="button" | ||
| 57 | class="u-mie-100" | ||
| 58 | pill | ||
| 59 | level="quiet" | ||
| 60 | icon={<Icon icon="magnifying-glass-minus" variant="bold" />} | ||
| 61 | variant="static-white" | ||
| 62 | /> | ||
| 63 | <ActionButton | ||
| 64 | x-show="!zoomedFull" | ||
| 65 | x-on:click="toggleZoom" | ||
| 66 | as="button" | ||
| 67 | class="u-mie-100" | ||
| 68 | pill | ||
| 69 | level="quiet" | ||
| 70 | icon={<Icon icon="magnifying-glass-plus" variant="bold" />} | ||
| 71 | variant="static-white" | ||
| 72 | /> | ||
| 73 | <ActionButton | ||
| 74 | x-bind:commandfor="$id('lightbox')" | ||
| 75 | command="close" | ||
| 76 | as="button" | ||
| 77 | pill | ||
| 78 | level="quiet" | ||
| 79 | icon={<Icon icon="x" variant="bold" />} | ||
| 80 | variant="static-white" | ||
| 81 | /> | ||
| 82 | </div> | ||
| 83 | |||
| 84 | <img | ||
| 85 | class="o-lightbox__img" | ||
| 86 | x-ref="img" | ||
| 87 | x-bind:src="currentItem?.src ?? ''" | ||
| 88 | x-bind:srcset="currentItem?.srcset ?? ''" | ||
| 89 | x-bind:alt="currentItem?.alt ?? ''" | ||
| 90 | x-on:load="loading = false" | ||
| 91 | x-on:mousemove="handleMouseMove" | ||
| 92 | x-on:touchmove="handleTouchMove" | ||
| 93 | x-on:dblclick="handleDoubleClick" | ||
| 94 | x-bind:class="{ 'is-hidden': loading }" | ||
| 95 | x-bind:style="{ transform: `translate(${offsetX}px, ${offsetY}px) scale(${zoom})` }" | ||
| 96 | {...{ | ||
| 97 | "x-on:mousedown.prevent": "handleMouseDown", | ||
| 98 | "x-on:mouseup.window": "handleMouseUp", | ||
| 99 | "x-on:wheel.prevent": "handleScroll", | ||
| 100 | }} | ||
| 101 | /> | ||
| 102 | |||
| 103 | <div class="o-lightbox__placeholder"> | ||
| 104 | <LoadingIndicator /> | ||
| 105 | </div> | ||
| 106 | |||
| 107 | <div class="o-lightbox__thumbnails" x-show="items.length > 1" x-ref="thumbnails"> | ||
| 108 | <template x-for="(item, i) in items"> | ||
| 109 | <Thumbnail | ||
| 110 | class="o-lightbox__thumbnail" | ||
| 111 | variant="static-white" | ||
| 112 | x-bind:src="item.thumbnail ?? item.src" | ||
| 113 | x-bind:srcset="item.srcset" | ||
| 114 | x-bind:alt="item.alt" | ||
| 115 | x-bind:href="item.src" | ||
| 116 | x-bind:class="{ 'is-selected': i === current }" | ||
| 117 | {...{ "x-on:click.prevent": "current = i" }} | ||
| 118 | /> | ||
| 119 | </template> | ||
| 120 | </div> | ||
| 121 | </dialog> | ||
| 122 | ); | ||
diff --git a/src/ui/objects/LoadingIndicator.tsx b/src/ui/objects/LoadingIndicator.tsx new file mode 100644 index 0000000..325cbb3 --- /dev/null +++ b/src/ui/objects/LoadingIndicator.tsx | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"svg">; | ||
| 4 | |||
| 5 | export const LoadingIndicator = (props: Props) => ( | ||
| 6 | <svg height="50" width="50" {...props}> | ||
| 7 | <title>Loading indicator</title> | ||
| 8 | <use href="#loading-indicator" /> | ||
| 9 | </svg> | ||
| 10 | ); | ||
diff --git a/src/ui/objects/Navbar.tsx b/src/ui/objects/Navbar.tsx new file mode 100644 index 0000000..4439e58 --- /dev/null +++ b/src/ui/objects/Navbar.tsx | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | /** Biome-ignore-all lint/a11y/noNoninteractiveElementToInteractiveRole: <explanation> */ | ||
| 2 | |||
| 3 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 4 | |||
| 5 | export type Props = HTMLAttributes<"nav"> & { | ||
| 6 | "adapt"?: boolean; | ||
| 7 | "quiet"?: boolean; | ||
| 8 | "align-block"?: boolean; | ||
| 9 | "variant"?: "static-black" | "static-white"; | ||
| 10 | }; | ||
| 11 | |||
| 12 | export const Navbar = ({ | ||
| 13 | adapt, | ||
| 14 | quiet, | ||
| 15 | "align-block": alignBlock, | ||
| 16 | variant, | ||
| 17 | class: cls, | ||
| 18 | ...props | ||
| 19 | }: Props) => ( | ||
| 20 | <nav | ||
| 21 | class:list={[ | ||
| 22 | "o-navbar", | ||
| 23 | { | ||
| 24 | "o-navbar--adapt": adapt, | ||
| 25 | "o-navbar--quiet": quiet, | ||
| 26 | "o-navbar--align-block": alignBlock, | ||
| 27 | [`o-navbar--${variant}`]: variant, | ||
| 28 | }, | ||
| 29 | "js-keyboard-nav", | ||
| 30 | cls, | ||
| 31 | ]} | ||
| 32 | data-keyboard-nav-item=".o-navbar__item" | ||
| 33 | data-keyboard-nav-mode="nav" | ||
| 34 | role="menubar" | ||
| 35 | {...props} | ||
| 36 | /> | ||
| 37 | ); | ||
diff --git a/src/ui/objects/NavbarItem.tsx b/src/ui/objects/NavbarItem.tsx new file mode 100644 index 0000000..c0e347f --- /dev/null +++ b/src/ui/objects/NavbarItem.tsx | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | as: Tag; | ||
| 5 | icon?: JSX.Element; | ||
| 6 | pre?: JSX.Element; | ||
| 7 | post?: JSX.Element; | ||
| 8 | selected?: boolean; | ||
| 9 | }>; | ||
| 10 | |||
| 11 | export const NavbarItem = <T extends HTMLTag = "a">({ | ||
| 12 | children, | ||
| 13 | as: Tag = "a" as T, | ||
| 14 | icon, | ||
| 15 | pre, | ||
| 16 | post, | ||
| 17 | selected, | ||
| 18 | class: cls, | ||
| 19 | ...props | ||
| 20 | }: Props<T>) => ( | ||
| 21 | <Tag | ||
| 22 | aria-current={selected ? "page" : undefined} | ||
| 23 | class:list={["o-navbar__item", { "is-selected": selected }, cls]} | ||
| 24 | role="menuitem" | ||
| 25 | {...props} | ||
| 26 | > | ||
| 27 | {icon && <div class="o-navbar__item-label">{children}</div>} | ||
| 28 | |||
| 29 | <div class="o-navbar__item-content"> | ||
| 30 | {pre} {icon ?? <span class="o-navbar__item-content-text">{children}</span>} {post} | ||
| 31 | </div> | ||
| 32 | </Tag> | ||
| 33 | ); | ||
diff --git a/src/ui/objects/PlaceholderBox.tsx b/src/ui/objects/PlaceholderBox.tsx new file mode 100644 index 0000000..d64866f --- /dev/null +++ b/src/ui/objects/PlaceholderBox.tsx | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | as: Tag; | ||
| 5 | }>; | ||
| 6 | |||
| 7 | export const PlaceholderBox = <T extends HTMLTag = "div">({ | ||
| 8 | as: Tag = "div" as T, | ||
| 9 | class: cls, | ||
| 10 | ...props | ||
| 11 | }: Props<T>) => <Tag class:list={["o-placeholder-box u-ta-center u-c-mute", cls]} {...props} />; | ||
diff --git a/src/ui/objects/SideNav.tsx b/src/ui/objects/SideNav.tsx new file mode 100644 index 0000000..f96ce7a --- /dev/null +++ b/src/ui/objects/SideNav.tsx | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | /** Biome-ignore-all lint/a11y/noNoninteractiveElementToInteractiveRole: <explanation> */ | ||
| 2 | |||
| 3 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 4 | |||
| 5 | export type Props = HTMLAttributes<"nav">; | ||
| 6 | |||
| 7 | export const SideNav = ({ class: cls, ...props }: Props) => ( | ||
| 8 | <nav | ||
| 9 | class:list={["o-side-nav", "js-keyboard-nav", cls]} | ||
| 10 | data-keyboard-nav-item=".o-side-nav__item" | ||
| 11 | data-keyboard-nav-mode="list" | ||
| 12 | {...props} | ||
| 13 | /> | ||
| 14 | ); | ||
diff --git a/src/ui/objects/SideNavItem.tsx b/src/ui/objects/SideNavItem.tsx new file mode 100644 index 0000000..fb8f523 --- /dev/null +++ b/src/ui/objects/SideNavItem.tsx | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props<Tag extends HTMLTag> = Polymorphic<{ | ||
| 4 | as: Tag; | ||
| 5 | icon?: JSX.Element; | ||
| 6 | pre?: JSX.Element; | ||
| 7 | post?: JSX.Element; | ||
| 8 | selected?: boolean; | ||
| 9 | }>; | ||
| 10 | |||
| 11 | export const SideNavItem = <T extends HTMLTag = "a">({ | ||
| 12 | children, | ||
| 13 | as: Tag = "a" as T, | ||
| 14 | icon, | ||
| 15 | pre, | ||
| 16 | post, | ||
| 17 | selected, | ||
| 18 | class: cls, | ||
| 19 | ...props | ||
| 20 | }: Props<T>) => ( | ||
| 21 | <Tag | ||
| 22 | aria-current={selected ? "location" : undefined} | ||
| 23 | class:list={["o-side-nav__item", { "is-selected": selected }, cls]} | ||
| 24 | role="menuitem" | ||
| 25 | {...props} | ||
| 26 | > | ||
| 27 | {icon && <div class="l-media__block o-side-nav__icon-slot">{icon}</div>} | ||
| 28 | |||
| 29 | <div class="l-media__block l-media__block--main"> | ||
| 30 | {pre} {children} {post} | ||
| 31 | </div> | ||
| 32 | </Tag> | ||
| 33 | ); | ||
diff --git a/src/ui/objects/StatusIndicator.tsx b/src/ui/objects/StatusIndicator.tsx new file mode 100644 index 0000000..06b6481 --- /dev/null +++ b/src/ui/objects/StatusIndicator.tsx | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"div"> & { | ||
| 4 | status?: "accent" | "positive" | "negative" | "warning"; | ||
| 5 | }; | ||
| 6 | |||
| 7 | export const StatusIndicator = ({ status, class: cls, ...props }: Props) => ( | ||
| 8 | <div | ||
| 9 | class:list={[ | ||
| 10 | "o-status-indicator", | ||
| 11 | { | ||
| 12 | [`is-${status}`]: status, | ||
| 13 | }, | ||
| 14 | cls, | ||
| 15 | ]} | ||
| 16 | {...props} | ||
| 17 | /> | ||
| 18 | ); | ||
diff --git a/src/ui/objects/Tab.tsx b/src/ui/objects/Tab.tsx new file mode 100644 index 0000000..15c1e5d --- /dev/null +++ b/src/ui/objects/Tab.tsx | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"a"> & { | ||
| 4 | selected?: boolean; | ||
| 5 | }; | ||
| 6 | |||
| 7 | export const Tab = ({ selected, class: cls, ...props }: Props) => ( | ||
| 8 | <a | ||
| 9 | aria-selected={`${selected}`} | ||
| 10 | class:list={["o-tabbar__tab", { "is-selected": selected }, cls]} | ||
| 11 | role="tab" | ||
| 12 | {...props} | ||
| 13 | /> | ||
| 14 | ); | ||
diff --git a/src/ui/objects/Tabbar.tsx b/src/ui/objects/Tabbar.tsx new file mode 100644 index 0000000..58c58fa --- /dev/null +++ b/src/ui/objects/Tabbar.tsx | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"nav"> & { | ||
| 4 | "variant"?: "accent"; | ||
| 5 | "quiet"?: boolean; | ||
| 6 | "adapt"?: boolean; | ||
| 7 | "tabs-class"?: unknown; | ||
| 8 | }; | ||
| 9 | |||
| 10 | export const Tabbar = ({ | ||
| 11 | children, | ||
| 12 | variant, | ||
| 13 | quiet, | ||
| 14 | adapt, | ||
| 15 | "tabs-class": tabsClass, | ||
| 16 | class: cls, | ||
| 17 | ...props | ||
| 18 | }: Props) => ( | ||
| 19 | <nav | ||
| 20 | class:list={[ | ||
| 21 | "o-tabbar", | ||
| 22 | { | ||
| 23 | [`o-tabbar--${variant}`]: variant, | ||
| 24 | "o-tabbar--quiet": quiet, | ||
| 25 | "o-tabbar--adapt": adapt, | ||
| 26 | }, | ||
| 27 | "js-keyboard-nav", | ||
| 28 | cls, | ||
| 29 | ]} | ||
| 30 | data-keyboard-nav-item=".o-tabbar__tab" | ||
| 31 | data-keyboard-nav-mode="nav" | ||
| 32 | {...props} | ||
| 33 | > | ||
| 34 | <div | ||
| 35 | aria-label={props["aria-label"]} | ||
| 36 | aria-orientation="horizontal" | ||
| 37 | class:list={["o-tabbar__tabs", tabsClass]} | ||
| 38 | role="tablist" | ||
| 39 | > | ||
| 40 | {children} | ||
| 41 | </div> | ||
| 42 | </nav> | ||
| 43 | ); | ||
diff --git a/src/ui/objects/TextField.tsx b/src/ui/objects/TextField.tsx new file mode 100644 index 0000000..dc7893a --- /dev/null +++ b/src/ui/objects/TextField.tsx | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 2 | |||
| 3 | export type Props = HTMLAttributes<"input"> & { | ||
| 4 | "pre"?: JSX.Element; | ||
| 5 | "post"?: JSX.Element; | ||
| 6 | "invalid"?: boolean; | ||
| 7 | "pill"?: boolean; | ||
| 8 | "x-model"?: string; | ||
| 9 | }; | ||
| 10 | |||
| 11 | export const TextField = ({ | ||
| 12 | pre, | ||
| 13 | post, | ||
| 14 | disabled, | ||
| 15 | invalid, | ||
| 16 | pill, | ||
| 17 | placeholder, | ||
| 18 | readonly, | ||
| 19 | value, | ||
| 20 | pattern, | ||
| 21 | required, | ||
| 22 | "x-model": xModel, | ||
| 23 | class: cls, | ||
| 24 | ...props | ||
| 25 | }: Props) => { | ||
| 26 | const extended = !!pre || !!post; | ||
| 27 | |||
| 28 | return ( | ||
| 29 | <div | ||
| 30 | class:list={[ | ||
| 31 | "o-text-field", | ||
| 32 | { | ||
| 33 | "is-disabled": disabled, | ||
| 34 | "is-invalid": invalid, | ||
| 35 | "l-media": extended, | ||
| 36 | "l-media--gapless": extended, | ||
| 37 | "o-text-field--extended": extended, | ||
| 38 | "o-text-field--pill": pill, | ||
| 39 | }, | ||
| 40 | cls, | ||
| 41 | ]} | ||
| 42 | {...props} | ||
| 43 | > | ||
| 44 | {pre}{" "} | ||
| 45 | <input | ||
| 46 | class="o-text-field__native" | ||
| 47 | class:list={{ | ||
| 48 | "l-media__block": extended, | ||
| 49 | "l-media__block--main": extended, | ||
| 50 | }} | ||
| 51 | disabled={disabled} | ||
| 52 | pattern={pattern} | ||
| 53 | placeholder={placeholder} | ||
| 54 | readonly={readonly} | ||
| 55 | required={required} | ||
| 56 | value={value} | ||
| 57 | x-model={xModel} | ||
| 58 | />{" "} | ||
| 59 | {post} <div class="o-text-field__bg"></div> | ||
| 60 | </div> | ||
| 61 | ); | ||
| 62 | }; | ||
diff --git a/src/ui/objects/Thumbnail.tsx b/src/ui/objects/Thumbnail.tsx new file mode 100644 index 0000000..4ef76d3 --- /dev/null +++ b/src/ui/objects/Thumbnail.tsx | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | import type { Props as ImageProps } from "corvos/plugins/images.ts"; | ||
| 2 | import type { HTMLAttributes } from "corvos/plugins/jsx.ts"; | ||
| 3 | |||
| 4 | import { Image } from "corvos/plugins/images.ts"; | ||
| 5 | |||
| 6 | export type Props = HTMLAttributes<"a"> & | ||
| 7 | ImageProps & { | ||
| 8 | "x-bind:src"?: string; | ||
| 9 | "x-bind:srcset"?: string; | ||
| 10 | "x-bind:alt"?: string; | ||
| 11 | "variant"?: "static-black" | "static-white"; | ||
| 12 | }; | ||
| 13 | |||
| 14 | export const Thumbnail = ({ | ||
| 15 | src, | ||
| 16 | srcset, | ||
| 17 | alt, | ||
| 18 | "x-bind:src": xbindSrc, | ||
| 19 | "x-bind:srcset": xbindSrcset, | ||
| 20 | "x-bind:alt": xbindAlt, | ||
| 21 | variant, | ||
| 22 | class: cls, | ||
| 23 | widths, | ||
| 24 | width, | ||
| 25 | height, | ||
| 26 | densities, | ||
| 27 | "no-transform": noTransform, | ||
| 28 | ...props | ||
| 29 | }: Props) => ( | ||
| 30 | <a class:list={["o-thumbnail", { [`o-thumbnail--${variant}`]: variant }, cls]} {...props}> | ||
| 31 | <Image | ||
| 32 | class="o-thumbnail__image" | ||
| 33 | src={src} | ||
| 34 | srcset={srcset} | ||
| 35 | widths={widths as undefined} | ||
| 36 | width={width} | ||
| 37 | height={height} | ||
| 38 | densities={densities} | ||
| 39 | no-transform={noTransform} | ||
| 40 | alt={alt} | ||
| 41 | x-bind:src={xbindSrc} | ||
| 42 | x-bind:srcset={xbindSrcset} | ||
| 43 | x-bind:alt={xbindAlt} | ||
| 44 | /> | ||
| 45 | </a> | ||
| 46 | ); | ||
