summaryrefslogtreecommitdiffstats
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/base/BaseLayout.tsx134
-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
-rw-r--r--src/ui/layouts/CardList.tsx52
-rw-r--r--src/ui/layouts/Container.tsx33
-rw-r--r--src/ui/layouts/EmojiGrid.tsx40
-rw-r--r--src/ui/layouts/HList.tsx44
-rw-r--r--src/ui/layouts/SplitView.tsx27
-rw-r--r--src/ui/layouts/SplitViewPanel.tsx33
-rw-r--r--src/ui/objects/ActionButton.tsx61
-rw-r--r--src/ui/objects/Alert.tsx21
-rw-r--r--src/ui/objects/Avatar.tsx69
-rw-r--r--src/ui/objects/Badge.tsx19
-rw-r--r--src/ui/objects/Body.tsx21
-rw-r--r--src/ui/objects/Button.tsx26
-rw-r--r--src/ui/objects/Card.tsx47
-rw-r--r--src/ui/objects/CardBody.tsx32
-rw-r--r--src/ui/objects/CardFooter.tsx11
-rw-r--r--src/ui/objects/CardImage.tsx40
-rw-r--r--src/ui/objects/CollapseCard.tsx27
-rw-r--r--src/ui/objects/ContentImage.tsx15
-rw-r--r--src/ui/objects/Deflist.tsx7
-rw-r--r--src/ui/objects/Dialog.tsx41
-rw-r--r--src/ui/objects/Divider.tsx51
-rw-r--r--src/ui/objects/Emoji.tsx21
-rw-r--r--src/ui/objects/Figure.tsx13
-rw-r--r--src/ui/objects/FigureCaption.tsx11
-rw-r--r--src/ui/objects/Heading.tsx49
-rw-r--r--src/ui/objects/Icon.tsx20
-rw-r--r--src/ui/objects/Lightbox.tsx122
-rw-r--r--src/ui/objects/LoadingIndicator.tsx10
-rw-r--r--src/ui/objects/Navbar.tsx37
-rw-r--r--src/ui/objects/NavbarItem.tsx33
-rw-r--r--src/ui/objects/PlaceholderBox.tsx11
-rw-r--r--src/ui/objects/SideNav.tsx14
-rw-r--r--src/ui/objects/SideNavItem.tsx33
-rw-r--r--src/ui/objects/StatusIndicator.tsx18
-rw-r--r--src/ui/objects/Tab.tsx14
-rw-r--r--src/ui/objects/Tabbar.tsx43
-rw-r--r--src/ui/objects/TextField.tsx62
-rw-r--r--src/ui/objects/Thumbnail.tsx46
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"
2import { getGenerator, type Select } from "corvos/mod.ts";
3
4import site from "@/corvos.config.ts";
5
6import { SiteFooter } from "../components/SiteFooter.tsx";
7import { SiteNav } from "../components/SiteNav.tsx";
8import { Lightbox } from "../objects/Lightbox.tsx";
9
10export 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
18export 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 @@
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};
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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export 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
23export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export 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
11export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"ul"> & {
4 id: string;
5 sprite: {
6 variants: { size: number; space: number }[];
7 wrap: number;
8 };
9 count: number;
10};
11
12export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3import type { Props as ActionButtonProps } from "../objects/ActionButton.tsx";
4
5export 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
15export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export type Props<Tag extends HTMLTag> = Polymorphic<{
4 "as": Tag;
5 "gapless"?: boolean;
6 "wrap-reverse"?: boolean;
7}>;
8
9export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export 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
11export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export 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
21export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export type Props<Tag extends HTMLTag> = Polymorphic<{
4 as?: Tag;
5 icon?: JSX.Element;
6 variant?: "accent" | "positive" | "negative" | "warning";
7}>;
8
9export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3import { StatusIndicator } from "$/ui/objects/StatusIndicator.tsx";
4
5export 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
20const hslRegex = /^hsl\(([\d.]+), ([\d.]+%), ([\d.]+%)\)$/;
21
22export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3import type { Props as ActionButtonProps } from "./ActionButton.tsx";
4
5import { ActionButton } from "./ActionButton.tsx";
6
7export 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
16export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"div">;
4
5export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3import type { Props as ActionButtonProps } from "./ActionButton.tsx";
4
5import { ActionButton } from "./ActionButton.tsx";
6
7export 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
16export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export 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
16export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export 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
11export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export type Props<Tag extends HTMLTag> = Polymorphic<{
4 as?: Tag;
5}>;
6
7export 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 @@
1import type { Props as ImageProps } from "corvos/plugins/images.ts";
2import type { CSSProperties } from "https://cdn.jsdelivr.net/gh/oscarotero/ssx@0.1.14/css.ts";
3
4import { Image } from "corvos/plugins/images.ts";
5
6export 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
15export 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 @@
1import { Card, type Props as CardProps } from "./Card.tsx";
2import { CardBody } from "./CardBody.tsx";
3import { Heading } from "./Heading.tsx";
4
5export type Props = CardProps<"details"> & {
6 "heading-level": "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
7 "title"?: JSX.Element;
8 "header"?: JSX.Element;
9};
10
11export 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 @@
1import type { ClassValue } from "corvos/deps/clsx.ts";
2import type { Props as ImageProps } from "corvos/plugins/images.ts";
3
4import { Image } from "corvos/plugins/images.ts";
5
6export type Props = Omit<ImageProps, "src"> & {
7 "src"?: string;
8 "img-class"?: ClassValue;
9};
10
11export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"dl">;
4
5export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3import { Icon } from "$/ui/objects/Icon.tsx";
4
5import { Container } from "../layouts/Container.tsx";
6import { ActionButton } from "./ActionButton.tsx";
7import { Divider } from "./Divider.tsx";
8import { Heading } from "./Heading.tsx";
9
10export type Props = HTMLAttributes<"dialog"> & {
11 title: string;
12 divider?: boolean;
13 children?: JSX.Element;
14};
15
16export 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 @@
1import type { HTMLAttributes, HTMLTag } from "corvos/plugins/jsx.ts";
2
3export 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
11export 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 @@
1import site from "@/corvos.config.ts";
2
3export interface Props {
4 pack: string;
5 emoji: string;
6 size?: "125" | "150" | "200";
7 class?: string;
8}
9
10export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"figure"> & {
4 children?: JSX.Element;
5 caption?: JSX.Element;
6};
7
8export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"figcaption"> & {
4 children?: JSX.Element;
5};
6
7export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export 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
12const levelTagMap = {
13 lg: "h3",
14 md: "h4",
15 sm: "h5",
16 xl: "h2",
17 xs: "h6",
18 xxl: "h1",
19} as const;
20
21export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3import site from "@/corvos.config.ts";
4
5export type Props = HTMLAttributes<"svg"> & {
6 icon: Parameters<typeof site.$.icon>[0];
7 variant?: Parameters<typeof site.$.icon>[1];
8};
9
10export 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
4import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
5
6import { Icon } from "$/ui/objects/Icon.tsx";
7
8import { ActionButton } from "./ActionButton.tsx";
9import { LoadingIndicator } from "./LoadingIndicator.tsx";
10import { Thumbnail } from "./Thumbnail.tsx";
11
12export type Props = HTMLAttributes<"dialog">;
13
14export 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">&#x2044;</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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"svg">;
4
5export 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
3import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
4
5export type Props = HTMLAttributes<"nav"> & {
6 "adapt"?: boolean;
7 "quiet"?: boolean;
8 "align-block"?: boolean;
9 "variant"?: "static-black" | "static-white";
10};
11
12export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export 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
11export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export type Props<Tag extends HTMLTag> = Polymorphic<{
4 as: Tag;
5}>;
6
7export 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
3import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
4
5export type Props = HTMLAttributes<"nav">;
6
7export 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 @@
1import type { HTMLTag, Polymorphic } from "corvos/plugins/jsx.ts";
2
3export 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
11export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"div"> & {
4 status?: "accent" | "positive" | "negative" | "warning";
5};
6
7export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"a"> & {
4 selected?: boolean;
5};
6
7export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"nav"> & {
4 "variant"?: "accent";
5 "quiet"?: boolean;
6 "adapt"?: boolean;
7 "tabs-class"?: unknown;
8};
9
10export 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 @@
1import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
2
3export type Props = HTMLAttributes<"input"> & {
4 "pre"?: JSX.Element;
5 "post"?: JSX.Element;
6 "invalid"?: boolean;
7 "pill"?: boolean;
8 "x-model"?: string;
9};
10
11export 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 @@
1import type { Props as ImageProps } from "corvos/plugins/images.ts";
2import type { HTMLAttributes } from "corvos/plugins/jsx.ts";
3
4import { Image } from "corvos/plugins/images.ts";
5
6export 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
14export 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);