mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
chore: initial Untitled UI Vite scaffold with FontAwesome Pro
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { X as CloseIcon, Menu02 } from "@untitledui/icons";
|
||||
import {
|
||||
Button as AriaButton,
|
||||
Dialog as AriaDialog,
|
||||
DialogTrigger as AriaDialogTrigger,
|
||||
Modal as AriaModal,
|
||||
ModalOverlay as AriaModalOverlay,
|
||||
} from "react-aria-components";
|
||||
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export const MobileNavigationHeader = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<AriaDialogTrigger>
|
||||
<header className="flex h-16 items-center justify-between border-b border-secondary bg-primary py-3 pr-2 pl-4 lg:hidden">
|
||||
<UntitledLogo />
|
||||
|
||||
<AriaButton
|
||||
aria-label="Expand navigation menu"
|
||||
className="group flex items-center justify-center rounded-lg bg-primary p-2 text-fg-secondary outline-focus-ring hover:bg-primary_hover hover:text-fg-secondary_hover focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
>
|
||||
<Menu02 className="size-6 transition duration-200 ease-in-out group-aria-expanded:opacity-0" />
|
||||
<CloseIcon className="absolute size-6 opacity-0 transition duration-200 ease-in-out group-aria-expanded:opacity-100" />
|
||||
</AriaButton>
|
||||
</header>
|
||||
|
||||
<AriaModalOverlay
|
||||
isDismissable
|
||||
className={({ isEntering, isExiting }) =>
|
||||
cx(
|
||||
"fixed inset-0 z-50 cursor-pointer bg-overlay/70 pr-16 backdrop-blur-md lg:hidden",
|
||||
isEntering && "duration-300 ease-in-out animate-in fade-in",
|
||||
isExiting && "duration-200 ease-in-out animate-out fade-out",
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ state }) => (
|
||||
<>
|
||||
<AriaButton
|
||||
aria-label="Close navigation menu"
|
||||
onPress={() => state.close()}
|
||||
className="fixed top-3 right-2 flex cursor-pointer items-center justify-center rounded-lg p-2 text-fg-white/70 outline-focus-ring hover:bg-white/10 hover:text-fg-white focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
>
|
||||
<CloseIcon className="size-6" />
|
||||
</AriaButton>
|
||||
|
||||
<AriaModal className="w-full cursor-auto will-change-transform">
|
||||
<AriaDialog className="h-dvh outline-hidden focus:outline-hidden">{children}</AriaDialog>
|
||||
</AriaModal>
|
||||
</>
|
||||
)}
|
||||
</AriaModalOverlay>
|
||||
</AriaDialogTrigger>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import type { FC, HTMLAttributes } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import type { Placement } from "@react-types/overlays";
|
||||
import { BookOpen01, ChevronSelectorVertical, LogOut01, Plus, Settings01, User01 } from "@untitledui/icons";
|
||||
import { useFocusManager } from "react-aria";
|
||||
import type { DialogProps as AriaDialogProps } from "react-aria-components";
|
||||
import { Button as AriaButton, Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Popover as AriaPopover } from "react-aria-components";
|
||||
import { AvatarLabelGroup } from "@/components/base/avatar/avatar-label-group";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { RadioButtonBase } from "@/components/base/radio-buttons/radio-buttons";
|
||||
import { useBreakpoint } from "@/hooks/use-breakpoint";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
type NavAccountType = {
|
||||
/** Unique identifier for the nav item. */
|
||||
id: string;
|
||||
/** Name of the account holder. */
|
||||
name: string;
|
||||
/** Email address of the account holder. */
|
||||
email: string;
|
||||
/** Avatar image URL. */
|
||||
avatar: string;
|
||||
/** Online status of the account holder. This is used to display the online status indicator. */
|
||||
status: "online" | "offline";
|
||||
};
|
||||
|
||||
const placeholderAccounts: NavAccountType[] = [
|
||||
{
|
||||
id: "olivia",
|
||||
name: "Olivia Rhye",
|
||||
email: "olivia@untitledui.com",
|
||||
avatar: "https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80",
|
||||
status: "online",
|
||||
},
|
||||
{
|
||||
id: "sienna",
|
||||
name: "Sienna Hewitt",
|
||||
email: "sienna@untitledui.com",
|
||||
avatar: "https://www.untitledui.com/images/avatars/transparent/sienna-hewitt?bg=%23E0E0E0",
|
||||
status: "online",
|
||||
},
|
||||
];
|
||||
|
||||
export const NavAccountMenu = ({
|
||||
className,
|
||||
selectedAccountId = "olivia",
|
||||
...dialogProps
|
||||
}: AriaDialogProps & { className?: string; accounts?: NavAccountType[]; selectedAccountId?: string }) => {
|
||||
const focusManager = useFocusManager();
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
focusManager?.focusNext({ tabbable: true, wrap: true });
|
||||
break;
|
||||
case "ArrowUp":
|
||||
focusManager?.focusPrevious({ tabbable: true, wrap: true });
|
||||
break;
|
||||
}
|
||||
},
|
||||
[focusManager],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const element = dialogRef.current;
|
||||
if (element) {
|
||||
element.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (element) {
|
||||
element.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
};
|
||||
}, [onKeyDown]);
|
||||
|
||||
return (
|
||||
<AriaDialog
|
||||
{...dialogProps}
|
||||
ref={dialogRef}
|
||||
className={cx("w-66 rounded-xl bg-secondary_alt shadow-lg ring ring-secondary_alt outline-hidden", className)}
|
||||
>
|
||||
<div className="rounded-xl bg-primary ring-1 ring-secondary">
|
||||
<div className="flex flex-col gap-0.5 py-1.5">
|
||||
<NavAccountCardMenuItem label="View profile" icon={User01} shortcut="⌘K->P" />
|
||||
<NavAccountCardMenuItem label="Account settings" icon={Settings01} shortcut="⌘S" />
|
||||
<NavAccountCardMenuItem label="Documentation" icon={BookOpen01} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 border-t border-secondary py-1.5">
|
||||
<div className="px-3 pt-1.5 pb-1 text-xs font-semibold text-tertiary">Switch account</div>
|
||||
|
||||
<div className="flex flex-col gap-0.5 px-1.5">
|
||||
{placeholderAccounts.map((account) => (
|
||||
<button
|
||||
key={account.id}
|
||||
className={cx(
|
||||
"relative w-full cursor-pointer rounded-md px-2 py-1.5 text-left outline-focus-ring hover:bg-primary_hover focus:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
account.id === selectedAccountId && "bg-primary_hover",
|
||||
)}
|
||||
>
|
||||
<AvatarLabelGroup status="online" size="md" src={account.avatar} title={account.name} subtitle={account.email} />
|
||||
|
||||
<RadioButtonBase isSelected={account.id === selectedAccountId} className="absolute top-2 right-2" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 px-2 pt-0.5 pb-2">
|
||||
<Button iconLeading={Plus} color="secondary" size="sm">
|
||||
Add account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-1 pb-1.5">
|
||||
<NavAccountCardMenuItem label="Sign out" icon={LogOut01} shortcut="⌥⇧Q" />
|
||||
</div>
|
||||
</AriaDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const NavAccountCardMenuItem = ({
|
||||
icon: Icon,
|
||||
label,
|
||||
shortcut,
|
||||
...buttonProps
|
||||
}: {
|
||||
icon?: FC<{ className?: string }>;
|
||||
label: string;
|
||||
shortcut?: string;
|
||||
} & HTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button {...buttonProps} className={cx("group/item w-full cursor-pointer px-1.5 focus:outline-hidden", buttonProps.className)}>
|
||||
<div
|
||||
className={cx(
|
||||
"flex w-full items-center justify-between gap-3 rounded-md p-2 group-hover/item:bg-primary_hover",
|
||||
// Focus styles.
|
||||
"outline-focus-ring group-focus-visible/item:outline-2 group-focus-visible/item:outline-offset-2",
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-2 text-sm font-semibold text-secondary group-hover/item:text-secondary_hover">
|
||||
{Icon && <Icon className="size-5 text-fg-quaternary" />} {label}
|
||||
</div>
|
||||
|
||||
{shortcut && (
|
||||
<kbd className="flex rounded px-1 py-px font-body text-xs font-medium text-tertiary ring-1 ring-secondary ring-inset">{shortcut}</kbd>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const NavAccountCard = ({
|
||||
popoverPlacement,
|
||||
selectedAccountId = "olivia",
|
||||
items = placeholderAccounts,
|
||||
}: {
|
||||
popoverPlacement?: Placement;
|
||||
selectedAccountId?: string;
|
||||
items?: NavAccountType[];
|
||||
}) => {
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const isDesktop = useBreakpoint("lg");
|
||||
|
||||
const selectedAccount = placeholderAccounts.find((account) => account.id === selectedAccountId);
|
||||
|
||||
if (!selectedAccount) {
|
||||
console.warn(`Account with ID ${selectedAccountId} not found in <NavAccountCard />`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={triggerRef} className="relative flex items-center gap-3 rounded-xl p-3 ring-1 ring-secondary ring-inset">
|
||||
<AvatarLabelGroup
|
||||
size="md"
|
||||
src={selectedAccount.avatar}
|
||||
title={selectedAccount.name}
|
||||
subtitle={selectedAccount.email}
|
||||
status={selectedAccount.status}
|
||||
/>
|
||||
|
||||
<div className="absolute top-1.5 right-1.5">
|
||||
<AriaDialogTrigger>
|
||||
<AriaButton className="flex cursor-pointer items-center justify-center rounded-md p-1.5 text-fg-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 pressed:bg-primary_hover pressed:text-fg-quaternary_hover">
|
||||
<ChevronSelectorVertical className="size-4 shrink-0" />
|
||||
</AriaButton>
|
||||
<AriaPopover
|
||||
placement={popoverPlacement ?? (isDesktop ? "right bottom" : "top right")}
|
||||
triggerRef={triggerRef}
|
||||
offset={8}
|
||||
className={({ isEntering, isExiting }) =>
|
||||
cx(
|
||||
"origin-(--trigger-anchor-point) will-change-transform",
|
||||
isEntering &&
|
||||
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||
isExiting &&
|
||||
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||
)
|
||||
}
|
||||
>
|
||||
<NavAccountMenu selectedAccountId={selectedAccountId} accounts={items} />
|
||||
</AriaPopover>
|
||||
</AriaDialogTrigger>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { FC, MouseEventHandler } from "react";
|
||||
import { Pressable } from "react-aria-components";
|
||||
import { Tooltip } from "@/components/base/tooltip/tooltip";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const styles = {
|
||||
md: {
|
||||
root: "size-10",
|
||||
icon: "size-5",
|
||||
},
|
||||
lg: {
|
||||
root: "size-12",
|
||||
icon: "size-6",
|
||||
},
|
||||
};
|
||||
|
||||
interface NavItemButtonProps {
|
||||
/** Whether the collapsible nav item is open. */
|
||||
open?: boolean;
|
||||
/** URL to navigate to when the button is clicked. */
|
||||
href?: string;
|
||||
/** Label text for the button. */
|
||||
label: string;
|
||||
/** Icon component to display. */
|
||||
icon: FC<{ className?: string }>;
|
||||
/** Whether the button is currently active. */
|
||||
current?: boolean;
|
||||
/** Size of the button. */
|
||||
size?: "md" | "lg";
|
||||
/** Handler for click events. */
|
||||
onClick?: MouseEventHandler;
|
||||
/** Additional CSS classes to apply to the button. */
|
||||
className?: string;
|
||||
/** Placement of the tooltip. */
|
||||
tooltipPlacement?: "top" | "right" | "bottom" | "left";
|
||||
}
|
||||
|
||||
export const NavItemButton = ({
|
||||
current: current,
|
||||
label,
|
||||
href,
|
||||
icon: Icon,
|
||||
size = "md",
|
||||
className,
|
||||
tooltipPlacement = "right",
|
||||
onClick,
|
||||
}: NavItemButtonProps) => {
|
||||
return (
|
||||
<Tooltip title={label} placement={tooltipPlacement}>
|
||||
<Pressable>
|
||||
<a
|
||||
href={href}
|
||||
aria-label={label}
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
"relative flex w-full cursor-pointer items-center justify-center rounded-md bg-primary p-2 text-fg-quaternary outline-focus-ring transition duration-100 ease-linear select-none hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
current && "bg-active text-fg-quaternary_hover hover:bg-secondary_hover",
|
||||
styles[size].root,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Icon aria-hidden="true" className={cx("shrink-0 transition-inherit-all", styles[size].icon)} />
|
||||
</a>
|
||||
</Pressable>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { FC, HTMLAttributes, MouseEventHandler, ReactNode } from "react";
|
||||
import { ChevronDown, Share04 } from "@untitledui/icons";
|
||||
import { Link as AriaLink } from "react-aria-components";
|
||||
import { Badge } from "@/components/base/badges/badges";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
|
||||
const styles = sortCx({
|
||||
root: "group relative flex w-full cursor-pointer items-center rounded-md bg-primary outline-focus-ring transition duration-100 ease-linear select-none hover:bg-primary_hover focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
rootSelected: "bg-active hover:bg-secondary_hover",
|
||||
});
|
||||
|
||||
interface NavItemBaseProps {
|
||||
/** Whether the nav item shows only an icon. */
|
||||
iconOnly?: boolean;
|
||||
/** Whether the collapsible nav item is open. */
|
||||
open?: boolean;
|
||||
/** URL to navigate to when the nav item is clicked. */
|
||||
href?: string;
|
||||
/** Type of the nav item. */
|
||||
type: "link" | "collapsible" | "collapsible-child";
|
||||
/** Icon component to display. */
|
||||
icon?: FC<HTMLAttributes<HTMLOrSVGElement>>;
|
||||
/** Badge to display. */
|
||||
badge?: ReactNode;
|
||||
/** Whether the nav item is currently active. */
|
||||
current?: boolean;
|
||||
/** Whether to truncate the label text. */
|
||||
truncate?: boolean;
|
||||
/** Handler for click events. */
|
||||
onClick?: MouseEventHandler;
|
||||
/** Content to display. */
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const NavItemBase = ({ current, type, badge, href, icon: Icon, children, truncate = true, onClick }: NavItemBaseProps) => {
|
||||
const iconElement = Icon && <Icon aria-hidden="true" className="mr-2 size-5 shrink-0 text-fg-quaternary transition-inherit-all" />;
|
||||
|
||||
const badgeElement =
|
||||
badge && (typeof badge === "string" || typeof badge === "number") ? (
|
||||
<Badge className="ml-3" color="gray" type="pill-color" size="sm">
|
||||
{badge}
|
||||
</Badge>
|
||||
) : (
|
||||
badge
|
||||
);
|
||||
|
||||
const labelElement = (
|
||||
<span
|
||||
className={cx(
|
||||
"flex-1 text-md font-semibold text-secondary transition-inherit-all group-hover:text-secondary_hover",
|
||||
truncate && "truncate",
|
||||
current && "text-secondary_hover",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
const isExternal = href && href.startsWith("http");
|
||||
const externalIcon = isExternal && <Share04 className="size-4 stroke-[2.5px] text-fg-quaternary" />;
|
||||
|
||||
if (type === "collapsible") {
|
||||
return (
|
||||
<summary className={cx("px-3 py-2", styles.root, current && styles.rootSelected)} onClick={onClick}>
|
||||
{iconElement}
|
||||
|
||||
{labelElement}
|
||||
|
||||
{badgeElement}
|
||||
|
||||
<ChevronDown aria-hidden="true" className="ml-3 size-4 shrink-0 stroke-[2.5px] text-fg-quaternary in-open:-scale-y-100" />
|
||||
</summary>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "collapsible-child") {
|
||||
return (
|
||||
<AriaLink
|
||||
href={href!}
|
||||
target={isExternal ? "_blank" : "_self"}
|
||||
rel="noopener noreferrer"
|
||||
className={cx("py-2 pr-3 pl-10", styles.root, current && styles.rootSelected)}
|
||||
onClick={onClick}
|
||||
aria-current={current ? "page" : undefined}
|
||||
>
|
||||
{labelElement}
|
||||
{externalIcon}
|
||||
{badgeElement}
|
||||
</AriaLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AriaLink
|
||||
href={href!}
|
||||
target={isExternal ? "_blank" : "_self"}
|
||||
rel="noopener noreferrer"
|
||||
className={cx("px-3 py-2", styles.root, current && styles.rootSelected)}
|
||||
onClick={onClick}
|
||||
aria-current={current ? "page" : undefined}
|
||||
>
|
||||
{iconElement}
|
||||
{labelElement}
|
||||
{externalIcon}
|
||||
{badgeElement}
|
||||
</AriaLink>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useState } from "react";
|
||||
import { cx } from "@/utils/cx";
|
||||
import type { NavItemDividerType, NavItemType } from "../config";
|
||||
import { NavItemBase } from "./nav-item";
|
||||
|
||||
interface NavListProps {
|
||||
/** URL of the currently active item. */
|
||||
activeUrl?: string;
|
||||
/** Additional CSS classes to apply to the list. */
|
||||
className?: string;
|
||||
/** List of items to display. */
|
||||
items: (NavItemType | NavItemDividerType)[];
|
||||
}
|
||||
|
||||
export const NavList = ({ activeUrl, items, className }: NavListProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const activeItem = items.find((item) => item.href === activeUrl || item.items?.some((subItem) => subItem.href === activeUrl));
|
||||
const [currentItem, setCurrentItem] = useState(activeItem);
|
||||
|
||||
return (
|
||||
<ul className={cx("mt-4 flex flex-col px-2 lg:px-4", className)}>
|
||||
{items.map((item, index) => {
|
||||
if (item.divider) {
|
||||
return (
|
||||
<li key={index} className="w-full px-0.5 py-2">
|
||||
<hr className="h-px w-full border-none bg-border-secondary" />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.items?.length) {
|
||||
return (
|
||||
<details
|
||||
key={item.label}
|
||||
open={activeItem?.href === item.href}
|
||||
className="appearance-none py-0.5"
|
||||
onToggle={(e) => {
|
||||
setOpen(e.currentTarget.open);
|
||||
setCurrentItem(item);
|
||||
}}
|
||||
>
|
||||
<NavItemBase href={item.href} badge={item.badge} icon={item.icon} type="collapsible">
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
|
||||
<dd>
|
||||
<ul className="py-0.5">
|
||||
{item.items.map((childItem) => (
|
||||
<li key={childItem.label} className="py-0.5">
|
||||
<NavItemBase
|
||||
href={childItem.href}
|
||||
badge={childItem.badge}
|
||||
type="collapsible-child"
|
||||
current={activeUrl === childItem.href}
|
||||
>
|
||||
{childItem.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</dd>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={item.label} className="py-0.5">
|
||||
<NavItemBase
|
||||
type="link"
|
||||
badge={item.badge}
|
||||
icon={item.icon}
|
||||
href={item.href}
|
||||
current={currentItem?.href === item.href}
|
||||
open={open && currentItem?.href === item.href}
|
||||
>
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
23
src/components/application/app-navigation/config.ts
Normal file
23
src/components/application/app-navigation/config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { FC, ReactNode } from "react";
|
||||
|
||||
export type NavItemType = {
|
||||
/** Label text for the nav item. */
|
||||
label: string;
|
||||
/** URL to navigate to when the nav item is clicked. */
|
||||
href?: string;
|
||||
/** Icon component to display. */
|
||||
icon?: FC<{ className?: string }>;
|
||||
/** Badge to display. */
|
||||
badge?: ReactNode;
|
||||
/** List of sub-items to display. */
|
||||
items?: { label: string; href: string; icon?: FC<{ className?: string }>; badge?: ReactNode }[];
|
||||
/** Whether this nav item is a divider. */
|
||||
divider?: boolean;
|
||||
};
|
||||
|
||||
export type NavItemDividerType = Omit<NavItemType, "icon" | "label" | "divider"> & {
|
||||
/** Label text for the divider. */
|
||||
label?: string;
|
||||
/** Whether this nav item is a divider. */
|
||||
divider: true;
|
||||
};
|
||||
202
src/components/application/app-navigation/header-navigation.tsx
Normal file
202
src/components/application/app-navigation/header-navigation.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { Bell01, LifeBuoy01, SearchLg, Settings01 } from "@untitledui/icons";
|
||||
import { Button as AriaButton, DialogTrigger, Popover } from "react-aria-components";
|
||||
import { Avatar } from "@/components/base/avatar/avatar";
|
||||
import { BadgeWithDot } from "@/components/base/badges/badges";
|
||||
import { Input } from "@/components/base/input/input";
|
||||
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { MobileNavigationHeader } from "./base-components/mobile-header";
|
||||
import { NavAccountCard, NavAccountMenu } from "./base-components/nav-account-card";
|
||||
import { NavItemBase } from "./base-components/nav-item";
|
||||
import { NavItemButton } from "./base-components/nav-item-button";
|
||||
import { NavList } from "./base-components/nav-list";
|
||||
|
||||
type NavItem = {
|
||||
/** Label text for the nav item. */
|
||||
label: string;
|
||||
/** URL to navigate to when the nav item is clicked. */
|
||||
href: string;
|
||||
/** Whether the nav item is currently active. */
|
||||
current?: boolean;
|
||||
/** Icon component to display. */
|
||||
icon?: FC<{ className?: string }>;
|
||||
/** Badge to display. */
|
||||
badge?: ReactNode;
|
||||
/** List of sub-items to display. */
|
||||
items?: NavItem[];
|
||||
};
|
||||
|
||||
interface HeaderNavigationBaseProps {
|
||||
/** URL of the currently active item. */
|
||||
activeUrl?: string;
|
||||
/** List of items to display. */
|
||||
items: NavItem[];
|
||||
/** List of sub-items to display. */
|
||||
subItems?: NavItem[];
|
||||
/** Content to display in the trailing position. */
|
||||
trailingContent?: ReactNode;
|
||||
/** Whether to show the avatar dropdown. */
|
||||
showAvatarDropdown?: boolean;
|
||||
/** Whether to hide the bottom border. */
|
||||
hideBorder?: boolean;
|
||||
}
|
||||
|
||||
export const HeaderNavigationBase = ({
|
||||
activeUrl,
|
||||
items,
|
||||
subItems,
|
||||
trailingContent,
|
||||
showAvatarDropdown = true,
|
||||
hideBorder = false,
|
||||
}: HeaderNavigationBaseProps) => {
|
||||
const activeSubNavItems = subItems || items.find((item) => item.current && item.items && item.items.length > 0)?.items;
|
||||
|
||||
const showSecondaryNav = activeSubNavItems && activeSubNavItems.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MobileNavigationHeader>
|
||||
<aside className="flex h-full max-w-full flex-col justify-between overflow-auto border-r border-secondary bg-primary pt-4 lg:pt-6">
|
||||
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||
<UntitledLogo className="h-8" />
|
||||
<Input shortcut size="sm" aria-label="Search" placeholder="Search" icon={SearchLg} />
|
||||
</div>
|
||||
|
||||
<NavList items={items} />
|
||||
|
||||
<div className="mt-auto flex flex-col gap-4 px-2 py-4 lg:px-4 lg:py-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<NavItemBase type="link" href="#" icon={LifeBuoy01}>
|
||||
Support
|
||||
</NavItemBase>
|
||||
<NavItemBase
|
||||
type="link"
|
||||
href="#"
|
||||
icon={Settings01}
|
||||
badge={
|
||||
<BadgeWithDot color="success" type="modern" size="sm">
|
||||
Online
|
||||
</BadgeWithDot>
|
||||
}
|
||||
>
|
||||
Settings
|
||||
</NavItemBase>
|
||||
<NavItemBase type="link" href="https://www.untitledui.com/" icon={Settings01}>
|
||||
Open in browser
|
||||
</NavItemBase>
|
||||
</div>
|
||||
|
||||
<NavAccountCard />
|
||||
</div>
|
||||
</aside>
|
||||
</MobileNavigationHeader>
|
||||
|
||||
<header className="max-lg:hidden">
|
||||
<section
|
||||
className={cx(
|
||||
"flex h-16 w-full items-center justify-center bg-primary md:h-18",
|
||||
(!hideBorder || showSecondaryNav) && "border-b border-secondary",
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full max-w-container justify-between pr-3 pl-4 md:px-8">
|
||||
<div className="flex flex-1 items-center gap-4">
|
||||
<a
|
||||
aria-label="Go to homepage"
|
||||
href="/"
|
||||
className="rounded-xs outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
>
|
||||
<UntitledLogo className="h-8" />
|
||||
</a>
|
||||
|
||||
<nav>
|
||||
<ul className="flex items-center gap-0.5">
|
||||
{items.map((item) => (
|
||||
<li key={item.label} className="py-0.5">
|
||||
<NavItemBase icon={item.icon} href={item.href} current={item.current} badge={item.badge} type="link">
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{trailingContent}
|
||||
|
||||
<div className="flex gap-0.5">
|
||||
<NavItemButton
|
||||
current={activeUrl === "/settings-01"}
|
||||
size="md"
|
||||
icon={Settings01}
|
||||
label="Settings"
|
||||
href="/settings-01"
|
||||
tooltipPlacement="bottom"
|
||||
/>
|
||||
<NavItemButton
|
||||
current={activeUrl === "/notifications-01"}
|
||||
size="md"
|
||||
icon={Bell01}
|
||||
label="Notifications"
|
||||
href="/notifications-01"
|
||||
tooltipPlacement="bottom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showAvatarDropdown && (
|
||||
<DialogTrigger>
|
||||
<AriaButton
|
||||
className={({ isPressed, isFocused }) =>
|
||||
cx(
|
||||
"group relative inline-flex cursor-pointer",
|
||||
(isPressed || isFocused) && "rounded-full outline-2 outline-offset-2 outline-focus-ring",
|
||||
)
|
||||
}
|
||||
>
|
||||
<Avatar alt="Olivia Rhye" src="https://www.untitledui.com/images/avatars/olivia-rhye?bg=%23E0E0E0" size="md" />
|
||||
</AriaButton>
|
||||
<Popover
|
||||
placement="bottom right"
|
||||
offset={8}
|
||||
className={({ isEntering, isExiting }) =>
|
||||
cx(
|
||||
"will-change-transform",
|
||||
isEntering &&
|
||||
"duration-300 ease-out animate-in fade-in placement-right:slide-in-from-left-2 placement-top:slide-in-from-bottom-2 placement-bottom:slide-in-from-top-2",
|
||||
isExiting &&
|
||||
"duration-150 ease-in animate-out fade-out placement-right:slide-out-to-left-2 placement-top:slide-out-to-bottom-2 placement-bottom:slide-out-to-top-2",
|
||||
)
|
||||
}
|
||||
>
|
||||
<NavAccountMenu />
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showSecondaryNav && (
|
||||
<section className={cx("flex h-16 w-full items-center justify-center bg-primary", !hideBorder && "border-b border-secondary")}>
|
||||
<div className="flex w-full max-w-container items-center justify-between gap-8 px-8">
|
||||
<nav>
|
||||
<ul className="flex items-center gap-0.5">
|
||||
{activeSubNavItems.map((item) => (
|
||||
<li key={item.label} className="py-0.5">
|
||||
<NavItemBase icon={item.icon} href={item.href} current={item.current} badge={item.badge} type="link">
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<Input shortcut aria-label="Search" placeholder="Search" icon={SearchLg} size="sm" className="max-w-xs" />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export { MobileNavigationHeader } from "./base-components/mobile-header";
|
||||
export { NavAccountCard } from "./base-components/nav-account-card";
|
||||
export { NavItemButton } from "./base-components/nav-item-button";
|
||||
export { NavItemBase } from "./base-components/nav-item";
|
||||
export { NavList } from "./base-components/nav-list";
|
||||
@@ -0,0 +1,149 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useState } from "react";
|
||||
import { SearchLg } from "@untitledui/icons";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { Input } from "@/components/base/input/input";
|
||||
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||
import { NavAccountCard } from "../base-components/nav-account-card";
|
||||
import { NavItemBase } from "../base-components/nav-item";
|
||||
import { NavList } from "../base-components/nav-list";
|
||||
import type { NavItemType } from "../config";
|
||||
|
||||
interface SidebarNavigationDualTierProps {
|
||||
/** URL of the currently active item. */
|
||||
activeUrl?: string;
|
||||
/** Feature card to display. */
|
||||
featureCard?: ReactNode;
|
||||
/** List of items to display. */
|
||||
items: NavItemType[];
|
||||
/** List of footer items to display. */
|
||||
footerItems?: NavItemType[];
|
||||
/** Whether to hide the right side border. */
|
||||
hideBorder?: boolean;
|
||||
}
|
||||
|
||||
export const SidebarNavigationDualTier = ({ activeUrl, hideBorder, items, footerItems = [], featureCard }: SidebarNavigationDualTierProps) => {
|
||||
const activeItem = [...items, ...footerItems].find((item) => item.href === activeUrl || item.items?.some((subItem) => subItem.href === activeUrl));
|
||||
const [currentItem, setCurrentItem] = useState(activeItem || items[1]);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
const isSecondarySidebarVisible = isHovering && Boolean(currentItem.items?.length);
|
||||
|
||||
const MAIN_SIDEBAR_WIDTH = 296;
|
||||
const SECONDARY_SIDEBAR_WIDTH = 256;
|
||||
|
||||
const mainSidebar = (
|
||||
<aside className="group flex h-full max-h-full max-w-full overflow-y-auto bg-primary">
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--width": `${MAIN_SIDEBAR_WIDTH}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cx(
|
||||
"relative flex w-full flex-col border-r border-secondary pt-4 transition duration-300 lg:w-(--width) lg:pt-6",
|
||||
hideBorder && !isSecondarySidebarVisible && "border-transparent",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||
<UntitledLogo className="h-8" />
|
||||
<Input shortcut size="sm" aria-label="Search" placeholder="Search" icon={SearchLg} />
|
||||
</div>
|
||||
|
||||
<NavList activeUrl={activeUrl} items={items} className="lg:hidden" />
|
||||
|
||||
<ul className="mt-4 hidden flex-col px-4 lg:flex">
|
||||
{items.map((item) => (
|
||||
<li key={item.label + item.href} className="py-0.5">
|
||||
<NavItemBase
|
||||
current={currentItem.href === item.href}
|
||||
href={item.href}
|
||||
badge={item.badge}
|
||||
icon={item.icon}
|
||||
type="link"
|
||||
onClick={() => setCurrentItem(item)}
|
||||
>
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-auto flex flex-col gap-4 px-2 py-4 lg:px-4 lg:py-6">
|
||||
{footerItems.length > 0 && (
|
||||
<ul className="flex flex-col">
|
||||
{footerItems.map((item) => (
|
||||
<li key={item.label + item.href} className="py-0.5">
|
||||
<NavItemBase
|
||||
current={currentItem.href === item.href}
|
||||
href={item.href}
|
||||
badge={item.badge}
|
||||
icon={item.icon}
|
||||
type="link"
|
||||
onClick={() => setCurrentItem(item)}
|
||||
>
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{featureCard}
|
||||
|
||||
<NavAccountCard />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
const secondarySidebar = (
|
||||
<AnimatePresence initial={false}>
|
||||
{isSecondarySidebarVisible && (
|
||||
<motion.div
|
||||
initial={{ width: 0, borderColor: "var(--color-border-secondary)" }}
|
||||
animate={{ width: SECONDARY_SIDEBAR_WIDTH, borderColor: "var(--color-border-secondary)" }}
|
||||
exit={{ width: 0, borderColor: "rgba(0,0,0,0)", transition: { borderColor: { type: "tween", delay: 0.05 } } }}
|
||||
transition={{ type: "spring", damping: 26, stiffness: 220, bounce: 0 }}
|
||||
className={cx("relative h-full overflow-x-hidden overflow-y-auto bg-primary", !hideBorder && "box-content border-r-[1.5px]")}
|
||||
>
|
||||
<ul style={{ width: SECONDARY_SIDEBAR_WIDTH }} className="flex h-full flex-col p-4 py-6">
|
||||
{currentItem.items?.map((item) => (
|
||||
<li key={item.label + item.href} className="py-0.5">
|
||||
<NavItemBase current={activeUrl === item.href} href={item.href} icon={item.icon} badge={item.badge} type="link">
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile header navigation */}
|
||||
<MobileNavigationHeader>{mainSidebar}</MobileNavigationHeader>
|
||||
|
||||
{/* Desktop sidebar navigation */}
|
||||
<div
|
||||
className="z-50 hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex"
|
||||
onPointerEnter={() => setIsHovering(true)}
|
||||
onPointerLeave={() => setIsHovering(false)}
|
||||
>
|
||||
{mainSidebar}
|
||||
{secondarySidebar}
|
||||
</div>
|
||||
|
||||
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: MAIN_SIDEBAR_WIDTH,
|
||||
}}
|
||||
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { SearchLg } from "@untitledui/icons";
|
||||
import { Input } from "@/components/base/input/input";
|
||||
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||
import { NavAccountCard } from "../base-components/nav-account-card";
|
||||
import { NavList } from "../base-components/nav-list";
|
||||
import type { NavItemDividerType, NavItemType } from "../config";
|
||||
|
||||
interface SidebarNavigationSectionDividersProps {
|
||||
/** URL of the currently active item. */
|
||||
activeUrl?: string;
|
||||
/** List of items to display. */
|
||||
items: (NavItemType | NavItemDividerType)[];
|
||||
}
|
||||
|
||||
export const SidebarNavigationSectionDividers = ({ activeUrl, items }: SidebarNavigationSectionDividersProps) => {
|
||||
const MAIN_SIDEBAR_WIDTH = 292;
|
||||
|
||||
const content = (
|
||||
<aside
|
||||
style={
|
||||
{
|
||||
"--width": `${MAIN_SIDEBAR_WIDTH}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="flex h-full w-full max-w-full flex-col justify-between overflow-auto border-secondary bg-primary pt-4 shadow-xs md:border-r lg:w-(--width) lg:rounded-xl lg:border lg:pt-5"
|
||||
>
|
||||
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||
<UntitledLogo className="h-8" />
|
||||
<Input shortcut size="sm" aria-label="Search" placeholder="Search" icon={SearchLg} />
|
||||
</div>
|
||||
|
||||
<NavList activeUrl={activeUrl} items={items} className="mt-5" />
|
||||
|
||||
<div className="mt-auto flex flex-col gap-5 px-2 py-4 lg:gap-6 lg:px-4 lg:py-4">
|
||||
<NavAccountCard />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile header navigation */}
|
||||
<MobileNavigationHeader>{content}</MobileNavigationHeader>
|
||||
|
||||
{/* Desktop sidebar navigation */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:py-1 lg:pl-1">{content}</div>
|
||||
|
||||
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: MAIN_SIDEBAR_WIDTH + 4, // Add 4px to account for the padding in the sidebar wrapper
|
||||
}}
|
||||
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||
import { NavAccountCard } from "../base-components/nav-account-card";
|
||||
import { NavItemBase } from "../base-components/nav-item";
|
||||
import type { NavItemType } from "../config";
|
||||
|
||||
interface SidebarNavigationSectionsSubheadingsProps {
|
||||
/** URL of the currently active item. */
|
||||
activeUrl?: string;
|
||||
/** List of items to display. */
|
||||
items: Array<{ label: string; items: NavItemType[] }>;
|
||||
}
|
||||
|
||||
export const SidebarNavigationSectionsSubheadings = ({ activeUrl = "/", items }: SidebarNavigationSectionsSubheadingsProps) => {
|
||||
const MAIN_SIDEBAR_WIDTH = 292;
|
||||
|
||||
const content = (
|
||||
<aside
|
||||
style={
|
||||
{
|
||||
"--width": `${MAIN_SIDEBAR_WIDTH}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="flex h-full w-full max-w-full flex-col justify-between overflow-auto border-secondary bg-primary pt-4 shadow-xs md:border-r lg:w-(--width) lg:rounded-xl lg:border lg:pt-5"
|
||||
>
|
||||
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||
<UntitledLogo className="h-8" />
|
||||
</div>
|
||||
|
||||
<ul className="mt-8">
|
||||
{items.map((group) => (
|
||||
<li key={group.label}>
|
||||
<div className="px-5 pb-1">
|
||||
<p className="text-xs font-bold text-quaternary uppercase">{group.label}</p>
|
||||
</div>
|
||||
<ul className="px-4 pb-5">
|
||||
{group.items.map((item) => (
|
||||
<li key={item.label} className="py-0.5">
|
||||
<NavItemBase icon={item.icon} href={item.href} badge={item.badge} type="link" current={item.href === activeUrl}>
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mt-auto flex flex-col gap-5 px-2 py-4 lg:gap-6 lg:px-4 lg:py-4">
|
||||
<NavAccountCard />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile header navigation */}
|
||||
<MobileNavigationHeader>{content}</MobileNavigationHeader>
|
||||
|
||||
{/* Desktop sidebar navigation */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:py-1 lg:pl-1">{content}</div>
|
||||
|
||||
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: MAIN_SIDEBAR_WIDTH + 4,
|
||||
}}
|
||||
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { SearchLg } from "@untitledui/icons";
|
||||
import { Input } from "@/components/base/input/input";
|
||||
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||
import { NavAccountCard } from "../base-components/nav-account-card";
|
||||
import { NavItemBase } from "../base-components/nav-item";
|
||||
import { NavList } from "../base-components/nav-list";
|
||||
import type { NavItemType } from "../config";
|
||||
|
||||
interface SidebarNavigationProps {
|
||||
/** URL of the currently active item. */
|
||||
activeUrl?: string;
|
||||
/** List of items to display. */
|
||||
items: NavItemType[];
|
||||
/** List of footer items to display. */
|
||||
footerItems?: NavItemType[];
|
||||
/** Feature card to display. */
|
||||
featureCard?: ReactNode;
|
||||
/** Whether to show the account card. */
|
||||
showAccountCard?: boolean;
|
||||
/** Whether to hide the right side border. */
|
||||
hideBorder?: boolean;
|
||||
/** Additional CSS classes to apply to the sidebar. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SidebarNavigationSimple = ({
|
||||
activeUrl,
|
||||
items,
|
||||
footerItems = [],
|
||||
featureCard,
|
||||
showAccountCard = true,
|
||||
hideBorder = false,
|
||||
className,
|
||||
}: SidebarNavigationProps) => {
|
||||
const MAIN_SIDEBAR_WIDTH = 296;
|
||||
|
||||
const content = (
|
||||
<aside
|
||||
style={
|
||||
{
|
||||
"--width": `${MAIN_SIDEBAR_WIDTH}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cx(
|
||||
"flex h-full w-full max-w-full flex-col justify-between overflow-auto bg-primary pt-4 lg:w-(--width) lg:pt-6",
|
||||
!hideBorder && "border-secondary md:border-r",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-5 px-4 lg:px-5">
|
||||
<UntitledLogo className="h-8" />
|
||||
<Input shortcut size="sm" aria-label="Search" placeholder="Search" icon={SearchLg} />
|
||||
</div>
|
||||
|
||||
<NavList activeUrl={activeUrl} items={items} />
|
||||
|
||||
<div className="mt-auto flex flex-col gap-4 px-2 py-4 lg:px-4 lg:py-6">
|
||||
{footerItems.length > 0 && (
|
||||
<ul className="flex flex-col">
|
||||
{footerItems.map((item) => (
|
||||
<li key={item.label} className="py-0.5">
|
||||
<NavItemBase badge={item.badge} icon={item.icon} href={item.href} type="link" current={item.href === activeUrl}>
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{featureCard}
|
||||
|
||||
{showAccountCard && <NavAccountCard />}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile header navigation */}
|
||||
<MobileNavigationHeader>{content}</MobileNavigationHeader>
|
||||
|
||||
{/* Desktop sidebar navigation */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex">{content}</div>
|
||||
|
||||
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: MAIN_SIDEBAR_WIDTH,
|
||||
}}
|
||||
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { LifeBuoy01, LogOut01, Settings01 } from "@untitledui/icons";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { Button as AriaButton, DialogTrigger as AriaDialogTrigger, Popover as AriaPopover } from "react-aria-components";
|
||||
import { Avatar } from "@/components/base/avatar/avatar";
|
||||
import { AvatarLabelGroup } from "@/components/base/avatar/avatar-label-group";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { ButtonUtility } from "@/components/base/buttons/button-utility";
|
||||
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
||||
import { UntitledLogoMinimal } from "@/components/foundations/logo/untitledui-logo-minimal";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { MobileNavigationHeader } from "../base-components/mobile-header";
|
||||
import { NavAccountMenu } from "../base-components/nav-account-card";
|
||||
import { NavItemBase } from "../base-components/nav-item";
|
||||
import { NavItemButton } from "../base-components/nav-item-button";
|
||||
import { NavList } from "../base-components/nav-list";
|
||||
import type { NavItemType } from "../config";
|
||||
|
||||
interface SidebarNavigationSlimProps {
|
||||
/** URL of the currently active item. */
|
||||
activeUrl?: string;
|
||||
/** List of items to display. */
|
||||
items: (NavItemType & { icon: FC<{ className?: string }> })[];
|
||||
/** List of footer items to display. */
|
||||
footerItems?: (NavItemType & { icon: FC<{ className?: string }> })[];
|
||||
/** Whether to hide the border. */
|
||||
hideBorder?: boolean;
|
||||
/** Whether to hide the right side border. */
|
||||
hideRightBorder?: boolean;
|
||||
}
|
||||
|
||||
export const SidebarNavigationSlim = ({ activeUrl, items, footerItems = [], hideBorder, hideRightBorder }: SidebarNavigationSlimProps) => {
|
||||
const activeItem = [...items, ...footerItems].find((item) => item.href === activeUrl || item.items?.some((subItem) => subItem.href === activeUrl));
|
||||
const [currentItem, setCurrentItem] = useState(activeItem || items[1]);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
const isSecondarySidebarVisible = isHovering && Boolean(currentItem.items?.length);
|
||||
|
||||
const MAIN_SIDEBAR_WIDTH = 68;
|
||||
const SECONDARY_SIDEBAR_WIDTH = 268;
|
||||
|
||||
const mainSidebar = (
|
||||
<aside
|
||||
style={{
|
||||
width: MAIN_SIDEBAR_WIDTH,
|
||||
}}
|
||||
className={cx(
|
||||
"group flex h-full max-h-full max-w-full overflow-y-auto py-1 pl-1 transition duration-100 ease-linear",
|
||||
isSecondarySidebarVisible && "bg-primary",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
"flex w-auto flex-col justify-between rounded-xl bg-primary pt-5 ring-1 ring-secondary transition duration-300 ring-inset",
|
||||
hideBorder && !isSecondarySidebarVisible && "ring-transparent",
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-center px-3">
|
||||
<UntitledLogoMinimal className="size-8" />
|
||||
</div>
|
||||
|
||||
<ul className="mt-4 flex flex-col gap-0.5 px-3">
|
||||
{items.map((item) => (
|
||||
<li key={item.label}>
|
||||
<NavItemButton
|
||||
size="md"
|
||||
current={currentItem.href === item.href}
|
||||
href={item.href}
|
||||
label={item.label || ""}
|
||||
icon={item.icon}
|
||||
onClick={() => setCurrentItem(item)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-auto flex flex-col gap-4 px-3 py-5">
|
||||
{footerItems.length > 0 && (
|
||||
<ul className="flex flex-col gap-0.5">
|
||||
{footerItems.map((item) => (
|
||||
<li key={item.label}>
|
||||
<NavItemButton
|
||||
size="md"
|
||||
current={currentItem.href === item.href}
|
||||
label={item.label || ""}
|
||||
href={item.href}
|
||||
icon={item.icon}
|
||||
onClick={() => setCurrentItem(item)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<AriaDialogTrigger>
|
||||
<AriaButton
|
||||
className={({ isPressed, isFocused }) =>
|
||||
cx("group relative inline-flex rounded-full", (isPressed || isFocused) && "outline-2 outline-offset-2 outline-focus-ring")
|
||||
}
|
||||
>
|
||||
<Avatar status="online" src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80" size="md" alt="Olivia Rhye" />
|
||||
</AriaButton>
|
||||
<AriaPopover
|
||||
placement="right bottom"
|
||||
offset={8}
|
||||
crossOffset={6}
|
||||
className={({ isEntering, isExiting }) =>
|
||||
cx(
|
||||
"will-change-transform",
|
||||
isEntering &&
|
||||
"duration-300 ease-out animate-in fade-in placement-right:slide-in-from-left-2 placement-top:slide-in-from-bottom-2 placement-bottom:slide-in-from-top-2",
|
||||
isExiting &&
|
||||
"duration-150 ease-in animate-out fade-out placement-right:slide-out-to-left-2 placement-top:slide-out-to-bottom-2 placement-bottom:slide-out-to-top-2",
|
||||
)
|
||||
}
|
||||
>
|
||||
<NavAccountMenu />
|
||||
</AriaPopover>
|
||||
</AriaDialogTrigger>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
const secondarySidebar = (
|
||||
<AnimatePresence initial={false}>
|
||||
{isSecondarySidebarVisible && (
|
||||
<motion.div
|
||||
initial={{ width: 0, borderColor: "var(--color-border-secondary)" }}
|
||||
animate={{ width: SECONDARY_SIDEBAR_WIDTH, borderColor: "var(--color-border-secondary)" }}
|
||||
exit={{ width: 0, borderColor: "rgba(0,0,0,0)", transition: { borderColor: { type: "tween", delay: 0.05 } } }}
|
||||
transition={{ type: "spring", damping: 26, stiffness: 220, bounce: 0 }}
|
||||
className={cx(
|
||||
"relative h-full overflow-x-hidden overflow-y-auto bg-primary",
|
||||
!(hideBorder || hideRightBorder) && "box-content border-r-[1.5px]",
|
||||
)}
|
||||
>
|
||||
<div style={{ width: SECONDARY_SIDEBAR_WIDTH }} className="flex h-full flex-col px-4 pt-6">
|
||||
<h3 className="text-sm font-semibold text-brand-secondary">{currentItem.label}</h3>
|
||||
<ul className="py-2">
|
||||
{currentItem.items?.map((item) => (
|
||||
<li key={item.label} className="py-0.5">
|
||||
<NavItemBase current={activeUrl === item.href} href={item.href} icon={item.icon} badge={item.badge} type="link">
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="sticky bottom-0 mt-auto flex justify-between border-t border-secondary bg-primary px-2 py-5">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-primary">Olivia Rhye</p>
|
||||
<p className="text-sm text-tertiary">olivia@untitledui.com</p>
|
||||
</div>
|
||||
<div className="absolute top-2.5 right-0">
|
||||
<ButtonUtility size="sm" color="tertiary" tooltip="Log out" icon={LogOut01} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop sidebar navigation */}
|
||||
<div
|
||||
className="z-50 hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex"
|
||||
onPointerEnter={() => setIsHovering(true)}
|
||||
onPointerLeave={() => setIsHovering(false)}
|
||||
>
|
||||
{mainSidebar}
|
||||
{secondarySidebar}
|
||||
</div>
|
||||
|
||||
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: MAIN_SIDEBAR_WIDTH,
|
||||
}}
|
||||
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||
/>
|
||||
|
||||
{/* Mobile header navigation */}
|
||||
<MobileNavigationHeader>
|
||||
<aside className="group flex h-full max-h-full w-full max-w-full flex-col justify-between overflow-y-auto bg-primary pt-4">
|
||||
<div className="px-4">
|
||||
<UntitledLogo className="h-8" />
|
||||
</div>
|
||||
|
||||
<NavList items={items} />
|
||||
|
||||
<div className="mt-auto flex flex-col gap-5 px-2 py-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<NavItemBase current={activeUrl === "/support"} type="link" href="/support" icon={LifeBuoy01}>
|
||||
Support
|
||||
</NavItemBase>
|
||||
<NavItemBase current={activeUrl === "/settings"} type="link" href="/settings" icon={Settings01}>
|
||||
Settings
|
||||
</NavItemBase>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center gap-3 border-t border-secondary pt-6 pr-8 pl-2">
|
||||
<AvatarLabelGroup
|
||||
status="online"
|
||||
size="md"
|
||||
src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80"
|
||||
title="Olivia Rhye"
|
||||
subtitle="olivia@untitledui.com"
|
||||
/>
|
||||
|
||||
<div className="absolute top-1/2 right-0 -translate-y-1/2">
|
||||
<Button
|
||||
size="sm"
|
||||
color="tertiary"
|
||||
iconLeading={<LogOut01 className="size-5 text-fg-quaternary transition-inherit-all group-hover:text-fg-quaternary_hover" />}
|
||||
className="p-1.5!"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</MobileNavigationHeader>
|
||||
</>
|
||||
);
|
||||
};
|
||||
99
src/components/application/date-picker/calendar.tsx
Normal file
99
src/components/application/date-picker/calendar.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { HTMLAttributes, PropsWithChildren } from "react";
|
||||
import { Fragment, useContext, useState } from "react";
|
||||
import { type CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
|
||||
import { ChevronLeft, ChevronRight } from "@untitledui/icons";
|
||||
import type { CalendarProps as AriaCalendarProps, DateValue } from "react-aria-components";
|
||||
import {
|
||||
Calendar as AriaCalendar,
|
||||
CalendarContext as AriaCalendarContext,
|
||||
CalendarGrid as AriaCalendarGrid,
|
||||
CalendarGridBody as AriaCalendarGridBody,
|
||||
CalendarGridHeader as AriaCalendarGridHeader,
|
||||
CalendarHeaderCell as AriaCalendarHeaderCell,
|
||||
CalendarStateContext as AriaCalendarStateContext,
|
||||
Heading as AriaHeading,
|
||||
useSlottedContext,
|
||||
} from "react-aria-components";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { CalendarCell } from "./cell";
|
||||
import { DateInput } from "./date-input";
|
||||
|
||||
export const CalendarContextProvider = ({ children }: PropsWithChildren) => {
|
||||
const [value, onChange] = useState<DateValue | null>(null);
|
||||
const [focusedValue, onFocusChange] = useState<DateValue | undefined>();
|
||||
|
||||
return <AriaCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }}>{children}</AriaCalendarContext.Provider>;
|
||||
};
|
||||
|
||||
const PresetButton = ({ value, children, ...props }: HTMLAttributes<HTMLButtonElement> & { value: CalendarDate }) => {
|
||||
const context = useContext(AriaCalendarStateContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("Preset must be used within a Calendar component");
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
context.setValue(value);
|
||||
context.setFocusedDate(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
// It's important to give `null` explicitly to the `slot` prop
|
||||
// otherwise the button will throw an error due to not using one of
|
||||
// the required slots inside the Calendar component.
|
||||
// Passing `null` will tell the button to not use a slot context.
|
||||
slot={null}
|
||||
size="md"
|
||||
color="secondary"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface CalendarProps extends AriaCalendarProps<DateValue> {
|
||||
/** The dates to highlight. */
|
||||
highlightedDates?: DateValue[];
|
||||
}
|
||||
|
||||
export const Calendar = ({ highlightedDates, className, ...props }: CalendarProps) => {
|
||||
const context = useSlottedContext(AriaCalendarContext)!;
|
||||
|
||||
const ContextWrapper = context ? Fragment : CalendarContextProvider;
|
||||
|
||||
return (
|
||||
<ContextWrapper>
|
||||
<AriaCalendar {...props} className={(state) => cx("flex flex-col gap-3", typeof className === "function" ? className(state) : className)}>
|
||||
<header className="flex items-center justify-between">
|
||||
<Button slot="previous" iconLeading={ChevronLeft} size="sm" color="tertiary" className="size-8" />
|
||||
<AriaHeading className="text-sm font-semibold text-fg-secondary" />
|
||||
<Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8" />
|
||||
</header>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<DateInput className="flex-1" />
|
||||
<PresetButton value={today(getLocalTimeZone())}>Today</PresetButton>
|
||||
</div>
|
||||
|
||||
<AriaCalendarGrid weekdayStyle="short" className="w-max">
|
||||
<AriaCalendarGridHeader className="border-b-4 border-transparent">
|
||||
{(day) => (
|
||||
<AriaCalendarHeaderCell className="p-0">
|
||||
<div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
|
||||
</AriaCalendarHeaderCell>
|
||||
)}
|
||||
</AriaCalendarGridHeader>
|
||||
<AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
|
||||
{(date) => (
|
||||
<CalendarCell date={date} isHighlighted={highlightedDates?.some((highlightedDate) => date.compare(highlightedDate) === 0)} />
|
||||
)}
|
||||
</AriaCalendarGridBody>
|
||||
</AriaCalendarGrid>
|
||||
</AriaCalendar>
|
||||
</ContextWrapper>
|
||||
);
|
||||
};
|
||||
104
src/components/application/date-picker/cell.tsx
Normal file
104
src/components/application/date-picker/cell.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { getDayOfWeek, getLocalTimeZone, isToday } from "@internationalized/date";
|
||||
import type { CalendarCellProps as AriaCalendarCellProps } from "react-aria-components";
|
||||
import { CalendarCell as AriaCalendarCell, RangeCalendarContext, useLocale, useSlottedContext } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface CalendarCellProps extends AriaCalendarCellProps {
|
||||
/** Whether the calendar is a range calendar. */
|
||||
isRangeCalendar?: boolean;
|
||||
/** Whether the cell is highlighted. */
|
||||
isHighlighted?: boolean;
|
||||
}
|
||||
|
||||
export const CalendarCell = ({ date, isHighlighted, ...props }: CalendarCellProps) => {
|
||||
const { locale } = useLocale();
|
||||
const dayOfWeek = getDayOfWeek(date, locale);
|
||||
const rangeCalendarContext = useSlottedContext(RangeCalendarContext);
|
||||
|
||||
const isRangeCalendar = !!rangeCalendarContext;
|
||||
|
||||
const start = rangeCalendarContext?.value?.start;
|
||||
const end = rangeCalendarContext?.value?.end;
|
||||
|
||||
const isAfterStart = start ? date.compare(start) > 0 : true;
|
||||
const isBeforeEnd = end ? date.compare(end) < 0 : true;
|
||||
|
||||
const isAfterOrOnStart = start && date.compare(start) >= 0;
|
||||
const isBeforeOrOnEnd = end && date.compare(end) <= 0;
|
||||
const isInRange = isAfterOrOnStart && isBeforeOrOnEnd;
|
||||
|
||||
const lastDayOfMonth = new Date(date.year, date.month, 0).getDate();
|
||||
const isLastDayOfMonth = date.day === lastDayOfMonth;
|
||||
const isFirstDayOfMonth = date.day === 1;
|
||||
|
||||
const isTodayDate = isToday(date, getLocalTimeZone());
|
||||
|
||||
return (
|
||||
<AriaCalendarCell
|
||||
{...props}
|
||||
date={date}
|
||||
className={({ isDisabled, isFocusVisible, isSelectionStart, isSelectionEnd, isSelected, isOutsideMonth }) => {
|
||||
const isRoundedLeft = isSelectionStart || dayOfWeek === 0;
|
||||
const isRoundedRight = isSelectionEnd || dayOfWeek === 6;
|
||||
|
||||
return cx(
|
||||
"relative size-10 focus:outline-none",
|
||||
isRoundedLeft && "rounded-l-full",
|
||||
isRoundedRight && "rounded-r-full",
|
||||
isInRange && isDisabled && "bg-active",
|
||||
isSelected && isRangeCalendar && "bg-active",
|
||||
isDisabled ? "pointer-events-none" : "cursor-pointer",
|
||||
isFocusVisible ? "z-10" : "z-0",
|
||||
isRangeCalendar && isOutsideMonth && "hidden",
|
||||
|
||||
// Show gradient on last day of month if it's within the selected range.
|
||||
isLastDayOfMonth &&
|
||||
isSelected &&
|
||||
isBeforeEnd &&
|
||||
isRangeCalendar &&
|
||||
"after:absolute after:inset-0 after:translate-x-full after:bg-gradient-to-l after:from-transparent after:to-bg-active in-[[role=gridcell]:last-child]:after:hidden",
|
||||
|
||||
// Show gradient on first day of month if it's within the selected range.
|
||||
isFirstDayOfMonth &&
|
||||
isSelected &&
|
||||
isAfterStart &&
|
||||
isRangeCalendar &&
|
||||
"after:absolute after:inset-0 after:-translate-x-full after:bg-gradient-to-r after:from-transparent after:to-bg-active in-[[role=gridcell]:first-child]:after:hidden",
|
||||
);
|
||||
}}
|
||||
>
|
||||
{({ isDisabled, isFocusVisible, isSelectionStart, isSelectionEnd, isSelected, formattedDate }) => {
|
||||
const markedAsSelected = isSelectionStart || isSelectionEnd || (isSelected && !isDisabled && !isRangeCalendar);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"relative flex size-full items-center justify-center rounded-full text-sm",
|
||||
// Disabled state.
|
||||
isDisabled ? "text-disabled" : "text-secondary hover:text-secondary_hover",
|
||||
// Focus ring, visible while the cell has keyboard focus.
|
||||
isFocusVisible ? "outline-2 outline-offset-2 outline-focus-ring" : "",
|
||||
// Hover state for cells in the middle of the range.
|
||||
isSelected && !isDisabled && isRangeCalendar ? "font-medium" : "",
|
||||
markedAsSelected && "bg-brand-solid font-medium text-white hover:bg-brand-solid_hover hover:text-white",
|
||||
// Hover state for non-selected cells.
|
||||
!isSelected && !isDisabled ? "hover:bg-primary_hover hover:font-medium!" : "",
|
||||
!isSelected && isTodayDate ? "bg-active font-medium hover:bg-secondary_hover" : "",
|
||||
)}
|
||||
>
|
||||
{formattedDate}
|
||||
|
||||
{(isHighlighted || isTodayDate) && (
|
||||
<div
|
||||
className={cx(
|
||||
"absolute bottom-1 left-1/2 size-1.25 -translate-x-1/2 rounded-full",
|
||||
isDisabled ? "bg-fg-disabled" : markedAsSelected ? "bg-fg-white" : "bg-fg-brand-primary",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</AriaCalendarCell>
|
||||
);
|
||||
};
|
||||
30
src/components/application/date-picker/date-input.tsx
Normal file
30
src/components/application/date-picker/date-input.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { DateInputProps as AriaDateInputProps } from "react-aria-components";
|
||||
import { DateInput as AriaDateInput, DateSegment as AriaDateSegment } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface DateInputProps extends Omit<AriaDateInputProps, "children"> {}
|
||||
|
||||
export const DateInput = (props: DateInputProps) => {
|
||||
return (
|
||||
<AriaDateInput
|
||||
{...props}
|
||||
className={cx(
|
||||
"flex rounded-lg bg-primary px-2.5 py-2 text-md shadow-xs ring-1 ring-primary ring-inset focus-within:ring-2 focus-within:ring-brand",
|
||||
typeof props.className === "string" && props.className,
|
||||
)}
|
||||
>
|
||||
{(segment) => (
|
||||
<AriaDateSegment
|
||||
segment={segment}
|
||||
className={cx(
|
||||
"rounded px-0.5 text-primary tabular-nums caret-transparent focus:bg-brand-solid focus:font-medium focus:text-white focus:outline-hidden",
|
||||
// The placeholder segment.
|
||||
segment.isPlaceholder && "text-placeholder uppercase",
|
||||
// The separator "/" segment.
|
||||
segment.type === "literal" && "text-fg-quaternary",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</AriaDateInput>
|
||||
);
|
||||
};
|
||||
84
src/components/application/date-picker/date-picker.tsx
Normal file
84
src/components/application/date-picker/date-picker.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { getLocalTimeZone, today } from "@internationalized/date";
|
||||
import { useControlledState } from "@react-stately/utils";
|
||||
import { Calendar as CalendarIcon } from "@untitledui/icons";
|
||||
import { useDateFormatter } from "react-aria";
|
||||
import type { DatePickerProps as AriaDatePickerProps, DateValue } from "react-aria-components";
|
||||
import { DatePicker as AriaDatePicker, Dialog as AriaDialog, Group as AriaGroup, Popover as AriaPopover } from "react-aria-components";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { Calendar } from "./calendar";
|
||||
|
||||
const highlightedDates = [today(getLocalTimeZone())];
|
||||
|
||||
interface DatePickerProps extends AriaDatePickerProps<DateValue> {
|
||||
/** The function to call when the apply button is clicked. */
|
||||
onApply?: () => void;
|
||||
/** The function to call when the cancel button is clicked. */
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export const DatePicker = ({ value: valueProp, defaultValue, onChange, onApply, onCancel, ...props }: DatePickerProps) => {
|
||||
const formatter = useDateFormatter({
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
const [value, setValue] = useControlledState(valueProp, defaultValue || null, onChange);
|
||||
|
||||
const formattedDate = value ? formatter.format(value.toDate(getLocalTimeZone())) : "Select date";
|
||||
|
||||
return (
|
||||
<AriaDatePicker shouldCloseOnSelect={false} {...props} value={value} onChange={setValue}>
|
||||
<AriaGroup>
|
||||
<Button size="md" color="secondary" iconLeading={CalendarIcon}>
|
||||
{formattedDate}
|
||||
</Button>
|
||||
</AriaGroup>
|
||||
<AriaPopover
|
||||
offset={8}
|
||||
placement="bottom right"
|
||||
className={({ isEntering, isExiting }) =>
|
||||
cx(
|
||||
"origin-(--trigger-anchor-point) will-change-transform",
|
||||
isEntering &&
|
||||
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||
isExiting &&
|
||||
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||
)
|
||||
}
|
||||
>
|
||||
<AriaDialog className="rounded-2xl bg-primary shadow-xl ring ring-secondary_alt">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<div className="flex px-6 py-5">
|
||||
<Calendar highlightedDates={highlightedDates} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 border-t border-secondary p-4">
|
||||
<Button
|
||||
size="md"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
onCancel?.();
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
onApply?.();
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AriaDialog>
|
||||
</AriaPopover>
|
||||
</AriaDatePicker>
|
||||
);
|
||||
};
|
||||
161
src/components/application/date-picker/date-range-picker.tsx
Normal file
161
src/components/application/date-picker/date-range-picker.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { endOfMonth, endOfWeek, getLocalTimeZone, startOfMonth, startOfWeek, today } from "@internationalized/date";
|
||||
import { useControlledState } from "@react-stately/utils";
|
||||
import { Calendar as CalendarIcon } from "@untitledui/icons";
|
||||
import { useDateFormatter } from "react-aria";
|
||||
import type { DateRangePickerProps as AriaDateRangePickerProps, DateValue } from "react-aria-components";
|
||||
import { DateRangePicker as AriaDateRangePicker, Dialog as AriaDialog, Group as AriaGroup, Popover as AriaPopover, useLocale } from "react-aria-components";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { DateInput } from "./date-input";
|
||||
import { RangeCalendar } from "./range-calendar";
|
||||
import { RangePresetButton } from "./range-preset";
|
||||
|
||||
const now = today(getLocalTimeZone());
|
||||
|
||||
const highlightedDates = [today(getLocalTimeZone())];
|
||||
|
||||
interface DateRangePickerProps extends AriaDateRangePickerProps<DateValue> {
|
||||
/** The function to call when the apply button is clicked. */
|
||||
onApply?: () => void;
|
||||
/** The function to call when the cancel button is clicked. */
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export const DateRangePicker = ({ value: valueProp, defaultValue, onChange, onApply, onCancel, ...props }: DateRangePickerProps) => {
|
||||
const { locale } = useLocale();
|
||||
const formatter = useDateFormatter({
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
const [value, setValue] = useControlledState(valueProp, defaultValue || null, onChange);
|
||||
const [focusedValue, setFocusedValue] = useState<DateValue | null>(null);
|
||||
|
||||
const formattedStartDate = value?.start ? formatter.format(value.start.toDate(getLocalTimeZone())) : "Select date";
|
||||
const formattedEndDate = value?.end ? formatter.format(value.end.toDate(getLocalTimeZone())) : "Select date";
|
||||
|
||||
const presets = useMemo(
|
||||
() => ({
|
||||
today: { label: "Today", value: { start: now, end: now } },
|
||||
yesterday: { label: "Yesterday", value: { start: now.subtract({ days: 1 }), end: now.subtract({ days: 1 }) } },
|
||||
thisWeek: { label: "This week", value: { start: startOfWeek(now, locale), end: endOfWeek(now, locale) } },
|
||||
lastWeek: {
|
||||
label: "Last week",
|
||||
value: {
|
||||
start: startOfWeek(now, locale).subtract({ weeks: 1 }),
|
||||
end: endOfWeek(now, locale).subtract({ weeks: 1 }),
|
||||
},
|
||||
},
|
||||
thisMonth: { label: "This month", value: { start: startOfMonth(now), end: endOfMonth(now) } },
|
||||
lastMonth: {
|
||||
label: "Last month",
|
||||
value: {
|
||||
start: startOfMonth(now).subtract({ months: 1 }),
|
||||
end: endOfMonth(now).subtract({ months: 1 }),
|
||||
},
|
||||
},
|
||||
thisYear: { label: "This year", value: { start: startOfMonth(now.set({ month: 1 })), end: endOfMonth(now.set({ month: 12 })) } },
|
||||
lastYear: {
|
||||
label: "Last year",
|
||||
value: {
|
||||
start: startOfMonth(now.set({ month: 1 }).subtract({ years: 1 })),
|
||||
end: endOfMonth(now.set({ month: 12 }).subtract({ years: 1 })),
|
||||
},
|
||||
},
|
||||
allTime: {
|
||||
label: "All time",
|
||||
value: {
|
||||
start: now.set({ year: 2000, month: 1, day: 1 }),
|
||||
end: now,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[locale],
|
||||
);
|
||||
|
||||
return (
|
||||
<AriaDateRangePicker aria-label="Date range picker" shouldCloseOnSelect={false} {...props} value={value} onChange={setValue}>
|
||||
<AriaGroup>
|
||||
<Button size="md" color="secondary" iconLeading={CalendarIcon}>
|
||||
{!value ? <span className="text-placeholder">Select dates</span> : `${formattedStartDate} – ${formattedEndDate}`}
|
||||
</Button>
|
||||
</AriaGroup>
|
||||
<AriaPopover
|
||||
placement="bottom right"
|
||||
offset={8}
|
||||
className={({ isEntering, isExiting }) =>
|
||||
cx(
|
||||
"origin-(--trigger-anchor-point) will-change-transform",
|
||||
isEntering &&
|
||||
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||
isExiting &&
|
||||
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||
)
|
||||
}
|
||||
>
|
||||
<AriaDialog className="flex rounded-2xl bg-primary shadow-xl ring ring-secondary_alt focus:outline-hidden">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<div className="hidden w-38 flex-col gap-0.5 border-r border-solid border-secondary p-3 lg:flex">
|
||||
{Object.values(presets).map((preset) => (
|
||||
<RangePresetButton
|
||||
key={preset.label}
|
||||
value={preset.value}
|
||||
onClick={() => {
|
||||
setValue(preset.value);
|
||||
setFocusedValue(preset.value.start);
|
||||
}}
|
||||
>
|
||||
{preset.label}
|
||||
</RangePresetButton>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<RangeCalendar
|
||||
focusedValue={focusedValue}
|
||||
onFocusChange={setFocusedValue}
|
||||
highlightedDates={highlightedDates}
|
||||
presets={{
|
||||
lastWeek: presets.lastWeek,
|
||||
lastMonth: presets.lastMonth,
|
||||
lastYear: presets.lastYear,
|
||||
}}
|
||||
/>
|
||||
<div className="flex justify-between gap-3 border-t border-secondary p-4">
|
||||
<div className="hidden items-center gap-3 md:flex">
|
||||
<DateInput slot="start" className="w-36" />
|
||||
<div className="text-md text-quaternary">–</div>
|
||||
<DateInput slot="end" className="w-36" />
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-2 gap-3 md:flex md:w-auto">
|
||||
<Button
|
||||
size="md"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
onCancel?.();
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
onApply?.();
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AriaDialog>
|
||||
</AriaPopover>
|
||||
</AriaDateRangePicker>
|
||||
);
|
||||
};
|
||||
159
src/components/application/date-picker/range-calendar.tsx
Normal file
159
src/components/application/date-picker/range-calendar.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { HTMLAttributes, PropsWithChildren } from "react";
|
||||
import { Fragment, useContext, useState } from "react";
|
||||
import type { CalendarDate } from "@internationalized/date";
|
||||
import { ChevronLeft, ChevronRight } from "@untitledui/icons";
|
||||
import { useDateFormatter } from "react-aria";
|
||||
import type { RangeCalendarProps as AriaRangeCalendarProps, DateValue } from "react-aria-components";
|
||||
import {
|
||||
CalendarGrid as AriaCalendarGrid,
|
||||
CalendarGridBody as AriaCalendarGridBody,
|
||||
CalendarGridHeader as AriaCalendarGridHeader,
|
||||
CalendarHeaderCell as AriaCalendarHeaderCell,
|
||||
RangeCalendar as AriaRangeCalendar,
|
||||
RangeCalendarContext,
|
||||
RangeCalendarStateContext,
|
||||
useSlottedContext,
|
||||
} from "react-aria-components";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { useBreakpoint } from "@/hooks/use-breakpoint";
|
||||
import { CalendarCell } from "./cell";
|
||||
import { DateInput } from "./date-input";
|
||||
|
||||
export const RangeCalendarContextProvider = ({ children }: PropsWithChildren) => {
|
||||
const [value, onChange] = useState<{ start: DateValue; end: DateValue } | null>(null);
|
||||
const [focusedValue, onFocusChange] = useState<DateValue | undefined>();
|
||||
|
||||
return <RangeCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }}>{children}</RangeCalendarContext.Provider>;
|
||||
};
|
||||
|
||||
const RangeCalendarTitle = ({ part }: { part: "start" | "end" }) => {
|
||||
const context = useContext(RangeCalendarStateContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("<RangeCalendarTitle /> must be used within a <RangeCalendar /> component.");
|
||||
}
|
||||
|
||||
const formatter = useDateFormatter({
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
calendar: context.visibleRange.start.calendar.identifier,
|
||||
timeZone: context.timeZone,
|
||||
});
|
||||
|
||||
return part === "start"
|
||||
? formatter.format(context.visibleRange.start.toDate(context.timeZone))
|
||||
: formatter.format(context.visibleRange.end.toDate(context.timeZone));
|
||||
};
|
||||
|
||||
const MobilePresetButton = ({ value, children, ...props }: HTMLAttributes<HTMLButtonElement> & { value: { start: DateValue; end: DateValue } }) => {
|
||||
const context = useContext(RangeCalendarStateContext);
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
slot={null}
|
||||
size="sm"
|
||||
color="link-color"
|
||||
onClick={() => {
|
||||
context?.setValue(value);
|
||||
context?.setFocusedDate(value.start as CalendarDate);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface RangeCalendarProps extends AriaRangeCalendarProps<DateValue> {
|
||||
/** The dates to highlight. */
|
||||
highlightedDates?: DateValue[];
|
||||
/** The date presets to display. */
|
||||
presets?: Record<string, { label: string; value: { start: DateValue; end: DateValue } }>;
|
||||
}
|
||||
|
||||
export const RangeCalendar = ({ presets, ...props }: RangeCalendarProps) => {
|
||||
const isDesktop = useBreakpoint("md");
|
||||
const context = useSlottedContext(RangeCalendarContext);
|
||||
|
||||
const ContextWrapper = context ? Fragment : RangeCalendarContextProvider;
|
||||
|
||||
return (
|
||||
<ContextWrapper>
|
||||
<AriaRangeCalendar
|
||||
className="flex items-start"
|
||||
visibleDuration={{
|
||||
months: isDesktop ? 2 : 1,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex flex-col gap-3 px-6 py-5">
|
||||
<header className="relative flex items-center justify-between md:justify-start">
|
||||
<Button slot="previous" iconLeading={ChevronLeft} size="sm" color="tertiary" className="size-8" />
|
||||
|
||||
<h2 className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm font-semibold text-fg-secondary">
|
||||
<RangeCalendarTitle part="start" />
|
||||
</h2>
|
||||
|
||||
<Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8 md:hidden" />
|
||||
</header>
|
||||
|
||||
{!isDesktop && (
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<DateInput slot="start" className="flex-1" />
|
||||
<div className="text-md text-quaternary">–</div>
|
||||
<DateInput slot="end" className="flex-1" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isDesktop && presets && (
|
||||
<div className="mt-1 flex justify-between gap-3 px-2 md:hidden">
|
||||
{Object.values(presets).map((preset) => (
|
||||
<MobilePresetButton key={preset.label} value={preset.value}>
|
||||
{preset.label}
|
||||
</MobilePresetButton>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AriaCalendarGrid weekdayStyle="short" className="w-max">
|
||||
<AriaCalendarGridHeader>
|
||||
{(day) => (
|
||||
<AriaCalendarHeaderCell className="border-b-4 border-transparent p-0">
|
||||
<div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
|
||||
</AriaCalendarHeaderCell>
|
||||
)}
|
||||
</AriaCalendarGridHeader>
|
||||
<AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
|
||||
{(date) => <CalendarCell date={date} />}
|
||||
</AriaCalendarGridBody>
|
||||
</AriaCalendarGrid>
|
||||
</div>
|
||||
|
||||
{isDesktop && (
|
||||
<div className="flex flex-col gap-3 border-l border-secondary px-6 py-5">
|
||||
<header className="relative flex items-center justify-end">
|
||||
<h2 className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm font-semibold text-fg-secondary">
|
||||
<RangeCalendarTitle part="end" />
|
||||
</h2>
|
||||
|
||||
<Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8" />
|
||||
</header>
|
||||
|
||||
<AriaCalendarGrid weekdayStyle="short" offset={{ months: 1 }} className="w-max">
|
||||
<AriaCalendarGridHeader>
|
||||
{(day) => (
|
||||
<AriaCalendarHeaderCell className="border-b-4 border-transparent p-0">
|
||||
<div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
|
||||
</AriaCalendarHeaderCell>
|
||||
)}
|
||||
</AriaCalendarGridHeader>
|
||||
<AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
|
||||
{(date) => <CalendarCell date={date} />}
|
||||
</AriaCalendarGridBody>
|
||||
</AriaCalendarGrid>
|
||||
</div>
|
||||
)}
|
||||
</AriaRangeCalendar>
|
||||
</ContextWrapper>
|
||||
);
|
||||
};
|
||||
26
src/components/application/date-picker/range-preset.tsx
Normal file
26
src/components/application/date-picker/range-preset.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { type HTMLAttributes } from "react";
|
||||
import { type DateValue, RangeCalendarContext, useSlottedContext } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface RangePresetButtonProps extends HTMLAttributes<HTMLButtonElement> {
|
||||
value: { start: DateValue; end: DateValue };
|
||||
}
|
||||
|
||||
export const RangePresetButton = ({ value, className, children, ...props }: RangePresetButtonProps) => {
|
||||
const context = useSlottedContext(RangeCalendarContext);
|
||||
|
||||
const isSelected = context?.value?.start?.compare(value.start) === 0 && context?.value?.end?.compare(value.end) === 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={cx(
|
||||
"cursor-pointer rounded-md px-3 py-2 text-left text-sm font-medium outline-focus-ring transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
isSelected ? "bg-active text-secondary_hover hover:bg-secondary_hover" : "text-secondary hover:bg-primary_hover hover:text-secondary_hover",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
144
src/components/application/empty-state/empty-state.tsx
Normal file
144
src/components/application/empty-state/empty-state.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import type { ComponentProps, ComponentPropsWithRef } from "react";
|
||||
import { Children, createContext, isValidElement, useContext } from "react";
|
||||
import { FileIcon } from "@untitledui/file-icons";
|
||||
import { SearchLg } from "@untitledui/icons";
|
||||
import { FeaturedIcon as FeaturedIconbase } from "@/components/foundations/featured-icon/featured-icon";
|
||||
import type { BackgroundPatternProps } from "@/components/shared-assets/background-patterns";
|
||||
import { BackgroundPattern } from "@/components/shared-assets/background-patterns";
|
||||
import { Illustration as Illustrations } from "@/components/shared-assets/illustrations";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface RootContextProps {
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const RootContext = createContext<RootContextProps>({ size: "lg" });
|
||||
|
||||
interface RootProps extends ComponentPropsWithRef<"div">, RootContextProps {}
|
||||
|
||||
const Root = ({ size = "lg", ...props }: RootProps) => {
|
||||
return (
|
||||
<RootContext.Provider value={{ size }}>
|
||||
<div {...props} className={cx("mx-auto flex w-full max-w-lg flex-col items-center justify-center", props.className)} />
|
||||
</RootContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const FeaturedIcon = ({ color = "gray", theme = "modern", icon = SearchLg, size = "lg", ...props }: ComponentPropsWithRef<typeof FeaturedIconbase>) => {
|
||||
const { size: rootSize } = useContext(RootContext);
|
||||
|
||||
return <FeaturedIconbase {...props} {...{ color, theme, icon }} size={rootSize === "lg" ? "xl" : size} />;
|
||||
};
|
||||
|
||||
const Illustration = ({ type = "cloud", color = "gray", size = "lg", ...props }: ComponentPropsWithRef<typeof Illustrations>) => {
|
||||
const { size: rootSize } = useContext(RootContext);
|
||||
|
||||
return (
|
||||
<Illustrations
|
||||
role="img"
|
||||
{...props}
|
||||
{...{ type, color }}
|
||||
size={rootSize === "sm" ? "sm" : rootSize === "md" ? "md" : size}
|
||||
className={cx("z-10", props.className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface FileTypeIconProps extends ComponentPropsWithRef<"div"> {
|
||||
type?: ComponentProps<typeof FileIcon>["type"];
|
||||
theme?: ComponentProps<typeof FileIcon>["variant"];
|
||||
}
|
||||
|
||||
const FileTypeIcon = ({ type = "folder", theme = "solid", ...props }: FileTypeIconProps) => {
|
||||
return (
|
||||
<div {...props} className={cx("relative z-10 flex rounded-full bg-linear-to-b from-gray-50 to-gray-200 p-8", props.className)}>
|
||||
<FileIcon type={type} variant={theme} className="size-10 drop-shadow-sm" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface HeaderProps extends ComponentPropsWithRef<"div"> {
|
||||
pattern?: "none" | BackgroundPatternProps["pattern"];
|
||||
patternSize?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const Header = ({ pattern = "circle", patternSize = "md", ...props }: HeaderProps) => {
|
||||
const { size } = useContext(RootContext);
|
||||
// Whether we are passing `Illustration` component as children.
|
||||
const hasIllustration = Children.toArray(props.children).some((headerChild) => isValidElement(headerChild) && headerChild.type === Illustration);
|
||||
|
||||
return (
|
||||
<header
|
||||
{...props}
|
||||
className={cx("relative mb-4", (size === "md" || size === "lg") && "mb-5", hasIllustration && size === "lg" && "mb-6!", props.className)}
|
||||
>
|
||||
{pattern !== "none" && (
|
||||
<BackgroundPattern size={patternSize} pattern={pattern} className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
)}
|
||||
{props.children}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
const Content = (props: ComponentPropsWithRef<"div">) => {
|
||||
const { size } = useContext(RootContext);
|
||||
|
||||
return (
|
||||
<main
|
||||
{...props}
|
||||
className={cx(
|
||||
"z-10 mb-6 flex w-full max-w-88 flex-col items-center justify-center gap-1",
|
||||
(size === "md" || size === "lg") && "mb-8 gap-2",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Footer = (props: ComponentPropsWithRef<"div">) => {
|
||||
return <footer {...props} className={cx("z-10 flex gap-3", props.className)} />;
|
||||
};
|
||||
|
||||
const Title = (props: ComponentPropsWithRef<"h1">) => {
|
||||
const { size } = useContext(RootContext);
|
||||
|
||||
return (
|
||||
<h1
|
||||
{...props}
|
||||
className={cx(
|
||||
"text-md font-semibold text-primary",
|
||||
size === "md" && "text-lg font-semibold",
|
||||
size === "lg" && "text-xl font-semibold",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Description = (props: ComponentPropsWithRef<"p">) => {
|
||||
const { size } = useContext(RootContext);
|
||||
|
||||
return <p {...props} className={cx("text-center text-sm text-tertiary", size === "lg" && "text-md", props.className)} />;
|
||||
};
|
||||
|
||||
const EmptyState = Root as typeof Root & {
|
||||
Title: typeof Title;
|
||||
Header: typeof Header;
|
||||
Footer: typeof Footer;
|
||||
Content: typeof Content;
|
||||
Description: typeof Description;
|
||||
Illustration: typeof Illustration;
|
||||
FeaturedIcon: typeof FeaturedIcon;
|
||||
FileTypeIcon: typeof FileTypeIcon;
|
||||
};
|
||||
|
||||
EmptyState.Title = Title;
|
||||
EmptyState.Header = Header;
|
||||
EmptyState.Footer = Footer;
|
||||
EmptyState.Content = Content;
|
||||
EmptyState.Description = Description;
|
||||
EmptyState.Illustration = Illustration;
|
||||
EmptyState.FeaturedIcon = FeaturedIcon;
|
||||
EmptyState.FileTypeIcon = FileTypeIcon;
|
||||
|
||||
export { EmptyState };
|
||||
394
src/components/application/file-upload/file-upload-base.tsx
Normal file
394
src/components/application/file-upload/file-upload-base.tsx
Normal file
@@ -0,0 +1,394 @@
|
||||
import type { ComponentProps, ComponentPropsWithRef } from "react";
|
||||
import { useId, useRef, useState } from "react";
|
||||
import type { FileIcon } from "@untitledui/file-icons";
|
||||
import { FileIcon as FileTypeIcon } from "@untitledui/file-icons";
|
||||
import { CheckCircle, Trash01, UploadCloud02, XCircle } from "@untitledui/icons";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { ButtonUtility } from "@/components/base/buttons/button-utility";
|
||||
import { ProgressBar } from "@/components/base/progress-indicators/progress-indicators";
|
||||
import { FeaturedIcon } from "@/components/foundations/featured-icon/featured-icon";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
/**
|
||||
* Returns a human-readable file size.
|
||||
* @param bytes - The size of the file in bytes.
|
||||
* @returns A string representing the file size in a human-readable format.
|
||||
*/
|
||||
export const getReadableFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 KB";
|
||||
|
||||
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
return Math.floor(bytes / Math.pow(1024, i)) + " " + suffixes[i];
|
||||
};
|
||||
|
||||
interface FileUploadDropZoneProps {
|
||||
/** The class name of the drop zone. */
|
||||
className?: string;
|
||||
/**
|
||||
* A hint text explaining what files can be dropped.
|
||||
*/
|
||||
hint?: string;
|
||||
/**
|
||||
* Disables dropping or uploading files.
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
/**
|
||||
* Specifies the types of files that the server accepts.
|
||||
* Examples: "image/*", ".pdf,image/*", "image/*,video/mpeg,application/pdf"
|
||||
*/
|
||||
accept?: string;
|
||||
/**
|
||||
* Allows multiple file uploads.
|
||||
*/
|
||||
allowsMultiple?: boolean;
|
||||
/**
|
||||
* Maximum file size in bytes.
|
||||
*/
|
||||
maxSize?: number;
|
||||
/**
|
||||
* Callback function that is called with the list of dropped files
|
||||
* when files are dropped on the drop zone.
|
||||
*/
|
||||
onDropFiles?: (files: FileList) => void;
|
||||
/**
|
||||
* Callback function that is called with the list of unaccepted files
|
||||
* when files are dropped on the drop zone.
|
||||
*/
|
||||
onDropUnacceptedFiles?: (files: FileList) => void;
|
||||
/**
|
||||
* Callback function that is called with the list of files that exceed
|
||||
* the size limit when files are dropped on the drop zone.
|
||||
*/
|
||||
onSizeLimitExceed?: (files: FileList) => void;
|
||||
}
|
||||
|
||||
export const FileUploadDropZone = ({
|
||||
className,
|
||||
hint,
|
||||
isDisabled,
|
||||
accept,
|
||||
allowsMultiple = true,
|
||||
maxSize,
|
||||
onDropFiles,
|
||||
onDropUnacceptedFiles,
|
||||
onSizeLimitExceed,
|
||||
}: FileUploadDropZoneProps) => {
|
||||
const id = useId();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isInvalid, setIsInvalid] = useState(false);
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||
|
||||
const isFileTypeAccepted = (file: File): boolean => {
|
||||
if (!accept) return true;
|
||||
|
||||
// Split the accept string into individual types
|
||||
const acceptedTypes = accept.split(",").map((type) => type.trim());
|
||||
|
||||
return acceptedTypes.some((acceptedType) => {
|
||||
// Handle file extensions (e.g., .pdf, .doc)
|
||||
if (acceptedType.startsWith(".")) {
|
||||
const extension = `.${file.name.split(".").pop()?.toLowerCase()}`;
|
||||
return extension === acceptedType.toLowerCase();
|
||||
}
|
||||
|
||||
// Handle wildcards (e.g., image/*)
|
||||
if (acceptedType.endsWith("/*")) {
|
||||
const typePrefix = acceptedType.split("/")[0];
|
||||
return file.type.startsWith(`${typePrefix}/`);
|
||||
}
|
||||
|
||||
// Handle exact MIME types (e.g., application/pdf)
|
||||
return file.type === acceptedType;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragIn = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
if (isDisabled) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsDraggingOver(true);
|
||||
};
|
||||
|
||||
const handleDragOut = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
if (isDisabled) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsDraggingOver(false);
|
||||
};
|
||||
|
||||
const processFiles = (files: File[]): void => {
|
||||
// Reset the invalid state when processing files.
|
||||
setIsInvalid(false);
|
||||
|
||||
const acceptedFiles: File[] = [];
|
||||
const unacceptedFiles: File[] = [];
|
||||
const oversizedFiles: File[] = [];
|
||||
|
||||
// If multiple files are not allowed, only process the first file
|
||||
const filesToProcess = allowsMultiple ? files : files.slice(0, 1);
|
||||
|
||||
filesToProcess.forEach((file) => {
|
||||
// Check file size first
|
||||
if (maxSize && file.size > maxSize) {
|
||||
oversizedFiles.push(file);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then check file type
|
||||
if (isFileTypeAccepted(file)) {
|
||||
acceptedFiles.push(file);
|
||||
} else {
|
||||
unacceptedFiles.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle oversized files
|
||||
if (oversizedFiles.length > 0 && typeof onSizeLimitExceed === "function") {
|
||||
const dataTransfer = new DataTransfer();
|
||||
oversizedFiles.forEach((file) => dataTransfer.items.add(file));
|
||||
|
||||
setIsInvalid(true);
|
||||
onSizeLimitExceed(dataTransfer.files);
|
||||
}
|
||||
|
||||
// Handle accepted files
|
||||
if (acceptedFiles.length > 0 && typeof onDropFiles === "function") {
|
||||
const dataTransfer = new DataTransfer();
|
||||
acceptedFiles.forEach((file) => dataTransfer.items.add(file));
|
||||
onDropFiles(dataTransfer.files);
|
||||
}
|
||||
|
||||
// Handle unaccepted files
|
||||
if (unacceptedFiles.length > 0 && typeof onDropUnacceptedFiles === "function") {
|
||||
const unacceptedDataTransfer = new DataTransfer();
|
||||
unacceptedFiles.forEach((file) => unacceptedDataTransfer.items.add(file));
|
||||
|
||||
setIsInvalid(true);
|
||||
onDropUnacceptedFiles(unacceptedDataTransfer.files);
|
||||
}
|
||||
|
||||
// Clear the input value to ensure the same file can be selected again
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
if (isDisabled) return;
|
||||
|
||||
handleDragOut(event);
|
||||
processFiles(Array.from(event.dataTransfer.files));
|
||||
};
|
||||
|
||||
const handleInputFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
processFiles(Array.from(event.target.files || []));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-dropzone
|
||||
onDragOver={handleDragIn}
|
||||
onDragEnter={handleDragIn}
|
||||
onDragLeave={handleDragOut}
|
||||
onDragEnd={handleDragOut}
|
||||
onDrop={handleDrop}
|
||||
className={cx(
|
||||
"relative flex flex-col items-center gap-3 rounded-xl bg-primary px-6 py-4 text-tertiary ring-1 ring-secondary transition duration-100 ease-linear ring-inset",
|
||||
isDraggingOver && "ring-2 ring-brand",
|
||||
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled_subtle",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<FeaturedIcon color="gray" theme="modern" size="md">
|
||||
<UploadCloud02 className="size-5" />
|
||||
</FeaturedIcon>
|
||||
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<div className="flex justify-center gap-1 text-center">
|
||||
<input
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
type="file"
|
||||
className="peer sr-only"
|
||||
disabled={isDisabled}
|
||||
accept={accept}
|
||||
multiple={allowsMultiple}
|
||||
onChange={handleInputFileChange}
|
||||
/>
|
||||
<label htmlFor={id} className="flex cursor-pointer">
|
||||
<Button color="link-color" size="md" isDisabled={isDisabled} onClick={() => inputRef.current?.click()}>
|
||||
Click to upload <span className="md:hidden">and attach files</span>
|
||||
</Button>
|
||||
</label>
|
||||
<span className="text-sm max-md:hidden">or drag and drop</span>
|
||||
</div>
|
||||
<p className={cx("text-xs transition duration-100 ease-linear", isInvalid && "text-error-primary")}>
|
||||
{hint || "SVG, PNG, JPG or GIF (max. 800x400px)"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface FileListItemProps {
|
||||
/** The name of the file. */
|
||||
name: string;
|
||||
/** The size of the file. */
|
||||
size: number;
|
||||
/** The upload progress of the file. */
|
||||
progress: number;
|
||||
/** Whether the file failed to upload. */
|
||||
failed?: boolean;
|
||||
/** The type of the file. */
|
||||
type?: ComponentProps<typeof FileIcon>["type"];
|
||||
/** The class name of the file list item. */
|
||||
className?: string;
|
||||
/** The variant of the file icon. */
|
||||
fileIconVariant?: ComponentProps<typeof FileTypeIcon>["variant"];
|
||||
/** The function to call when the file is deleted. */
|
||||
onDelete?: () => void;
|
||||
/** The function to call when the file upload is retried. */
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
export const FileListItemProgressBar = ({ name, size, progress, failed, type, fileIconVariant, onDelete, onRetry, className }: FileListItemProps) => {
|
||||
const isComplete = progress === 100;
|
||||
|
||||
return (
|
||||
<motion.li
|
||||
layout="position"
|
||||
className={cx(
|
||||
"relative flex gap-3 rounded-xl bg-primary p-4 ring-1 ring-secondary transition-shadow duration-100 ease-linear ring-inset",
|
||||
failed && "ring-2 ring-error",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<FileTypeIcon className="size-10 shrink-0 dark:hidden" type={type ?? "empty"} theme="light" variant={fileIconVariant ?? "default"} />
|
||||
<FileTypeIcon className="size-10 shrink-0 not-dark:hidden" type={type ?? "empty"} theme="dark" variant={fileIconVariant ?? "default"} />
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start">
|
||||
<div className="flex w-full max-w-full min-w-0 flex-1">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-secondary">{name}</p>
|
||||
|
||||
<div className="mt-0.5 flex items-center gap-2">
|
||||
<p className="truncate text-sm whitespace-nowrap text-tertiary">{getReadableFileSize(size)}</p>
|
||||
|
||||
<hr className="h-3 w-px rounded-t-full rounded-b-full border-none bg-border-primary" />
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{isComplete && <CheckCircle className="size-4 stroke-[2.5px] text-fg-success-primary" />}
|
||||
{isComplete && <p className="text-sm font-medium text-success-primary">Complete</p>}
|
||||
|
||||
{!isComplete && !failed && <UploadCloud02 className="stroke-[2.5px size-4 text-fg-quaternary" />}
|
||||
{!isComplete && !failed && <p className="text-sm font-medium text-quaternary">Uploading...</p>}
|
||||
|
||||
{failed && <XCircle className="size-4 text-fg-error-primary" />}
|
||||
{failed && <p className="text-sm font-medium text-error-primary">Failed</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ButtonUtility color="tertiary" tooltip="Delete" icon={Trash01} size="xs" className="-mt-2 -mr-2 self-start" onClick={onDelete} />
|
||||
</div>
|
||||
|
||||
{!failed && (
|
||||
<div className="mt-1 w-full">
|
||||
<ProgressBar labelPosition="right" max={100} min={0} value={progress} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{failed && (
|
||||
<Button color="link-destructive" size="sm" onClick={onRetry} className="mt-1.5">
|
||||
Try again
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.li>
|
||||
);
|
||||
};
|
||||
|
||||
export const FileListItemProgressFill = ({ name, size, progress, failed, type, fileIconVariant, onDelete, onRetry, className }: FileListItemProps) => {
|
||||
const isComplete = progress === 100;
|
||||
|
||||
return (
|
||||
<motion.li layout="position" className={cx("relative flex gap-3 overflow-hidden rounded-xl bg-primary p-4", className)}>
|
||||
{/* Progress fill. */}
|
||||
<div
|
||||
style={{ transform: `translateX(-${100 - progress}%)` }}
|
||||
className={cx("absolute inset-0 size-full bg-secondary transition duration-75 ease-linear", isComplete && "opacity-0")}
|
||||
role="progressbar"
|
||||
aria-valuenow={progress}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
/>
|
||||
{/* Inner ring. */}
|
||||
<div
|
||||
className={cx(
|
||||
"absolute inset-0 size-full rounded-[inherit] ring-1 ring-secondary transition duration-100 ease-linear ring-inset",
|
||||
failed && "ring-2 ring-error",
|
||||
)}
|
||||
/>
|
||||
<FileTypeIcon className="relative size-10 shrink-0 dark:hidden" type={type ?? "empty"} theme="light" variant={fileIconVariant ?? "solid"} />
|
||||
<FileTypeIcon className="relative size-10 shrink-0 not-dark:hidden" type={type ?? "empty"} theme="dark" variant={fileIconVariant ?? "solid"} />
|
||||
|
||||
<div className="relative flex min-w-0 flex-1">
|
||||
<div className="relative flex min-w-0 flex-1 flex-col items-start">
|
||||
<div className="w-full min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-secondary">{name}</p>
|
||||
|
||||
<div className="mt-0.5 flex items-center gap-2">
|
||||
<p className="text-sm text-tertiary">{failed ? "Upload failed, please try again" : getReadableFileSize(size)}</p>
|
||||
|
||||
{!failed && (
|
||||
<>
|
||||
<hr className="h-3 w-px rounded-t-full rounded-b-full border-none bg-border-primary" />
|
||||
<div className="flex items-center gap-1">
|
||||
{isComplete && <CheckCircle className="size-4 stroke-[2.5px] text-fg-success-primary" />}
|
||||
{!isComplete && <UploadCloud02 className="size-4 stroke-[2.5px] text-fg-quaternary" />}
|
||||
|
||||
<p className="text-sm text-tertiary">{progress}%</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{failed && (
|
||||
<Button color="link-destructive" size="sm" onClick={onRetry} className="mt-1.5">
|
||||
Try again
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ButtonUtility color="tertiary" tooltip="Delete" icon={Trash01} size="xs" className="-mt-2 -mr-2 self-start" onClick={onDelete} />
|
||||
</div>
|
||||
</motion.li>
|
||||
);
|
||||
};
|
||||
|
||||
const FileUploadRoot = (props: ComponentPropsWithRef<"div">) => (
|
||||
<div {...props} className={cx("flex flex-col gap-4", props.className)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const FileUploadList = (props: ComponentPropsWithRef<"ul">) => (
|
||||
<ul {...props} className={cx("flex flex-col gap-3", props.className)}>
|
||||
<AnimatePresence initial={false}>{props.children}</AnimatePresence>
|
||||
</ul>
|
||||
);
|
||||
|
||||
export const FileUpload = {
|
||||
Root: FileUploadRoot,
|
||||
List: FileUploadList,
|
||||
DropZone: FileUploadDropZone,
|
||||
ListItemProgressBar: FileListItemProgressBar,
|
||||
ListItemProgressFill: FileListItemProgressFill,
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const styles = {
|
||||
sm: {
|
||||
root: "gap-4",
|
||||
label: "text-sm font-medium",
|
||||
spinner: "size-8",
|
||||
},
|
||||
md: {
|
||||
root: "gap-4",
|
||||
label: "text-sm font-medium",
|
||||
spinner: "size-12",
|
||||
},
|
||||
lg: {
|
||||
root: "gap-4",
|
||||
label: "text-lg font-medium",
|
||||
spinner: "size-14",
|
||||
},
|
||||
xl: {
|
||||
root: "gap-5",
|
||||
label: "text-lg font-medium",
|
||||
spinner: "size-16",
|
||||
},
|
||||
};
|
||||
|
||||
interface LoadingIndicatorProps {
|
||||
/**
|
||||
* The visual style of the loading indicator.
|
||||
* @default 'line-simple'
|
||||
*/
|
||||
type?: "line-simple" | "line-spinner" | "dot-circle";
|
||||
/**
|
||||
* The size of the loading indicator.
|
||||
* @default 'sm'
|
||||
*/
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
/**
|
||||
* Optional text label displayed below the indicator.
|
||||
*/
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const LoadingIndicator = ({ type = "line-simple", size = "sm", label }: LoadingIndicatorProps) => {
|
||||
const renderSpinner = () => {
|
||||
if (type === "line-spinner") {
|
||||
return (
|
||||
<svg className={cx("animate-spin", styles[size].spinner)} viewBox="0 0 32 32" fill="none">
|
||||
<circle
|
||||
className="stroke-fg-brand-primary"
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="14"
|
||||
fill="none"
|
||||
strokeWidth="4"
|
||||
strokeDashoffset="40"
|
||||
strokeDasharray="100"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "dot-circle") {
|
||||
return (
|
||||
<svg className={cx("animate-spin text-fg-brand-primary", styles[size].spinner)} viewBox="0 0 36 36" fill="none">
|
||||
<path
|
||||
d="M34 18C34 15.8989 33.5861 13.8183 32.7821 11.8771C31.978 9.93586 30.7994 8.17203 29.3137 6.68629C27.828 5.20055 26.0641 4.022 24.1229 3.21793C22.1817 2.41385 20.1011 2 18 2C15.8988 2 13.8183 2.41385 11.8771 3.21793C9.93585 4.022 8.17203 5.20055 6.68629 6.68629C5.20055 8.17203 4.022 9.93586 3.21793 11.8771C2.41385 13.8183 2 15.8989 2 18"
|
||||
stroke="url(#paint0)"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray="0.1 8"
|
||||
/>
|
||||
<path
|
||||
d="M3.21793 24.1229C4.022 26.0641 5.20055 27.828 6.68629 29.3137C8.17203 30.7994 9.93585 31.978 11.8771 32.7821C13.8183 33.5861 15.8988 34 18 34C20.1011 34 22.1817 33.5861 24.1229 32.7821C26.0641 31.978 27.828 30.7994 29.3137 29.3137C30.7994 27.828 31.978 26.0641 32.7821 24.1229"
|
||||
stroke="url(#paint1)"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray="0.1 8"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="paint0" x1="34" y1="18" x2="2" y2="18" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="currentColor" />
|
||||
<stop offset="1" stopColor="currentColor" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1" x1="33" y1="23.5" x2="3" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stopOpacity="0" stopColor="currentColor" />
|
||||
<stop offset="1" stopColor="currentColor" stopOpacity="0.48" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Default case: type === "line-simple"
|
||||
return (
|
||||
<svg className={cx("animate-spin", styles[size].spinner)} viewBox="0 0 32 32" fill="none">
|
||||
<circle className="text-bg-tertiary" cx="16" cy="16" r="14" stroke="currentColor" strokeWidth="4" />
|
||||
<circle
|
||||
className="stroke-fg-brand-primary"
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="14"
|
||||
fill="none"
|
||||
strokeWidth="4"
|
||||
strokeDashoffset="75"
|
||||
strokeDasharray="100"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx("flex flex-col items-center justify-center", styles[size].root)}>
|
||||
{renderSpinner()}
|
||||
{label && <span className={cx("text-secondary", styles[size].label)}>{label}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
src/components/application/modals/modal.tsx
Normal file
39
src/components/application/modals/modal.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { DialogProps as AriaDialogProps, ModalOverlayProps as AriaModalOverlayProps } from "react-aria-components";
|
||||
import { Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Modal as AriaModal, ModalOverlay as AriaModalOverlay } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export const DialogTrigger = AriaDialogTrigger;
|
||||
|
||||
export const ModalOverlay = (props: AriaModalOverlayProps) => {
|
||||
return (
|
||||
<AriaModalOverlay
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"fixed inset-0 z-50 flex min-h-dvh w-full items-end justify-center overflow-y-auto bg-overlay/70 px-4 pt-4 pb-[clamp(16px,8vh,64px)] outline-hidden backdrop-blur-[6px] sm:items-center sm:justify-center sm:p-8",
|
||||
state.isEntering && "duration-300 ease-out animate-in fade-in",
|
||||
state.isExiting && "duration-200 ease-in animate-out fade-out",
|
||||
typeof props.className === "function" ? props.className(state) : props.className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Modal = (props: AriaModalOverlayProps) => (
|
||||
<AriaModal
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"max-h-full w-full align-middle outline-hidden max-sm:overflow-y-auto max-sm:rounded-xl",
|
||||
state.isEntering && "duration-300 ease-out animate-in zoom-in-95",
|
||||
state.isExiting && "duration-200 ease-in animate-out zoom-out-95",
|
||||
typeof props.className === "function" ? props.className(state) : props.className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Dialog = (props: AriaDialogProps) => (
|
||||
<AriaDialog {...props} className={cx("flex w-full items-center justify-center outline-hidden", props.className)} />
|
||||
);
|
||||
376
src/components/application/pagination/pagination-base.tsx
Normal file
376
src/components/application/pagination/pagination-base.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
import type { CSSProperties, FC, HTMLAttributes, ReactNode } from "react";
|
||||
import React, { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useState } from "react";
|
||||
|
||||
type PaginationPage = {
|
||||
/** The type of the pagination item. */
|
||||
type: "page";
|
||||
/** The value of the pagination item. */
|
||||
value: number;
|
||||
/** Whether the pagination item is the current page. */
|
||||
isCurrent: boolean;
|
||||
};
|
||||
|
||||
type PaginationEllipsisType = {
|
||||
type: "ellipsis";
|
||||
key: number;
|
||||
};
|
||||
|
||||
type PaginationItemType = PaginationPage | PaginationEllipsisType;
|
||||
|
||||
interface PaginationContextType {
|
||||
/** The pages of the pagination. */
|
||||
pages: PaginationItemType[];
|
||||
/** The current page of the pagination. */
|
||||
currentPage: number;
|
||||
/** The total number of pages. */
|
||||
total: number;
|
||||
/** The function to call when the page changes. */
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
const PaginationContext = createContext<PaginationContextType | undefined>(undefined);
|
||||
|
||||
export interface PaginationRootProps {
|
||||
/** Number of sibling pages to show on each side of the current page */
|
||||
siblingCount?: number;
|
||||
/** Current active page number */
|
||||
page: number;
|
||||
/** Total number of pages */
|
||||
total: number;
|
||||
children: ReactNode;
|
||||
/** The style of the pagination root. */
|
||||
style?: CSSProperties;
|
||||
/** The class name of the pagination root. */
|
||||
className?: string;
|
||||
/** Callback function that's called when the page changes with the new page number. */
|
||||
onPageChange?: (page: number) => void;
|
||||
}
|
||||
|
||||
const PaginationRoot = ({ total, siblingCount = 1, page, onPageChange, children, style, className }: PaginationRootProps) => {
|
||||
const [pages, setPages] = useState<PaginationItemType[]>([]);
|
||||
|
||||
const createPaginationItems = useCallback((): PaginationItemType[] => {
|
||||
const items: PaginationItemType[] = [];
|
||||
// Calculate the maximum number of pagination elements (pages, potential ellipsis, first and last) to show
|
||||
const totalPageNumbers = siblingCount * 2 + 5;
|
||||
|
||||
// If the total number of items to show is greater than or equal to the total pages,
|
||||
// we can simply list all pages without needing to collapse with ellipsis
|
||||
if (totalPageNumbers >= total) {
|
||||
for (let i = 1; i <= total; i++) {
|
||||
items.push({
|
||||
type: "page",
|
||||
value: i,
|
||||
isCurrent: i === page,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Calculate left and right sibling boundaries around the current page
|
||||
const leftSiblingIndex = Math.max(page - siblingCount, 1);
|
||||
const rightSiblingIndex = Math.min(page + siblingCount, total);
|
||||
|
||||
// Determine if we need to show ellipsis on either side
|
||||
const showLeftEllipsis = leftSiblingIndex > 2;
|
||||
const showRightEllipsis = rightSiblingIndex < total - 1;
|
||||
|
||||
// Case 1: No left ellipsis, but right ellipsis is needed
|
||||
if (!showLeftEllipsis && showRightEllipsis) {
|
||||
// Calculate how many page numbers to show starting from the beginning
|
||||
const leftItemCount = siblingCount * 2 + 3;
|
||||
const leftRange = range(1, leftItemCount);
|
||||
|
||||
leftRange.forEach((pageNum) =>
|
||||
items.push({
|
||||
type: "page",
|
||||
value: pageNum,
|
||||
isCurrent: pageNum === page,
|
||||
}),
|
||||
);
|
||||
|
||||
// Insert ellipsis after the left range and add the last page
|
||||
items.push({ type: "ellipsis", key: leftItemCount + 1 });
|
||||
items.push({
|
||||
type: "page",
|
||||
value: total,
|
||||
isCurrent: total === page,
|
||||
});
|
||||
}
|
||||
// Case 2: Left ellipsis needed, but right ellipsis is not needed
|
||||
else if (showLeftEllipsis && !showRightEllipsis) {
|
||||
// Determine how many items from the end should be shown
|
||||
const rightItemCount = siblingCount * 2 + 3;
|
||||
const rightRange = range(total - rightItemCount + 1, total);
|
||||
|
||||
// Always show the first page, then add an ellipsis to indicate skipped pages
|
||||
items.push({
|
||||
type: "page",
|
||||
value: 1,
|
||||
isCurrent: page === 1,
|
||||
});
|
||||
items.push({ type: "ellipsis", key: total - rightItemCount });
|
||||
rightRange.forEach((pageNum) =>
|
||||
items.push({
|
||||
type: "page",
|
||||
value: pageNum,
|
||||
isCurrent: pageNum === page,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Case 3: Both left and right ellipsis are needed
|
||||
else if (showLeftEllipsis && showRightEllipsis) {
|
||||
// Always show the first page
|
||||
items.push({
|
||||
type: "page",
|
||||
value: 1,
|
||||
isCurrent: page === 1,
|
||||
});
|
||||
// Insert left ellipsis after the first page
|
||||
items.push({ type: "ellipsis", key: leftSiblingIndex - 1 });
|
||||
|
||||
// Show a range of pages around the current page
|
||||
const middleRange = range(leftSiblingIndex, rightSiblingIndex);
|
||||
middleRange.forEach((pageNum) =>
|
||||
items.push({
|
||||
type: "page",
|
||||
value: pageNum,
|
||||
isCurrent: pageNum === page,
|
||||
}),
|
||||
);
|
||||
|
||||
// Insert right ellipsis and finally the last page
|
||||
items.push({ type: "ellipsis", key: rightSiblingIndex + 1 });
|
||||
items.push({
|
||||
type: "page",
|
||||
value: total,
|
||||
isCurrent: total === page,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [total, siblingCount, page]);
|
||||
|
||||
useEffect(() => {
|
||||
const paginationItems = createPaginationItems();
|
||||
setPages(paginationItems);
|
||||
}, [createPaginationItems]);
|
||||
|
||||
const onPageChangeHandler = (newPage: number) => {
|
||||
onPageChange?.(newPage);
|
||||
};
|
||||
|
||||
const paginationContextValue: PaginationContextType = {
|
||||
pages,
|
||||
currentPage: page,
|
||||
total,
|
||||
onPageChange: onPageChangeHandler,
|
||||
};
|
||||
|
||||
return (
|
||||
<PaginationContext.Provider value={paginationContextValue}>
|
||||
<nav aria-label="Pagination Navigation" style={style} className={className}>
|
||||
{children}
|
||||
</nav>
|
||||
</PaginationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an array of numbers from start to end.
|
||||
* @param start - The start number.
|
||||
* @param end - The end number.
|
||||
* @returns An array of numbers from start to end.
|
||||
*/
|
||||
const range = (start: number, end: number): number[] => {
|
||||
const length = end - start + 1;
|
||||
|
||||
return Array.from({ length }, (_, index) => index + start);
|
||||
};
|
||||
|
||||
interface TriggerRenderProps {
|
||||
isDisabled: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface TriggerProps {
|
||||
/** The children of the trigger. Can be a render prop or a valid element. */
|
||||
children: ReactNode | ((props: TriggerRenderProps) => ReactNode);
|
||||
/** The style of the trigger. */
|
||||
style?: CSSProperties;
|
||||
/** The class name of the trigger. */
|
||||
className?: string | ((args: { isDisabled: boolean }) => string);
|
||||
/** If true, the child element will be cloned and passed down the prop of the trigger. */
|
||||
asChild?: boolean;
|
||||
/** The direction of the trigger. */
|
||||
direction: "prev" | "next";
|
||||
/** The aria label of the trigger. */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
const Trigger: FC<TriggerProps> = ({ children, style, className, asChild = false, direction, ariaLabel }) => {
|
||||
const context = useContext(PaginationContext);
|
||||
if (!context) {
|
||||
throw new Error("Pagination components must be used within a Pagination.Root");
|
||||
}
|
||||
|
||||
const { currentPage, total, onPageChange } = context;
|
||||
|
||||
const isDisabled = direction === "prev" ? currentPage <= 1 : currentPage >= total;
|
||||
|
||||
const handleClick = () => {
|
||||
if (isDisabled) return;
|
||||
|
||||
const newPage = direction === "prev" ? currentPage - 1 : currentPage + 1;
|
||||
onPageChange?.(newPage);
|
||||
};
|
||||
|
||||
const computedClassName = typeof className === "function" ? className({ isDisabled }) : className;
|
||||
|
||||
const defaultAriaLabel = direction === "prev" ? "Previous Page" : "Next Page";
|
||||
|
||||
// If the children is a render prop, we need to pass the isDisabled and onClick to the render prop.
|
||||
if (typeof children === "function") {
|
||||
return <>{children({ isDisabled, onClick: handleClick })}</>;
|
||||
}
|
||||
|
||||
// If the children is a valid element, we need to clone it and pass the isDisabled and onClick to the cloned element.
|
||||
if (asChild && isValidElement(children)) {
|
||||
return cloneElement(children, {
|
||||
onClick: handleClick,
|
||||
disabled: isDisabled,
|
||||
isDisabled,
|
||||
"aria-label": ariaLabel || defaultAriaLabel,
|
||||
style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
|
||||
className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
|
||||
} as HTMLAttributes<HTMLElement>);
|
||||
}
|
||||
|
||||
return (
|
||||
<button aria-label={ariaLabel || defaultAriaLabel} onClick={handleClick} disabled={isDisabled} style={style} className={computedClassName}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const PaginationPrevTrigger: FC<Omit<TriggerProps, "direction">> = (props) => <Trigger {...props} direction="prev" />;
|
||||
|
||||
const PaginationNextTrigger: FC<Omit<TriggerProps, "direction">> = (props) => <Trigger {...props} direction="next" />;
|
||||
|
||||
interface PaginationItemRenderProps {
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
value: number;
|
||||
"aria-current"?: "page";
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
export interface PaginationItemProps {
|
||||
/** The value of the pagination item. */
|
||||
value: number;
|
||||
/** Whether the pagination item is the current page. */
|
||||
isCurrent: boolean;
|
||||
/** The children of the pagination item. Can be a render prop or a valid element. */
|
||||
children?: ReactNode | ((props: PaginationItemRenderProps) => ReactNode);
|
||||
/** The style object of the pagination item. */
|
||||
style?: CSSProperties;
|
||||
/** The class name of the pagination item. */
|
||||
className?: string | ((args: { isSelected: boolean }) => string);
|
||||
/** The aria label of the pagination item. */
|
||||
ariaLabel?: string;
|
||||
/** If true, the child element will be cloned and passed down the prop of the item. */
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const PaginationItem = ({ value, isCurrent, children, style, className, ariaLabel, asChild = false }: PaginationItemProps) => {
|
||||
const context = useContext(PaginationContext);
|
||||
if (!context) {
|
||||
throw new Error("Pagination components must be used within a <Pagination.Root />");
|
||||
}
|
||||
|
||||
const { onPageChange } = context;
|
||||
|
||||
const isSelected = isCurrent;
|
||||
|
||||
const handleClick = () => {
|
||||
onPageChange?.(value);
|
||||
};
|
||||
|
||||
const computedClassName = typeof className === "function" ? className({ isSelected }) : className;
|
||||
|
||||
// If the children is a render prop, we need to pass the necessary props to the render prop.
|
||||
if (typeof children === "function") {
|
||||
return (
|
||||
<>
|
||||
{children({
|
||||
isSelected,
|
||||
onClick: handleClick,
|
||||
value,
|
||||
"aria-current": isCurrent ? "page" : undefined,
|
||||
"aria-label": ariaLabel || `Page ${value}`,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// If the children is a valid element, we need to clone it and pass the necessary props to the cloned element.
|
||||
if (asChild && isValidElement(children)) {
|
||||
return cloneElement(children, {
|
||||
onClick: handleClick,
|
||||
"aria-current": isCurrent ? "page" : undefined,
|
||||
"aria-label": ariaLabel || `Page ${value}`,
|
||||
style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
|
||||
className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
|
||||
} as HTMLAttributes<HTMLElement>);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
style={style}
|
||||
className={computedClassName}
|
||||
aria-current={isCurrent ? "page" : undefined}
|
||||
aria-label={ariaLabel || `Page ${value}`}
|
||||
role="listitem"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
interface PaginationEllipsisProps {
|
||||
key: number;
|
||||
children?: ReactNode;
|
||||
style?: CSSProperties;
|
||||
className?: string | (() => string);
|
||||
}
|
||||
|
||||
const PaginationEllipsis: FC<PaginationEllipsisProps> = ({ children, style, className }) => {
|
||||
const computedClassName = typeof className === "function" ? className() : className;
|
||||
|
||||
return (
|
||||
<span style={style} className={computedClassName} aria-hidden="true">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface PaginationContextComponentProps {
|
||||
children: (pagination: PaginationContextType) => ReactNode;
|
||||
}
|
||||
|
||||
const PaginationContextComponent: FC<PaginationContextComponentProps> = ({ children }) => {
|
||||
const context = useContext(PaginationContext);
|
||||
if (!context) {
|
||||
throw new Error("Pagination components must be used within a Pagination.Root");
|
||||
}
|
||||
|
||||
return <>{children(context)}</>;
|
||||
};
|
||||
|
||||
export const Pagination = {
|
||||
Root: PaginationRoot,
|
||||
PrevTrigger: PaginationPrevTrigger,
|
||||
NextTrigger: PaginationNextTrigger,
|
||||
Item: PaginationItem,
|
||||
Ellipsis: PaginationEllipsis,
|
||||
Context: PaginationContextComponent,
|
||||
};
|
||||
52
src/components/application/pagination/pagination-dot.tsx
Normal file
52
src/components/application/pagination/pagination-dot.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
import type { PaginationRootProps } from "./pagination-base";
|
||||
import { Pagination } from "./pagination-base";
|
||||
|
||||
interface PaginationDotProps extends Omit<PaginationRootProps, "children"> {
|
||||
/** The size of the pagination dot. */
|
||||
size?: "md" | "lg";
|
||||
/** Whether the pagination uses brand colors. */
|
||||
isBrand?: boolean;
|
||||
/** Whether the pagination is displayed in a card. */
|
||||
framed?: boolean;
|
||||
}
|
||||
|
||||
export const PaginationDot = ({ framed, className, size = "md", isBrand, ...props }: PaginationDotProps) => {
|
||||
const sizes = {
|
||||
md: {
|
||||
root: cx("gap-3", framed && "p-2"),
|
||||
button: "h-2 w-2 after:-inset-x-1.5 after:-inset-y-2",
|
||||
},
|
||||
lg: {
|
||||
root: cx("gap-4", framed && "p-3"),
|
||||
button: "h-2.5 w-2.5 after:-inset-x-2 after:-inset-y-3",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Pagination.Root {...props} className={cx("flex h-max w-max", sizes[size].root, framed && "rounded-full bg-alpha-white/90 backdrop-blur", className)}>
|
||||
<Pagination.Context>
|
||||
{({ pages }) =>
|
||||
pages.map((page, index) =>
|
||||
page.type === "page" ? (
|
||||
<Pagination.Item
|
||||
{...page}
|
||||
asChild
|
||||
key={index}
|
||||
className={cx(
|
||||
"relative cursor-pointer rounded-full bg-quaternary outline-focus-ring after:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
sizes[size].button,
|
||||
page.isCurrent && "bg-fg-brand-primary_alt",
|
||||
isBrand && "bg-fg-brand-secondary",
|
||||
isBrand && page.isCurrent && "bg-fg-white",
|
||||
)}
|
||||
></Pagination.Item>
|
||||
) : (
|
||||
<Pagination.Ellipsis {...page} key={index} />
|
||||
),
|
||||
)
|
||||
}
|
||||
</Pagination.Context>
|
||||
</Pagination.Root>
|
||||
);
|
||||
};
|
||||
48
src/components/application/pagination/pagination-line.tsx
Normal file
48
src/components/application/pagination/pagination-line.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
import type { PaginationRootProps } from "./pagination-base";
|
||||
import { Pagination } from "./pagination-base";
|
||||
|
||||
interface PaginationLineProps extends Omit<PaginationRootProps, "children"> {
|
||||
/** The size of the pagination line. */
|
||||
size?: "md" | "lg";
|
||||
/** Whether the pagination is displayed in a card. */
|
||||
framed?: boolean;
|
||||
}
|
||||
|
||||
export const PaginationLine = ({ framed, className, size = "md", ...props }: PaginationLineProps) => {
|
||||
const sizes = {
|
||||
md: {
|
||||
root: cx("gap-2", framed && "p-2"),
|
||||
button: "h-1.5 w-full after:-inset-x-1.5 after:-inset-y-2",
|
||||
},
|
||||
lg: {
|
||||
root: cx("gap-3", framed && "p-3"),
|
||||
button: "h-2 w-full after:-inset-x-2 after:-inset-y-3",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Pagination.Root {...props} className={cx("flex h-max w-max", sizes[size].root, framed && "rounded-full bg-alpha-white/90 backdrop-blur", className)}>
|
||||
<Pagination.Context>
|
||||
{({ pages }) =>
|
||||
pages.map((page, index) =>
|
||||
page.type === "page" ? (
|
||||
<Pagination.Item
|
||||
{...page}
|
||||
asChild
|
||||
key={index}
|
||||
className={cx(
|
||||
"relative cursor-pointer rounded-full bg-quaternary outline-focus-ring after:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
sizes[size].button,
|
||||
page.isCurrent && "bg-fg-brand-primary_alt",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Pagination.Ellipsis {...page} key={index} />
|
||||
),
|
||||
)
|
||||
}
|
||||
</Pagination.Context>
|
||||
</Pagination.Root>
|
||||
);
|
||||
};
|
||||
328
src/components/application/pagination/pagination.tsx
Normal file
328
src/components/application/pagination/pagination.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
import { ArrowLeft, ArrowRight } from "@untitledui/icons";
|
||||
import { ButtonGroup, ButtonGroupItem } from "@/components/base/button-group/button-group";
|
||||
import { Button } from "@/components/base/buttons/button";
|
||||
import { useBreakpoint } from "@/hooks/use-breakpoint";
|
||||
import { cx } from "@/utils/cx";
|
||||
import type { PaginationRootProps } from "./pagination-base";
|
||||
import { Pagination } from "./pagination-base";
|
||||
|
||||
interface PaginationProps extends Partial<Omit<PaginationRootProps, "children">> {
|
||||
/** Whether the pagination buttons are rounded. */
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
const PaginationItem = ({ value, rounded, isCurrent }: { value: number; rounded?: boolean; isCurrent: boolean }) => {
|
||||
return (
|
||||
<Pagination.Item
|
||||
value={value}
|
||||
isCurrent={isCurrent}
|
||||
className={({ isSelected }) =>
|
||||
cx(
|
||||
"flex size-10 cursor-pointer items-center justify-center p-3 text-sm font-medium text-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-secondary focus-visible:z-10 focus-visible:bg-primary_hover focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
rounded ? "rounded-full" : "rounded-lg",
|
||||
isSelected && "bg-primary_hover text-secondary",
|
||||
)
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</Pagination.Item>
|
||||
);
|
||||
};
|
||||
|
||||
interface MobilePaginationProps {
|
||||
/** The current page. */
|
||||
page?: number;
|
||||
/** The total number of pages. */
|
||||
total?: number;
|
||||
/** The class name of the pagination component. */
|
||||
className?: string;
|
||||
/** The function to call when the page changes. */
|
||||
onPageChange?: (page: number) => void;
|
||||
}
|
||||
|
||||
const MobilePagination = ({ page = 1, total = 10, className, onPageChange }: MobilePaginationProps) => {
|
||||
return (
|
||||
<nav aria-label="Pagination" className={cx("flex items-center justify-between md:hidden", className)}>
|
||||
<Button
|
||||
aria-label="Go to previous page"
|
||||
iconLeading={ArrowLeft}
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => onPageChange?.(Math.max(0, page - 1))}
|
||||
/>
|
||||
|
||||
<span className="text-sm text-fg-secondary">
|
||||
Page <span className="font-medium">{page}</span> of <span className="font-medium">{total}</span>
|
||||
</span>
|
||||
|
||||
<Button
|
||||
aria-label="Go to next page"
|
||||
iconLeading={ArrowRight}
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => onPageChange?.(Math.min(total, page + 1))}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export const PaginationPageDefault = ({ rounded, page = 1, total = 10, className, ...props }: PaginationProps) => {
|
||||
const isDesktop = useBreakpoint("md");
|
||||
|
||||
return (
|
||||
<Pagination.Root
|
||||
{...props}
|
||||
page={page}
|
||||
total={total}
|
||||
className={cx("flex w-full items-center justify-between gap-3 border-t border-secondary pt-4 md:pt-5", className)}
|
||||
>
|
||||
<div className="hidden flex-1 justify-start md:flex">
|
||||
<Pagination.PrevTrigger asChild>
|
||||
<Button iconLeading={ArrowLeft} color="link-gray" size="sm">
|
||||
{isDesktop ? "Previous" : undefined}{" "}
|
||||
</Button>
|
||||
</Pagination.PrevTrigger>
|
||||
</div>
|
||||
|
||||
<Pagination.PrevTrigger asChild className="md:hidden">
|
||||
<Button iconLeading={ArrowLeft} color="secondary" size="sm">
|
||||
{isDesktop ? "Previous" : undefined}
|
||||
</Button>
|
||||
</Pagination.PrevTrigger>
|
||||
|
||||
<Pagination.Context>
|
||||
{({ pages, currentPage, total }) => (
|
||||
<>
|
||||
<div className="hidden justify-center gap-0.5 md:flex">
|
||||
{pages.map((page, index) =>
|
||||
page.type === "page" ? (
|
||||
<PaginationItem key={index} rounded={rounded} {...page} />
|
||||
) : (
|
||||
<Pagination.Ellipsis key={index} className="flex size-10 shrink-0 items-center justify-center text-tertiary">
|
||||
…
|
||||
</Pagination.Ellipsis>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center text-sm whitespace-pre text-fg-secondary md:hidden">
|
||||
Page <span className="font-medium">{currentPage}</span> of <span className="font-medium">{total}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Pagination.Context>
|
||||
|
||||
<div className="hidden flex-1 justify-end md:flex">
|
||||
<Pagination.NextTrigger asChild>
|
||||
<Button iconTrailing={ArrowRight} color="link-gray" size="sm">
|
||||
{isDesktop ? "Next" : undefined}
|
||||
</Button>
|
||||
</Pagination.NextTrigger>
|
||||
</div>
|
||||
<Pagination.NextTrigger asChild className="md:hidden">
|
||||
<Button iconTrailing={ArrowRight} color="secondary" size="sm">
|
||||
{isDesktop ? "Next" : undefined}
|
||||
</Button>
|
||||
</Pagination.NextTrigger>
|
||||
</Pagination.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export const PaginationPageMinimalCenter = ({ rounded, page = 1, total = 10, className, ...props }: PaginationProps) => {
|
||||
const isDesktop = useBreakpoint("md");
|
||||
|
||||
return (
|
||||
<Pagination.Root
|
||||
{...props}
|
||||
page={page}
|
||||
total={total}
|
||||
className={cx("flex w-full items-center justify-between gap-3 border-t border-secondary pt-4 md:pt-5", className)}
|
||||
>
|
||||
<div className="flex flex-1 justify-start">
|
||||
<Pagination.PrevTrigger asChild>
|
||||
<Button iconLeading={ArrowLeft} color="secondary" size="sm">
|
||||
{isDesktop ? "Previous" : undefined}
|
||||
</Button>
|
||||
</Pagination.PrevTrigger>
|
||||
</div>
|
||||
|
||||
<Pagination.Context>
|
||||
{({ pages, currentPage, total }) => (
|
||||
<>
|
||||
<div className="hidden justify-center gap-0.5 md:flex">
|
||||
{pages.map((page, index) =>
|
||||
page.type === "page" ? (
|
||||
<PaginationItem key={index} rounded={rounded} {...page} />
|
||||
) : (
|
||||
<Pagination.Ellipsis key={index} className="flex size-10 shrink-0 items-center justify-center text-tertiary">
|
||||
…
|
||||
</Pagination.Ellipsis>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center text-sm whitespace-pre text-fg-secondary md:hidden">
|
||||
Page <span className="font-medium">{currentPage}</span> of <span className="font-medium">{total}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Pagination.Context>
|
||||
|
||||
<div className="flex flex-1 justify-end">
|
||||
<Pagination.NextTrigger asChild>
|
||||
<Button iconTrailing={ArrowRight} color="secondary" size="sm">
|
||||
{isDesktop ? "Next" : undefined}
|
||||
</Button>
|
||||
</Pagination.NextTrigger>
|
||||
</div>
|
||||
</Pagination.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export const PaginationCardDefault = ({ rounded, page = 1, total = 10, ...props }: PaginationProps) => {
|
||||
const isDesktop = useBreakpoint("md");
|
||||
|
||||
return (
|
||||
<Pagination.Root
|
||||
{...props}
|
||||
page={page}
|
||||
total={total}
|
||||
className="flex w-full items-center justify-between gap-3 border-t border-secondary px-4 py-3 md:px-6 md:pt-3 md:pb-4"
|
||||
>
|
||||
<div className="flex flex-1 justify-start">
|
||||
<Pagination.PrevTrigger asChild>
|
||||
<Button iconLeading={ArrowLeft} color="secondary" size="sm">
|
||||
{isDesktop ? "Previous" : undefined}
|
||||
</Button>
|
||||
</Pagination.PrevTrigger>
|
||||
</div>
|
||||
|
||||
<Pagination.Context>
|
||||
{({ pages, currentPage, total }) => (
|
||||
<>
|
||||
<div className="hidden justify-center gap-0.5 md:flex">
|
||||
{pages.map((page, index) =>
|
||||
page.type === "page" ? (
|
||||
<PaginationItem key={index} rounded={rounded} {...page} />
|
||||
) : (
|
||||
<Pagination.Ellipsis key={index} className="flex size-10 shrink-0 items-center justify-center text-tertiary">
|
||||
…
|
||||
</Pagination.Ellipsis>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center text-sm whitespace-pre text-fg-secondary md:hidden">
|
||||
Page <span className="font-medium">{currentPage}</span> of <span className="font-medium">{total}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Pagination.Context>
|
||||
|
||||
<div className="flex flex-1 justify-end">
|
||||
<Pagination.NextTrigger asChild>
|
||||
<Button iconTrailing={ArrowRight} color="secondary" size="sm">
|
||||
{isDesktop ? "Next" : undefined}
|
||||
</Button>
|
||||
</Pagination.NextTrigger>
|
||||
</div>
|
||||
</Pagination.Root>
|
||||
);
|
||||
};
|
||||
|
||||
interface PaginationCardMinimalProps {
|
||||
/** The current page. */
|
||||
page?: number;
|
||||
/** The total number of pages. */
|
||||
total?: number;
|
||||
/** The alignment of the pagination. */
|
||||
align?: "left" | "center" | "right";
|
||||
/** The class name of the pagination component. */
|
||||
className?: string;
|
||||
/** The function to call when the page changes. */
|
||||
onPageChange?: (page: number) => void;
|
||||
}
|
||||
|
||||
export const PaginationCardMinimal = ({ page = 1, total = 10, align = "left", onPageChange, className }: PaginationCardMinimalProps) => {
|
||||
return (
|
||||
<div className={cx("border-t border-secondary px-4 py-3 md:px-6 md:pt-3 md:pb-4", className)}>
|
||||
<MobilePagination page={page} total={total} onPageChange={onPageChange} />
|
||||
|
||||
<nav aria-label="Pagination" className={cx("hidden items-center gap-3 md:flex", align === "center" && "justify-between")}>
|
||||
<div className={cx(align === "center" && "flex flex-1 justify-start")}>
|
||||
<Button isDisabled={page === 1} color="secondary" size="sm" onClick={() => onPageChange?.(Math.max(0, page - 1))}>
|
||||
Previous
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={cx(
|
||||
"text-sm font-medium text-fg-secondary",
|
||||
align === "right" && "order-first mr-auto",
|
||||
align === "left" && "order-last ml-auto",
|
||||
)}
|
||||
>
|
||||
Page {page} of {total}
|
||||
</span>
|
||||
|
||||
<div className={cx(align === "center" && "flex flex-1 justify-end")}>
|
||||
<Button isDisabled={page === total} color="secondary" size="sm" onClick={() => onPageChange?.(Math.min(total, page + 1))}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface PaginationButtonGroupProps extends Partial<Omit<PaginationRootProps, "children">> {
|
||||
/** The alignment of the pagination. */
|
||||
align?: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
export const PaginationButtonGroup = ({ align = "left", page = 1, total = 10, ...props }: PaginationButtonGroupProps) => {
|
||||
const isDesktop = useBreakpoint("md");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"flex border-t border-secondary px-4 py-3 md:px-6 md:pt-3 md:pb-4",
|
||||
align === "left" && "justify-start",
|
||||
align === "center" && "justify-center",
|
||||
align === "right" && "justify-end",
|
||||
)}
|
||||
>
|
||||
<Pagination.Root {...props} page={page} total={total}>
|
||||
<Pagination.Context>
|
||||
{({ pages }) => (
|
||||
<ButtonGroup size="md">
|
||||
<Pagination.PrevTrigger asChild>
|
||||
<ButtonGroupItem iconLeading={ArrowLeft}>{isDesktop ? "Previous" : undefined}</ButtonGroupItem>
|
||||
</Pagination.PrevTrigger>
|
||||
|
||||
{pages.map((page, index) =>
|
||||
page.type === "page" ? (
|
||||
<Pagination.Item key={index} {...page} asChild>
|
||||
<ButtonGroupItem isSelected={page.isCurrent} className="size-10 items-center justify-center">
|
||||
{page.value}
|
||||
</ButtonGroupItem>
|
||||
</Pagination.Item>
|
||||
) : (
|
||||
<Pagination.Ellipsis key={index}>
|
||||
<ButtonGroupItem className="pointer-events-none size-10 items-center justify-center rounded-none!">
|
||||
…
|
||||
</ButtonGroupItem>
|
||||
</Pagination.Ellipsis>
|
||||
),
|
||||
)}
|
||||
|
||||
<Pagination.NextTrigger asChild>
|
||||
<ButtonGroupItem iconTrailing={ArrowRight}>{isDesktop ? "Next" : undefined}</ButtonGroupItem>
|
||||
</Pagination.NextTrigger>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</Pagination.Context>
|
||||
</Pagination.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
120
src/components/application/slideout-menus/slideout-menu.tsx
Normal file
120
src/components/application/slideout-menus/slideout-menu.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { type ComponentPropsWithRef, type ReactNode, type RefAttributes } from "react";
|
||||
import type {
|
||||
DialogProps as AriaDialogProps,
|
||||
ModalOverlayProps as AriaModalOverlayProps,
|
||||
ModalRenderProps as AriaModalRenderProps,
|
||||
} from "react-aria-components";
|
||||
import { Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Modal as AriaModal, ModalOverlay as AriaModalOverlay } from "react-aria-components";
|
||||
import { CloseButton } from "@/components/base/buttons/close-button";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface ModalOverlayProps extends AriaModalOverlayProps, RefAttributes<HTMLDivElement> {}
|
||||
|
||||
export const ModalOverlay = (props: ModalOverlayProps) => {
|
||||
return (
|
||||
<AriaModalOverlay
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"fixed inset-0 flex min-h-dvh w-full items-center justify-end bg-overlay/70 pl-6 outline-hidden ease-linear md:pl-10",
|
||||
state.isEntering && "duration-300 animate-in fade-in",
|
||||
state.isExiting && "duration-500 animate-out fade-out",
|
||||
typeof props.className === "function" ? props.className(state) : props.className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ModalOverlay.displayName = "ModalOverlay";
|
||||
|
||||
interface ModalProps extends AriaModalOverlayProps, RefAttributes<HTMLDivElement> {}
|
||||
|
||||
export const Modal = (props: ModalProps) => (
|
||||
<AriaModal
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"inset-y-0 right-0 h-full w-full max-w-100 shadow-xl transition",
|
||||
state.isEntering && "duration-300 animate-in slide-in-from-right",
|
||||
state.isExiting && "duration-500 animate-out slide-out-to-right",
|
||||
typeof props.className === "function" ? props.className(state) : props.className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
Modal.displayName = "Modal";
|
||||
|
||||
interface DialogProps extends AriaDialogProps, RefAttributes<HTMLElement> {}
|
||||
|
||||
export const Dialog = (props: DialogProps) => (
|
||||
<AriaDialog
|
||||
role="dialog"
|
||||
{...props}
|
||||
className={cx(
|
||||
"relative flex size-full flex-col items-start gap-6 overflow-y-auto bg-primary ring-1 ring-secondary_alt outline-hidden",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
Dialog.displayName = "Dialog";
|
||||
|
||||
interface SlideoutMenuProps extends Omit<AriaModalOverlayProps, "children">, RefAttributes<HTMLDivElement> {
|
||||
children: ReactNode | ((children: AriaModalRenderProps & { close: () => void }) => ReactNode);
|
||||
dialogClassName?: string;
|
||||
}
|
||||
|
||||
const Menu = ({ children, dialogClassName, ...props }: SlideoutMenuProps) => {
|
||||
return (
|
||||
<ModalOverlay {...props}>
|
||||
<Modal className={(state) => cx(typeof props.className === "function" ? props.className(state) : props.className)}>
|
||||
{(state) => (
|
||||
<Dialog className={dialogClassName}>
|
||||
{({ close }) => {
|
||||
return typeof children === "function" ? children({ ...state, close }) : children;
|
||||
}}
|
||||
</Dialog>
|
||||
)}
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
};
|
||||
Menu.displayName = "SlideoutMenu";
|
||||
|
||||
const Content = ({ role = "main", ...props }: ComponentPropsWithRef<"div">) => {
|
||||
return <div role={role} {...props} className={cx("flex size-full flex-col gap-6 overflow-y-auto overscroll-auto px-4 md:px-6", props.className)} />;
|
||||
};
|
||||
Content.displayName = "SlideoutContent";
|
||||
|
||||
interface SlideoutHeaderProps extends ComponentPropsWithRef<"header"> {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const Header = ({ className, children, onClose, ...props }: SlideoutHeaderProps) => {
|
||||
return (
|
||||
<header {...props} className={cx("relative z-1 w-full px-4 pt-6 md:px-6", className)}>
|
||||
{children}
|
||||
<CloseButton size="md" className="absolute top-3 right-3 shrink-0" onClick={onClose} />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
Header.displayName = "SlideoutHeader";
|
||||
|
||||
const Footer = (props: ComponentPropsWithRef<"footer">) => {
|
||||
return <footer {...props} className={cx("w-full p-4 shadow-[inset_0px_1px_0px_0px] shadow-border-secondary md:px-6", props.className)} />;
|
||||
};
|
||||
Footer.displayName = "SlideoutFooter";
|
||||
|
||||
const SlideoutMenu = Menu as typeof Menu & {
|
||||
Trigger: typeof AriaDialogTrigger;
|
||||
Content: typeof Content;
|
||||
Header: typeof Header;
|
||||
Footer: typeof Footer;
|
||||
};
|
||||
SlideoutMenu.displayName = "SlideoutMenu";
|
||||
|
||||
SlideoutMenu.Trigger = AriaDialogTrigger;
|
||||
SlideoutMenu.Content = Content;
|
||||
SlideoutMenu.Header = Header;
|
||||
SlideoutMenu.Footer = Footer;
|
||||
|
||||
export { SlideoutMenu };
|
||||
298
src/components/application/table/table.tsx
Normal file
298
src/components/application/table/table.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import type { ComponentPropsWithRef, HTMLAttributes, ReactNode, Ref, TdHTMLAttributes, ThHTMLAttributes } from "react";
|
||||
import { createContext, isValidElement, useContext } from "react";
|
||||
import { ArrowDown, ChevronSelectorVertical, Copy01, Edit01, HelpCircle, Trash01 } from "@untitledui/icons";
|
||||
import type {
|
||||
CellProps as AriaCellProps,
|
||||
ColumnProps as AriaColumnProps,
|
||||
RowProps as AriaRowProps,
|
||||
TableHeaderProps as AriaTableHeaderProps,
|
||||
TableProps as AriaTableProps,
|
||||
} from "react-aria-components";
|
||||
import {
|
||||
Cell as AriaCell,
|
||||
Collection as AriaCollection,
|
||||
Column as AriaColumn,
|
||||
Group as AriaGroup,
|
||||
Row as AriaRow,
|
||||
Table as AriaTable,
|
||||
TableBody as AriaTableBody,
|
||||
TableHeader as AriaTableHeader,
|
||||
useTableOptions,
|
||||
} from "react-aria-components";
|
||||
import { Badge } from "@/components/base/badges/badges";
|
||||
import { Checkbox } from "@/components/base/checkbox/checkbox";
|
||||
import { Dropdown } from "@/components/base/dropdown/dropdown";
|
||||
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export const TableRowActionsDropdown = () => (
|
||||
<Dropdown.Root>
|
||||
<Dropdown.DotsButton />
|
||||
|
||||
<Dropdown.Popover className="w-min">
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item icon={Edit01}>
|
||||
<span className="pr-4">Edit</span>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item icon={Copy01}>
|
||||
<span className="pr-4">Copy link</span>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item icon={Trash01}>
|
||||
<span className="pr-4">Delete</span>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Popover>
|
||||
</Dropdown.Root>
|
||||
);
|
||||
|
||||
const TableContext = createContext<{ size: "sm" | "md" }>({ size: "md" });
|
||||
|
||||
const TableCardRoot = ({ children, className, size = "md", ...props }: HTMLAttributes<HTMLDivElement> & { size?: "sm" | "md" }) => {
|
||||
return (
|
||||
<TableContext.Provider value={{ size }}>
|
||||
<div {...props} className={cx("overflow-hidden rounded-xl bg-primary shadow-xs ring-1 ring-secondary", className)}>
|
||||
{children}
|
||||
</div>
|
||||
</TableContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
interface TableCardHeaderProps {
|
||||
/** The title of the table card header. */
|
||||
title: string;
|
||||
/** The badge displayed next to the title. */
|
||||
badge?: ReactNode;
|
||||
/** The description of the table card header. */
|
||||
description?: string;
|
||||
/** The content displayed after the title and badge. */
|
||||
contentTrailing?: ReactNode;
|
||||
/** The class name of the table card header. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TableCardHeader = ({ title, badge, description, contentTrailing, className }: TableCardHeaderProps) => {
|
||||
const { size } = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"relative flex flex-col items-start gap-4 border-b border-secondary bg-primary px-4 md:flex-row",
|
||||
size === "sm" ? "py-4 md:px-5" : "py-5 md:px-6",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-1 flex-col gap-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className={cx("font-semibold text-primary", size === "sm" ? "text-md" : "text-lg")}>{title}</h2>
|
||||
{badge ? (
|
||||
isValidElement(badge) ? (
|
||||
badge
|
||||
) : (
|
||||
<Badge color="brand" size="sm">
|
||||
{badge}
|
||||
</Badge>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
{description && <p className="text-sm text-tertiary">{description}</p>}
|
||||
</div>
|
||||
{contentTrailing}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TableRootProps extends AriaTableProps, Omit<ComponentPropsWithRef<"table">, "className" | "slot" | "style"> {
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
const TableRoot = ({ className, size = "md", ...props }: TableRootProps) => {
|
||||
const context = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<TableContext.Provider value={{ size: context?.size ?? size }}>
|
||||
<div className="overflow-x-auto">
|
||||
<AriaTable className={(state) => cx("w-full overflow-x-hidden", typeof className === "function" ? className(state) : className)} {...props} />
|
||||
</div>
|
||||
</TableContext.Provider>
|
||||
);
|
||||
};
|
||||
TableRoot.displayName = "Table";
|
||||
|
||||
interface TableHeaderProps<T extends object>
|
||||
extends AriaTableHeaderProps<T>,
|
||||
Omit<ComponentPropsWithRef<"thead">, "children" | "className" | "slot" | "style"> {
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
const TableHeader = <T extends object>({ columns, children, bordered = true, className, ...props }: TableHeaderProps<T>) => {
|
||||
const { size } = useContext(TableContext);
|
||||
const { selectionBehavior, selectionMode } = useTableOptions();
|
||||
|
||||
return (
|
||||
<AriaTableHeader
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"relative bg-secondary",
|
||||
size === "sm" ? "h-9" : "h-11",
|
||||
|
||||
// Row border—using an "after" pseudo-element to avoid the border taking up space.
|
||||
bordered &&
|
||||
"[&>tr>th]:after:pointer-events-none [&>tr>th]:after:absolute [&>tr>th]:after:inset-x-0 [&>tr>th]:after:bottom-0 [&>tr>th]:after:h-px [&>tr>th]:after:bg-border-secondary [&>tr>th]:focus-visible:after:bg-transparent",
|
||||
|
||||
typeof className === "function" ? className(state) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{selectionBehavior === "toggle" && (
|
||||
<AriaColumn className={cx("relative py-2 pr-0 pl-4", size === "sm" ? "w-9 md:pl-5" : "w-11 md:pl-6")}>
|
||||
{selectionMode === "multiple" && (
|
||||
<div className="flex items-start">
|
||||
<Checkbox slot="selection" size={size} />
|
||||
</div>
|
||||
)}
|
||||
</AriaColumn>
|
||||
)}
|
||||
<AriaCollection items={columns}>{children}</AriaCollection>
|
||||
</AriaTableHeader>
|
||||
);
|
||||
};
|
||||
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
interface TableHeadProps extends AriaColumnProps, Omit<ThHTMLAttributes<HTMLTableCellElement>, "children" | "className" | "style" | "id"> {
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
const TableHead = ({ className, tooltip, label, children, ...props }: TableHeadProps) => {
|
||||
const { selectionBehavior } = useTableOptions();
|
||||
|
||||
return (
|
||||
<AriaColumn
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"relative p-0 px-6 py-2 outline-hidden focus-visible:z-1 focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-bg-primary focus-visible:ring-inset",
|
||||
selectionBehavior === "toggle" && "nth-2:pl-3",
|
||||
state.allowsSorting && "cursor-pointer",
|
||||
typeof className === "function" ? className(state) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{(state) => (
|
||||
<AriaGroup className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{label && <span className="text-xs font-semibold whitespace-nowrap text-quaternary">{label}</span>}
|
||||
{typeof children === "function" ? children(state) : children}
|
||||
</div>
|
||||
|
||||
{tooltip && (
|
||||
<Tooltip title={tooltip} placement="top">
|
||||
<TooltipTrigger className="cursor-pointer text-fg-quaternary transition duration-100 ease-linear hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover">
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{state.allowsSorting &&
|
||||
(state.sortDirection ? (
|
||||
<ArrowDown className={cx("size-3 stroke-[3px] text-fg-quaternary", state.sortDirection === "ascending" && "rotate-180")} />
|
||||
) : (
|
||||
<ChevronSelectorVertical size={12} strokeWidth={3} className="text-fg-quaternary" />
|
||||
))}
|
||||
</AriaGroup>
|
||||
)}
|
||||
</AriaColumn>
|
||||
);
|
||||
};
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
interface TableRowProps<T extends object>
|
||||
extends AriaRowProps<T>,
|
||||
Omit<ComponentPropsWithRef<"tr">, "children" | "className" | "onClick" | "slot" | "style" | "id"> {
|
||||
highlightSelectedRow?: boolean;
|
||||
}
|
||||
|
||||
const TableRow = <T extends object>({ columns, children, className, highlightSelectedRow = true, ...props }: TableRowProps<T>) => {
|
||||
const { size } = useContext(TableContext);
|
||||
const { selectionBehavior } = useTableOptions();
|
||||
|
||||
return (
|
||||
<AriaRow
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"relative outline-focus-ring transition-colors after:pointer-events-none hover:bg-secondary focus-visible:outline-2 focus-visible:-outline-offset-2",
|
||||
size === "sm" ? "h-14" : "h-18",
|
||||
highlightSelectedRow && "selected:bg-secondary",
|
||||
|
||||
// Row border—using an "after" pseudo-element to avoid the border taking up space.
|
||||
"[&>td]:after:absolute [&>td]:after:inset-x-0 [&>td]:after:bottom-0 [&>td]:after:h-px [&>td]:after:w-full [&>td]:after:bg-border-secondary last:[&>td]:after:hidden [&>td]:focus-visible:after:opacity-0 focus-visible:[&>td]:after:opacity-0",
|
||||
|
||||
typeof className === "function" ? className(state) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{selectionBehavior === "toggle" && (
|
||||
<AriaCell className={cx("relative py-2 pr-0 pl-4", size === "sm" ? "md:pl-5" : "md:pl-6")}>
|
||||
<div className="flex items-end">
|
||||
<Checkbox slot="selection" size={size} />
|
||||
</div>
|
||||
</AriaCell>
|
||||
)}
|
||||
<AriaCollection items={columns}>{children}</AriaCollection>
|
||||
</AriaRow>
|
||||
);
|
||||
};
|
||||
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
interface TableCellProps extends AriaCellProps, Omit<TdHTMLAttributes<HTMLTableCellElement>, "children" | "className" | "style" | "id"> {
|
||||
ref?: Ref<HTMLTableCellElement>;
|
||||
}
|
||||
|
||||
const TableCell = ({ className, children, ...props }: TableCellProps) => {
|
||||
const { size } = useContext(TableContext);
|
||||
const { selectionBehavior } = useTableOptions();
|
||||
|
||||
return (
|
||||
<AriaCell
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"relative text-sm text-tertiary outline-focus-ring focus-visible:z-1 focus-visible:outline-2 focus-visible:-outline-offset-2",
|
||||
size === "sm" && "px-5 py-3",
|
||||
size === "md" && "px-6 py-4",
|
||||
|
||||
selectionBehavior === "toggle" && "nth-2:pl-3",
|
||||
|
||||
typeof className === "function" ? className(state) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</AriaCell>
|
||||
);
|
||||
};
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCard = {
|
||||
Root: TableCardRoot,
|
||||
Header: TableCardHeader,
|
||||
};
|
||||
|
||||
const Table = TableRoot as typeof TableRoot & {
|
||||
Body: typeof AriaTableBody;
|
||||
Cell: typeof TableCell;
|
||||
Head: typeof TableHead;
|
||||
Header: typeof TableHeader;
|
||||
Row: typeof TableRow;
|
||||
};
|
||||
Table.Body = AriaTableBody;
|
||||
Table.Cell = TableCell;
|
||||
Table.Head = TableHead;
|
||||
Table.Header = TableHeader;
|
||||
Table.Row = TableRow;
|
||||
|
||||
export { Table, TableCard };
|
||||
223
src/components/application/tabs/tabs.tsx
Normal file
223
src/components/application/tabs/tabs.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import type { ComponentPropsWithRef, ReactNode } from "react";
|
||||
import { Fragment, createContext, useContext } from "react";
|
||||
import type { TabListProps as AriaTabListProps, TabProps as AriaTabProps, TabRenderProps as AriaTabRenderProps } from "react-aria-components";
|
||||
import { Tab as AriaTab, TabList as AriaTabList, TabPanel as AriaTabPanel, Tabs as AriaTabs, TabsContext, useSlottedContext } from "react-aria-components";
|
||||
import type { BadgeColors } from "@/components/base/badges/badge-types";
|
||||
import { Badge } from "@/components/base/badges/badges";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
type Orientation = "horizontal" | "vertical";
|
||||
|
||||
// Types for different orientations
|
||||
type HorizontalTypes = "button-brand" | "button-gray" | "button-border" | "button-minimal" | "underline";
|
||||
type VerticalTypes = "button-brand" | "button-gray" | "button-border" | "button-minimal" | "line";
|
||||
type TabTypeColors<T> = T extends "horizontal" ? HorizontalTypes : VerticalTypes;
|
||||
|
||||
// Styles for different types of tab
|
||||
const getTabStyles = ({ isFocusVisible, isSelected, isHovered }: AriaTabRenderProps) => ({
|
||||
"button-brand": cx(
|
||||
"outline-focus-ring",
|
||||
isFocusVisible && "outline-2 -outline-offset-2",
|
||||
(isSelected || isHovered) && "bg-brand-primary_alt text-brand-secondary",
|
||||
),
|
||||
"button-gray": cx(
|
||||
"outline-focus-ring",
|
||||
isHovered && "bg-primary_hover text-secondary",
|
||||
isFocusVisible && "outline-2 -outline-offset-2",
|
||||
isSelected && "bg-active text-secondary",
|
||||
),
|
||||
"button-border": cx(
|
||||
"outline-focus-ring",
|
||||
(isSelected || isHovered) && "bg-primary_alt text-secondary shadow-sm",
|
||||
isFocusVisible && "outline-2 -outline-offset-2",
|
||||
),
|
||||
"button-minimal": cx(
|
||||
"rounded-lg outline-focus-ring",
|
||||
isHovered && "text-secondary",
|
||||
isFocusVisible && "outline-2 -outline-offset-2",
|
||||
isSelected && "bg-primary_alt text-secondary shadow-xs ring-1 ring-primary ring-inset",
|
||||
),
|
||||
underline: cx(
|
||||
"rounded-none border-b-2 border-transparent outline-focus-ring",
|
||||
(isSelected || isHovered) && "border-fg-brand-primary_alt text-brand-secondary",
|
||||
isFocusVisible && "outline-2 -outline-offset-2",
|
||||
),
|
||||
line: cx(
|
||||
"rounded-none border-l-2 border-transparent outline-focus-ring",
|
||||
(isSelected || isHovered) && "border-fg-brand-primary_alt text-brand-secondary",
|
||||
isFocusVisible && "outline-2 -outline-offset-2",
|
||||
),
|
||||
});
|
||||
|
||||
const sizes = {
|
||||
sm: {
|
||||
"button-brand": "text-sm font-semibold py-2 px-3",
|
||||
"button-gray": "text-sm font-semibold py-2 px-3",
|
||||
"button-border": "text-sm font-semibold py-2 px-3",
|
||||
"button-minimal": "text-sm font-semibold py-2 px-3",
|
||||
underline: "text-sm font-semibold px-1 pb-2.5 pt-0",
|
||||
line: "text-sm font-semibold pl-2.5 pr-3 py-0.5",
|
||||
},
|
||||
md: {
|
||||
"button-brand": "text-md font-semibold py-2.5 px-3",
|
||||
"button-gray": "text-md font-semibold py-2.5 px-3",
|
||||
"button-border": "text-md font-semibold py-2.5 px-3",
|
||||
"button-minimal": "text-md font-semibold py-2.5 px-3",
|
||||
underline: "text-md font-semibold px-1 pb-2.5 pt-0",
|
||||
line: "text-md font-semibold pr-3.5 pl-3 py-1",
|
||||
},
|
||||
};
|
||||
|
||||
// Styles for different types of horizontal tabs
|
||||
const getHorizontalStyles = ({ size, fullWidth }: { size?: "sm" | "md"; fullWidth?: boolean }) => ({
|
||||
"button-brand": "gap-1",
|
||||
"button-gray": "gap-1",
|
||||
"button-border": cx("gap-1 rounded-[10px] bg-secondary_alt p-1 ring-1 ring-secondary ring-inset", size === "md" && "rounded-xl p-1.5"),
|
||||
"button-minimal": "gap-0.5 rounded-lg bg-secondary_alt ring-1 ring-inset ring-secondary",
|
||||
underline: cx("gap-3", fullWidth && "w-full gap-4"),
|
||||
line: "gap-2",
|
||||
});
|
||||
|
||||
const getColorStyles = ({ isSelected, isHovered }: Partial<AriaTabRenderProps>) => ({
|
||||
"button-brand": isSelected || isHovered ? "brand" : "gray",
|
||||
"button-gray": "gray",
|
||||
"button-border": "gray",
|
||||
"button-minimal": "gray",
|
||||
underline: isSelected || isHovered ? "brand" : "gray",
|
||||
line: isSelected || isHovered ? "brand" : "gray",
|
||||
});
|
||||
|
||||
interface TabListComponentProps<T extends object, K extends Orientation> extends AriaTabListProps<T> {
|
||||
/** The size of the tab list. */
|
||||
size?: keyof typeof sizes;
|
||||
/** The type of the tab list. */
|
||||
type?: TabTypeColors<K>;
|
||||
/** The orientation of the tab list. */
|
||||
orientation?: K;
|
||||
/** The items of the tab list. */
|
||||
items: T[];
|
||||
/** Whether the tab list is full width. */
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const TabListContext = createContext<Omit<TabListComponentProps<TabComponentProps, Orientation>, "items">>({
|
||||
size: "sm",
|
||||
type: "button-brand",
|
||||
});
|
||||
|
||||
export const TabList = <T extends Orientation>({
|
||||
size = "sm",
|
||||
type = "button-brand",
|
||||
orientation: orientationProp,
|
||||
fullWidth,
|
||||
className,
|
||||
children,
|
||||
...otherProps
|
||||
}: TabListComponentProps<TabComponentProps, T>) => {
|
||||
const context = useSlottedContext(TabsContext);
|
||||
|
||||
const orientation = orientationProp ?? context?.orientation ?? "horizontal";
|
||||
|
||||
return (
|
||||
<TabListContext.Provider value={{ size, type, orientation, fullWidth }}>
|
||||
<AriaTabList
|
||||
{...otherProps}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"group flex",
|
||||
|
||||
getHorizontalStyles({
|
||||
size,
|
||||
fullWidth,
|
||||
})[type as HorizontalTypes],
|
||||
|
||||
orientation === "vertical" && "w-max flex-col",
|
||||
|
||||
// Only horizontal tabs with underline type have bottom border
|
||||
orientation === "horizontal" &&
|
||||
type === "underline" &&
|
||||
"relative before:absolute before:inset-x-0 before:bottom-0 before:h-px before:bg-border-secondary",
|
||||
|
||||
typeof className === "function" ? className(state) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{children ?? ((item) => <Tab {...item}>{item.children}</Tab>)}
|
||||
</AriaTabList>
|
||||
</TabListContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const TabPanel = (props: ComponentPropsWithRef<typeof AriaTabPanel>) => {
|
||||
return (
|
||||
<AriaTabPanel
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
typeof props.className === "function" ? props.className(state) : props.className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface TabComponentProps extends AriaTabProps {
|
||||
/** The label of the tab. */
|
||||
label?: ReactNode;
|
||||
/** The children of the tab. */
|
||||
children?: ReactNode | ((props: AriaTabRenderProps) => ReactNode);
|
||||
/** The badge displayed next to the label. */
|
||||
badge?: number | string;
|
||||
}
|
||||
|
||||
export const Tab = (props: TabComponentProps) => {
|
||||
const { label, children, badge, ...otherProps } = props;
|
||||
const { size = "sm", type = "button-brand", fullWidth } = useContext(TabListContext);
|
||||
|
||||
return (
|
||||
<AriaTab
|
||||
{...otherProps}
|
||||
className={(prop) =>
|
||||
cx(
|
||||
"z-10 flex h-max cursor-pointer items-center justify-center gap-2 rounded-md whitespace-nowrap text-quaternary transition duration-100 ease-linear",
|
||||
"group-orientation-vertical:justify-start",
|
||||
fullWidth && "w-full flex-1",
|
||||
sizes[size][type],
|
||||
getTabStyles(prop)[type],
|
||||
typeof props.className === "function" ? props.className(prop) : props.className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{(state) => (
|
||||
<Fragment>
|
||||
{typeof children === "function" ? children(state) : children || label}
|
||||
{badge && (
|
||||
<Badge
|
||||
size={size}
|
||||
type="pill-color"
|
||||
color={getColorStyles(state)[type] as BadgeColors}
|
||||
className={cx("hidden transition-inherit-all md:flex", size === "sm" && "-my-px")}
|
||||
>
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</AriaTab>
|
||||
);
|
||||
};
|
||||
|
||||
export const Tabs = ({ className, ...props }: ComponentPropsWithRef<typeof AriaTabs>) => {
|
||||
return (
|
||||
<AriaTabs
|
||||
keyboardActivation="manual"
|
||||
{...props}
|
||||
className={(state) => cx("flex w-full flex-col", typeof className === "function" ? className(state) : className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Tabs.Panel = TabPanel;
|
||||
Tabs.List = TabList;
|
||||
Tabs.Item = Tab;
|
||||
28
src/components/base/avatar/avatar-label-group.tsx
Normal file
28
src/components/base/avatar/avatar-label-group.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { Avatar, type AvatarProps } from "./avatar";
|
||||
|
||||
const styles = {
|
||||
sm: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-xs" },
|
||||
md: { root: "gap-2", title: "text-sm font-semibold", subtitle: "text-sm" },
|
||||
lg: { root: "gap-3", title: "text-md font-semibold", subtitle: "text-md" },
|
||||
xl: { root: "gap-4", title: "text-lg font-semibold", subtitle: "text-md" },
|
||||
};
|
||||
|
||||
interface AvatarLabelGroupProps extends AvatarProps {
|
||||
size: "sm" | "md" | "lg" | "xl";
|
||||
title: string | ReactNode;
|
||||
subtitle: string | ReactNode;
|
||||
}
|
||||
|
||||
export const AvatarLabelGroup = ({ title, subtitle, className, ...props }: AvatarLabelGroupProps) => {
|
||||
return (
|
||||
<figure className={cx("group flex min-w-0 flex-1 items-center", styles[props.size].root, className)}>
|
||||
<Avatar {...props} />
|
||||
<figcaption className="min-w-0 flex-1">
|
||||
<p className={cx("text-primary", styles[props.size].title)}>{title}</p>
|
||||
<p className={cx("truncate text-tertiary", styles[props.size].subtitle)}>{subtitle}</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
};
|
||||
123
src/components/base/avatar/avatar-profile-photo.tsx
Normal file
123
src/components/base/avatar/avatar-profile-photo.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useState } from "react";
|
||||
import { User01 } from "@untitledui/icons";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { type AvatarProps } from "./avatar";
|
||||
import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
|
||||
|
||||
const styles = {
|
||||
sm: {
|
||||
root: "size-18 p-0.75",
|
||||
rootWithPlaceholder: "p-1",
|
||||
content: "",
|
||||
icon: "size-9",
|
||||
initials: "text-display-sm font-semibold",
|
||||
badge: "bottom-0.5 right-0.5",
|
||||
},
|
||||
md: {
|
||||
root: "size-24 p-1",
|
||||
rootWithPlaceholder: "p-1.25",
|
||||
content: "shadow-xl",
|
||||
icon: "size-12",
|
||||
initials: "text-display-md font-semibold",
|
||||
badge: "bottom-1 right-1",
|
||||
},
|
||||
lg: {
|
||||
root: "size-40 p-1.5",
|
||||
rootWithPlaceholder: "p-1.75",
|
||||
content: "shadow-2xl",
|
||||
icon: "size-20",
|
||||
initials: "text-display-xl font-semibold",
|
||||
badge: "bottom-2 right-2",
|
||||
},
|
||||
};
|
||||
|
||||
const tickSizeMap = {
|
||||
sm: "2xl",
|
||||
md: "3xl",
|
||||
lg: "4xl",
|
||||
} as const;
|
||||
|
||||
interface AvatarProfilePhotoProps extends AvatarProps {
|
||||
size: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export const AvatarProfilePhoto = ({
|
||||
contrastBorder = true,
|
||||
size = "md",
|
||||
src,
|
||||
alt,
|
||||
initials,
|
||||
placeholder,
|
||||
placeholderIcon: PlaceholderIcon,
|
||||
verified,
|
||||
badge,
|
||||
status,
|
||||
className,
|
||||
}: AvatarProfilePhotoProps) => {
|
||||
const [isFailed, setIsFailed] = useState(false);
|
||||
|
||||
const renderMainContent = () => {
|
||||
if (src && !isFailed) {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
onError={() => setIsFailed(true)}
|
||||
className={cx(
|
||||
"size-full rounded-full object-cover",
|
||||
contrastBorder && "outline-1 -outline-offset-1 outline-avatar-contrast-border",
|
||||
styles[size].content,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (initials) {
|
||||
return (
|
||||
<div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
|
||||
<span className={cx("text-quaternary", styles[size].initials)}>{initials}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (PlaceholderIcon) {
|
||||
return (
|
||||
<div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
|
||||
<PlaceholderIcon className={cx("text-fg-quaternary", styles[size].icon)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx("flex size-full items-center justify-center rounded-full bg-tertiary ring-1 ring-secondary_alt", styles[size].content)}>
|
||||
{placeholder || <User01 className={cx("text-fg-quaternary", styles[size].icon)} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBadgeContent = () => {
|
||||
if (status) {
|
||||
return <AvatarOnlineIndicator status={status} size={tickSizeMap[size]} className={styles[size].badge} />;
|
||||
}
|
||||
|
||||
if (verified) {
|
||||
return <VerifiedTick size={tickSizeMap[size]} className={cx("absolute", styles[size].badge)} />;
|
||||
}
|
||||
|
||||
return badge;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"relative flex shrink-0 items-center justify-center rounded-full bg-primary ring-1 ring-secondary_alt",
|
||||
styles[size].root,
|
||||
(!src || isFailed) && styles[size].rootWithPlaceholder,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{renderMainContent()}
|
||||
{renderBadgeContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
129
src/components/base/avatar/avatar.tsx
Normal file
129
src/components/base/avatar/avatar.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import { User01 } from "@untitledui/icons";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
|
||||
|
||||
type AvatarSize = "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
|
||||
export interface AvatarProps {
|
||||
size?: AvatarSize;
|
||||
className?: string;
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
/**
|
||||
* Display a contrast border around the avatar.
|
||||
*/
|
||||
contrastBorder?: boolean;
|
||||
/**
|
||||
* Display a badge (i.e. company logo).
|
||||
*/
|
||||
badge?: ReactNode;
|
||||
/**
|
||||
* Display a status indicator.
|
||||
*/
|
||||
status?: "online" | "offline";
|
||||
/**
|
||||
* Display a verified tick icon.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
verified?: boolean;
|
||||
|
||||
/**
|
||||
* The initials of the user to display if no image is available.
|
||||
*/
|
||||
initials?: string;
|
||||
/**
|
||||
* An icon to display if no image is available.
|
||||
*/
|
||||
placeholderIcon?: FC<{ className?: string }>;
|
||||
/**
|
||||
* A placeholder to display if no image is available.
|
||||
*/
|
||||
placeholder?: ReactNode;
|
||||
|
||||
/**
|
||||
* Whether the avatar should show a focus ring when the parent group is in focus.
|
||||
* For example, when the avatar is wrapped inside a link.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
focusable?: boolean;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
xxs: { root: "size-4 outline-[0.5px] -outline-offset-[0.5px]", initials: "text-xs font-semibold", icon: "size-3" },
|
||||
xs: { root: "size-6 outline-[0.5px] -outline-offset-[0.5px]", initials: "text-xs font-semibold", icon: "size-4" },
|
||||
sm: { root: "size-8 outline-[0.75px] -outline-offset-[0.75px]", initials: "text-sm font-semibold", icon: "size-5" },
|
||||
md: { root: "size-10 outline-1 -outline-offset-1", initials: "text-md font-semibold", icon: "size-6" },
|
||||
lg: { root: "size-12 outline-1 -outline-offset-1", initials: "text-lg font-semibold", icon: "size-7" },
|
||||
xl: { root: "size-14 outline-1 -outline-offset-1", initials: "text-xl font-semibold", icon: "size-8" },
|
||||
"2xl": { root: "size-16 outline-1 -outline-offset-1", initials: "text-display-xs font-semibold", icon: "size-8" },
|
||||
};
|
||||
|
||||
export const Avatar = ({
|
||||
contrastBorder = true,
|
||||
size = "md",
|
||||
src,
|
||||
alt,
|
||||
initials,
|
||||
placeholder,
|
||||
placeholderIcon: PlaceholderIcon,
|
||||
badge,
|
||||
status,
|
||||
verified,
|
||||
focusable = false,
|
||||
className,
|
||||
}: AvatarProps) => {
|
||||
const [isFailed, setIsFailed] = useState(false);
|
||||
|
||||
const renderMainContent = () => {
|
||||
if (src && !isFailed) {
|
||||
return <img data-avatar-img className="size-full rounded-full object-cover" src={src} alt={alt} onError={() => setIsFailed(true)} />;
|
||||
}
|
||||
|
||||
if (initials) {
|
||||
return <span className={cx("text-quaternary", styles[size].initials)}>{initials}</span>;
|
||||
}
|
||||
|
||||
if (PlaceholderIcon) {
|
||||
return <PlaceholderIcon className={cx("text-fg-quaternary", styles[size].icon)} />;
|
||||
}
|
||||
|
||||
return placeholder || <User01 className={cx("text-fg-quaternary", styles[size].icon)} />;
|
||||
};
|
||||
|
||||
const renderBadgeContent = () => {
|
||||
if (status) {
|
||||
return <AvatarOnlineIndicator status={status} size={size === "xxs" ? "xs" : size} />;
|
||||
}
|
||||
|
||||
if (verified) {
|
||||
return (
|
||||
<VerifiedTick
|
||||
size={size === "xxs" ? "xs" : size}
|
||||
className={cx("absolute right-0 bottom-0", (size === "xxs" || size === "xs") && "-right-px -bottom-px")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return badge;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-avatar
|
||||
className={cx(
|
||||
"relative inline-flex shrink-0 items-center justify-center rounded-full bg-avatar-bg outline-transparent",
|
||||
// Focus styles
|
||||
focusable && "group-outline-focus-ring group-focus-visible:outline-2 group-focus-visible:outline-offset-2",
|
||||
contrastBorder && "outline outline-avatar-contrast-border",
|
||||
styles[size].root,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{renderMainContent()}
|
||||
{renderBadgeContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Plus } from "@untitledui/icons";
|
||||
import type { ButtonProps as AriaButtonProps } from "react-aria-components";
|
||||
import { Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const sizes = {
|
||||
xs: { root: "size-6", icon: "size-4" },
|
||||
sm: { root: "size-8", icon: "size-4" },
|
||||
md: { root: "size-10", icon: "size-5" },
|
||||
};
|
||||
|
||||
interface AvatarAddButtonProps extends AriaButtonProps {
|
||||
size: "xs" | "sm" | "md";
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AvatarAddButton = ({ size, className, title = "Add user", ...props }: AvatarAddButtonProps) => (
|
||||
<AriaTooltip title={title}>
|
||||
<AriaTooltipTrigger
|
||||
{...props}
|
||||
aria-label={title}
|
||||
className={cx(
|
||||
"flex cursor-pointer items-center justify-center rounded-full border border-dashed border-primary bg-primary text-fg-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 disabled:border-gray-200 disabled:bg-secondary disabled:text-gray-200",
|
||||
sizes[size].root,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Plus className={cx("text-current transition-inherit-all", sizes[size].icon)} />
|
||||
</AriaTooltipTrigger>
|
||||
</AriaTooltip>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const sizes = {
|
||||
xs: "size-2",
|
||||
sm: "size-3",
|
||||
md: "size-3.5",
|
||||
lg: "size-4",
|
||||
xl: "size-4.5",
|
||||
"2xl": "size-5 ring-[1.67px]",
|
||||
};
|
||||
|
||||
interface AvatarCompanyIconProps {
|
||||
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
src: string;
|
||||
alt?: string;
|
||||
}
|
||||
|
||||
export const AvatarCompanyIcon = ({ size, src, alt }: AvatarCompanyIconProps) => (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={cx("bg-primary-25 absolute -right-0.5 -bottom-0.5 rounded-full object-cover ring-[1.5px] ring-bg-primary", sizes[size])}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const sizes = {
|
||||
xs: "size-1.5",
|
||||
sm: "size-2",
|
||||
md: "size-2.5",
|
||||
lg: "size-3",
|
||||
xl: "size-3.5",
|
||||
"2xl": "size-4",
|
||||
"3xl": "size-4.5",
|
||||
"4xl": "size-5",
|
||||
};
|
||||
|
||||
interface AvatarOnlineIndicatorProps {
|
||||
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||
status: "online" | "offline";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AvatarOnlineIndicator = ({ size, status, className }: AvatarOnlineIndicatorProps) => (
|
||||
<span
|
||||
className={cx(
|
||||
"absolute right-0 bottom-0 rounded-full ring-[1.5px] ring-bg-primary",
|
||||
status === "online" ? "bg-fg-success-secondary" : "bg-fg-disabled_subtle",
|
||||
sizes[size],
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
4
src/components/base/avatar/base-components/index.tsx
Normal file
4
src/components/base/avatar/base-components/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./avatar-add-button";
|
||||
export * from "./avatar-company-icon";
|
||||
export * from "./avatar-online-indicator";
|
||||
export * from "./verified-tick";
|
||||
32
src/components/base/avatar/base-components/verified-tick.tsx
Normal file
32
src/components/base/avatar/base-components/verified-tick.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const sizes = {
|
||||
xs: { root: "size-2.5", tick: "size-[4.38px" },
|
||||
sm: { root: "size-3", tick: "size-[5.25px]" },
|
||||
md: { root: "size-3.5", tick: "size-[6.13px]" },
|
||||
lg: { root: "size-4", tick: "size-[7px]" },
|
||||
xl: { root: "size-4.5", tick: "size-[7.88px]" },
|
||||
"2xl": { root: "size-5", tick: "size-[8.75px]" },
|
||||
"3xl": { root: "size-6", tick: "size-[10.5px]" },
|
||||
"4xl": { root: "size-8", tick: "size-[14px]" },
|
||||
};
|
||||
|
||||
interface VerifiedTickProps {
|
||||
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const VerifiedTick = ({ size, className }: VerifiedTickProps) => (
|
||||
<svg data-verified className={cx("z-10 text-utility-blue-500", sizes[size].root, className)} viewBox="0 0 10 10" fill="none">
|
||||
<path
|
||||
d="M7.72237 1.77098C7.81734 2.00068 7.99965 2.18326 8.2292 2.27858L9.03413 2.61199C9.26384 2.70714 9.44635 2.88965 9.5415 3.11936C9.63665 3.34908 9.63665 3.60718 9.5415 3.83689L9.20833 4.64125C9.11313 4.87106 9.113 5.12943 9.20863 5.35913L9.54122 6.16325C9.58839 6.27702 9.61268 6.39897 9.6127 6.52214C9.61272 6.6453 9.58847 6.76726 9.54134 6.88105C9.4942 6.99484 9.42511 7.09823 9.33801 7.18531C9.2509 7.27238 9.14749 7.34144 9.03369 7.38854L8.22934 7.72171C7.99964 7.81669 7.81706 7.99899 7.72174 8.22855L7.38833 9.03348C7.29318 9.26319 7.11067 9.4457 6.88096 9.54085C6.65124 9.636 6.39314 9.636 6.16343 9.54085L5.35907 9.20767C5.12935 9.11276 4.87134 9.11295 4.64177 9.20821L3.83684 9.54115C3.60725 9.63608 3.34937 9.636 3.11984 9.54092C2.89032 9.44585 2.70791 9.26356 2.6127 9.03409L2.27918 8.22892C2.18421 7.99923 2.0019 7.81665 1.77235 7.72133L0.967421 7.38792C0.737807 7.29281 0.555355 7.11041 0.460169 6.88083C0.364983 6.65125 0.364854 6.39327 0.45981 6.16359L0.792984 5.35924C0.8879 5.12952 0.887707 4.87151 0.792445 4.64193L0.459749 3.83642C0.41258 3.72265 0.388291 3.60069 0.388272 3.47753C0.388252 3.35436 0.412501 3.2324 0.459634 3.11861C0.506767 3.00482 0.57586 2.90144 0.662965 2.81436C0.75007 2.72728 0.853479 2.65822 0.967283 2.61113L1.77164 2.27795C2.00113 2.18306 2.1836 2.00099 2.27899 1.7717L2.6124 0.966768C2.70755 0.737054 2.89006 0.554547 3.11978 0.459397C3.34949 0.364246 3.60759 0.364246 3.83731 0.459397L4.64166 0.792571C4.87138 0.887487 5.12939 0.887293 5.35897 0.792031L6.16424 0.459913C6.39392 0.364816 6.65197 0.364836 6.88164 0.459968C7.11131 0.555099 7.29379 0.737554 7.38895 0.967208L7.72247 1.77238L7.72237 1.77098Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.95829 3.68932C7.02509 3.58439 7.04747 3.45723 7.02051 3.3358C6.99356 3.21437 6.91946 3.10862 6.81454 3.04182C6.70961 2.97502 6.58245 2.95264 6.46102 2.97959C6.33959 3.00655 6.23384 3.08064 6.16704 3.18557L4.33141 6.06995L3.49141 5.01995C3.41375 4.92281 3.30069 4.8605 3.17709 4.84673C3.05349 4.83296 2.92949 4.86885 2.83235 4.94651C2.73522 5.02417 2.67291 5.13723 2.65914 5.26083C2.64536 5.38443 2.68125 5.50843 2.75891 5.60557L4.00891 7.16807C4.0555 7.22638 4.11533 7.27271 4.18344 7.30323C4.25154 7.33375 4.32595 7.34757 4.40047 7.34353C4.47499 7.3395 4.54747 7.31773 4.61188 7.28004C4.67629 7.24234 4.73077 7.18981 4.77079 7.12682L6.95829 3.68932Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
12
src/components/base/avatar/utils.ts
Normal file
12
src/components/base/avatar/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Extracts the initials from a full name.
|
||||
*
|
||||
* @param name - The full name from which to extract initials.
|
||||
* @returns The initials of the provided name. If the name contains only one word,
|
||||
* it returns the first character of that word. If the name contains two words,
|
||||
* it returns the first character of each word.
|
||||
*/
|
||||
export const getInitials = (name: string) => {
|
||||
const [firstName, lastName] = name.split(" ");
|
||||
return firstName.charAt(0) + (lastName ? lastName.charAt(0) : "");
|
||||
};
|
||||
174
src/components/base/badges/badge-groups.tsx
Normal file
174
src/components/base/badges/badge-groups.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { isValidElement } from "react";
|
||||
import { ArrowRight } from "@untitledui/icons";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
import { isReactComponent } from "@/utils/is-react-component";
|
||||
|
||||
type Size = "md" | "lg";
|
||||
type Color = "brand" | "warning" | "error" | "gray" | "success";
|
||||
type Theme = "light" | "modern";
|
||||
type Align = "leading" | "trailing";
|
||||
|
||||
const baseClasses: Record<Theme, { root?: string; addon?: string; icon?: string }> = {
|
||||
light: {
|
||||
root: "rounded-full ring-1 ring-inset",
|
||||
addon: "rounded-full ring-1 ring-inset",
|
||||
},
|
||||
modern: {
|
||||
root: "rounded-[10px] bg-primary text-secondary shadow-xs ring-1 ring-inset ring-primary hover:bg-secondary",
|
||||
addon: "flex items-center rounded-md bg-primary shadow-xs ring-1 ring-inset ring-primary",
|
||||
icon: "text-utility-gray-500",
|
||||
},
|
||||
};
|
||||
|
||||
const getSizeClasses = (
|
||||
theme?: Theme,
|
||||
text?: boolean,
|
||||
icon?: boolean,
|
||||
): Record<Align, Record<Size, { root?: string; addon?: string; icon?: string; dot?: string }>> => ({
|
||||
leading: {
|
||||
md: {
|
||||
root: cx("py-1 pr-2 pl-1 text-xs font-medium", !text && !icon && "pr-1"),
|
||||
addon: cx("px-2 py-0.5", theme === "modern" && "gap-1 px-1.5", text && "mr-2"),
|
||||
icon: "ml-1 size-4",
|
||||
},
|
||||
lg: {
|
||||
root: cx("py-1 pr-2 pl-1 text-sm font-medium", !text && !icon && "pr-1"),
|
||||
addon: cx("px-2.5 py-0.5", theme === "modern" && "gap-1.5 px-2", text && "mr-2"),
|
||||
icon: "ml-1 size-4",
|
||||
},
|
||||
},
|
||||
trailing: {
|
||||
md: {
|
||||
root: cx("py-1 pr-1 pl-3 text-xs font-medium", theme === "modern" && "pl-2.5"),
|
||||
addon: cx("py-0.5 pr-1.5 pl-2", theme === "modern" && "pr-1.5 pl-2", text && "ml-2"),
|
||||
icon: "ml-0.5 size-3 stroke-[3px]",
|
||||
dot: "mr-1.5",
|
||||
},
|
||||
lg: {
|
||||
root: "py-1 pr-1 pl-3 text-sm font-medium",
|
||||
addon: cx("py-0.5 pr-2 pl-2.5", theme === "modern" && "pr-1.5 pl-2", text && "ml-2"),
|
||||
icon: "ml-1 size-3 stroke-[3px]",
|
||||
dot: "mr-2",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const colorClasses: Record<Theme, Record<Color, { root?: string; addon?: string; icon?: string; dot?: string }>> = sortCx({
|
||||
light: {
|
||||
brand: {
|
||||
root: "bg-utility-brand-50 text-utility-brand-700 ring-utility-brand-200 hover:bg-utility-brand-100",
|
||||
addon: "bg-primary text-current ring-utility-brand-200",
|
||||
icon: "text-utility-brand-500",
|
||||
},
|
||||
gray: {
|
||||
root: "bg-utility-gray-50 text-utility-gray-700 ring-utility-gray-200 hover:bg-utility-gray-100",
|
||||
addon: "bg-primary text-current ring-utility-gray-200",
|
||||
icon: "text-utility-gray-500",
|
||||
},
|
||||
error: {
|
||||
root: "bg-utility-error-50 text-utility-error-700 ring-utility-error-200 hover:bg-utility-error-100",
|
||||
addon: "bg-primary text-current ring-utility-error-200",
|
||||
icon: "text-utility-error-500",
|
||||
},
|
||||
warning: {
|
||||
root: "bg-utility-warning-50 text-utility-warning-700 ring-utility-warning-200 hover:bg-utility-warning-100",
|
||||
addon: "bg-primary text-current ring-utility-warning-200",
|
||||
icon: "text-utility-warning-500",
|
||||
},
|
||||
success: {
|
||||
root: "bg-utility-success-50 text-utility-success-700 ring-utility-success-200 hover:bg-utility-success-100",
|
||||
addon: "bg-primary text-current ring-utility-success-200",
|
||||
icon: "text-utility-success-500",
|
||||
},
|
||||
},
|
||||
modern: {
|
||||
brand: {
|
||||
dot: "bg-utility-brand-500 outline-3 -outline-offset-1 outline-utility-brand-100",
|
||||
},
|
||||
gray: {
|
||||
dot: "bg-utility-gray-500 outline-3 -outline-offset-1 outline-utility-gray-100",
|
||||
},
|
||||
error: {
|
||||
dot: "bg-utility-error-500 outline-3 -outline-offset-1 outline-utility-error-100",
|
||||
},
|
||||
warning: {
|
||||
dot: "bg-utility-warning-500 outline-3 -outline-offset-1 outline-utility-warning-100",
|
||||
},
|
||||
success: {
|
||||
dot: "bg-utility-success-500 outline-3 -outline-offset-1 outline-utility-success-100",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface BadgeGroupProps {
|
||||
children?: string | ReactNode;
|
||||
addonText: string;
|
||||
size?: Size;
|
||||
color: Color;
|
||||
theme?: Theme;
|
||||
/**
|
||||
* Alignment of the badge addon element.
|
||||
*/
|
||||
align?: Align;
|
||||
iconTrailing?: FC<{ className?: string }> | ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const BadgeGroup = ({
|
||||
children,
|
||||
addonText,
|
||||
size = "md",
|
||||
color = "brand",
|
||||
theme = "light",
|
||||
align = "leading",
|
||||
className,
|
||||
iconTrailing: IconTrailing = ArrowRight,
|
||||
}: BadgeGroupProps) => {
|
||||
const colors = colorClasses[theme][color];
|
||||
const sizes = getSizeClasses(theme, !!children, !!IconTrailing)[align][size];
|
||||
|
||||
const rootClasses = cx(
|
||||
"inline-flex w-max cursor-pointer items-center transition duration-100 ease-linear",
|
||||
baseClasses[theme].root,
|
||||
sizes.root,
|
||||
colors.root,
|
||||
className,
|
||||
);
|
||||
const addonClasses = cx("inline-flex items-center", baseClasses[theme].addon, sizes.addon, colors.addon);
|
||||
const dotClasses = cx("inline-block size-2 shrink-0 rounded-full", sizes.dot, colors.dot);
|
||||
const iconClasses = cx(baseClasses[theme].icon, sizes.icon, colors.icon);
|
||||
|
||||
if (align === "trailing") {
|
||||
return (
|
||||
<div className={rootClasses}>
|
||||
{theme === "modern" && <span className={dotClasses} />}
|
||||
|
||||
{children}
|
||||
|
||||
<span className={addonClasses}>
|
||||
{addonText}
|
||||
|
||||
{/* Trailing icon */}
|
||||
{isReactComponent(IconTrailing) && <IconTrailing className={iconClasses} />}
|
||||
{isValidElement(IconTrailing) && IconTrailing}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={rootClasses}>
|
||||
<span className={addonClasses}>
|
||||
{theme === "modern" && <span className={dotClasses} />}
|
||||
{addonText}
|
||||
</span>
|
||||
|
||||
{children}
|
||||
|
||||
{/* Trailing icon */}
|
||||
{isReactComponent(IconTrailing) && <IconTrailing className={iconClasses} />}
|
||||
{isValidElement(IconTrailing) && IconTrailing}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
264
src/components/base/badges/badge-types.ts
Normal file
264
src/components/base/badges/badge-types.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
export type IconComponentType = React.FunctionComponent<{ className?: string; strokeWidth?: string | number }>;
|
||||
|
||||
export type Sizes = "sm" | "md" | "lg";
|
||||
|
||||
export type BadgeColors = "gray" | "brand" | "error" | "warning" | "success" | "gray-blue" | "blue-light" | "blue" | "indigo" | "purple" | "pink" | "orange";
|
||||
|
||||
export type FlagTypes =
|
||||
| "AD"
|
||||
| "AE"
|
||||
| "AF"
|
||||
| "AG"
|
||||
| "AI"
|
||||
| "AL"
|
||||
| "AM"
|
||||
| "AO"
|
||||
| "AR"
|
||||
| "AS"
|
||||
| "AT"
|
||||
| "AU"
|
||||
| "AW"
|
||||
| "AX"
|
||||
| "AZ"
|
||||
| "BA"
|
||||
| "BB"
|
||||
| "BD"
|
||||
| "BE"
|
||||
| "BF"
|
||||
| "BG"
|
||||
| "BH"
|
||||
| "BI"
|
||||
| "BJ"
|
||||
| "BL"
|
||||
| "BM"
|
||||
| "BN"
|
||||
| "BO"
|
||||
| "BQ-1"
|
||||
| "BQ-2"
|
||||
| "BQ"
|
||||
| "BR"
|
||||
| "BS"
|
||||
| "BT"
|
||||
| "BW"
|
||||
| "BY"
|
||||
| "BZ"
|
||||
| "CA"
|
||||
| "CC"
|
||||
| "CD-1"
|
||||
| "CD"
|
||||
| "CF"
|
||||
| "CH"
|
||||
| "CK"
|
||||
| "CL"
|
||||
| "CM"
|
||||
| "CN"
|
||||
| "CO"
|
||||
| "CR"
|
||||
| "CU"
|
||||
| "CW"
|
||||
| "CX"
|
||||
| "CY"
|
||||
| "CZ"
|
||||
| "DE"
|
||||
| "DJ"
|
||||
| "DK"
|
||||
| "DM"
|
||||
| "DO"
|
||||
| "DS"
|
||||
| "DZ"
|
||||
| "earth"
|
||||
| "EC"
|
||||
| "EE"
|
||||
| "EG"
|
||||
| "EH"
|
||||
| "ER"
|
||||
| "ES"
|
||||
| "ET"
|
||||
| "FI"
|
||||
| "FJ"
|
||||
| "FK"
|
||||
| "FM"
|
||||
| "FO"
|
||||
| "FR"
|
||||
| "GA"
|
||||
| "GB-2"
|
||||
| "GB"
|
||||
| "GD"
|
||||
| "GE"
|
||||
| "GG"
|
||||
| "GH"
|
||||
| "GI"
|
||||
| "GL"
|
||||
| "GM"
|
||||
| "GN"
|
||||
| "GQ"
|
||||
| "GR"
|
||||
| "GT"
|
||||
| "GU"
|
||||
| "GW"
|
||||
| "GY"
|
||||
| "HK"
|
||||
| "HN"
|
||||
| "HR"
|
||||
| "HT"
|
||||
| "HU"
|
||||
| "ID"
|
||||
| "IE"
|
||||
| "IL"
|
||||
| "IM"
|
||||
| "IN"
|
||||
| "IO"
|
||||
| "IQ"
|
||||
| "IR"
|
||||
| "IS"
|
||||
| "IT"
|
||||
| "JE"
|
||||
| "JM"
|
||||
| "JO"
|
||||
| "JP"
|
||||
| "KE"
|
||||
| "KG"
|
||||
| "KH"
|
||||
| "KI"
|
||||
| "KM"
|
||||
| "KN"
|
||||
| "KP"
|
||||
| "KR"
|
||||
| "KW"
|
||||
| "KY"
|
||||
| "KZ"
|
||||
| "LA"
|
||||
| "LB"
|
||||
| "LC"
|
||||
| "LI"
|
||||
| "LK"
|
||||
| "LR"
|
||||
| "LS"
|
||||
| "LT"
|
||||
| "LU"
|
||||
| "LV"
|
||||
| "LY"
|
||||
| "MA"
|
||||
| "MC"
|
||||
| "MD"
|
||||
| "ME"
|
||||
| "MG"
|
||||
| "MH"
|
||||
| "MK"
|
||||
| "ML"
|
||||
| "MM"
|
||||
| "MN"
|
||||
| "MO"
|
||||
| "MP"
|
||||
| "MQ"
|
||||
| "MR"
|
||||
| "MS"
|
||||
| "MT"
|
||||
| "MU"
|
||||
| "MV"
|
||||
| "MW"
|
||||
| "MX"
|
||||
| "MY"
|
||||
| "MZ"
|
||||
| "NA"
|
||||
| "NE"
|
||||
| "NF"
|
||||
| "NG"
|
||||
| "NI"
|
||||
| "NL"
|
||||
| "NO"
|
||||
| "NP"
|
||||
| "NR"
|
||||
| "NU"
|
||||
| "NZ"
|
||||
| "OM"
|
||||
| "PA"
|
||||
| "PE"
|
||||
| "PF"
|
||||
| "PG"
|
||||
| "PH"
|
||||
| "PK"
|
||||
| "PL"
|
||||
| "PM"
|
||||
| "PN"
|
||||
| "PR"
|
||||
| "PT"
|
||||
| "PW"
|
||||
| "PY"
|
||||
| "QA"
|
||||
| "RE"
|
||||
| "RO"
|
||||
| "RS"
|
||||
| "RU"
|
||||
| "RW"
|
||||
| "SA"
|
||||
| "SB"
|
||||
| "SC"
|
||||
| "SD"
|
||||
| "SE"
|
||||
| "SG"
|
||||
| "SH"
|
||||
| "SI"
|
||||
| "SJ"
|
||||
| "SK"
|
||||
| "SL"
|
||||
| "SM"
|
||||
| "SN"
|
||||
| "SO"
|
||||
| "SR"
|
||||
| "SS"
|
||||
| "ST"
|
||||
| "SV"
|
||||
| "SX"
|
||||
| "SY"
|
||||
| "SZ"
|
||||
| "TC"
|
||||
| "TD"
|
||||
| "TF"
|
||||
| "TG"
|
||||
| "TH"
|
||||
| "TJ"
|
||||
| "TK"
|
||||
| "TL"
|
||||
| "TM"
|
||||
| "TN"
|
||||
| "TO"
|
||||
| "TR"
|
||||
| "TT"
|
||||
| "TV"
|
||||
| "TZ"
|
||||
| "UA"
|
||||
| "UG"
|
||||
| "UM"
|
||||
| "US"
|
||||
| "UY"
|
||||
| "UZ"
|
||||
| "VA"
|
||||
| "VC"
|
||||
| "VE"
|
||||
| "VG"
|
||||
| "VI"
|
||||
| "VN"
|
||||
| "VU"
|
||||
| "WF"
|
||||
| "WS"
|
||||
| "YE"
|
||||
| "YT"
|
||||
| "ZA"
|
||||
| "ZM"
|
||||
| "ZW";
|
||||
|
||||
export type ExtractColorKeys<T> = T extends { styles: infer C } ? keyof C : never;
|
||||
export type ExtractBadgeKeys<T> = keyof T;
|
||||
export type BadgeTypeToColorMap<T> = {
|
||||
[K in ExtractBadgeKeys<T>]: ExtractColorKeys<T[K]>;
|
||||
};
|
||||
export type BadgeTypeColors<T> = ExtractColorKeys<T[keyof T]>;
|
||||
|
||||
export const badgeTypes = {
|
||||
pillColor: "pill-color",
|
||||
badgeColor: "color",
|
||||
badgeModern: "modern",
|
||||
} as const;
|
||||
|
||||
export type BadgeTypes = (typeof badgeTypes)[keyof typeof badgeTypes];
|
||||
415
src/components/base/badges/badges.tsx
Normal file
415
src/components/base/badges/badges.tsx
Normal file
@@ -0,0 +1,415 @@
|
||||
import type { MouseEventHandler, ReactNode } from "react";
|
||||
import { X as CloseX } from "@untitledui/icons";
|
||||
import { Dot } from "@/components/foundations/dot-icon";
|
||||
import { cx } from "@/utils/cx";
|
||||
import type { BadgeColors, BadgeTypeToColorMap, BadgeTypes, FlagTypes, IconComponentType, Sizes } from "./badge-types";
|
||||
import { badgeTypes } from "./badge-types";
|
||||
|
||||
export const filledColors: Record<BadgeColors, { root: string; addon: string; addonButton: string }> = {
|
||||
gray: {
|
||||
root: "bg-utility-gray-50 text-utility-gray-700 ring-utility-gray-200",
|
||||
addon: "text-utility-gray-500",
|
||||
addonButton: "hover:bg-utility-gray-100 text-utility-gray-400 hover:text-utility-gray-500",
|
||||
},
|
||||
brand: {
|
||||
root: "bg-utility-brand-50 text-utility-brand-700 ring-utility-brand-200",
|
||||
addon: "text-utility-brand-500",
|
||||
addonButton: "hover:bg-utility-brand-100 text-utility-brand-400 hover:text-utility-brand-500",
|
||||
},
|
||||
error: {
|
||||
root: "bg-utility-error-50 text-utility-error-700 ring-utility-error-200",
|
||||
addon: "text-utility-error-500",
|
||||
addonButton: "hover:bg-utility-error-100 text-utility-error-400 hover:text-utility-error-500",
|
||||
},
|
||||
warning: {
|
||||
root: "bg-utility-warning-50 text-utility-warning-700 ring-utility-warning-200",
|
||||
addon: "text-utility-warning-500",
|
||||
addonButton: "hover:bg-utility-warning-100 text-utility-warning-400 hover:text-utility-warning-500",
|
||||
},
|
||||
success: {
|
||||
root: "bg-utility-success-50 text-utility-success-700 ring-utility-success-200",
|
||||
addon: "text-utility-success-500",
|
||||
addonButton: "hover:bg-utility-success-100 text-utility-success-400 hover:text-utility-success-500",
|
||||
},
|
||||
"gray-blue": {
|
||||
root: "bg-utility-gray-blue-50 text-utility-gray-blue-700 ring-utility-gray-blue-200",
|
||||
addon: "text-utility-gray-blue-500",
|
||||
addonButton: "hover:bg-utility-gray-blue-100 text-utility-gray-blue-400 hover:text-utility-gray-blue-500",
|
||||
},
|
||||
"blue-light": {
|
||||
root: "bg-utility-blue-light-50 text-utility-blue-light-700 ring-utility-blue-light-200",
|
||||
addon: "text-utility-blue-light-500",
|
||||
addonButton: "hover:bg-utility-blue-light-100 text-utility-blue-light-400 hover:text-utility-blue-light-500",
|
||||
},
|
||||
blue: {
|
||||
root: "bg-utility-blue-50 text-utility-blue-700 ring-utility-blue-200",
|
||||
addon: "text-utility-blue-500",
|
||||
addonButton: "hover:bg-utility-blue-100 text-utility-blue-400 hover:text-utility-blue-500",
|
||||
},
|
||||
indigo: {
|
||||
root: "bg-utility-indigo-50 text-utility-indigo-700 ring-utility-indigo-200",
|
||||
addon: "text-utility-indigo-500",
|
||||
addonButton: "hover:bg-utility-indigo-100 text-utility-indigo-400 hover:text-utility-indigo-500",
|
||||
},
|
||||
purple: {
|
||||
root: "bg-utility-purple-50 text-utility-purple-700 ring-utility-purple-200",
|
||||
addon: "text-utility-purple-500",
|
||||
addonButton: "hover:bg-utility-purple-100 text-utility-purple-400 hover:text-utility-purple-500",
|
||||
},
|
||||
pink: {
|
||||
root: "bg-utility-pink-50 text-utility-pink-700 ring-utility-pink-200",
|
||||
addon: "text-utility-pink-500",
|
||||
addonButton: "hover:bg-utility-pink-100 text-utility-pink-400 hover:text-utility-pink-500",
|
||||
},
|
||||
orange: {
|
||||
root: "bg-utility-orange-50 text-utility-orange-700 ring-utility-orange-200",
|
||||
addon: "text-utility-orange-500",
|
||||
addonButton: "hover:bg-utility-orange-100 text-utility-orange-400 hover:text-utility-orange-500",
|
||||
},
|
||||
};
|
||||
|
||||
const addonOnlyColors = Object.fromEntries(Object.entries(filledColors).map(([key, value]) => [key, { root: "", addon: value.addon }])) as Record<
|
||||
BadgeColors,
|
||||
{ root: string; addon: string }
|
||||
>;
|
||||
|
||||
const withPillTypes = {
|
||||
[badgeTypes.pillColor]: {
|
||||
common: "size-max flex items-center whitespace-nowrap rounded-full ring-1 ring-inset",
|
||||
styles: filledColors,
|
||||
},
|
||||
[badgeTypes.badgeColor]: {
|
||||
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset",
|
||||
styles: filledColors,
|
||||
},
|
||||
[badgeTypes.badgeModern]: {
|
||||
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset shadow-xs",
|
||||
styles: {
|
||||
gray: {
|
||||
root: "bg-primary text-secondary ring-primary",
|
||||
addon: "text-gray-500",
|
||||
addonButton: "hover:bg-utility-gray-100 text-utility-gray-400 hover:text-utility-gray-500",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const withBadgeTypes = {
|
||||
[badgeTypes.pillColor]: {
|
||||
common: "size-max flex items-center whitespace-nowrap rounded-full ring-1 ring-inset",
|
||||
styles: filledColors,
|
||||
},
|
||||
[badgeTypes.badgeColor]: {
|
||||
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset",
|
||||
styles: filledColors,
|
||||
},
|
||||
[badgeTypes.badgeModern]: {
|
||||
common: "size-max flex items-center whitespace-nowrap rounded-md ring-1 ring-inset bg-primary text-secondary ring-primary shadow-xs",
|
||||
styles: addonOnlyColors,
|
||||
},
|
||||
};
|
||||
|
||||
export type BadgeColor<T extends BadgeTypes> = BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||
|
||||
interface BadgeProps<T extends BadgeTypes> {
|
||||
type?: T;
|
||||
size?: Sizes;
|
||||
color?: BadgeColor<T>;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Badge = <T extends BadgeTypes>(props: BadgeProps<T>) => {
|
||||
const { type = "pill-color", size = "md", color = "gray", children } = props;
|
||||
const colors = withPillTypes[type];
|
||||
|
||||
const pillSizes = {
|
||||
sm: "py-0.5 px-2 text-xs font-medium",
|
||||
md: "py-0.5 px-2.5 text-sm font-medium",
|
||||
lg: "py-1 px-3 text-sm font-medium",
|
||||
};
|
||||
const badgeSizes = {
|
||||
sm: "py-0.5 px-1.5 text-xs font-medium",
|
||||
md: "py-0.5 px-2 text-sm font-medium",
|
||||
lg: "py-1 px-2.5 text-sm font-medium rounded-lg",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
[badgeTypes.pillColor]: pillSizes,
|
||||
[badgeTypes.badgeColor]: badgeSizes,
|
||||
[badgeTypes.badgeModern]: badgeSizes,
|
||||
};
|
||||
|
||||
return <span className={cx(colors.common, sizes[type][size], colors.styles[color].root, props.className)}>{children}</span>;
|
||||
};
|
||||
|
||||
interface BadgeWithDotProps<T extends BadgeTypes> {
|
||||
type?: T;
|
||||
size?: Sizes;
|
||||
color?: BadgeTypeToColorMap<typeof withBadgeTypes>[T];
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const BadgeWithDot = <T extends BadgeTypes>(props: BadgeWithDotProps<T>) => {
|
||||
const { size = "md", color = "gray", type = "pill-color", className, children } = props;
|
||||
|
||||
const colors = withBadgeTypes[type];
|
||||
|
||||
const pillSizes = {
|
||||
sm: "gap-1 py-0.5 pl-1.5 pr-2 text-xs font-medium",
|
||||
md: "gap-1.5 py-0.5 pl-2 pr-2.5 text-sm font-medium",
|
||||
lg: "gap-1.5 py-1 pl-2.5 pr-3 text-sm font-medium",
|
||||
};
|
||||
|
||||
const badgeSizes = {
|
||||
sm: "gap-1 py-0.5 px-1.5 text-xs font-medium",
|
||||
md: "gap-1.5 py-0.5 px-2 text-sm font-medium",
|
||||
lg: "gap-1.5 py-1 px-2.5 text-sm font-medium rounded-lg",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
[badgeTypes.pillColor]: pillSizes,
|
||||
[badgeTypes.badgeColor]: badgeSizes,
|
||||
[badgeTypes.badgeModern]: badgeSizes,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root, className)}>
|
||||
<Dot className={colors.styles[color].addon} size="sm" />
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface BadgeWithIconProps<T extends BadgeTypes> {
|
||||
type?: T;
|
||||
size?: Sizes;
|
||||
color?: BadgeTypeToColorMap<typeof withBadgeTypes>[T];
|
||||
iconLeading?: IconComponentType;
|
||||
iconTrailing?: IconComponentType;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const BadgeWithIcon = <T extends BadgeTypes>(props: BadgeWithIconProps<T>) => {
|
||||
const { size = "md", color = "gray", type = "pill-color", iconLeading: IconLeading, iconTrailing: IconTrailing, children, className } = props;
|
||||
|
||||
const colors = withBadgeTypes[type];
|
||||
|
||||
const icon = IconLeading ? "leading" : "trailing";
|
||||
|
||||
const pillSizes = {
|
||||
sm: {
|
||||
trailing: "gap-0.5 py-0.5 pl-2 pr-1.5 text-xs font-medium",
|
||||
leading: "gap-0.5 py-0.5 pr-2 pl-1.5 text-xs font-medium",
|
||||
},
|
||||
md: {
|
||||
trailing: "gap-1 py-0.5 pl-2.5 pr-2 text-sm font-medium",
|
||||
leading: "gap-1 py-0.5 pr-2.5 pl-2 text-sm font-medium",
|
||||
},
|
||||
lg: {
|
||||
trailing: "gap-1 py-1 pl-3 pr-2.5 text-sm font-medium",
|
||||
leading: "gap-1 py-1 pr-3 pl-2.5 text-sm font-medium",
|
||||
},
|
||||
};
|
||||
const badgeSizes = {
|
||||
sm: {
|
||||
trailing: "gap-0.5 py-0.5 pl-2 pr-1.5 text-xs font-medium",
|
||||
leading: "gap-0.5 py-0.5 pr-2 pl-1.5 text-xs font-medium",
|
||||
},
|
||||
md: {
|
||||
trailing: "gap-1 py-0.5 pl-2 pr-1.5 text-sm font-medium",
|
||||
leading: "gap-1 py-0.5 pr-2 pl-1.5 text-sm font-medium",
|
||||
},
|
||||
lg: {
|
||||
trailing: "gap-1 py-1 pl-2.5 pr-2 text-sm font-medium rounded-lg",
|
||||
leading: "gap-1 py-1 pr-2.5 pl-2 text-sm font-medium rounded-lg",
|
||||
},
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
[badgeTypes.pillColor]: pillSizes,
|
||||
[badgeTypes.badgeColor]: badgeSizes,
|
||||
[badgeTypes.badgeModern]: badgeSizes,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={cx(colors.common, sizes[type][size][icon], colors.styles[color].root, className)}>
|
||||
{IconLeading && <IconLeading className={cx(colors.styles[color].addon, "size-3 stroke-3")} />}
|
||||
{children}
|
||||
{IconTrailing && <IconTrailing className={cx(colors.styles[color].addon, "size-3 stroke-3")} />}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface BadgeWithFlagProps<T extends BadgeTypes> {
|
||||
type?: T;
|
||||
size?: Sizes;
|
||||
flag?: FlagTypes;
|
||||
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const BadgeWithFlag = <T extends BadgeTypes>(props: BadgeWithFlagProps<T>) => {
|
||||
const { size = "md", color = "gray", flag = "AU", type = "pill-color", children } = props;
|
||||
|
||||
const colors = withPillTypes[type];
|
||||
|
||||
const pillSizes = {
|
||||
sm: "gap-1 py-0.5 pl-0.75 pr-2 text-xs font-medium",
|
||||
md: "gap-1.5 py-0.5 pl-1 pr-2.5 text-sm font-medium",
|
||||
lg: "gap-1.5 py-1 pl-1.5 pr-3 text-sm font-medium",
|
||||
};
|
||||
const badgeSizes = {
|
||||
sm: "gap-1 py-0.5 pl-1 pr-1.5 text-xs font-medium",
|
||||
md: "gap-1.5 py-0.5 pl-1.5 pr-2 text-sm font-medium",
|
||||
lg: "gap-1.5 py-1 pl-2 pr-2.5 text-sm font-medium rounded-lg",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
[badgeTypes.pillColor]: pillSizes,
|
||||
[badgeTypes.badgeColor]: badgeSizes,
|
||||
[badgeTypes.badgeModern]: badgeSizes,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
||||
<img src={`https://www.untitledui.com/images/flags/${flag}.svg`} className="size-4 max-w-none rounded-full" alt={`${flag} flag`} />
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface BadgeWithImageProps<T extends BadgeTypes> {
|
||||
type?: T;
|
||||
size?: Sizes;
|
||||
imgSrc: string;
|
||||
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const BadgeWithImage = <T extends BadgeTypes>(props: BadgeWithImageProps<T>) => {
|
||||
const { size = "md", color = "gray", type = "pill-color", imgSrc, children } = props;
|
||||
|
||||
const colors = withPillTypes[type];
|
||||
|
||||
const pillSizes = {
|
||||
sm: "gap-1 py-0.5 pl-0.75 pr-2 text-xs font-medium",
|
||||
md: "gap-1.5 py-0.5 pl-1 pr-2.5 text-sm font-medium",
|
||||
lg: "gap-1.5 py-1 pl-1.5 pr-3 text-sm font-medium",
|
||||
};
|
||||
const badgeSizes = {
|
||||
sm: "gap-1 py-0.5 pl-1 pr-1.5 text-xs font-medium",
|
||||
md: "gap-1.5 py-0.5 pl-1.5 pr-2 text-sm font-medium",
|
||||
lg: "gap-1.5 py-1 pl-2 pr-2.5 text-sm font-medium rounded-lg",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
[badgeTypes.pillColor]: pillSizes,
|
||||
[badgeTypes.badgeColor]: badgeSizes,
|
||||
[badgeTypes.badgeModern]: badgeSizes,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
||||
<img src={imgSrc} className="size-4 max-w-none rounded-full" alt="Badge image" />
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface BadgeWithButtonProps<T extends BadgeTypes> {
|
||||
type?: T;
|
||||
size?: Sizes;
|
||||
icon?: IconComponentType;
|
||||
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||
children: ReactNode;
|
||||
/**
|
||||
* The label for the button.
|
||||
*/
|
||||
buttonLabel?: string;
|
||||
/**
|
||||
* The click event handler for the button.
|
||||
*/
|
||||
onButtonClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export const BadgeWithButton = <T extends BadgeTypes>(props: BadgeWithButtonProps<T>) => {
|
||||
const { size = "md", color = "gray", type = "pill-color", icon: Icon = CloseX, buttonLabel, children } = props;
|
||||
|
||||
const colors = withPillTypes[type];
|
||||
|
||||
const pillSizes = {
|
||||
sm: "gap-0.5 py-0.5 pl-2 pr-0.75 text-xs font-medium",
|
||||
md: "gap-0.5 py-0.5 pl-2.5 pr-1 text-sm font-medium",
|
||||
lg: "gap-0.5 py-1 pl-3 pr-1.5 text-sm font-medium",
|
||||
};
|
||||
const badgeSizes = {
|
||||
sm: "gap-0.5 py-0.5 pl-1.5 pr-0.75 text-xs font-medium",
|
||||
md: "gap-0.5 py-0.5 pl-2 pr-1 text-sm font-medium",
|
||||
lg: "gap-0.5 py-1 pl-2.5 pr-1.5 text-sm font-medium rounded-lg",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
[badgeTypes.pillColor]: pillSizes,
|
||||
[badgeTypes.badgeColor]: badgeSizes,
|
||||
[badgeTypes.badgeModern]: badgeSizes,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
||||
{children}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={buttonLabel}
|
||||
onClick={props.onButtonClick}
|
||||
className={cx(
|
||||
"flex cursor-pointer items-center justify-center p-0.5 outline-focus-ring transition duration-100 ease-linear focus-visible:outline-2",
|
||||
colors.styles[color].addonButton,
|
||||
type === "pill-color" ? "rounded-full" : "rounded-[3px]",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3 stroke-[3px] transition-inherit-all" />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface BadgeIconProps<T extends BadgeTypes> {
|
||||
type?: T;
|
||||
size?: Sizes;
|
||||
icon: IconComponentType;
|
||||
color?: BadgeTypeToColorMap<typeof withPillTypes>[T];
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const BadgeIcon = <T extends BadgeTypes>(props: BadgeIconProps<T>) => {
|
||||
const { size = "md", color = "gray", type = "pill-color", icon: Icon } = props;
|
||||
|
||||
const colors = withPillTypes[type];
|
||||
|
||||
const pillSizes = {
|
||||
sm: "p-1.25",
|
||||
md: "p-1.5",
|
||||
lg: "p-2",
|
||||
};
|
||||
|
||||
const badgeSizes = {
|
||||
sm: "p-1.25",
|
||||
md: "p-1.5",
|
||||
lg: "p-2 rounded-lg",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
[badgeTypes.pillColor]: pillSizes,
|
||||
[badgeTypes.badgeColor]: badgeSizes,
|
||||
[badgeTypes.badgeModern]: badgeSizes,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={cx(colors.common, sizes[type][size], colors.styles[color].root)}>
|
||||
<Icon className={cx("size-3 stroke-[3px]", colors.styles[color].addon)} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
104
src/components/base/button-group/button-group.tsx
Normal file
104
src/components/base/button-group/button-group.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { type FC, type PropsWithChildren, type ReactNode, type RefAttributes, createContext, isValidElement, useContext } from "react";
|
||||
import {
|
||||
ToggleButton as AriaToggleButton,
|
||||
ToggleButtonGroup as AriaToggleButtonGroup,
|
||||
type ToggleButtonGroupProps,
|
||||
type ToggleButtonProps,
|
||||
} from "react-aria-components";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
import { isReactComponent } from "@/utils/is-react-component";
|
||||
|
||||
export const styles = sortCx({
|
||||
common: {
|
||||
root: [
|
||||
"group/button-group inline-flex h-max cursor-pointer items-center bg-primary font-semibold whitespace-nowrap text-secondary shadow-skeumorphic ring-1 ring-primary outline-brand transition duration-100 ease-linear ring-inset",
|
||||
// Hover and focus styles
|
||||
"hover:bg-primary_hover hover:text-secondary_hover focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
// Disabled styles
|
||||
"disabled:cursor-not-allowed disabled:bg-primary disabled:text-disabled",
|
||||
// Selected styles
|
||||
"selected:bg-active selected:text-secondary_hover selected:disabled:bg-disabled_subtle",
|
||||
].join(" "),
|
||||
icon: "pointer-events-none text-fg-quaternary transition-[inherit] group-hover/button-group:text-fg-quaternary_hover group-disabled/button-group:text-fg-disabled_subtle",
|
||||
},
|
||||
|
||||
sizes: {
|
||||
sm: {
|
||||
root: "gap-1.5 px-3.5 py-2 text-sm not-last:pr-[calc(calc(var(--spacing)*3.5)+1px)] first:rounded-l-lg last:rounded-r-lg data-icon-leading:pl-3 data-icon-only:p-2",
|
||||
icon: "size-5",
|
||||
},
|
||||
md: {
|
||||
root: "gap-1.5 px-4 py-2.5 text-sm not-last:pr-[calc(calc(var(--spacing)*4)+1px)] first:rounded-l-lg last:rounded-r-lg data-icon-leading:pl-3.5 data-icon-only:px-3",
|
||||
icon: "size-5",
|
||||
},
|
||||
lg: {
|
||||
root: "gap-2 px-4.5 py-2.5 text-md not-last:pr-[calc(calc(var(--spacing)*4.5)+1px)] first:rounded-l-lg last:rounded-r-lg data-icon-leading:pl-4 data-icon-only:p-3",
|
||||
icon: "size-5",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type ButtonSize = keyof typeof styles.sizes;
|
||||
|
||||
const ButtonGroupContext = createContext<{ size: ButtonSize }>({ size: "md" });
|
||||
|
||||
interface ButtonGroupItemProps extends ToggleButtonProps, RefAttributes<HTMLButtonElement> {
|
||||
iconLeading?: FC<{ className?: string }> | ReactNode;
|
||||
iconTrailing?: FC<{ className?: string }> | ReactNode;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ButtonGroupItem = ({
|
||||
iconLeading: IconLeading,
|
||||
iconTrailing: IconTrailing,
|
||||
children,
|
||||
className,
|
||||
...otherProps
|
||||
}: PropsWithChildren<ButtonGroupItemProps>) => {
|
||||
const context = useContext(ButtonGroupContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("ButtonGroupItem must be used within a ButtonGroup component");
|
||||
}
|
||||
|
||||
const { size } = context;
|
||||
|
||||
const isIcon = (IconLeading || IconTrailing) && !children;
|
||||
|
||||
return (
|
||||
<AriaToggleButton
|
||||
{...otherProps}
|
||||
data-icon-only={isIcon ? true : undefined}
|
||||
data-icon-leading={IconLeading ? true : undefined}
|
||||
className={cx(styles.common.root, styles.sizes[size].root, className)}
|
||||
>
|
||||
{isReactComponent(IconLeading) && <IconLeading className={cx(styles.common.icon, styles.sizes[size].icon)} />}
|
||||
{isValidElement(IconLeading) && IconLeading}
|
||||
|
||||
{children}
|
||||
|
||||
{isReactComponent(IconTrailing) && <IconTrailing className={cx(styles.common.icon, styles.sizes[size].icon)} />}
|
||||
{isValidElement(IconTrailing) && IconTrailing}
|
||||
</AriaToggleButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface ButtonGroupProps extends Omit<ToggleButtonGroupProps, "orientation">, RefAttributes<HTMLDivElement> {
|
||||
size?: ButtonSize;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ButtonGroup = ({ children, size = "md", className, ...otherProps }: ButtonGroupProps) => {
|
||||
return (
|
||||
<ButtonGroupContext.Provider value={{ size }}>
|
||||
<AriaToggleButtonGroup
|
||||
selectionMode="single"
|
||||
className={cx("relative z-0 inline-flex w-max -space-x-px rounded-lg shadow-xs", className)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</AriaToggleButtonGroup>
|
||||
</ButtonGroupContext.Provider>
|
||||
);
|
||||
};
|
||||
376
src/components/base/buttons/app-store-buttons-outline.tsx
Normal file
376
src/components/base/buttons/app-store-buttons-outline.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
import type { AnchorHTMLAttributes } from "react";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export const GooglePlayButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||
return (
|
||||
<a
|
||||
aria-label="Get it on Google Play"
|
||||
href="#"
|
||||
{...props}
|
||||
className={cx(
|
||||
"rounded-[7px] text-fg-primary ring-1 ring-fg-primary outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<svg width={size === "md" ? 135 : 149} height={size === "md" ? 40 : 44} viewBox="0 0 135 40" fill="none">
|
||||
<path
|
||||
d="M68.136 21.7511C65.784 21.7511 63.867 23.5401 63.867 26.0041C63.867 28.4531 65.784 30.2571 68.136 30.2571C70.489 30.2571 72.406 28.4531 72.406 26.0041C72.405 23.5401 70.488 21.7511 68.136 21.7511ZM68.136 28.5831C66.847 28.5831 65.736 27.5201 65.736 26.0051C65.736 24.4741 66.848 23.4271 68.136 23.4271C69.425 23.4271 70.536 24.4741 70.536 26.0051C70.536 27.5191 69.425 28.5831 68.136 28.5831ZM58.822 21.7511C56.47 21.7511 54.553 23.5401 54.553 26.0041C54.553 28.4531 56.47 30.2571 58.822 30.2571C61.175 30.2571 63.092 28.4531 63.092 26.0041C63.092 23.5401 61.175 21.7511 58.822 21.7511ZM58.822 28.5831C57.533 28.5831 56.422 27.5201 56.422 26.0051C56.422 24.4741 57.534 23.4271 58.822 23.4271C60.111 23.4271 61.222 24.4741 61.222 26.0051C61.223 27.5191 60.111 28.5831 58.822 28.5831ZM47.744 23.0571V24.8611H52.062C51.933 25.8761 51.595 26.6171 51.079 27.1321C50.451 27.7601 49.468 28.4531 47.744 28.4531C45.086 28.4531 43.008 26.3101 43.008 23.6521C43.008 20.9941 45.086 18.8511 47.744 18.8511C49.178 18.8511 50.225 19.4151 50.998 20.1401L52.271 18.8671C51.191 17.8361 49.758 17.0471 47.744 17.0471C44.103 17.0471 41.042 20.0111 41.042 23.6521C41.042 27.2931 44.103 30.2571 47.744 30.2571C49.709 30.2571 51.192 29.6121 52.351 28.4041C53.543 27.2121 53.914 25.5361 53.914 24.1831C53.914 23.7651 53.882 23.3781 53.817 23.0561H47.744V23.0571ZM93.052 24.4581C92.698 23.5081 91.618 21.7511 89.411 21.7511C87.22 21.7511 85.399 23.4751 85.399 26.0041C85.399 28.3881 87.204 30.2571 89.62 30.2571C91.569 30.2571 92.697 29.0651 93.165 28.3721L91.715 27.4051C91.232 28.1141 90.571 28.5811 89.62 28.5811C88.67 28.5811 87.993 28.1461 87.558 27.2921L93.245 24.9401L93.052 24.4581ZM87.252 25.8761C87.204 24.2321 88.525 23.3951 89.476 23.3951C90.217 23.3951 90.845 23.7661 91.055 24.2971L87.252 25.8761ZM82.629 30.0001H84.497V17.4991H82.629V30.0001ZM79.567 22.7021H79.503C79.084 22.2021 78.278 21.7511 77.264 21.7511C75.137 21.7511 73.188 23.6201 73.188 26.0211C73.188 28.4051 75.137 30.2581 77.264 30.2581C78.279 30.2581 79.084 29.8071 79.503 29.2921H79.567V29.9041C79.567 31.5311 78.697 32.4011 77.296 32.4011C76.152 32.4011 75.443 31.5801 75.153 30.8871L73.526 31.5641C73.993 32.6911 75.233 34.0771 77.296 34.0771C79.487 34.0771 81.34 32.7881 81.34 29.6461V22.0101H79.568V22.7021H79.567ZM77.425 28.5831C76.136 28.5831 75.057 27.5031 75.057 26.0211C75.057 24.5221 76.136 23.4271 77.425 23.4271C78.697 23.4271 79.696 24.5221 79.696 26.0211C79.696 27.5031 78.697 28.5831 77.425 28.5831ZM101.806 17.4991H97.335V30.0001H99.2V25.2641H101.805C103.873 25.2641 105.907 23.7671 105.907 21.3821C105.907 18.9971 103.874 17.4991 101.806 17.4991ZM101.854 23.5241H99.2V19.2391H101.854C103.249 19.2391 104.041 20.3941 104.041 21.3821C104.041 22.3501 103.249 23.5241 101.854 23.5241ZM113.386 21.7291C112.035 21.7291 110.636 22.3241 110.057 23.6431L111.713 24.3341C112.067 23.6431 112.727 23.4171 113.418 23.4171C114.383 23.4171 115.364 23.9961 115.38 25.0251V25.1541C115.042 24.9611 114.318 24.6721 113.434 24.6721C111.649 24.6721 109.831 25.6531 109.831 27.4861C109.831 29.1591 111.295 30.2361 112.935 30.2361C114.189 30.2361 114.881 29.6731 115.315 29.0131H115.379V29.9781H117.181V25.1851C117.182 22.9671 115.524 21.7291 113.386 21.7291ZM113.16 28.5801C112.55 28.5801 111.697 28.2741 111.697 27.5181C111.697 26.5531 112.759 26.1831 113.676 26.1831C114.495 26.1831 114.882 26.3601 115.38 26.6011C115.235 27.7601 114.238 28.5801 113.16 28.5801ZM123.743 22.0021L121.604 27.4221H121.54L119.32 22.0021H117.31L120.639 29.5771L118.741 33.7911H120.687L125.818 22.0021H123.743ZM106.937 30.0001H108.802V17.4991H106.937V30.0001Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M47.418 10.2429C47.418 11.0809 47.1701 11.7479 46.673 12.2459C46.109 12.8379 45.3731 13.1339 44.4691 13.1339C43.6031 13.1339 42.8661 12.8339 42.2611 12.2339C41.6551 11.6329 41.3521 10.8889 41.3521 10.0009C41.3521 9.11194 41.6551 8.36794 42.2611 7.76794C42.8661 7.16694 43.6031 6.86694 44.4691 6.86694C44.8991 6.86694 45.3101 6.95094 45.7001 7.11794C46.0911 7.28594 46.404 7.50894 46.6381 7.78794L46.111 8.31594C45.714 7.84094 45.167 7.60394 44.468 7.60394C43.836 7.60394 43.29 7.82594 42.829 8.26994C42.368 8.71394 42.1381 9.29094 42.1381 9.99994C42.1381 10.7089 42.368 11.2859 42.829 11.7299C43.29 12.1739 43.836 12.3959 44.468 12.3959C45.138 12.3959 45.6971 12.1729 46.1441 11.7259C46.4341 11.4349 46.602 11.0299 46.647 10.5109H44.468V9.78994H47.375C47.405 9.94694 47.418 10.0979 47.418 10.2429Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path d="M52.0281 7.737H49.2961V9.639H51.7601V10.36H49.2961V12.262H52.0281V13H48.5251V7H52.0281V7.737Z" className="fill-current" />
|
||||
<path d="M55.279 13H54.508V7.737H52.832V7H56.955V7.737H55.279V13Z" className="fill-current" />
|
||||
<path d="M59.938 13V7H60.709V13H59.938Z" className="fill-current" />
|
||||
<path d="M64.1281 13H63.3572V7.737H61.6812V7H65.8042V7.737H64.1281V13Z" className="fill-current" />
|
||||
<path
|
||||
d="M73.6089 12.225C73.0189 12.831 72.2859 13.134 71.4089 13.134C70.5319 13.134 69.7989 12.831 69.2099 12.225C68.6199 11.619 68.3259 10.877 68.3259 9.99999C68.3259 9.12299 68.6199 8.38099 69.2099 7.77499C69.7989 7.16899 70.5319 6.86499 71.4089 6.86499C72.2809 6.86499 73.0129 7.16999 73.6049 7.77899C74.1969 8.38799 74.4929 9.12799 74.4929 9.99999C74.4929 10.877 74.1979 11.619 73.6089 12.225ZM69.7789 11.722C70.2229 12.172 70.7659 12.396 71.4089 12.396C72.0519 12.396 72.5959 12.171 73.0389 11.722C73.4829 11.272 73.7059 10.698 73.7059 9.99999C73.7059 9.30199 73.4829 8.72799 73.0389 8.27799C72.5959 7.82799 72.0519 7.60399 71.4089 7.60399C70.7659 7.60399 70.2229 7.82899 69.7789 8.27799C69.3359 8.72799 69.1129 9.30199 69.1129 9.99999C69.1129 10.698 69.3359 11.272 69.7789 11.722Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M75.5749 13V7H76.513L79.429 11.667H79.4619L79.429 10.511V7H80.1999V13H79.3949L76.344 8.106H76.3109L76.344 9.262V13H75.5749Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M47.418 10.2429C47.418 11.0809 47.1701 11.7479 46.673 12.2459C46.109 12.8379 45.3731 13.1339 44.4691 13.1339C43.6031 13.1339 42.8661 12.8339 42.2611 12.2339C41.6551 11.6329 41.3521 10.8889 41.3521 10.0009C41.3521 9.11194 41.6551 8.36794 42.2611 7.76794C42.8661 7.16694 43.6031 6.86694 44.4691 6.86694C44.8991 6.86694 45.3101 6.95094 45.7001 7.11794C46.0911 7.28594 46.404 7.50894 46.6381 7.78794L46.111 8.31594C45.714 7.84094 45.167 7.60394 44.468 7.60394C43.836 7.60394 43.29 7.82594 42.829 8.26994C42.368 8.71394 42.1381 9.29094 42.1381 9.99994C42.1381 10.7089 42.368 11.2859 42.829 11.7299C43.29 12.1739 43.836 12.3959 44.468 12.3959C45.138 12.3959 45.6971 12.1729 46.1441 11.7259C46.4341 11.4349 46.602 11.0299 46.647 10.5109H44.468V9.78994H47.375C47.405 9.94694 47.418 10.0979 47.418 10.2429Z"
|
||||
className="stroke-current"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M52.0281 7.737H49.2961V9.639H51.7601V10.36H49.2961V12.262H52.0281V13H48.5251V7H52.0281V7.737Z"
|
||||
className="stroke-current"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path d="M55.279 13H54.508V7.737H52.832V7H56.955V7.737H55.279V13Z" className="stroke-current" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||
<path d="M59.938 13V7H60.709V13H59.938Z" className="stroke-current" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||
<path d="M64.1281 13H63.3572V7.737H61.6812V7H65.8042V7.737H64.1281V13Z" className="stroke-current" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||
<path
|
||||
d="M73.6089 12.225C73.0189 12.831 72.2859 13.134 71.4089 13.134C70.5319 13.134 69.7989 12.831 69.2099 12.225C68.6199 11.619 68.3259 10.877 68.3259 9.99999C68.3259 9.12299 68.6199 8.38099 69.2099 7.77499C69.7989 7.16899 70.5319 6.86499 71.4089 6.86499C72.2809 6.86499 73.0129 7.16999 73.6049 7.77899C74.1969 8.38799 74.4929 9.12799 74.4929 9.99999C74.4929 10.877 74.1979 11.619 73.6089 12.225ZM69.7789 11.722C70.2229 12.172 70.7659 12.396 71.4089 12.396C72.0519 12.396 72.5959 12.171 73.0389 11.722C73.4829 11.272 73.7059 10.698 73.7059 9.99999C73.7059 9.30199 73.4829 8.72799 73.0389 8.27799C72.5959 7.82799 72.0519 7.60399 71.4089 7.60399C70.7659 7.60399 70.2229 7.82899 69.7789 8.27799C69.3359 8.72799 69.1129 9.30199 69.1129 9.99999C69.1129 10.698 69.3359 11.272 69.7789 11.722Z"
|
||||
className="stroke-current"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M75.5749 13V7H76.513L79.429 11.667H79.4619L79.429 10.511V7H80.1999V13H79.3949L76.344 8.106H76.3109L76.344 9.262V13H75.5749Z"
|
||||
className="stroke-current"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.1565 7.96648C10.0384 8.23398 9.97314 8.56174 9.97314 8.943V31.059C9.97314 31.4411 10.0385 31.7689 10.1567 32.0363L22.1907 20.0006L10.1565 7.96648ZM10.8517 32.7555C11.2978 32.9464 11.8797 32.8858 12.5141 32.526L26.6712 24.4812L22.8978 20.7077L10.8517 32.7555ZM27.5737 23.9695L32.0151 21.446C33.4121 20.651 33.4121 19.352 32.0151 18.558L27.5717 16.0331L23.6048 20.0006L27.5737 23.9695ZM26.6699 15.5207L12.5141 7.47701C11.8796 7.11643 11.2977 7.05626 10.8516 7.2474L22.8977 19.2935L26.6699 15.5207Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppStoreButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||
return (
|
||||
<a
|
||||
aria-label="Download on the App Store"
|
||||
href="#"
|
||||
{...props}
|
||||
className={cx(
|
||||
"rounded-[7px] text-fg-primary ring-1 ring-fg-primary outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<svg width={size === "md" ? 120 : 132} height={size === "md" ? 40 : 44} viewBox="0 0 120 40" fill="none">
|
||||
<path
|
||||
d="M81.5257 19.2009V21.4919H80.0896V22.9944H81.5257V28.0994C81.5257 29.8425 82.3143 30.5398 84.2981 30.5398C84.6468 30.5398 84.9788 30.4983 85.2693 30.4485V28.9626C85.0203 28.9875 84.8626 29.0041 84.5887 29.0041C83.7005 29.0041 83.3104 28.5891 83.3104 27.6428V22.9944H85.2693V21.4919H83.3104V19.2009H81.5257Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M90.3232 30.6643C92.9628 30.6643 94.5815 28.8962 94.5815 25.9661C94.5815 23.0525 92.9545 21.2761 90.3232 21.2761C87.6835 21.2761 86.0566 23.0525 86.0566 25.9661C86.0566 28.8962 87.6752 30.6643 90.3232 30.6643ZM90.3232 29.0789C88.7709 29.0789 87.8994 27.9416 87.8994 25.9661C87.8994 24.0071 88.7709 22.8616 90.3232 22.8616C91.8671 22.8616 92.747 24.0071 92.747 25.9661C92.747 27.9333 91.8671 29.0789 90.3232 29.0789Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M95.9664 30.49H97.7511V25.1526C97.7511 23.8826 98.7056 23.0276 100.059 23.0276C100.374 23.0276 100.905 23.0857 101.055 23.1355V21.3757C100.864 21.3259 100.524 21.301 100.258 21.301C99.0792 21.301 98.0748 21.9485 97.8175 22.8367H97.6846V21.4504H95.9664V30.49Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M105.486 22.7952C106.806 22.7952 107.669 23.7165 107.711 25.136H103.145C103.245 23.7248 104.166 22.7952 105.486 22.7952ZM107.702 28.0496C107.37 28.7551 106.632 29.1453 105.552 29.1453C104.125 29.1453 103.203 28.1409 103.145 26.5554V26.4558H109.529V25.8332C109.529 22.9944 108.009 21.2761 105.494 21.2761C102.946 21.2761 101.327 23.1106 101.327 25.9993C101.327 28.8879 102.913 30.6643 105.503 30.6643C107.57 30.6643 109.014 29.6682 109.421 28.0496H107.702Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M69.8221 27.1518C69.9598 29.3715 71.8095 30.7911 74.5626 30.7911C77.505 30.7911 79.3462 29.3027 79.3462 26.9281C79.3462 25.0612 78.2966 24.0287 75.7499 23.4351L74.382 23.0996C72.7645 22.721 72.1106 22.2134 72.1106 21.3272C72.1106 20.2088 73.1259 19.4775 74.6487 19.4775C76.0941 19.4775 77.0921 20.1916 77.2727 21.3358H79.1483C79.0365 19.2452 77.1953 17.774 74.6745 17.774C71.9644 17.774 70.1576 19.2452 70.1576 21.4563C70.1576 23.2802 71.1815 24.3643 73.427 24.8891L75.0272 25.2763C76.6705 25.6634 77.3932 26.2312 77.3932 27.1776C77.3932 28.2789 76.2575 29.079 74.7089 29.079C73.0484 29.079 71.8955 28.3305 71.7321 27.1518H69.8221Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M51.3348 21.301C50.1063 21.301 49.0437 21.9153 48.4959 22.9446H48.3631V21.4504H46.6448V33.4949H48.4295V29.1204H48.5706C49.0437 30.0749 50.0647 30.6394 51.3514 30.6394C53.6341 30.6394 55.0867 28.8381 55.0867 25.9661C55.0867 23.094 53.6341 21.301 51.3348 21.301ZM50.8284 29.0373C49.3343 29.0373 48.3963 27.8586 48.3963 25.9744C48.3963 24.0818 49.3343 22.9031 50.8367 22.9031C52.3475 22.9031 53.2522 24.0569 53.2522 25.9661C53.2522 27.8835 52.3475 29.0373 50.8284 29.0373Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M61.3316 21.301C60.103 21.301 59.0405 21.9153 58.4927 22.9446H58.3599V21.4504H56.6416V33.4949H58.4263V29.1204H58.5674C59.0405 30.0749 60.0615 30.6394 61.3482 30.6394C63.6309 30.6394 65.0835 28.8381 65.0835 25.9661C65.0835 23.094 63.6309 21.301 61.3316 21.301ZM60.8252 29.0373C59.3311 29.0373 58.3931 27.8586 58.3931 25.9744C58.3931 24.0818 59.3311 22.9031 60.8335 22.9031C62.3443 22.9031 63.249 24.0569 63.249 25.9661C63.249 27.8835 62.3443 29.0373 60.8252 29.0373Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M43.4428 30.49H45.4905L41.008 18.0751H38.9346L34.4521 30.49H36.431L37.5752 27.1948H42.3072L43.4428 30.49ZM39.8724 20.3292H40.0186L41.8168 25.5774H38.0656L39.8724 20.3292Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M35.6514 8.71094V14.7H37.8137C39.5984 14.7 40.6318 13.6001 40.6318 11.6868C40.6318 9.80249 39.5901 8.71094 37.8137 8.71094H35.6514ZM36.5811 9.55762H37.71C38.9509 9.55762 39.6855 10.3462 39.6855 11.6992C39.6855 13.073 38.9634 13.8533 37.71 13.8533H36.5811V9.55762Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M43.7969 14.7871C45.1167 14.7871 45.9261 13.9031 45.9261 12.438C45.9261 10.9812 45.1126 10.093 43.7969 10.093C42.4771 10.093 41.6636 10.9812 41.6636 12.438C41.6636 13.9031 42.4729 14.7871 43.7969 14.7871ZM43.7969 13.9944C43.0208 13.9944 42.585 13.4258 42.585 12.438C42.585 11.4585 43.0208 10.8857 43.7969 10.8857C44.5689 10.8857 45.0088 11.4585 45.0088 12.438C45.0088 13.4216 44.5689 13.9944 43.7969 13.9944Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M52.8182 10.1802H51.9259L51.1207 13.6292H51.0501L50.1205 10.1802H49.2655L48.3358 13.6292H48.2694L47.4601 10.1802H46.5553L47.8004 14.7H48.7176L49.6473 11.3713H49.7179L50.6517 14.7H51.5772L52.8182 10.1802Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M53.8458 14.7H54.7382V12.0562C54.7382 11.3506 55.1574 10.9106 55.8173 10.9106C56.4772 10.9106 56.7926 11.2717 56.7926 11.998V14.7H57.685V11.7739C57.685 10.699 57.1288 10.093 56.1203 10.093C55.4396 10.093 54.9914 10.396 54.7714 10.8982H54.705V10.1802H53.8458V14.7Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path d="M59.0903 14.7H59.9826V8.41626H59.0903V14.7Z" className="fill-current" />
|
||||
<path
|
||||
d="M63.3386 14.7871C64.6584 14.7871 65.4678 13.9031 65.4678 12.438C65.4678 10.9812 64.6543 10.093 63.3386 10.093C62.0188 10.093 61.2053 10.9812 61.2053 12.438C61.2053 13.9031 62.0146 14.7871 63.3386 14.7871ZM63.3386 13.9944C62.5625 13.9944 62.1267 13.4258 62.1267 12.438C62.1267 11.4585 62.5625 10.8857 63.3386 10.8857C64.1106 10.8857 64.5505 11.4585 64.5505 12.438C64.5505 13.4216 64.1106 13.9944 63.3386 13.9944Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M68.1265 14.0234C67.6409 14.0234 67.2881 13.7869 67.2881 13.3801C67.2881 12.9817 67.5704 12.77 68.1929 12.7285L69.2969 12.658V13.0356C69.2969 13.5959 68.7989 14.0234 68.1265 14.0234ZM67.8982 14.7747C68.4917 14.7747 68.9856 14.5173 69.2554 14.0649H69.326V14.7H70.1851V11.6121C70.1851 10.6575 69.5459 10.093 68.4129 10.093C67.3877 10.093 66.6573 10.5911 66.566 11.3672H67.4292C67.5289 11.0476 67.8733 10.865 68.3714 10.865C68.9815 10.865 69.2969 11.1348 69.2969 11.6121V12.0022L68.0726 12.0728C66.9976 12.1392 66.3916 12.6082 66.3916 13.4216C66.3916 14.2476 67.0267 14.7747 67.8982 14.7747Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M73.2132 14.7747C73.8358 14.7747 74.3629 14.48 74.6327 13.9861H74.7032V14.7H75.5582V8.41626H74.6659V10.8982H74.5995C74.3546 10.4001 73.8316 10.1055 73.2132 10.1055C72.0719 10.1055 71.3373 11.0103 71.3373 12.438C71.3373 13.8699 72.0636 14.7747 73.2132 14.7747ZM73.4664 10.9065C74.2135 10.9065 74.6825 11.5 74.6825 12.4421C74.6825 13.3884 74.2176 13.9736 73.4664 13.9736C72.711 13.9736 72.2586 13.3967 72.2586 12.438C72.2586 11.4875 72.7152 10.9065 73.4664 10.9065Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M81.3447 14.7871C82.6645 14.7871 83.4738 13.9031 83.4738 12.438C83.4738 10.9812 82.6604 10.093 81.3447 10.093C80.0249 10.093 79.2114 10.9812 79.2114 12.438C79.2114 13.9031 80.0207 14.7871 81.3447 14.7871ZM81.3447 13.9944C80.5686 13.9944 80.1328 13.4258 80.1328 12.438C80.1328 11.4585 80.5686 10.8857 81.3447 10.8857C82.1166 10.8857 82.5566 11.4585 82.5566 12.438C82.5566 13.4216 82.1166 13.9944 81.3447 13.9944Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M84.655 14.7H85.5474V12.0562C85.5474 11.3506 85.9666 10.9106 86.6265 10.9106C87.2864 10.9106 87.6018 11.2717 87.6018 11.998V14.7H88.4941V11.7739C88.4941 10.699 87.938 10.093 86.9294 10.093C86.2488 10.093 85.8005 10.396 85.5806 10.8982H85.5142V10.1802H84.655V14.7Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M92.6039 9.05542V10.2009H91.8858V10.9521H92.6039V13.5046C92.6039 14.3762 92.9981 14.7249 93.9901 14.7249C94.1644 14.7249 94.3304 14.7041 94.4757 14.6792V13.9363C94.3512 13.9487 94.2723 13.957 94.1353 13.957C93.6913 13.957 93.4962 13.7495 93.4962 13.2764V10.9521H94.4757V10.2009H93.4962V9.05542H92.6039Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M95.6735 14.7H96.5658V12.0603C96.5658 11.3755 96.9726 10.9148 97.703 10.9148C98.3339 10.9148 98.6701 11.28 98.6701 12.0022V14.7H99.5624V11.7822C99.5624 10.7073 98.9689 10.0972 98.006 10.0972C97.3253 10.0972 96.848 10.4001 96.6281 10.9065H96.5575V8.41626H95.6735V14.7Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M102.781 10.8525C103.441 10.8525 103.873 11.3132 103.894 12.0229H101.611C101.661 11.3174 102.122 10.8525 102.781 10.8525ZM103.89 13.4797C103.724 13.8325 103.354 14.0276 102.815 14.0276C102.101 14.0276 101.64 13.5254 101.611 12.7327V12.6829H104.803V12.3716C104.803 10.9521 104.043 10.093 102.786 10.093C101.511 10.093 100.702 11.0103 100.702 12.4546C100.702 13.8989 101.495 14.7871 102.79 14.7871C103.823 14.7871 104.545 14.2891 104.749 13.4797H103.89Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M24.769 20.3008C24.7907 18.6198 25.6934 17.0292 27.1256 16.1488C26.2221 14.8584 24.7088 14.0403 23.1344 13.9911C21.4552 13.8148 19.8272 14.9959 18.9715 14.9959C18.0992 14.9959 16.7817 14.0086 15.363 14.0378C13.5137 14.0975 11.7898 15.1489 10.8901 16.7656C8.95607 20.1141 10.3987 25.0351 12.2513 27.7417C13.1782 29.0671 14.2615 30.5475 15.6789 30.495C17.066 30.4375 17.584 29.6105 19.2583 29.6105C20.9171 29.6105 21.4031 30.495 22.8493 30.4616C24.3377 30.4375 25.2754 29.1304 26.1698 27.7925C26.8358 26.8481 27.3483 25.8044 27.6882 24.7C25.9391 23.9602 24.771 22.2 24.769 20.3008Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M22.0373 12.2111C22.8489 11.2369 23.2487 9.98469 23.1518 8.72046C21.912 8.85068 20.7668 9.44324 19.9443 10.3801C19.14 11.2954 18.7214 12.5255 18.8006 13.7415C20.0408 13.7542 21.2601 13.1777 22.0373 12.2111Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const GalaxyStoreButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||
return (
|
||||
<a
|
||||
aria-label="Available on Galaxy Store"
|
||||
href="#"
|
||||
{...props}
|
||||
className={cx(
|
||||
"rounded-[7px] text-fg-primary ring-1 ring-fg-primary outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<svg width={size === "md" ? 147 : 162} height={size === "md" ? 40 : 44} viewBox="0 0 147 40" fill="none">
|
||||
<path d="M64.7516 20.3987H66.7715V31.3885H64.7516V20.3987Z" className="fill-current" />
|
||||
<path
|
||||
d="M42.5 25.9699C42.5 22.8811 44.8314 20.4009 48.039 20.4009C50.2816 20.4009 52.0489 21.5444 52.9695 23.1779L51.1875 24.2473C50.5193 23.0146 49.4799 22.3611 48.054 22.3611C46.0343 22.3611 44.5047 23.9946 44.5047 25.9699C44.5047 27.9745 46.0196 29.5786 48.1281 29.5786C49.7469 29.5786 50.8757 28.6578 51.2616 27.2322H47.8017V25.3017H53.4298V26.1037C53.4298 29.0289 51.3657 31.5392 48.1281 31.5392C44.7423 31.5392 42.5 28.9695 42.5 25.9699Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M54.3525 27.0543C54.3525 24.1732 56.4613 22.525 58.6592 22.525C59.8027 22.525 60.8274 22.9999 61.4512 23.7426V22.7032H63.4558V31.3907H61.4512V30.2616C60.8124 31.0636 59.773 31.5689 58.6295 31.5689C56.5354 31.5689 54.3525 29.8904 54.3525 27.0543ZM61.555 27.0246C61.555 25.5543 60.4412 24.3514 58.9562 24.3514C57.4713 24.3514 56.3278 25.5249 56.3278 27.0246C56.3278 28.539 57.4713 29.7272 58.9562 29.7272C60.4412 29.7272 61.555 28.5243 61.555 27.0246Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M67.7938 27.0543C67.7938 24.1732 69.9026 22.525 72.1005 22.525C73.244 22.525 74.2686 22.9999 74.8925 23.7426V22.7032H76.8971V31.3907H74.8925V30.2616C74.2539 31.0636 73.2146 31.5689 72.0707 31.5689C69.977 31.5689 67.7938 29.8904 67.7938 27.0543ZM74.9966 27.0246C74.9966 25.5543 73.8828 24.3514 72.3981 24.3514C70.9128 24.3514 69.7693 25.5249 69.7693 27.0246C69.7693 28.539 70.9128 29.7272 72.3981 29.7272C73.8825 29.7272 74.9966 28.5243 74.9966 27.0246Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M80.8048 26.9652L77.6566 22.7032H80.1072L82.0818 25.4652L84.0424 22.7032H86.4182L83.2994 26.9949L86.6261 31.3907H84.1759L82.0524 28.4949L79.988 31.3907H77.5375L80.8048 26.9652Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M90.3846 30.9598L86.8206 22.7029H88.9588L91.3796 28.554L93.6663 22.7029H95.7757L90.5926 35.4H88.5582L90.3846 30.9598Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M99.8907 29.5936L101.732 28.1384C102.282 29.074 103.128 29.6083 104.108 29.6083C105.178 29.6083 105.757 28.911 105.757 28.1531C105.757 27.2325 104.658 26.9505 103.499 26.594C102.044 26.1334 100.426 25.5693 100.426 23.5049C100.426 21.7676 101.94 20.4012 104.019 20.4012C105.772 20.4012 106.781 21.0697 107.658 21.9756L105.994 23.2376C105.534 22.5547 104.895 22.1982 104.034 22.1982C103.054 22.1982 102.519 22.7329 102.519 23.4305C102.519 24.292 103.559 24.5743 104.732 24.9605C106.203 25.4355 107.866 26.089 107.866 28.1681C107.866 29.8757 106.499 31.5395 104.123 31.5395C102.163 31.5392 100.871 30.7071 99.8907 29.5936Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M108.03 22.7029H109.515V21.3812L111.535 20V22.7029H113.302V24.4999H111.535V27.7519C111.535 29.2669 111.743 29.5045 113.302 29.5045V31.3904H113.02C110.332 31.3904 109.515 30.5292 109.515 27.7672V24.4999H108.03L108.03 22.7029Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M113.838 27.0543C113.838 24.5296 115.827 22.5247 118.337 22.5247C120.832 22.5247 122.837 24.5296 122.837 27.0543C122.837 29.5639 120.832 31.5689 118.337 31.5689C115.827 31.5689 113.838 29.5639 113.838 27.0543ZM120.862 27.0543C120.862 25.599 119.747 24.4108 118.337 24.4108C116.896 24.4108 115.812 25.599 115.812 27.0543C115.812 28.4949 116.896 29.6828 118.337 29.6828C119.747 29.6828 120.862 28.4949 120.862 27.0543Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M125.853 22.7029V23.9949C126.254 23.1335 126.981 22.7029 128.08 22.7029C128.704 22.7029 129.223 22.8514 129.61 23.0741L128.852 24.9749C128.555 24.782 128.214 24.6334 127.649 24.6334C126.491 24.6334 125.867 25.2573 125.867 26.7569V31.3904H123.862V22.7029H125.853Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M129.834 27.069C129.834 24.5296 131.808 22.5247 134.348 22.5247C136.932 22.5247 138.818 24.4255 138.818 26.9949V27.7519H131.749C132.017 28.9701 132.997 29.8016 134.423 29.8016C135.536 29.8016 136.413 29.1928 136.828 28.2719L138.477 29.2222C137.719 30.6186 136.353 31.5686 134.423 31.5686C131.69 31.5686 129.834 29.5786 129.834 27.069ZM131.853 26.074H136.784C136.487 24.9158 135.596 24.292 134.348 24.292C133.145 24.292 132.21 25.0196 131.853 26.074Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M46.6986 13.974H43.9018L43.3866 15.4H42.5034L44.8218 9.0244H45.7878L48.097 15.4H47.2138L46.6986 13.974ZM46.4594 13.2932L45.3002 10.0548L44.141 13.2932H46.4594Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path d="M50.4322 14.6272L51.9962 10.3584H52.8886L50.9106 15.4H49.9354L47.9574 10.3584H48.859L50.4322 14.6272Z" className="fill-current" />
|
||||
<path
|
||||
d="M53.4917 12.8608C53.4917 12.3456 53.5959 11.8948 53.8045 11.5084C54.013 11.1159 54.2982 10.8123 54.6601 10.5976C55.0281 10.3829 55.4359 10.2756 55.8837 10.2756C56.3253 10.2756 56.7086 10.3707 57.0337 10.5608C57.3587 10.7509 57.601 10.9901 57.7605 11.2784V10.3584H58.6069V15.4H57.7605V14.4616C57.5949 14.756 57.3465 15.0013 57.0153 15.1976C56.6902 15.3877 56.3099 15.4828 55.8745 15.4828C55.4267 15.4828 55.0219 15.3724 54.6601 15.1516C54.2982 14.9308 54.013 14.6211 53.8045 14.2224C53.5959 13.8237 53.4917 13.3699 53.4917 12.8608ZM57.7605 12.87C57.7605 12.4897 57.6838 12.1585 57.5305 11.8764C57.3771 11.5943 57.1686 11.3796 56.9049 11.2324C56.6473 11.0791 56.3621 11.0024 56.0493 11.0024C55.7365 11.0024 55.4513 11.076 55.1937 11.2232C54.9361 11.3704 54.7306 11.5851 54.5773 11.8672C54.4239 12.1493 54.3473 12.4805 54.3473 12.8608C54.3473 13.2472 54.4239 13.5845 54.5773 13.8728C54.7306 14.1549 54.9361 14.3727 55.1937 14.526C55.4513 14.6732 55.7365 14.7468 56.0493 14.7468C56.3621 14.7468 56.6473 14.6732 56.9049 14.526C57.1686 14.3727 57.3771 14.1549 57.5305 13.8728C57.6838 13.5845 57.7605 13.2503 57.7605 12.87Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M60.2701 9.5396C60.1106 9.5396 59.9757 9.4844 59.8653 9.374C59.7549 9.2636 59.6997 9.12867 59.6997 8.9692C59.6997 8.80974 59.7549 8.6748 59.8653 8.5644C59.9757 8.454 60.1106 8.3988 60.2701 8.3988C60.4234 8.3988 60.5522 8.454 60.6565 8.5644C60.7669 8.6748 60.8221 8.80974 60.8221 8.9692C60.8221 9.12867 60.7669 9.2636 60.6565 9.374C60.5522 9.4844 60.4234 9.5396 60.2701 9.5396ZM60.6749 10.3584V15.4H59.8377V10.3584H60.6749Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path d="M62.7549 8.592V15.4H61.9177V8.592H62.7549Z" className="fill-current" />
|
||||
<path
|
||||
d="M63.869 12.8608C63.869 12.3456 63.9732 11.8948 64.1818 11.5084C64.3903 11.1159 64.6755 10.8123 65.0374 10.5976C65.4054 10.3829 65.8132 10.2756 66.261 10.2756C66.7026 10.2756 67.0859 10.3707 67.411 10.5608C67.736 10.7509 67.9783 10.9901 68.1378 11.2784V10.3584H68.9842V15.4H68.1378V14.4616C67.9722 14.756 67.7238 15.0013 67.3926 15.1976C67.0675 15.3877 66.6872 15.4828 66.2518 15.4828C65.804 15.4828 65.3992 15.3724 65.0374 15.1516C64.6755 14.9308 64.3903 14.6211 64.1818 14.2224C63.9732 13.8237 63.869 13.3699 63.869 12.8608ZM68.1378 12.87C68.1378 12.4897 68.0611 12.1585 67.9078 11.8764C67.7544 11.5943 67.5459 11.3796 67.2822 11.2324C67.0246 11.0791 66.7394 11.0024 66.4266 11.0024C66.1138 11.0024 65.8286 11.076 65.571 11.2232C65.3134 11.3704 65.1079 11.5851 64.9546 11.8672C64.8012 12.1493 64.7246 12.4805 64.7246 12.8608C64.7246 13.2472 64.8012 13.5845 64.9546 13.8728C65.1079 14.1549 65.3134 14.3727 65.571 14.526C65.8286 14.6732 66.1138 14.7468 66.4266 14.7468C66.7394 14.7468 67.0246 14.6732 67.2822 14.526C67.5459 14.3727 67.7544 14.1549 67.9078 13.8728C68.0611 13.5845 68.1378 13.2503 68.1378 12.87Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M71.3282 11.2968C71.4999 10.9963 71.7514 10.7509 72.0826 10.5608C72.4138 10.3707 72.791 10.2756 73.2142 10.2756C73.668 10.2756 74.0759 10.3829 74.4378 10.5976C74.7996 10.8123 75.0848 11.1159 75.2934 11.5084C75.5019 11.8948 75.6062 12.3456 75.6062 12.8608C75.6062 13.3699 75.5019 13.8237 75.2934 14.2224C75.0848 14.6211 74.7966 14.9308 74.4286 15.1516C74.0667 15.3724 73.6619 15.4828 73.2142 15.4828C72.7787 15.4828 72.3954 15.3877 72.0642 15.1976C71.7391 15.0075 71.4938 14.7652 71.3282 14.4708V15.4H70.491V8.592H71.3282V11.2968ZM74.7506 12.8608C74.7506 12.4805 74.6739 12.1493 74.5206 11.8672C74.3672 11.5851 74.1587 11.3704 73.895 11.2232C73.6374 11.076 73.3522 11.0024 73.0394 11.0024C72.7327 11.0024 72.4475 11.0791 72.1838 11.2324C71.9262 11.3796 71.7176 11.5973 71.5582 11.8856C71.4048 12.1677 71.3282 12.4959 71.3282 12.87C71.3282 13.2503 71.4048 13.5845 71.5582 13.8728C71.7176 14.1549 71.9262 14.3727 72.1838 14.526C72.4475 14.6732 72.7327 14.7468 73.0394 14.7468C73.3522 14.7468 73.6374 14.6732 73.895 14.526C74.1587 14.3727 74.3672 14.1549 74.5206 13.8728C74.6739 13.5845 74.7506 13.2472 74.7506 12.8608Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path d="M77.5454 8.592V15.4H76.7082V8.592H77.5454Z" className="fill-current" />
|
||||
<path
|
||||
d="M83.6642 12.686C83.6642 12.8455 83.655 13.0141 83.6366 13.192H79.607C79.6377 13.6888 79.8064 14.0783 80.113 14.3604C80.4258 14.6364 80.803 14.7744 81.2446 14.7744C81.6065 14.7744 81.907 14.6916 82.1462 14.526C82.3916 14.3543 82.5633 14.1273 82.6614 13.8452H83.563C83.4281 14.3297 83.1582 14.7253 82.7534 15.032C82.3486 15.3325 81.8457 15.4828 81.2446 15.4828C80.7662 15.4828 80.3369 15.3755 79.9566 15.1608C79.5825 14.9461 79.2881 14.6425 79.0734 14.25C78.8587 13.8513 78.7514 13.3913 78.7514 12.87C78.7514 12.3487 78.8557 11.8917 79.0642 11.4992C79.2728 11.1067 79.5641 10.8061 79.9382 10.5976C80.3185 10.3829 80.754 10.2756 81.2446 10.2756C81.723 10.2756 82.1462 10.3799 82.5142 10.5884C82.8822 10.7969 83.1643 11.0852 83.3606 11.4532C83.563 11.8151 83.6642 12.226 83.6642 12.686ZM82.7994 12.5112C82.7994 12.1923 82.7289 11.9193 82.5878 11.6924C82.4467 11.4593 82.2536 11.2845 82.0082 11.168C81.769 11.0453 81.5022 10.984 81.2078 10.984C80.7846 10.984 80.4227 11.1189 80.1222 11.3888C79.8278 11.6587 79.6591 12.0328 79.6162 12.5112H82.7994Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M89.6048 15.4828C89.1326 15.4828 88.7032 15.3755 88.3168 15.1608C87.9366 14.9461 87.636 14.6425 87.4152 14.25C87.2006 13.8513 87.0932 13.3913 87.0932 12.87C87.0932 12.3548 87.2036 11.9009 87.4244 11.5084C87.6514 11.1097 87.958 10.8061 88.3444 10.5976C88.7308 10.3829 89.1632 10.2756 89.6416 10.2756C90.12 10.2756 90.5524 10.3829 90.9388 10.5976C91.3252 10.8061 91.6288 11.1067 91.8496 11.4992C92.0766 11.8917 92.19 12.3487 92.19 12.87C92.19 13.3913 92.0735 13.8513 91.8404 14.25C91.6135 14.6425 91.3038 14.9461 90.9112 15.1608C90.5187 15.3755 90.0832 15.4828 89.6048 15.4828ZM89.6048 14.7468C89.9054 14.7468 90.1875 14.6763 90.4512 14.5352C90.715 14.3941 90.9266 14.1825 91.086 13.9004C91.2516 13.6183 91.3344 13.2748 91.3344 12.87C91.3344 12.4652 91.2547 12.1217 91.0952 11.8396C90.9358 11.5575 90.7272 11.3489 90.4696 11.214C90.212 11.0729 89.933 11.0024 89.6324 11.0024C89.3258 11.0024 89.0436 11.0729 88.786 11.214C88.5346 11.3489 88.3322 11.5575 88.1788 11.8396C88.0255 12.1217 87.9488 12.4652 87.9488 12.87C87.9488 13.2809 88.0224 13.6275 88.1696 13.9096C88.323 14.1917 88.5254 14.4033 88.7768 14.5444C89.0283 14.6793 89.3043 14.7468 89.6048 14.7468Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M95.9312 10.2664C96.5445 10.2664 97.0413 10.4535 97.4216 10.8276C97.8019 11.1956 97.992 11.7292 97.992 12.4284V15.4H97.164V12.548C97.164 12.0451 97.0383 11.6617 96.7868 11.398C96.5353 11.1281 96.1919 10.9932 95.7564 10.9932C95.3148 10.9932 94.9621 11.1312 94.6984 11.4072C94.4408 11.6832 94.312 12.0849 94.312 12.6124V15.4H93.4748V10.3584H94.312V11.076C94.4776 10.8184 94.7015 10.6191 94.9836 10.478C95.2719 10.3369 95.5877 10.2664 95.9312 10.2664Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.2763 14.6655C9 15.8164 9 17.2109 9 20C9 22.7891 9 24.1836 9.2763 25.3345C10.1541 28.9909 13.0091 31.8459 16.6655 32.7237C17.8164 33 19.2109 33 22 33C24.7891 33 26.1836 33 27.3345 32.7237C30.9909 31.8459 33.8459 28.9909 34.7237 25.3345C35 24.1836 35 22.7891 35 20C35 17.2109 35 15.8164 34.7237 14.6655C33.8459 11.0091 30.9909 8.15415 27.3345 7.2763C26.1836 7 24.7891 7 22 7C19.2109 7 17.8164 7 16.6655 7.2763C13.0091 8.15415 10.1541 11.0091 9.2763 14.6655ZM25.738 16.3341L25.7914 16.5695H28.8609L28.0334 24.2828C27.8677 25.8515 26.5446 27.0421 24.9669 27.0421H19.0326C17.4549 27.0421 16.1318 25.8515 15.966 24.2826L15.1387 16.5695H18.2081L18.2617 16.3341C18.2617 14.2733 19.9385 12.5968 21.9997 12.5968C24.0609 12.5968 25.738 14.2733 25.738 16.3341ZM19.8934 16.3341L19.8331 16.5695H24.1664L24.1063 16.3341C24.1063 15.1729 23.1613 14.2281 21.9997 14.2281C20.8383 14.2281 19.8934 15.1729 19.8934 16.3341Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppGalleryButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||
return (
|
||||
<a
|
||||
aria-label="Explore it on AppGallery"
|
||||
href="#"
|
||||
{...props}
|
||||
className={cx(
|
||||
"rounded-[7px] text-fg-primary ring-1 ring-fg-primary outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<svg width={size === "md" ? 133 : 147} height={size === "md" ? 40 : 44} viewBox="0 0 133 40" fill="none">
|
||||
<path
|
||||
d="M45.3962 25.4116H48.8919L47.6404 22.0615C47.4682 21.5986 47.2989 21.0875 47.1319 20.5276C46.9813 21.0229 46.817 21.5286 46.6394 22.0453L45.3962 25.4116ZM49.4893 27.0021H44.8068L43.6607 30.1344H41.6021L46.1874 18.4368H48.133L52.8234 30.1344H50.6599L49.4893 27.0021Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M58.8962 28.0072C59.3026 27.461 59.5058 26.663 59.5058 25.6135C59.5058 24.6396 59.3375 23.933 59.0013 23.4942C58.6647 23.0557 58.2167 22.8364 57.657 22.8364C57.2695 22.8364 56.9117 22.9281 56.5834 23.1109C56.2551 23.2939 55.9429 23.5387 55.6469 23.8455V28.5117C55.8461 28.6085 56.0775 28.6853 56.3413 28.7416C56.605 28.7984 56.8661 28.8266 57.1242 28.8266C57.8994 28.8266 58.4898 28.5533 58.8962 28.0072ZM53.653 23.555C53.653 22.9092 53.6314 22.1986 53.5885 21.4237H55.4613C55.5311 21.7844 55.5797 22.1531 55.6066 22.5297C56.3815 21.6849 57.2695 21.2622 58.2706 21.2622C58.8519 21.2622 59.3901 21.4088 59.8853 21.7021C60.3802 21.9955 60.7799 22.4583 61.0839 23.0906C61.3882 23.7231 61.5402 24.5265 61.5402 25.5005C61.5402 26.5177 61.3666 27.387 61.0194 28.108C60.6722 28.8294 60.1866 29.3754 59.5623 29.747C58.9381 30.1181 58.2167 30.3039 57.3989 30.3039C56.8066 30.3039 56.2226 30.2042 55.6469 30.0053V33.6056L53.653 33.7752V23.555Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M67.9935 28.0072C68.3999 27.461 68.6031 26.663 68.6031 25.6135C68.6031 24.6396 68.4349 23.933 68.0986 23.4942C67.7621 23.0557 67.3141 22.8364 66.7543 22.8364C66.3669 22.8364 66.009 22.9281 65.6807 23.1109C65.3522 23.2939 65.0402 23.5387 64.7442 23.8455V28.5117C64.9434 28.6085 65.1746 28.6853 65.4386 28.7416C65.7021 28.7984 65.9631 28.8266 66.2215 28.8266C66.9964 28.8266 67.5871 28.5533 67.9935 28.0072ZM62.7501 23.555C62.7501 22.9092 62.7285 22.1986 62.6855 21.4237H64.5586C64.6285 21.7844 64.677 22.1531 64.7039 22.5297C65.4789 21.6849 66.3668 21.2622 67.3679 21.2622C67.9493 21.2622 68.4871 21.4088 68.9826 21.7021C69.4775 21.9955 69.8772 22.4583 70.1815 23.0906C70.4852 23.7231 70.6375 24.5265 70.6375 25.5005C70.6375 26.5177 70.4637 27.387 70.1167 28.108C69.7695 28.8294 69.2837 29.3754 68.6597 29.747C68.0351 30.1181 67.314 30.3039 66.4959 30.3039C65.9039 30.3039 65.3199 30.2042 64.7442 30.0053V33.6056L62.7501 33.7752V23.555Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M74.3005 29.5813C73.4391 29.1053 72.7773 28.4229 72.3146 27.5349C71.8514 26.6469 71.6202 25.5974 71.6202 24.3864C71.6202 23.0734 71.8866 21.9578 72.4194 21.0401C72.9522 20.1227 73.6775 19.4337 74.5949 18.9735C75.5127 18.5134 76.5418 18.2833 77.6831 18.2833C78.3557 18.2833 78.9975 18.3574 79.6085 18.5053C80.2191 18.6534 80.7882 18.8564 81.3159 19.1149L80.8071 20.6486C79.7468 20.1428 78.7351 19.8898 77.7719 19.8898C76.9591 19.8898 76.2474 20.0634 75.6367 20.4106C75.0258 20.7578 74.5506 21.2677 74.2117 21.9404C73.8725 22.6132 73.7031 23.4258 73.7031 24.3783C73.7031 25.2128 73.8335 25.9526 74.0943 26.5984C74.3557 27.2442 74.7674 27.7557 75.3298 28.1323C75.8922 28.509 76.6013 28.6973 77.457 28.6973C77.8445 28.6973 78.2319 28.6651 78.6194 28.6005C79.0069 28.536 79.3703 28.4418 79.7093 28.3179V25.9526H77.005V24.4026H81.6469V29.3272C80.9794 29.6392 80.2783 29.8789 79.5439 30.0456C78.809 30.2123 78.0786 30.2957 77.3519 30.2957C76.1786 30.2957 75.1613 30.0579 74.3005 29.5813Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M87.5381 28.5197C87.9522 28.3208 88.2914 28.073 88.5551 27.7771V26.1625C88.0114 26.1034 87.5674 26.0737 87.2231 26.0737C86.3997 26.0737 85.8303 26.2068 85.5159 26.4734C85.2007 26.7394 85.0434 27.0989 85.0434 27.5509C85.0434 27.9818 85.1578 28.3005 85.3866 28.5077C85.6154 28.7149 85.9261 28.8184 86.3189 28.8184C86.717 28.8184 87.1234 28.7189 87.5381 28.5197ZM88.7327 30.1344C88.6626 29.7952 88.6167 29.4106 88.5954 28.98C88.2887 29.3461 87.8893 29.6568 87.3965 29.9125C86.9045 30.168 86.3485 30.2957 85.7295 30.2957C85.229 30.2957 84.773 30.1976 84.361 30.001C83.9498 29.8048 83.6226 29.5087 83.3805 29.113C83.1383 28.7176 83.017 28.2347 83.017 27.6639C83.017 26.8192 83.321 26.145 83.9293 25.6416C84.5375 25.1385 85.5519 24.887 86.9727 24.887C87.5055 24.887 88.033 24.9248 88.5551 25V24.8305C88.5551 24.0609 88.3909 23.5187 88.0626 23.2036C87.7343 22.889 87.2634 22.7316 86.6501 22.7316C86.2247 22.7316 85.7701 22.7935 85.2855 22.9172C84.8013 23.0411 84.3759 23.189 84.0101 23.3612L83.6951 21.9081C84.0503 21.7465 84.5186 21.5986 85.0999 21.464C85.6813 21.3297 86.2946 21.2622 86.9405 21.2622C87.6941 21.2622 88.3343 21.3765 88.8618 21.6052C89.3893 21.834 89.801 22.2271 90.097 22.7838C90.393 23.3411 90.541 24.0906 90.541 25.0323V28.4954C90.541 28.8562 90.5623 29.4027 90.6055 30.1344H88.7327Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M92.0709 28.0434V17.7505L94.0567 17.5891V27.6882C94.0567 28.0597 94.1199 28.3218 94.2463 28.4754C94.3727 28.6285 94.5732 28.7056 94.8479 28.7056C94.9716 28.7056 95.1466 28.676 95.3724 28.6168L95.6066 30.0456C95.418 30.121 95.1882 30.1816 94.9167 30.2272C94.6447 30.2728 94.3876 30.2957 94.1455 30.2957C92.762 30.2957 92.0709 29.5451 92.0709 28.0434Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M97.0356 28.0434V17.7505L99.0215 17.5891V27.6882C99.0215 28.0597 99.0847 28.3218 99.2111 28.4754C99.3378 28.6285 99.5383 28.7056 99.8127 28.7056C99.9365 28.7056 100.111 28.676 100.338 28.6168L100.572 30.0456C100.383 30.121 100.153 30.1816 99.8815 30.2272C99.6095 30.2728 99.3524 30.2957 99.1103 30.2957C97.7271 30.2957 97.0356 29.5451 97.0356 28.0434Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M106.795 24.7659C106.755 24.0716 106.581 23.551 106.269 23.2036C105.957 22.8568 105.539 22.683 105.019 22.683C104.512 22.683 104.091 22.8581 103.755 23.2078C103.419 23.5578 103.197 24.0771 103.096 24.7659H106.795ZM108.739 26.0333H103.04C103.131 27.8579 104 28.7701 105.648 28.7701C106.056 28.7701 106.475 28.7203 106.904 28.6208C107.331 28.521 107.741 28.388 108.133 28.221L108.571 29.5853C107.595 30.0592 106.501 30.2957 105.285 30.2957C104.357 30.2957 103.579 30.121 102.944 29.7711C102.307 29.4213 101.829 28.9181 101.509 28.2613C101.19 27.6051 101.03 26.8138 101.03 25.888C101.03 24.9248 101.199 24.0958 101.539 23.4016C101.877 22.7073 102.349 22.1773 102.955 21.8112C103.56 21.4453 104.259 21.2622 105.051 21.2622C105.875 21.2622 106.56 21.4547 107.112 21.8396C107.664 22.2241 108.072 22.737 108.339 23.3773C108.605 24.018 108.739 24.7255 108.739 25.5005V26.0333Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M110.248 23.6115C110.248 23.1326 110.224 22.4034 110.181 21.4237H112.048C112.08 21.6659 112.109 21.9552 112.141 22.2916C112.171 22.6279 112.189 22.901 112.2 23.1109C112.432 22.7288 112.659 22.4073 112.883 22.1463C113.107 21.8851 113.368 21.6727 113.667 21.5083C113.965 21.3443 114.304 21.2622 114.688 21.2622C114.995 21.2622 115.256 21.2946 115.477 21.3592L115.227 23.0868C115.035 23.0276 114.819 22.9979 114.581 22.9979C114.115 22.9979 113.704 23.1177 113.355 23.3573C113.005 23.5965 112.632 23.9908 112.232 24.5398V30.1344H110.248V23.6115Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M116.597 33.6984C116.307 33.6475 116.067 33.5896 115.88 33.5251L116.243 32.072C116.376 32.1094 116.547 32.1458 116.752 32.1808C116.955 32.216 117.149 32.2333 117.333 32.2333C118.216 32.2333 118.877 31.6653 119.317 30.5299L119.448 30.207L116.235 21.4237H118.373L119.989 26.332C120.251 27.1717 120.421 27.8149 120.496 28.2613C120.648 27.6317 120.824 27.0021 121.029 26.3724L122.669 21.4237H124.677L121.475 30.2475C121.173 31.0816 120.845 31.7541 120.496 32.2656C120.147 32.7768 119.733 33.1562 119.259 33.4039C118.781 33.6512 118.208 33.7752 117.533 33.7752C117.2 33.7752 116.888 33.7499 116.597 33.6984Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M41.5933 7.30106H45.1474L45.0474 8.15946H42.6349V9.82186H44.9098V10.6261H42.6349V12.4677H45.1765L45.089 13.3344H41.5933V7.30106Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M47.5973 10.2552L45.7223 7.30106H46.9055L48.1765 9.472L49.514 7.30106H50.6639L48.8055 10.2176L50.8181 13.3344H49.6181L48.1807 10.9595L46.7181 13.3344H45.5682L47.5973 10.2552Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M54.1973 9.99466C54.4194 9.7936 54.5306 9.50933 54.5306 9.14266C54.5306 8.7704 54.4172 8.50266 54.1911 8.33866C53.9647 8.17466 53.6319 8.0928 53.1932 8.0928H52.6266V10.2344C52.8876 10.276 53.0876 10.2968 53.2266 10.2968C53.6514 10.2968 53.9751 10.1963 54.1973 9.99466ZM51.5847 7.30106H53.2098C53.9735 7.30106 54.5583 7.4568 54.9639 7.76773C55.3695 8.07893 55.5722 8.5288 55.5722 9.1176C55.5722 9.5176 55.4813 9.8672 55.2994 10.1656C55.1173 10.4643 54.8639 10.6936 54.5388 10.8531C54.214 11.0131 53.8402 11.0928 53.418 11.0928C53.1876 11.0928 52.9236 11.0651 52.6266 11.0093V13.3344H51.5847V7.30106Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path d="M56.6015 7.30106H57.6431V12.4427H60.1389L60.0514 13.3344H56.6015V7.30106Z" className="fill-current" />
|
||||
<path
|
||||
d="M64.4223 12.2989C64.714 12.1032 64.9319 11.8339 65.0764 11.4907C65.221 11.1477 65.2932 10.7552 65.2932 10.3136C65.2932 9.88026 65.2292 9.49439 65.1015 9.15519C64.9735 8.81625 64.7695 8.54639 64.489 8.34479C64.2084 8.14346 63.8474 8.04266 63.4055 8.04266C62.9834 8.04266 62.625 8.14479 62.3306 8.34906C62.0362 8.55306 61.8154 8.82692 61.6682 9.16986C61.521 9.51306 61.4474 9.89145 61.4474 10.3053C61.4474 10.7413 61.5167 11.1317 61.6556 11.476C61.7943 11.8205 62.0068 12.0928 62.2932 12.2928C62.5791 12.4928 62.9332 12.5928 63.3556 12.5928C63.7751 12.5928 64.1306 12.4947 64.4223 12.2989ZM61.7223 13.0387C61.2807 12.7859 60.9431 12.4309 60.7098 11.9739C60.4764 11.5171 60.3599 10.9859 60.3599 10.3803C60.3599 9.74426 60.4839 9.18799 60.7327 8.71146C60.9813 8.23519 61.3396 7.86719 61.8076 7.60719C62.2756 7.34772 62.8277 7.21759 63.4639 7.21759C64.0722 7.21759 64.5959 7.34346 65.0348 7.59466C65.4735 7.84639 65.8084 8.19972 66.0388 8.65519C66.2695 9.11092 66.3847 9.63865 66.3847 10.2387C66.3847 10.8859 66.2591 11.4485 66.0076 11.9261C65.7562 12.4037 65.3981 12.772 64.9327 13.0301C64.4674 13.2885 63.921 13.4177 63.2932 13.4177C62.6876 13.4177 62.1639 13.2915 61.7223 13.0387Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M69.1807 10.1552C69.4333 10.1552 69.657 10.1061 69.8514 10.0072C70.0458 9.9088 70.1973 9.76986 70.3055 9.59066C70.4141 9.41146 70.4682 9.20373 70.4682 8.96773C70.4682 8.66506 70.3722 8.44346 70.1807 8.3032C69.989 8.16293 69.7098 8.0928 69.3431 8.0928H68.589V10.1552H69.1807ZM67.5474 7.30106H69.4349C70.1237 7.30106 70.6453 7.43866 70.9994 7.7136C71.3535 7.98853 71.5306 8.38186 71.5306 8.8928C71.5306 9.21226 71.4666 9.4936 71.3389 9.73653C71.2111 9.97973 71.0527 10.1776 70.864 10.3301C70.6749 10.4829 70.4807 10.5968 70.2807 10.672L72.1349 13.3344H70.9266L69.3557 10.9427H68.589V13.3344H67.5474V7.30106Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M72.9599 7.30106H76.5141L76.4141 8.15946H74.0015V9.82186H76.2765V10.6261H74.0015V12.4677H76.5431L76.4556 13.3344H72.9599V7.30106Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path d="M80.3807 7.30106H81.4223V13.3344H80.3807V7.30106Z" className="fill-current" />
|
||||
<path d="M84.1765 8.172H82.3055L82.3973 7.30106H87.0933L86.9973 8.172H85.218V13.3344H84.1765V8.172Z" className="fill-current" />
|
||||
<path
|
||||
d="M94.3141 12.2989C94.6055 12.1032 94.8237 11.8339 94.9682 11.4907C95.1125 11.1477 95.1847 10.7552 95.1847 10.3136C95.1847 9.88026 95.1207 9.49439 94.9933 9.15519C94.8653 8.81625 94.661 8.54639 94.3807 8.34479C94.0999 8.14346 93.7389 8.04266 93.2973 8.04266C92.8749 8.04266 92.5167 8.14479 92.2223 8.34906C91.9277 8.55306 91.7069 8.82692 91.5599 9.16986C91.4125 9.51306 91.3389 9.89145 91.3389 10.3053C91.3389 10.7413 91.4082 11.1317 91.5474 11.476C91.6861 11.8205 91.8986 12.0928 92.1847 12.2928C92.4709 12.4928 92.825 12.5928 93.2474 12.5928C93.6666 12.5928 94.0223 12.4947 94.3141 12.2989ZM91.6141 13.0387C91.1722 12.7859 90.8349 12.4309 90.6015 11.9739C90.3682 11.5171 90.2514 10.9859 90.2514 10.3803C90.2514 9.74426 90.3757 9.18799 90.6245 8.71146C90.8727 8.23519 91.2311 7.86719 91.6994 7.60719C92.1674 7.34772 92.7194 7.21759 93.3557 7.21759C93.9639 7.21759 94.4874 7.34346 94.9266 7.59466C95.3653 7.84639 95.6999 8.19972 95.9306 8.65519C96.161 9.11092 96.2765 9.63865 96.2765 10.2387C96.2765 10.8859 96.1506 11.4485 95.8994 11.9261C95.6479 12.4037 95.2895 12.772 94.8245 13.0301C94.3591 13.2885 93.8125 13.4177 93.1847 13.4177C92.5791 13.4177 92.0557 13.2915 91.6141 13.0387Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path
|
||||
d="M97.4389 7.30106H98.6349L101.619 11.976C101.592 11.5317 101.581 11.1219 101.581 10.7469V7.30106H102.547V13.3344H101.389L98.3599 8.58426C98.3903 9.12346 98.4055 9.60106 98.4055 10.0176V13.3344H97.4389V7.30106Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
<path d="M19.7881 22.0255H20.8041L20.2945 20.8404L19.7881 22.0255Z" className="fill-current" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M26.9417 7.33331H15.8633C10.6446 7.33331 8.73584 9.24211 8.73584 14.4608V25.5394C8.73584 30.7579 10.6446 32.6666 15.8633 32.6666H26.9382C32.1569 32.6666 34.0692 30.7579 34.0692 25.5394V14.4608C34.0692 9.24211 32.1604 7.33331 26.9417 7.33331ZM19.538 22.6229L19.2366 23.3125H18.5505L20.0097 20.0022H20.6025L22.0558 23.3125H21.3513L21.0537 22.6229H19.538ZM30.5787 23.3102H31.242V20.0022H30.5787V23.3102ZM27.9396 21.8887H29.1617V21.2859H27.9396V20.6078H29.7137V20.0042H27.2766V23.3122H29.7777V22.7088H27.9396V21.8887ZM25.3046 22.2797L24.5526 20.0021H24.0043L23.2524 22.2797L22.5206 20.0036H21.8054L22.9598 23.314H23.5161L24.2692 21.1396L25.022 23.314H25.5833L26.7348 20.0036H26.038L25.3046 22.2797ZM17.5382 21.8979C17.5382 22.4364 17.2708 22.7241 16.7852 22.7241C16.2969 22.7241 16.0281 22.4283 16.0281 21.875V20.004H15.3561V21.8979C15.3561 22.8297 15.8737 23.3638 16.7761 23.3638C17.6873 23.3638 18.2099 22.8194 18.2099 21.8705V20.0021H17.5382V21.8979ZM13.7526 20.0022H14.4243V23.3142H13.7526V21.9693H12.235V23.3142H11.563V20.0022H12.235V21.3383H13.7526V20.0022ZM17.1857 11.5683C17.1857 13.8933 19.0774 15.7851 21.4025 15.7851C23.7276 15.7851 25.6193 13.8933 25.6193 11.5683H25.0236C25.0236 13.5649 23.3993 15.1894 21.4025 15.1894C19.4057 15.1894 17.7814 13.5649 17.7814 11.5683H17.1857Z"
|
||||
className="fill-current"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
565
src/components/base/buttons/app-store-buttons.tsx
Normal file
565
src/components/base/buttons/app-store-buttons.tsx
Normal file
@@ -0,0 +1,565 @@
|
||||
import type { AnchorHTMLAttributes } from "react";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export const GooglePlayButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||
return (
|
||||
<a
|
||||
aria-label="Get it on Google Play"
|
||||
href="#"
|
||||
{...props}
|
||||
className={cx(
|
||||
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<svg width={size === "md" ? 135 : 149} height={size === "md" ? 40 : 44} viewBox="0 0 135 40" fill="none">
|
||||
<path
|
||||
d="M68.136 21.7511C65.784 21.7511 63.867 23.5401 63.867 26.0041C63.867 28.4531 65.784 30.2571 68.136 30.2571C70.489 30.2571 72.406 28.4531 72.406 26.0041C72.405 23.5401 70.488 21.7511 68.136 21.7511ZM68.136 28.5831C66.847 28.5831 65.736 27.5201 65.736 26.0051C65.736 24.4741 66.848 23.4271 68.136 23.4271C69.425 23.4271 70.536 24.4741 70.536 26.0051C70.536 27.5191 69.425 28.5831 68.136 28.5831ZM58.822 21.7511C56.47 21.7511 54.553 23.5401 54.553 26.0041C54.553 28.4531 56.47 30.2571 58.822 30.2571C61.175 30.2571 63.092 28.4531 63.092 26.0041C63.092 23.5401 61.175 21.7511 58.822 21.7511ZM58.822 28.5831C57.533 28.5831 56.422 27.5201 56.422 26.0051C56.422 24.4741 57.534 23.4271 58.822 23.4271C60.111 23.4271 61.222 24.4741 61.222 26.0051C61.223 27.5191 60.111 28.5831 58.822 28.5831ZM47.744 23.0571V24.8611H52.062C51.933 25.8761 51.595 26.6171 51.079 27.1321C50.451 27.7601 49.468 28.4531 47.744 28.4531C45.086 28.4531 43.008 26.3101 43.008 23.6521C43.008 20.9941 45.086 18.8511 47.744 18.8511C49.178 18.8511 50.225 19.4151 50.998 20.1401L52.271 18.8671C51.191 17.8361 49.758 17.0471 47.744 17.0471C44.103 17.0471 41.042 20.0111 41.042 23.6521C41.042 27.2931 44.103 30.2571 47.744 30.2571C49.709 30.2571 51.192 29.6121 52.351 28.4041C53.543 27.2121 53.914 25.5361 53.914 24.1831C53.914 23.7651 53.882 23.3781 53.817 23.0561H47.744V23.0571ZM93.052 24.4581C92.698 23.5081 91.618 21.7511 89.411 21.7511C87.22 21.7511 85.399 23.4751 85.399 26.0041C85.399 28.3881 87.204 30.2571 89.62 30.2571C91.569 30.2571 92.697 29.0651 93.165 28.3721L91.715 27.4051C91.232 28.1141 90.571 28.5811 89.62 28.5811C88.67 28.5811 87.993 28.1461 87.558 27.2921L93.245 24.9401L93.052 24.4581ZM87.252 25.8761C87.204 24.2321 88.525 23.3951 89.476 23.3951C90.217 23.3951 90.845 23.7661 91.055 24.2971L87.252 25.8761ZM82.629 30.0001H84.497V17.4991H82.629V30.0001ZM79.567 22.7021H79.503C79.084 22.2021 78.278 21.7511 77.264 21.7511C75.137 21.7511 73.188 23.6201 73.188 26.0211C73.188 28.4051 75.137 30.2581 77.264 30.2581C78.279 30.2581 79.084 29.8071 79.503 29.2921H79.567V29.9041C79.567 31.5311 78.697 32.4011 77.296 32.4011C76.152 32.4011 75.443 31.5801 75.153 30.8871L73.526 31.5641C73.993 32.6911 75.233 34.0771 77.296 34.0771C79.487 34.0771 81.34 32.7881 81.34 29.6461V22.0101H79.568V22.7021H79.567ZM77.425 28.5831C76.136 28.5831 75.057 27.5031 75.057 26.0211C75.057 24.5221 76.136 23.4271 77.425 23.4271C78.697 23.4271 79.696 24.5221 79.696 26.0211C79.696 27.5031 78.697 28.5831 77.425 28.5831ZM101.806 17.4991H97.335V30.0001H99.2V25.2641H101.805C103.873 25.2641 105.907 23.7671 105.907 21.3821C105.907 18.9971 103.874 17.4991 101.806 17.4991ZM101.854 23.5241H99.2V19.2391H101.854C103.249 19.2391 104.041 20.3941 104.041 21.3821C104.041 22.3501 103.249 23.5241 101.854 23.5241ZM113.386 21.7291C112.035 21.7291 110.636 22.3241 110.057 23.6431L111.713 24.3341C112.067 23.6431 112.727 23.4171 113.418 23.4171C114.383 23.4171 115.364 23.9961 115.38 25.0251V25.1541C115.042 24.9611 114.318 24.6721 113.434 24.6721C111.649 24.6721 109.831 25.6531 109.831 27.4861C109.831 29.1591 111.295 30.2361 112.935 30.2361C114.189 30.2361 114.881 29.6731 115.315 29.0131H115.379V29.9781H117.181V25.1851C117.182 22.9671 115.524 21.7291 113.386 21.7291ZM113.16 28.5801C112.55 28.5801 111.697 28.2741 111.697 27.5181C111.697 26.5531 112.759 26.1831 113.676 26.1831C114.495 26.1831 114.882 26.3601 115.38 26.6011C115.235 27.7601 114.238 28.5801 113.16 28.5801ZM123.743 22.0021L121.604 27.4221H121.54L119.32 22.0021H117.31L120.639 29.5771L118.741 33.7911H120.687L125.818 22.0021H123.743ZM106.937 30.0001H108.802V17.4991H106.937V30.0001Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M47.418 10.2429C47.418 11.0809 47.1701 11.7479 46.673 12.2459C46.109 12.8379 45.3731 13.1339 44.4691 13.1339C43.6031 13.1339 42.8661 12.8339 42.2611 12.2339C41.6551 11.6329 41.3521 10.8889 41.3521 10.0009C41.3521 9.11194 41.6551 8.36794 42.2611 7.76794C42.8661 7.16694 43.6031 6.86694 44.4691 6.86694C44.8991 6.86694 45.3101 6.95094 45.7001 7.11794C46.0911 7.28594 46.404 7.50894 46.6381 7.78794L46.111 8.31594C45.714 7.84094 45.167 7.60394 44.468 7.60394C43.836 7.60394 43.29 7.82594 42.829 8.26994C42.368 8.71394 42.1381 9.29094 42.1381 9.99994C42.1381 10.7089 42.368 11.2859 42.829 11.7299C43.29 12.1739 43.836 12.3959 44.468 12.3959C45.138 12.3959 45.6971 12.1729 46.1441 11.7259C46.4341 11.4349 46.602 11.0299 46.647 10.5109H44.468V9.78994H47.375C47.405 9.94694 47.418 10.0979 47.418 10.2429Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M52.0281 7.737H49.2961V9.639H51.7601V10.36H49.2961V12.262H52.0281V13H48.5251V7H52.0281V7.737Z" fill="white" />
|
||||
<path d="M55.279 13H54.508V7.737H52.832V7H56.955V7.737H55.279V13Z" fill="white" />
|
||||
<path d="M59.938 13V7H60.709V13H59.938Z" fill="white" />
|
||||
<path d="M64.1281 13H63.3572V7.737H61.6812V7H65.8042V7.737H64.1281V13Z" fill="white" />
|
||||
<path
|
||||
d="M73.6089 12.225C73.0189 12.831 72.2859 13.134 71.4089 13.134C70.5319 13.134 69.7989 12.831 69.2099 12.225C68.6199 11.619 68.3259 10.877 68.3259 9.99999C68.3259 9.12299 68.6199 8.38099 69.2099 7.77499C69.7989 7.16899 70.5319 6.86499 71.4089 6.86499C72.2809 6.86499 73.0129 7.16999 73.6049 7.77899C74.1969 8.38799 74.4929 9.12799 74.4929 9.99999C74.4929 10.877 74.1979 11.619 73.6089 12.225ZM69.7789 11.722C70.2229 12.172 70.7659 12.396 71.4089 12.396C72.0519 12.396 72.5959 12.171 73.0389 11.722C73.4829 11.272 73.7059 10.698 73.7059 9.99999C73.7059 9.30199 73.4829 8.72799 73.0389 8.27799C72.5959 7.82799 72.0519 7.60399 71.4089 7.60399C70.7659 7.60399 70.2229 7.82899 69.7789 8.27799C69.3359 8.72799 69.1129 9.30199 69.1129 9.99999C69.1129 10.698 69.3359 11.272 69.7789 11.722Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M75.5749 13V7H76.513L79.429 11.667H79.4619L79.429 10.511V7H80.1999V13H79.3949L76.344 8.106H76.3109L76.344 9.262V13H75.5749Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M47.418 10.2429C47.418 11.0809 47.1701 11.7479 46.673 12.2459C46.109 12.8379 45.3731 13.1339 44.4691 13.1339C43.6031 13.1339 42.8661 12.8339 42.2611 12.2339C41.6551 11.6329 41.3521 10.8889 41.3521 10.0009C41.3521 9.11194 41.6551 8.36794 42.2611 7.76794C42.8661 7.16694 43.6031 6.86694 44.4691 6.86694C44.8991 6.86694 45.3101 6.95094 45.7001 7.11794C46.0911 7.28594 46.404 7.50894 46.6381 7.78794L46.111 8.31594C45.714 7.84094 45.167 7.60394 44.468 7.60394C43.836 7.60394 43.29 7.82594 42.829 8.26994C42.368 8.71394 42.1381 9.29094 42.1381 9.99994C42.1381 10.7089 42.368 11.2859 42.829 11.7299C43.29 12.1739 43.836 12.3959 44.468 12.3959C45.138 12.3959 45.6971 12.1729 46.1441 11.7259C46.4341 11.4349 46.602 11.0299 46.647 10.5109H44.468V9.78994H47.375C47.405 9.94694 47.418 10.0979 47.418 10.2429Z"
|
||||
stroke="white"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M52.0281 7.737H49.2961V9.639H51.7601V10.36H49.2961V12.262H52.0281V13H48.5251V7H52.0281V7.737Z"
|
||||
stroke="white"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path d="M55.279 13H54.508V7.737H52.832V7H56.955V7.737H55.279V13Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||
<path d="M59.938 13V7H60.709V13H59.938Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||
<path d="M64.1281 13H63.3572V7.737H61.6812V7H65.8042V7.737H64.1281V13Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||
<path
|
||||
d="M73.6089 12.225C73.0189 12.831 72.2859 13.134 71.4089 13.134C70.5319 13.134 69.7989 12.831 69.2099 12.225C68.6199 11.619 68.3259 10.877 68.3259 9.99999C68.3259 9.12299 68.6199 8.38099 69.2099 7.77499C69.7989 7.16899 70.5319 6.86499 71.4089 6.86499C72.2809 6.86499 73.0129 7.16999 73.6049 7.77899C74.1969 8.38799 74.4929 9.12799 74.4929 9.99999C74.4929 10.877 74.1979 11.619 73.6089 12.225ZM69.7789 11.722C70.2229 12.172 70.7659 12.396 71.4089 12.396C72.0519 12.396 72.5959 12.171 73.0389 11.722C73.4829 11.272 73.7059 10.698 73.7059 9.99999C73.7059 9.30199 73.4829 8.72799 73.0389 8.27799C72.5959 7.82799 72.0519 7.60399 71.4089 7.60399C70.7659 7.60399 70.2229 7.82899 69.7789 8.27799C69.3359 8.72799 69.1129 9.30199 69.1129 9.99999C69.1129 10.698 69.3359 11.272 69.7789 11.722Z"
|
||||
stroke="white"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M75.5749 13V7H76.513L79.429 11.667H79.4619L79.429 10.511V7H80.1999V13H79.3949L76.344 8.106H76.3109L76.344 9.262V13H75.5749Z"
|
||||
stroke="white"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<g filter="url(#filter0_ii_1303_2188)">
|
||||
<path
|
||||
d="M10.4361 7.53803C10.1451 7.84603 9.97314 8.32403 9.97314 8.94303V31.059C9.97314 31.679 10.1451 32.156 10.4361 32.464L10.5101 32.536L22.8991 20.147V20.001V19.855L10.5101 7.46503L10.4361 7.53803Z"
|
||||
fill="url(#paint0_linear_1303_2188)"
|
||||
/>
|
||||
<path
|
||||
d="M27.0279 24.278L22.8989 20.147V20.001V19.855L27.0289 15.725L27.1219 15.778L32.0149 18.558C33.4119 19.352 33.4119 20.651 32.0149 21.446L27.1219 24.226L27.0279 24.278Z"
|
||||
fill="url(#paint1_linear_1303_2188)"
|
||||
/>
|
||||
<g filter="url(#filter1_i_1303_2188)">
|
||||
<path
|
||||
d="M27.122 24.225L22.898 20.001L10.436 32.464C10.896 32.952 11.657 33.012 12.514 32.526L27.122 24.225Z"
|
||||
fill="url(#paint2_linear_1303_2188)"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M27.122 15.777L12.514 7.47701C11.657 6.99001 10.896 7.05101 10.436 7.53901L22.899 20.002L27.122 15.777Z"
|
||||
fill="url(#paint3_linear_1303_2188)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_ii_1303_2188"
|
||||
x="9.97314"
|
||||
y="7.14093"
|
||||
width="23.0894"
|
||||
height="25.7207"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="-0.15" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_1303_2188" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="0.15" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_1303_2188" result="effect2_innerShadow_1303_2188" />
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_i_1303_2188"
|
||||
x="10.436"
|
||||
y="20.001"
|
||||
width="16.686"
|
||||
height="12.8607"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="-0.15" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" />
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_1303_2188" />
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1303_2188" x1="21.8009" y1="8.70903" x2="5.01895" y2="25.491" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#00A0FF" />
|
||||
<stop offset="0.0066" stopColor="#00A1FF" />
|
||||
<stop offset="0.2601" stopColor="#00BEFF" />
|
||||
<stop offset="0.5122" stopColor="#00D2FF" />
|
||||
<stop offset="0.7604" stopColor="#00DFFF" />
|
||||
<stop offset="1" stopColor="#00E3FF" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1303_2188" x1="33.8334" y1="20.001" x2="9.63753" y2="20.001" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#FFE000" />
|
||||
<stop offset="0.4087" stopColor="#FFBD00" />
|
||||
<stop offset="0.7754" stopColor="#FFA500" />
|
||||
<stop offset="1" stopColor="#FF9C00" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1303_2188" x1="24.8281" y1="22.2949" x2="2.06964" y2="45.0534" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#FF3A44" />
|
||||
<stop offset="1" stopColor="#C31162" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_1303_2188" x1="7.29743" y1="0.176806" x2="17.4597" y2="10.3391" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#32A071" />
|
||||
<stop offset="0.0685" stopColor="#2DA771" />
|
||||
<stop offset="0.4762" stopColor="#15CF74" />
|
||||
<stop offset="0.8009" stopColor="#06E775" />
|
||||
<stop offset="1" stopColor="#00F076" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const GooglePlayWhiteButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||
return (
|
||||
<a
|
||||
aria-label="Get it on Google Play"
|
||||
href="#"
|
||||
{...props}
|
||||
className={cx(
|
||||
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<svg width={size === "md" ? 135 : 149} height={size === "md" ? 40 : 44} viewBox="0 0 135 40" fill="none">
|
||||
<rect x="0.5" y="0.5" width="134" height="39" rx="6.5" stroke="white" />
|
||||
<path
|
||||
d="M68.136 21.7509C65.784 21.7509 63.867 23.5399 63.867 26.0039C63.867 28.4529 65.784 30.2569 68.136 30.2569C70.489 30.2569 72.406 28.4529 72.406 26.0039C72.405 23.5399 70.488 21.7509 68.136 21.7509ZM68.136 28.5829C66.847 28.5829 65.736 27.5199 65.736 26.0049C65.736 24.4739 66.848 23.4269 68.136 23.4269C69.425 23.4269 70.536 24.4739 70.536 26.0049C70.536 27.5189 69.425 28.5829 68.136 28.5829ZM58.822 21.7509C56.47 21.7509 54.553 23.5399 54.553 26.0039C54.553 28.4529 56.47 30.2569 58.822 30.2569C61.175 30.2569 63.092 28.4529 63.092 26.0039C63.092 23.5399 61.175 21.7509 58.822 21.7509ZM58.822 28.5829C57.533 28.5829 56.422 27.5199 56.422 26.0049C56.422 24.4739 57.534 23.4269 58.822 23.4269C60.111 23.4269 61.222 24.4739 61.222 26.0049C61.223 27.5189 60.111 28.5829 58.822 28.5829ZM47.744 23.0569V24.8609H52.062C51.933 25.8759 51.595 26.6169 51.079 27.1319C50.451 27.7599 49.468 28.4529 47.744 28.4529C45.086 28.4529 43.008 26.3099 43.008 23.6519C43.008 20.9939 45.086 18.8509 47.744 18.8509C49.178 18.8509 50.225 19.4149 50.998 20.1399L52.271 18.8669C51.191 17.8359 49.758 17.0469 47.744 17.0469C44.103 17.0469 41.042 20.0109 41.042 23.6519C41.042 27.2929 44.103 30.2569 47.744 30.2569C49.709 30.2569 51.192 29.6119 52.351 28.4039C53.543 27.2119 53.914 25.5359 53.914 24.1829C53.914 23.7649 53.882 23.3779 53.817 23.0559H47.744V23.0569ZM93.052 24.4579C92.698 23.5079 91.618 21.7509 89.411 21.7509C87.22 21.7509 85.399 23.4749 85.399 26.0039C85.399 28.3879 87.204 30.2569 89.62 30.2569C91.569 30.2569 92.697 29.0649 93.165 28.3719L91.715 27.4049C91.232 28.1139 90.571 28.5809 89.62 28.5809C88.67 28.5809 87.993 28.1459 87.558 27.2919L93.245 24.9399L93.052 24.4579ZM87.252 25.8759C87.204 24.2319 88.525 23.3949 89.476 23.3949C90.217 23.3949 90.845 23.7659 91.055 24.2969L87.252 25.8759ZM82.629 29.9999H84.497V17.4989H82.629V29.9999ZM79.567 22.7019H79.503C79.084 22.2019 78.278 21.7509 77.264 21.7509C75.137 21.7509 73.188 23.6199 73.188 26.0209C73.188 28.4049 75.137 30.2579 77.264 30.2579C78.279 30.2579 79.084 29.8069 79.503 29.2919H79.567V29.9039C79.567 31.5309 78.697 32.4009 77.296 32.4009C76.152 32.4009 75.443 31.5799 75.153 30.8869L73.526 31.5639C73.993 32.6909 75.233 34.0769 77.296 34.0769C79.487 34.0769 81.34 32.7879 81.34 29.6459V22.0099H79.568V22.7019H79.567ZM77.425 28.5829C76.136 28.5829 75.057 27.5029 75.057 26.0209C75.057 24.5219 76.136 23.4269 77.425 23.4269C78.697 23.4269 79.696 24.5219 79.696 26.0209C79.696 27.5029 78.697 28.5829 77.425 28.5829ZM101.806 17.4989H97.335V29.9999H99.2V25.2639H101.805C103.873 25.2639 105.907 23.7669 105.907 21.3819C105.907 18.9969 103.874 17.4989 101.806 17.4989ZM101.854 23.5239H99.2V19.2389H101.854C103.249 19.2389 104.041 20.3939 104.041 21.3819C104.041 22.3499 103.249 23.5239 101.854 23.5239ZM113.386 21.7289C112.035 21.7289 110.636 22.3239 110.057 23.6429L111.713 24.3339C112.067 23.6429 112.727 23.4169 113.418 23.4169C114.383 23.4169 115.364 23.9959 115.38 25.0249V25.1539C115.042 24.9609 114.318 24.6719 113.434 24.6719C111.649 24.6719 109.831 25.6529 109.831 27.4859C109.831 29.1589 111.295 30.2359 112.935 30.2359C114.189 30.2359 114.881 29.6729 115.315 29.0129H115.379V29.9779H117.181V25.1849C117.182 22.9669 115.524 21.7289 113.386 21.7289ZM113.16 28.5799C112.55 28.5799 111.697 28.2739 111.697 27.5179C111.697 26.5529 112.759 26.1829 113.676 26.1829C114.495 26.1829 114.882 26.3599 115.38 26.6009C115.235 27.7599 114.238 28.5799 113.16 28.5799ZM123.743 22.0019L121.604 27.4219H121.54L119.32 22.0019H117.31L120.639 29.5769L118.741 33.7909H120.687L125.818 22.0019H123.743ZM106.937 29.9999H108.802V17.4989H106.937V29.9999Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M47.418 10.2432C47.418 11.0812 47.1701 11.7482 46.673 12.2462C46.109 12.8382 45.3731 13.1342 44.4691 13.1342C43.6031 13.1342 42.8661 12.8342 42.2611 12.2342C41.6551 11.6332 41.3521 10.8892 41.3521 10.0012C41.3521 9.11219 41.6551 8.36819 42.2611 7.76819C42.8661 7.16719 43.6031 6.86719 44.4691 6.86719C44.8991 6.86719 45.3101 6.95119 45.7001 7.11819C46.0911 7.28619 46.404 7.50919 46.6381 7.78819L46.111 8.31619C45.714 7.84119 45.167 7.60419 44.468 7.60419C43.836 7.60419 43.29 7.82619 42.829 8.27019C42.368 8.71419 42.1381 9.29119 42.1381 10.0002C42.1381 10.7092 42.368 11.2862 42.829 11.7302C43.29 12.1742 43.836 12.3962 44.468 12.3962C45.138 12.3962 45.6971 12.1732 46.1441 11.7262C46.4341 11.4352 46.602 11.0302 46.647 10.5112H44.468V9.79019H47.375C47.405 9.94719 47.418 10.0982 47.418 10.2432Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M52.0281 7.73724H49.2961V9.63924H51.7601V10.3602H49.2961V12.2622H52.0281V13.0002H48.5251V7.00024H52.0281V7.73724Z" fill="white" />
|
||||
<path d="M55.279 13.0002H54.508V7.73724H52.832V7.00024H56.955V7.73724H55.279V13.0002Z" fill="white" />
|
||||
<path d="M59.938 13.0002V7.00024H60.709V13.0002H59.938Z" fill="white" />
|
||||
<path d="M64.1281 13.0002H63.3572V7.73724H61.6812V7.00024H65.8042V7.73724H64.1281V13.0002Z" fill="white" />
|
||||
<path
|
||||
d="M73.6089 12.2252C73.0189 12.8312 72.2859 13.1342 71.4089 13.1342C70.5319 13.1342 69.7989 12.8312 69.2099 12.2252C68.6199 11.6192 68.3259 10.8772 68.3259 10.0002C68.3259 9.12323 68.6199 8.38123 69.2099 7.77523C69.7989 7.16923 70.5319 6.86523 71.4089 6.86523C72.2809 6.86523 73.0129 7.17023 73.6049 7.77923C74.1969 8.38823 74.4929 9.12823 74.4929 10.0002C74.4929 10.8772 74.1979 11.6192 73.6089 12.2252ZM69.7789 11.7222C70.2229 12.1722 70.7659 12.3962 71.4089 12.3962C72.0519 12.3962 72.5959 12.1712 73.0389 11.7222C73.4829 11.2722 73.7059 10.6982 73.7059 10.0002C73.7059 9.30223 73.4829 8.72823 73.0389 8.27823C72.5959 7.82823 72.0519 7.60423 71.4089 7.60423C70.7659 7.60423 70.2229 7.82923 69.7789 8.27823C69.3359 8.72823 69.1129 9.30223 69.1129 10.0002C69.1129 10.6982 69.3359 11.2722 69.7789 11.7222Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M75.5749 13.0002V7.00024H76.513L79.429 11.6672H79.4619L79.429 10.5112V7.00024H80.1999V13.0002H79.3949L76.344 8.10625H76.3109L76.344 9.26224V13.0002H75.5749Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M47.418 10.2432C47.418 11.0812 47.1701 11.7482 46.673 12.2462C46.109 12.8382 45.3731 13.1342 44.4691 13.1342C43.6031 13.1342 42.8661 12.8342 42.2611 12.2342C41.6551 11.6332 41.3521 10.8892 41.3521 10.0012C41.3521 9.11219 41.6551 8.36819 42.2611 7.76819C42.8661 7.16719 43.6031 6.86719 44.4691 6.86719C44.8991 6.86719 45.3101 6.95119 45.7001 7.11819C46.0911 7.28619 46.404 7.50919 46.6381 7.78819L46.111 8.31619C45.714 7.84119 45.167 7.60419 44.468 7.60419C43.836 7.60419 43.29 7.82619 42.829 8.27019C42.368 8.71419 42.1381 9.29119 42.1381 10.0002C42.1381 10.7092 42.368 11.2862 42.829 11.7302C43.29 12.1742 43.836 12.3962 44.468 12.3962C45.138 12.3962 45.6971 12.1732 46.1441 11.7262C46.4341 11.4352 46.602 11.0302 46.647 10.5112H44.468V9.79019H47.375C47.405 9.94719 47.418 10.0982 47.418 10.2432Z"
|
||||
stroke="white"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M52.0281 7.73724H49.2961V9.63924H51.7601V10.3602H49.2961V12.2622H52.0281V13.0002H48.5251V7.00024H52.0281V7.73724Z"
|
||||
stroke="white"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path d="M55.279 13.0002H54.508V7.73724H52.832V7.00024H56.955V7.73724H55.279V13.0002Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||
<path d="M59.938 13.0002V7.00024H60.709V13.0002H59.938Z" stroke="white" strokeWidth="0.2" strokeMiterlimit="10" />
|
||||
<path
|
||||
d="M64.1281 13.0002H63.3572V7.73724H61.6812V7.00024H65.8042V7.73724H64.1281V13.0002Z"
|
||||
stroke="white"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M73.6089 12.2252C73.0189 12.8312 72.2859 13.1342 71.4089 13.1342C70.5319 13.1342 69.7989 12.8312 69.2099 12.2252C68.6199 11.6192 68.3259 10.8772 68.3259 10.0002C68.3259 9.12323 68.6199 8.38123 69.2099 7.77523C69.7989 7.16923 70.5319 6.86523 71.4089 6.86523C72.2809 6.86523 73.0129 7.17023 73.6049 7.77923C74.1969 8.38823 74.4929 9.12823 74.4929 10.0002C74.4929 10.8772 74.1979 11.6192 73.6089 12.2252ZM69.7789 11.7222C70.2229 12.1722 70.7659 12.3962 71.4089 12.3962C72.0519 12.3962 72.5959 12.1712 73.0389 11.7222C73.4829 11.2722 73.7059 10.6982 73.7059 10.0002C73.7059 9.30223 73.4829 8.72823 73.0389 8.27823C72.5959 7.82823 72.0519 7.60423 71.4089 7.60423C70.7659 7.60423 70.2229 7.82923 69.7789 8.27823C69.3359 8.72823 69.1129 9.30223 69.1129 10.0002C69.1129 10.6982 69.3359 11.2722 69.7789 11.7222Z"
|
||||
stroke="white"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M75.5749 13.0002V7.00024H76.513L79.429 11.6672H79.4619L79.429 10.5112V7.00024H80.1999V13.0002H79.3949L76.344 8.10625H76.3109L76.344 9.26224V13.0002H75.5749Z"
|
||||
stroke="white"
|
||||
strokeWidth="0.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.1565 7.96617C10.0384 8.23367 9.97314 8.56144 9.97314 8.94269V31.0587C9.97314 31.4408 10.0385 31.7686 10.1567 32.036L22.1907 20.0003L10.1565 7.96617ZM10.8517 32.7552C11.2978 32.9461 11.8797 32.8855 12.5141 32.5257L26.6712 24.4809L22.8978 20.7074L10.8517 32.7552ZM27.5737 23.9691L32.0151 21.4457C33.4121 20.6507 33.4121 19.3517 32.0151 18.5577L27.5717 16.0328L23.6048 20.0003L27.5737 23.9691ZM26.6699 15.5204L12.5141 7.4767C11.8796 7.11612 11.2977 7.05596 10.8516 7.2471L22.8977 19.2932L26.6699 15.5204Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppStoreButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||
return (
|
||||
<a
|
||||
aria-label="Download on the App Store"
|
||||
href="#"
|
||||
{...props}
|
||||
className={cx(
|
||||
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<svg width={size === "md" ? 120 : 132} height={size === "md" ? 40 : 44} viewBox="0 0 120 40" fill="none">
|
||||
<path
|
||||
d="M81.5257 19.2009V21.4919H80.0896V22.9944H81.5257V28.0994C81.5257 29.8425 82.3143 30.5398 84.2981 30.5398C84.6468 30.5398 84.9788 30.4983 85.2693 30.4485V28.9626C85.0203 28.9875 84.8626 29.0041 84.5887 29.0041C83.7005 29.0041 83.3104 28.5891 83.3104 27.6428V22.9944H85.2693V21.4919H83.3104V19.2009H81.5257Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M90.3232 30.6643C92.9628 30.6643 94.5815 28.8962 94.5815 25.9661C94.5815 23.0525 92.9545 21.2761 90.3232 21.2761C87.6835 21.2761 86.0566 23.0525 86.0566 25.9661C86.0566 28.8962 87.6752 30.6643 90.3232 30.6643ZM90.3232 29.0789C88.7709 29.0789 87.8994 27.9416 87.8994 25.9661C87.8994 24.0071 88.7709 22.8616 90.3232 22.8616C91.8671 22.8616 92.747 24.0071 92.747 25.9661C92.747 27.9333 91.8671 29.0789 90.3232 29.0789Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M95.9664 30.49H97.7511V25.1526C97.7511 23.8826 98.7056 23.0276 100.059 23.0276C100.374 23.0276 100.905 23.0857 101.055 23.1355V21.3757C100.864 21.3259 100.524 21.301 100.258 21.301C99.0792 21.301 98.0748 21.9485 97.8175 22.8367H97.6846V21.4504H95.9664V30.49Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M105.486 22.7952C106.806 22.7952 107.669 23.7165 107.711 25.136H103.145C103.245 23.7248 104.166 22.7952 105.486 22.7952ZM107.702 28.0496C107.37 28.7551 106.632 29.1453 105.552 29.1453C104.125 29.1453 103.203 28.1409 103.145 26.5554V26.4558H109.529V25.8332C109.529 22.9944 108.009 21.2761 105.494 21.2761C102.946 21.2761 101.327 23.1106 101.327 25.9993C101.327 28.8879 102.913 30.6643 105.503 30.6643C107.57 30.6643 109.014 29.6682 109.421 28.0496H107.702Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M69.8221 27.1518C69.9598 29.3715 71.8095 30.7911 74.5626 30.7911C77.505 30.7911 79.3462 29.3027 79.3462 26.9281C79.3462 25.0612 78.2966 24.0287 75.7499 23.4351L74.382 23.0996C72.7645 22.721 72.1106 22.2134 72.1106 21.3272C72.1106 20.2088 73.1259 19.4775 74.6487 19.4775C76.0941 19.4775 77.0921 20.1916 77.2727 21.3358H79.1483C79.0365 19.2452 77.1953 17.774 74.6745 17.774C71.9644 17.774 70.1576 19.2452 70.1576 21.4563C70.1576 23.2802 71.1815 24.3643 73.427 24.8891L75.0272 25.2763C76.6705 25.6634 77.3932 26.2312 77.3932 27.1776C77.3932 28.2789 76.2575 29.079 74.7089 29.079C73.0484 29.079 71.8955 28.3305 71.7321 27.1518H69.8221Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M51.3348 21.301C50.1063 21.301 49.0437 21.9153 48.4959 22.9446H48.3631V21.4504H46.6448V33.4949H48.4295V29.1204H48.5706C49.0437 30.0749 50.0647 30.6394 51.3514 30.6394C53.6341 30.6394 55.0867 28.8381 55.0867 25.9661C55.0867 23.094 53.6341 21.301 51.3348 21.301ZM50.8284 29.0373C49.3343 29.0373 48.3963 27.8586 48.3963 25.9744C48.3963 24.0818 49.3343 22.9031 50.8367 22.9031C52.3475 22.9031 53.2522 24.0569 53.2522 25.9661C53.2522 27.8835 52.3475 29.0373 50.8284 29.0373Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M61.3316 21.301C60.103 21.301 59.0405 21.9153 58.4927 22.9446H58.3599V21.4504H56.6416V33.4949H58.4263V29.1204H58.5674C59.0405 30.0749 60.0615 30.6394 61.3482 30.6394C63.6309 30.6394 65.0835 28.8381 65.0835 25.9661C65.0835 23.094 63.6309 21.301 61.3316 21.301ZM60.8252 29.0373C59.3311 29.0373 58.3931 27.8586 58.3931 25.9744C58.3931 24.0818 59.3311 22.9031 60.8335 22.9031C62.3443 22.9031 63.249 24.0569 63.249 25.9661C63.249 27.8835 62.3443 29.0373 60.8252 29.0373Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M43.4428 30.49H45.4905L41.008 18.0751H38.9346L34.4521 30.49H36.431L37.5752 27.1948H42.3072L43.4428 30.49ZM39.8724 20.3292H40.0186L41.8168 25.5774H38.0656L39.8724 20.3292Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M35.6514 8.71094V14.7H37.8137C39.5984 14.7 40.6318 13.6001 40.6318 11.6868C40.6318 9.80249 39.5901 8.71094 37.8137 8.71094H35.6514ZM36.5811 9.55762H37.71C38.9509 9.55762 39.6855 10.3462 39.6855 11.6992C39.6855 13.073 38.9634 13.8533 37.71 13.8533H36.5811V9.55762Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M43.7969 14.7871C45.1167 14.7871 45.9261 13.9031 45.9261 12.438C45.9261 10.9812 45.1126 10.093 43.7969 10.093C42.4771 10.093 41.6636 10.9812 41.6636 12.438C41.6636 13.9031 42.4729 14.7871 43.7969 14.7871ZM43.7969 13.9944C43.0208 13.9944 42.585 13.4258 42.585 12.438C42.585 11.4585 43.0208 10.8857 43.7969 10.8857C44.5689 10.8857 45.0088 11.4585 45.0088 12.438C45.0088 13.4216 44.5689 13.9944 43.7969 13.9944Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M52.8182 10.1802H51.9259L51.1207 13.6292H51.0501L50.1205 10.1802H49.2655L48.3358 13.6292H48.2694L47.4601 10.1802H46.5553L47.8004 14.7H48.7176L49.6473 11.3713H49.7179L50.6517 14.7H51.5772L52.8182 10.1802Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M53.8458 14.7H54.7382V12.0562C54.7382 11.3506 55.1574 10.9106 55.8173 10.9106C56.4772 10.9106 56.7926 11.2717 56.7926 11.998V14.7H57.685V11.7739C57.685 10.699 57.1288 10.093 56.1203 10.093C55.4396 10.093 54.9914 10.396 54.7714 10.8982H54.705V10.1802H53.8458V14.7Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M59.0903 14.7H59.9826V8.41626H59.0903V14.7Z" fill="white" />
|
||||
<path
|
||||
d="M63.3386 14.7871C64.6584 14.7871 65.4678 13.9031 65.4678 12.438C65.4678 10.9812 64.6543 10.093 63.3386 10.093C62.0188 10.093 61.2053 10.9812 61.2053 12.438C61.2053 13.9031 62.0146 14.7871 63.3386 14.7871ZM63.3386 13.9944C62.5625 13.9944 62.1267 13.4258 62.1267 12.438C62.1267 11.4585 62.5625 10.8857 63.3386 10.8857C64.1106 10.8857 64.5505 11.4585 64.5505 12.438C64.5505 13.4216 64.1106 13.9944 63.3386 13.9944Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M68.1265 14.0234C67.6409 14.0234 67.2881 13.7869 67.2881 13.3801C67.2881 12.9817 67.5704 12.77 68.1929 12.7285L69.2969 12.658V13.0356C69.2969 13.5959 68.7989 14.0234 68.1265 14.0234ZM67.8982 14.7747C68.4917 14.7747 68.9856 14.5173 69.2554 14.0649H69.326V14.7H70.1851V11.6121C70.1851 10.6575 69.5459 10.093 68.4129 10.093C67.3877 10.093 66.6573 10.5911 66.566 11.3672H67.4292C67.5289 11.0476 67.8733 10.865 68.3714 10.865C68.9815 10.865 69.2969 11.1348 69.2969 11.6121V12.0022L68.0726 12.0728C66.9976 12.1392 66.3916 12.6082 66.3916 13.4216C66.3916 14.2476 67.0267 14.7747 67.8982 14.7747Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M73.2132 14.7747C73.8358 14.7747 74.3629 14.48 74.6327 13.9861H74.7032V14.7H75.5582V8.41626H74.6659V10.8982H74.5995C74.3546 10.4001 73.8316 10.1055 73.2132 10.1055C72.0719 10.1055 71.3373 11.0103 71.3373 12.438C71.3373 13.8699 72.0636 14.7747 73.2132 14.7747ZM73.4664 10.9065C74.2135 10.9065 74.6825 11.5 74.6825 12.4421C74.6825 13.3884 74.2176 13.9736 73.4664 13.9736C72.711 13.9736 72.2586 13.3967 72.2586 12.438C72.2586 11.4875 72.7152 10.9065 73.4664 10.9065Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M81.3447 14.7871C82.6645 14.7871 83.4738 13.9031 83.4738 12.438C83.4738 10.9812 82.6604 10.093 81.3447 10.093C80.0249 10.093 79.2114 10.9812 79.2114 12.438C79.2114 13.9031 80.0207 14.7871 81.3447 14.7871ZM81.3447 13.9944C80.5686 13.9944 80.1328 13.4258 80.1328 12.438C80.1328 11.4585 80.5686 10.8857 81.3447 10.8857C82.1166 10.8857 82.5566 11.4585 82.5566 12.438C82.5566 13.4216 82.1166 13.9944 81.3447 13.9944Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M84.655 14.7H85.5474V12.0562C85.5474 11.3506 85.9666 10.9106 86.6265 10.9106C87.2864 10.9106 87.6018 11.2717 87.6018 11.998V14.7H88.4941V11.7739C88.4941 10.699 87.938 10.093 86.9294 10.093C86.2488 10.093 85.8005 10.396 85.5806 10.8982H85.5142V10.1802H84.655V14.7Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M92.6039 9.05542V10.2009H91.8858V10.9521H92.6039V13.5046C92.6039 14.3762 92.9981 14.7249 93.9901 14.7249C94.1644 14.7249 94.3304 14.7041 94.4757 14.6792V13.9363C94.3512 13.9487 94.2723 13.957 94.1353 13.957C93.6913 13.957 93.4962 13.7495 93.4962 13.2764V10.9521H94.4757V10.2009H93.4962V9.05542H92.6039Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M95.6735 14.7H96.5658V12.0603C96.5658 11.3755 96.9726 10.9148 97.703 10.9148C98.3339 10.9148 98.6701 11.28 98.6701 12.0022V14.7H99.5624V11.7822C99.5624 10.7073 98.9689 10.0972 98.006 10.0972C97.3253 10.0972 96.848 10.4001 96.6281 10.9065H96.5575V8.41626H95.6735V14.7Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M102.781 10.8525C103.441 10.8525 103.873 11.3132 103.894 12.0229H101.611C101.661 11.3174 102.122 10.8525 102.781 10.8525ZM103.89 13.4797C103.724 13.8325 103.354 14.0276 102.815 14.0276C102.101 14.0276 101.64 13.5254 101.611 12.7327V12.6829H104.803V12.3716C104.803 10.9521 104.043 10.093 102.786 10.093C101.511 10.093 100.702 11.0103 100.702 12.4546C100.702 13.8989 101.495 14.7871 102.79 14.7871C103.823 14.7871 104.545 14.2891 104.749 13.4797H103.89Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M24.769 20.3008C24.7907 18.6198 25.6934 17.0292 27.1256 16.1488C26.2221 14.8584 24.7088 14.0403 23.1344 13.9911C21.4552 13.8148 19.8272 14.9959 18.9715 14.9959C18.0992 14.9959 16.7817 14.0086 15.363 14.0378C13.5137 14.0975 11.7898 15.1489 10.8901 16.7656C8.95607 20.1141 10.3987 25.0351 12.2513 27.7417C13.1782 29.0671 14.2615 30.5475 15.6789 30.495C17.066 30.4375 17.584 29.6105 19.2583 29.6105C20.9171 29.6105 21.4031 30.495 22.8493 30.4616C24.3377 30.4375 25.2754 29.1304 26.1698 27.7925C26.8358 26.8481 27.3483 25.8044 27.6882 24.7C25.9391 23.9602 24.771 22.2 24.769 20.3008Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M22.0373 12.2111C22.8489 11.2369 23.2487 9.98469 23.1518 8.72046C21.912 8.85068 20.7668 9.44324 19.9443 10.3801C19.14 11.2954 18.7214 12.5255 18.8006 13.7415C20.0408 13.7542 21.2601 13.1777 22.0373 12.2111Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const GalaxyStoreButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||
return (
|
||||
<a
|
||||
aria-label="Available on Galaxy Store"
|
||||
href="#"
|
||||
{...props}
|
||||
className={cx(
|
||||
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<svg width={size === "md" ? 147 : 162} height={size === "md" ? 40 : 44} viewBox="0 0 147 40" fill="none">
|
||||
<path d="M64.7516 20.3987H66.7715V31.3885H64.7516V20.3987Z" fill="white" />
|
||||
<path
|
||||
d="M42.5 25.9699C42.5 22.8811 44.8314 20.4009 48.039 20.4009C50.2816 20.4009 52.0489 21.5444 52.9695 23.1779L51.1875 24.2473C50.5193 23.0146 49.4799 22.3611 48.054 22.3611C46.0343 22.3611 44.5047 23.9946 44.5047 25.9699C44.5047 27.9745 46.0196 29.5786 48.1281 29.5786C49.7469 29.5786 50.8757 28.6578 51.2616 27.2322H47.8017V25.3017H53.4298V26.1037C53.4298 29.0289 51.3657 31.5392 48.1281 31.5392C44.7423 31.5392 42.5 28.9695 42.5 25.9699Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M54.3525 27.0543C54.3525 24.1732 56.4613 22.525 58.6592 22.525C59.8027 22.525 60.8274 22.9999 61.4512 23.7426V22.7032H63.4558V31.3907H61.4512V30.2616C60.8124 31.0636 59.773 31.5689 58.6295 31.5689C56.5354 31.5689 54.3525 29.8904 54.3525 27.0543ZM61.555 27.0246C61.555 25.5543 60.4412 24.3514 58.9562 24.3514C57.4713 24.3514 56.3278 25.5249 56.3278 27.0246C56.3278 28.539 57.4713 29.7272 58.9562 29.7272C60.4412 29.7272 61.555 28.5243 61.555 27.0246Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M67.7938 27.0543C67.7938 24.1732 69.9026 22.525 72.1005 22.525C73.244 22.525 74.2686 22.9999 74.8925 23.7426V22.7032H76.8971V31.3907H74.8925V30.2616C74.2539 31.0636 73.2146 31.5689 72.0707 31.5689C69.977 31.5689 67.7938 29.8904 67.7938 27.0543ZM74.9966 27.0246C74.9966 25.5543 73.8828 24.3514 72.3981 24.3514C70.9128 24.3514 69.7693 25.5249 69.7693 27.0246C69.7693 28.539 70.9128 29.7272 72.3981 29.7272C73.8825 29.7272 74.9966 28.5243 74.9966 27.0246Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M80.8048 26.9652L77.6566 22.7032H80.1072L82.0818 25.4652L84.0424 22.7032H86.4182L83.2994 26.9949L86.6261 31.3907H84.1759L82.0524 28.4949L79.988 31.3907H77.5375L80.8048 26.9652Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M90.3846 30.9598L86.8206 22.7029H88.9588L91.3796 28.554L93.6663 22.7029H95.7757L90.5926 35.4H88.5582L90.3846 30.9598Z" fill="white" />
|
||||
<path
|
||||
d="M99.8907 29.5936L101.732 28.1384C102.282 29.074 103.128 29.6083 104.108 29.6083C105.178 29.6083 105.757 28.911 105.757 28.1531C105.757 27.2325 104.658 26.9505 103.499 26.594C102.044 26.1334 100.426 25.5693 100.426 23.5049C100.426 21.7676 101.94 20.4012 104.019 20.4012C105.772 20.4012 106.781 21.0697 107.658 21.9756L105.994 23.2376C105.534 22.5547 104.895 22.1982 104.034 22.1982C103.054 22.1982 102.519 22.7329 102.519 23.4305C102.519 24.292 103.559 24.5743 104.732 24.9605C106.203 25.4355 107.866 26.089 107.866 28.1681C107.866 29.8757 106.499 31.5395 104.123 31.5395C102.163 31.5392 100.871 30.7071 99.8907 29.5936Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M108.03 22.7029H109.515V21.3812L111.535 20V22.7029H113.302V24.4999H111.535V27.7519C111.535 29.2669 111.743 29.5045 113.302 29.5045V31.3904H113.02C110.332 31.3904 109.515 30.5292 109.515 27.7672V24.4999H108.03L108.03 22.7029Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M113.838 27.0543C113.838 24.5296 115.827 22.5247 118.337 22.5247C120.832 22.5247 122.837 24.5296 122.837 27.0543C122.837 29.5639 120.832 31.5689 118.337 31.5689C115.827 31.5689 113.838 29.5639 113.838 27.0543ZM120.862 27.0543C120.862 25.599 119.747 24.4108 118.337 24.4108C116.896 24.4108 115.812 25.599 115.812 27.0543C115.812 28.4949 116.896 29.6828 118.337 29.6828C119.747 29.6828 120.862 28.4949 120.862 27.0543Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M125.853 22.7029V23.9949C126.254 23.1335 126.981 22.7029 128.08 22.7029C128.704 22.7029 129.223 22.8514 129.61 23.0741L128.852 24.9749C128.555 24.782 128.214 24.6334 127.649 24.6334C126.491 24.6334 125.867 25.2573 125.867 26.7569V31.3904H123.862V22.7029H125.853Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M129.834 27.069C129.834 24.5296 131.808 22.5247 134.348 22.5247C136.932 22.5247 138.818 24.4255 138.818 26.9949V27.7519H131.749C132.017 28.9701 132.997 29.8016 134.423 29.8016C135.536 29.8016 136.413 29.1928 136.828 28.2719L138.477 29.2222C137.719 30.6186 136.353 31.5686 134.423 31.5686C131.69 31.5686 129.834 29.5786 129.834 27.069ZM131.853 26.074H136.784C136.487 24.9158 135.596 24.292 134.348 24.292C133.145 24.292 132.21 25.0196 131.853 26.074Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M46.6986 13.974H43.9018L43.3866 15.4H42.5034L44.8218 9.0244H45.7878L48.097 15.4H47.2138L46.6986 13.974ZM46.4594 13.2932L45.3002 10.0548L44.141 13.2932H46.4594Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M50.4322 14.6272L51.9962 10.3584H52.8886L50.9106 15.4H49.9354L47.9574 10.3584H48.859L50.4322 14.6272Z" fill="white" />
|
||||
<path
|
||||
d="M53.4917 12.8608C53.4917 12.3456 53.5959 11.8948 53.8045 11.5084C54.013 11.1159 54.2982 10.8123 54.6601 10.5976C55.0281 10.3829 55.4359 10.2756 55.8837 10.2756C56.3253 10.2756 56.7086 10.3707 57.0337 10.5608C57.3587 10.7509 57.601 10.9901 57.7605 11.2784V10.3584H58.6069V15.4H57.7605V14.4616C57.5949 14.756 57.3465 15.0013 57.0153 15.1976C56.6902 15.3877 56.3099 15.4828 55.8745 15.4828C55.4267 15.4828 55.0219 15.3724 54.6601 15.1516C54.2982 14.9308 54.013 14.6211 53.8045 14.2224C53.5959 13.8237 53.4917 13.3699 53.4917 12.8608ZM57.7605 12.87C57.7605 12.4897 57.6838 12.1585 57.5305 11.8764C57.3771 11.5943 57.1686 11.3796 56.9049 11.2324C56.6473 11.0791 56.3621 11.0024 56.0493 11.0024C55.7365 11.0024 55.4513 11.076 55.1937 11.2232C54.9361 11.3704 54.7306 11.5851 54.5773 11.8672C54.4239 12.1493 54.3473 12.4805 54.3473 12.8608C54.3473 13.2472 54.4239 13.5845 54.5773 13.8728C54.7306 14.1549 54.9361 14.3727 55.1937 14.526C55.4513 14.6732 55.7365 14.7468 56.0493 14.7468C56.3621 14.7468 56.6473 14.6732 56.9049 14.526C57.1686 14.3727 57.3771 14.1549 57.5305 13.8728C57.6838 13.5845 57.7605 13.2503 57.7605 12.87Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M60.2701 9.5396C60.1106 9.5396 59.9757 9.4844 59.8653 9.374C59.7549 9.2636 59.6997 9.12867 59.6997 8.9692C59.6997 8.80974 59.7549 8.6748 59.8653 8.5644C59.9757 8.454 60.1106 8.3988 60.2701 8.3988C60.4234 8.3988 60.5522 8.454 60.6565 8.5644C60.7669 8.6748 60.8221 8.80974 60.8221 8.9692C60.8221 9.12867 60.7669 9.2636 60.6565 9.374C60.5522 9.4844 60.4234 9.5396 60.2701 9.5396ZM60.6749 10.3584V15.4H59.8377V10.3584H60.6749Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M62.7549 8.592V15.4H61.9177V8.592H62.7549Z" fill="white" />
|
||||
<path
|
||||
d="M63.869 12.8608C63.869 12.3456 63.9732 11.8948 64.1818 11.5084C64.3903 11.1159 64.6755 10.8123 65.0374 10.5976C65.4054 10.3829 65.8132 10.2756 66.261 10.2756C66.7026 10.2756 67.0859 10.3707 67.411 10.5608C67.736 10.7509 67.9783 10.9901 68.1378 11.2784V10.3584H68.9842V15.4H68.1378V14.4616C67.9722 14.756 67.7238 15.0013 67.3926 15.1976C67.0675 15.3877 66.6872 15.4828 66.2518 15.4828C65.804 15.4828 65.3992 15.3724 65.0374 15.1516C64.6755 14.9308 64.3903 14.6211 64.1818 14.2224C63.9732 13.8237 63.869 13.3699 63.869 12.8608ZM68.1378 12.87C68.1378 12.4897 68.0611 12.1585 67.9078 11.8764C67.7544 11.5943 67.5459 11.3796 67.2822 11.2324C67.0246 11.0791 66.7394 11.0024 66.4266 11.0024C66.1138 11.0024 65.8286 11.076 65.571 11.2232C65.3134 11.3704 65.1079 11.5851 64.9546 11.8672C64.8012 12.1493 64.7246 12.4805 64.7246 12.8608C64.7246 13.2472 64.8012 13.5845 64.9546 13.8728C65.1079 14.1549 65.3134 14.3727 65.571 14.526C65.8286 14.6732 66.1138 14.7468 66.4266 14.7468C66.7394 14.7468 67.0246 14.6732 67.2822 14.526C67.5459 14.3727 67.7544 14.1549 67.9078 13.8728C68.0611 13.5845 68.1378 13.2503 68.1378 12.87Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M71.3282 11.2968C71.4999 10.9963 71.7514 10.7509 72.0826 10.5608C72.4138 10.3707 72.791 10.2756 73.2142 10.2756C73.668 10.2756 74.0759 10.3829 74.4378 10.5976C74.7996 10.8123 75.0848 11.1159 75.2934 11.5084C75.5019 11.8948 75.6062 12.3456 75.6062 12.8608C75.6062 13.3699 75.5019 13.8237 75.2934 14.2224C75.0848 14.6211 74.7966 14.9308 74.4286 15.1516C74.0667 15.3724 73.6619 15.4828 73.2142 15.4828C72.7787 15.4828 72.3954 15.3877 72.0642 15.1976C71.7391 15.0075 71.4938 14.7652 71.3282 14.4708V15.4H70.491V8.592H71.3282V11.2968ZM74.7506 12.8608C74.7506 12.4805 74.6739 12.1493 74.5206 11.8672C74.3672 11.5851 74.1587 11.3704 73.895 11.2232C73.6374 11.076 73.3522 11.0024 73.0394 11.0024C72.7327 11.0024 72.4475 11.0791 72.1838 11.2324C71.9262 11.3796 71.7176 11.5973 71.5582 11.8856C71.4048 12.1677 71.3282 12.4959 71.3282 12.87C71.3282 13.2503 71.4048 13.5845 71.5582 13.8728C71.7176 14.1549 71.9262 14.3727 72.1838 14.526C72.4475 14.6732 72.7327 14.7468 73.0394 14.7468C73.3522 14.7468 73.6374 14.6732 73.895 14.526C74.1587 14.3727 74.3672 14.1549 74.5206 13.8728C74.6739 13.5845 74.7506 13.2472 74.7506 12.8608Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M77.5454 8.592V15.4H76.7082V8.592H77.5454Z" fill="white" />
|
||||
<path
|
||||
d="M83.6642 12.686C83.6642 12.8455 83.655 13.0141 83.6366 13.192H79.607C79.6377 13.6888 79.8064 14.0783 80.113 14.3604C80.4258 14.6364 80.803 14.7744 81.2446 14.7744C81.6065 14.7744 81.907 14.6916 82.1462 14.526C82.3916 14.3543 82.5633 14.1273 82.6614 13.8452H83.563C83.4281 14.3297 83.1582 14.7253 82.7534 15.032C82.3486 15.3325 81.8457 15.4828 81.2446 15.4828C80.7662 15.4828 80.3369 15.3755 79.9566 15.1608C79.5825 14.9461 79.2881 14.6425 79.0734 14.25C78.8587 13.8513 78.7514 13.3913 78.7514 12.87C78.7514 12.3487 78.8557 11.8917 79.0642 11.4992C79.2728 11.1067 79.5641 10.8061 79.9382 10.5976C80.3185 10.3829 80.754 10.2756 81.2446 10.2756C81.723 10.2756 82.1462 10.3799 82.5142 10.5884C82.8822 10.7969 83.1643 11.0852 83.3606 11.4532C83.563 11.8151 83.6642 12.226 83.6642 12.686ZM82.7994 12.5112C82.7994 12.1923 82.7289 11.9193 82.5878 11.6924C82.4467 11.4593 82.2536 11.2845 82.0082 11.168C81.769 11.0453 81.5022 10.984 81.2078 10.984C80.7846 10.984 80.4227 11.1189 80.1222 11.3888C79.8278 11.6587 79.6591 12.0328 79.6162 12.5112H82.7994Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M89.6048 15.4828C89.1326 15.4828 88.7032 15.3755 88.3168 15.1608C87.9366 14.9461 87.636 14.6425 87.4152 14.25C87.2006 13.8513 87.0932 13.3913 87.0932 12.87C87.0932 12.3548 87.2036 11.9009 87.4244 11.5084C87.6514 11.1097 87.958 10.8061 88.3444 10.5976C88.7308 10.3829 89.1632 10.2756 89.6416 10.2756C90.12 10.2756 90.5524 10.3829 90.9388 10.5976C91.3252 10.8061 91.6288 11.1067 91.8496 11.4992C92.0766 11.8917 92.19 12.3487 92.19 12.87C92.19 13.3913 92.0735 13.8513 91.8404 14.25C91.6135 14.6425 91.3038 14.9461 90.9112 15.1608C90.5187 15.3755 90.0832 15.4828 89.6048 15.4828ZM89.6048 14.7468C89.9054 14.7468 90.1875 14.6763 90.4512 14.5352C90.715 14.3941 90.9266 14.1825 91.086 13.9004C91.2516 13.6183 91.3344 13.2748 91.3344 12.87C91.3344 12.4652 91.2547 12.1217 91.0952 11.8396C90.9358 11.5575 90.7272 11.3489 90.4696 11.214C90.212 11.0729 89.933 11.0024 89.6324 11.0024C89.3258 11.0024 89.0436 11.0729 88.786 11.214C88.5346 11.3489 88.3322 11.5575 88.1788 11.8396C88.0255 12.1217 87.9488 12.4652 87.9488 12.87C87.9488 13.2809 88.0224 13.6275 88.1696 13.9096C88.323 14.1917 88.5254 14.4033 88.7768 14.5444C89.0283 14.6793 89.3043 14.7468 89.6048 14.7468Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M95.9312 10.2664C96.5445 10.2664 97.0413 10.4535 97.4216 10.8276C97.8019 11.1956 97.992 11.7292 97.992 12.4284V15.4H97.164V12.548C97.164 12.0451 97.0383 11.6617 96.7868 11.398C96.5353 11.1281 96.1919 10.9932 95.7564 10.9932C95.3148 10.9932 94.9621 11.1312 94.6984 11.4072C94.4408 11.6832 94.312 12.0849 94.312 12.6124V15.4H93.4748V10.3584H94.312V11.076C94.4776 10.8184 94.7015 10.6191 94.9836 10.478C95.2719 10.3369 95.5877 10.2664 95.9312 10.2664Z"
|
||||
fill="white"
|
||||
/>
|
||||
<rect x="9" y="7" width="26" height="26" rx="10" fill="url(#paint0_angular_1303_2200)" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M25.7914 16.5695L25.738 16.3341C25.738 14.2733 24.0609 12.5968 21.9997 12.5968C19.9385 12.5968 18.2617 14.2733 18.2617 16.3341L18.2081 16.5695H15.1387L15.966 24.2826C16.1318 25.8515 17.4549 27.0421 19.0326 27.0421H24.9669C26.5446 27.0421 27.8677 25.8515 28.0334 24.2828L28.8609 16.5695H25.7914ZM19.8331 16.5695L19.8934 16.3341C19.8934 15.1729 20.8383 14.2281 21.9997 14.2281C23.1613 14.2281 24.1063 15.1729 24.1063 16.3341L24.1664 16.5695H19.8331Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_angular_1303_2200"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(22 20) rotate(88.8309) scale(13.0027)"
|
||||
>
|
||||
<stop offset="0.000415415" stopColor="#F4605E" />
|
||||
<stop offset="0.0642377" stopColor="#E94B80" />
|
||||
<stop offset="0.128396" stopColor="#DE33A4" />
|
||||
<stop offset="0.186654" stopColor="#D41AC8" />
|
||||
<stop offset="0.250949" stopColor="#CB06E5" />
|
||||
<stop offset="0.281719" stopColor="#C902EC" />
|
||||
<stop offset="0.316858" stopColor="#CB04E5" />
|
||||
<stop offset="0.371347" stopColor="#D108D3" />
|
||||
<stop offset="0.43351" stopColor="#D80DBA" />
|
||||
<stop offset="0.504024" stopColor="#E1139E" />
|
||||
<stop offset="0.59576" stopColor="#EC1E7B" />
|
||||
<stop offset="0.673288" stopColor="#F22A65" />
|
||||
<stop offset="0.713795" stopColor="#F5355B" />
|
||||
<stop offset="0.754495" stopColor="#F74452" />
|
||||
<stop offset="0.818581" stopColor="#F75651" />
|
||||
<stop offset="0.878478" stopColor="#F76051" />
|
||||
<stop offset="0.938046" stopColor="#F76551" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppGalleryButton = ({ size = "md", ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { size?: "md" | "lg" }) => {
|
||||
return (
|
||||
<a
|
||||
aria-label="Explore it on AppGallery"
|
||||
href="#"
|
||||
{...props}
|
||||
className={cx(
|
||||
"rounded-[7px] bg-black ring-1 ring-app-store-badge-border outline-focus-ring ring-inset focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<svg width={size === "md" ? 133 : 147} height={size === "md" ? 40 : 44} viewBox="0 0 133 40" fill="none">
|
||||
<path
|
||||
d="M45.3962 25.4116H48.8919L47.6404 22.0615C47.4682 21.5986 47.2989 21.0875 47.1319 20.5276C46.9813 21.0229 46.817 21.5286 46.6394 22.0453L45.3962 25.4116ZM49.4893 27.0021H44.8068L43.6607 30.1344H41.6021L46.1874 18.4368H48.133L52.8234 30.1344H50.6599L49.4893 27.0021Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M58.8962 28.0072C59.3026 27.461 59.5058 26.663 59.5058 25.6135C59.5058 24.6396 59.3375 23.933 59.0013 23.4942C58.6647 23.0557 58.2167 22.8364 57.657 22.8364C57.2695 22.8364 56.9117 22.9281 56.5834 23.1109C56.2551 23.2939 55.9429 23.5387 55.6469 23.8455V28.5117C55.8461 28.6085 56.0775 28.6853 56.3413 28.7416C56.605 28.7984 56.8661 28.8266 57.1242 28.8266C57.8994 28.8266 58.4898 28.5533 58.8962 28.0072ZM53.653 23.555C53.653 22.9092 53.6314 22.1986 53.5885 21.4237H55.4613C55.5311 21.7844 55.5797 22.1531 55.6066 22.5297C56.3815 21.6849 57.2695 21.2622 58.2706 21.2622C58.8519 21.2622 59.3901 21.4088 59.8853 21.7021C60.3802 21.9955 60.7799 22.4583 61.0839 23.0906C61.3882 23.7231 61.5402 24.5265 61.5402 25.5005C61.5402 26.5177 61.3666 27.387 61.0194 28.108C60.6722 28.8294 60.1866 29.3754 59.5623 29.747C58.9381 30.1181 58.2167 30.3039 57.3989 30.3039C56.8066 30.3039 56.2226 30.2042 55.6469 30.0053V33.6056L53.653 33.7752V23.555Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M67.9935 28.0072C68.3999 27.461 68.6031 26.663 68.6031 25.6135C68.6031 24.6396 68.4349 23.933 68.0986 23.4942C67.7621 23.0557 67.3141 22.8364 66.7543 22.8364C66.3669 22.8364 66.009 22.9281 65.6807 23.1109C65.3522 23.2939 65.0402 23.5387 64.7442 23.8455V28.5117C64.9434 28.6085 65.1746 28.6853 65.4386 28.7416C65.7021 28.7984 65.9631 28.8266 66.2215 28.8266C66.9964 28.8266 67.5871 28.5533 67.9935 28.0072ZM62.7501 23.555C62.7501 22.9092 62.7285 22.1986 62.6855 21.4237H64.5586C64.6285 21.7844 64.677 22.1531 64.7039 22.5297C65.4789 21.6849 66.3669 21.2622 67.3679 21.2622C67.9493 21.2622 68.4871 21.4088 68.9826 21.7021C69.4775 21.9955 69.8772 22.4583 70.1815 23.0906C70.4852 23.7231 70.6375 24.5265 70.6375 25.5005C70.6375 26.5177 70.4637 27.387 70.1167 28.108C69.7695 28.8294 69.2837 29.3754 68.6597 29.747C68.0351 30.1181 67.314 30.3039 66.4959 30.3039C65.9039 30.3039 65.3199 30.2042 64.7442 30.0053V33.6056L62.7501 33.7752V23.555Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M74.3005 29.5813C73.4391 29.1053 72.7773 28.4229 72.3146 27.5349C71.8514 26.6469 71.6202 25.5974 71.6202 24.3864C71.6202 23.0734 71.8866 21.9578 72.4194 21.0401C72.9522 20.1227 73.6775 19.4337 74.5949 18.9735C75.5127 18.5134 76.5418 18.2833 77.6831 18.2833C78.3557 18.2833 78.9975 18.3574 79.6085 18.5053C80.2191 18.6534 80.7882 18.8564 81.3159 19.1149L80.8071 20.6486C79.7469 20.1428 78.7351 19.8898 77.7719 19.8898C76.9591 19.8898 76.2474 20.0634 75.6367 20.4106C75.0258 20.7578 74.5506 21.2677 74.2117 21.9404C73.8725 22.6132 73.7031 23.4258 73.7031 24.3783C73.7031 25.2128 73.8335 25.9526 74.0943 26.5984C74.3557 27.2442 74.7674 27.7557 75.3298 28.1323C75.8922 28.509 76.6013 28.6973 77.457 28.6973C77.8445 28.6973 78.2319 28.6651 78.6194 28.6005C79.0069 28.536 79.3703 28.4418 79.7093 28.3179V25.9526H77.005V24.4026H81.6469V29.3272C80.9794 29.6392 80.2783 29.8789 79.5439 30.0456C78.809 30.2123 78.0786 30.2957 77.3519 30.2957C76.1786 30.2957 75.1613 30.0579 74.3005 29.5813Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M87.5381 28.5197C87.9522 28.3208 88.2914 28.073 88.5551 27.7771V26.1625C88.0114 26.1034 87.5674 26.0737 87.2231 26.0737C86.3997 26.0737 85.8303 26.2068 85.5159 26.4734C85.2007 26.7394 85.0434 27.0989 85.0434 27.5509C85.0434 27.9818 85.1578 28.3005 85.3866 28.5077C85.6154 28.7149 85.9261 28.8184 86.3189 28.8184C86.717 28.8184 87.1234 28.7189 87.5381 28.5197ZM88.7327 30.1344C88.6626 29.7952 88.6167 29.4106 88.5954 28.98C88.2887 29.3461 87.8893 29.6568 87.3965 29.9125C86.9045 30.168 86.3485 30.2957 85.7295 30.2957C85.229 30.2957 84.773 30.1976 84.361 30.001C83.9498 29.8048 83.6226 29.5087 83.3805 29.113C83.1383 28.7176 83.017 28.2347 83.017 27.6639C83.017 26.8192 83.321 26.145 83.9293 25.6416C84.5375 25.1385 85.5519 24.887 86.9727 24.887C87.5055 24.887 88.033 24.9248 88.5551 25V24.8305C88.5551 24.0609 88.3909 23.5187 88.0626 23.2036C87.7343 22.889 87.2634 22.7316 86.6501 22.7316C86.2247 22.7316 85.7701 22.7935 85.2855 22.9172C84.8013 23.0411 84.3759 23.189 84.0101 23.3612L83.6951 21.9081C84.0503 21.7465 84.5186 21.5986 85.0999 21.464C85.6813 21.3297 86.2946 21.2622 86.9405 21.2622C87.6941 21.2622 88.3343 21.3765 88.8618 21.6052C89.3893 21.834 89.801 22.2271 90.097 22.7838C90.393 23.3411 90.541 24.0906 90.541 25.0323V28.4954C90.541 28.8562 90.5623 29.4027 90.6055 30.1344H88.7327Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M92.0709 28.0434V17.7505L94.0567 17.5891V27.6882C94.0567 28.0597 94.1199 28.3218 94.2463 28.4754C94.3727 28.6285 94.5732 28.7056 94.8479 28.7056C94.9716 28.7056 95.1466 28.676 95.3724 28.6168L95.6066 30.0456C95.418 30.121 95.1882 30.1816 94.9167 30.2272C94.6447 30.2728 94.3876 30.2957 94.1455 30.2957C92.762 30.2957 92.0709 29.5451 92.0709 28.0434Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M97.0356 28.0434V17.7505L99.0215 17.5891V27.6882C99.0215 28.0597 99.0847 28.3218 99.2111 28.4754C99.3378 28.6285 99.5383 28.7056 99.8127 28.7056C99.9365 28.7056 100.111 28.676 100.338 28.6168L100.572 30.0456C100.383 30.121 100.153 30.1816 99.8815 30.2272C99.6095 30.2728 99.3524 30.2957 99.1103 30.2957C97.7271 30.2957 97.0356 29.5451 97.0356 28.0434Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M106.795 24.7659C106.755 24.0716 106.581 23.551 106.269 23.2036C105.957 22.8568 105.539 22.683 105.019 22.683C104.512 22.683 104.091 22.8581 103.755 23.2078C103.419 23.5578 103.197 24.0771 103.096 24.7659H106.795ZM108.739 26.0333H103.04C103.131 27.8579 104 28.7701 105.648 28.7701C106.056 28.7701 106.475 28.7203 106.904 28.6208C107.331 28.521 107.741 28.388 108.133 28.221L108.571 29.5853C107.595 30.0592 106.501 30.2957 105.285 30.2957C104.357 30.2957 103.579 30.121 102.944 29.7711C102.307 29.4213 101.829 28.9181 101.509 28.2613C101.19 27.6051 101.03 26.8138 101.03 25.888C101.03 24.9248 101.199 24.0958 101.539 23.4016C101.877 22.7073 102.349 22.1773 102.955 21.8112C103.56 21.4453 104.259 21.2622 105.051 21.2622C105.875 21.2622 106.56 21.4547 107.112 21.8396C107.664 22.2241 108.072 22.737 108.339 23.3773C108.605 24.018 108.739 24.7255 108.739 25.5005V26.0333Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M110.248 23.6115C110.248 23.1326 110.224 22.4034 110.181 21.4237H112.048C112.08 21.6659 112.109 21.9552 112.141 22.2916C112.171 22.6279 112.189 22.901 112.2 23.1109C112.432 22.7288 112.659 22.4073 112.883 22.1463C113.107 21.8851 113.368 21.6727 113.667 21.5083C113.965 21.3443 114.304 21.2622 114.688 21.2622C114.995 21.2622 115.256 21.2946 115.477 21.3592L115.227 23.0868C115.035 23.0276 114.819 22.9979 114.581 22.9979C114.115 22.9979 113.704 23.1177 113.355 23.3573C113.005 23.5965 112.632 23.9908 112.232 24.5398V30.1344H110.248V23.6115Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M116.597 33.6984C116.307 33.6475 116.067 33.5896 115.88 33.5251L116.243 32.072C116.376 32.1094 116.547 32.1458 116.752 32.1808C116.955 32.216 117.149 32.2333 117.333 32.2333C118.216 32.2333 118.877 31.6653 119.317 30.5299L119.448 30.207L116.235 21.4237H118.373L119.989 26.332C120.251 27.1717 120.421 27.8149 120.496 28.2613C120.648 27.6317 120.824 27.0021 121.029 26.3724L122.669 21.4237H124.677L121.475 30.2475C121.173 31.0816 120.845 31.7541 120.496 32.2656C120.147 32.7768 119.733 33.1562 119.259 33.4039C118.781 33.6512 118.208 33.7752 117.533 33.7752C117.2 33.7752 116.888 33.7499 116.597 33.6984Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M41.5933 7.30106H45.1474L45.0474 8.15946H42.6349V9.82186H44.9098V10.6261H42.6349V12.4677H45.1765L45.089 13.3344H41.5933V7.30106Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M47.5973 10.2552L45.7223 7.30106H46.9055L48.1765 9.472L49.514 7.30106H50.6639L48.8055 10.2176L50.8181 13.3344H49.6181L48.1807 10.9595L46.7181 13.3344H45.5682L47.5973 10.2552Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M54.1973 9.99466C54.4194 9.7936 54.5306 9.50933 54.5306 9.14266C54.5306 8.7704 54.4172 8.50266 54.1911 8.33866C53.9647 8.17466 53.6319 8.0928 53.1932 8.0928H52.6266V10.2344C52.8876 10.276 53.0876 10.2968 53.2266 10.2968C53.6514 10.2968 53.9751 10.1963 54.1973 9.99466ZM51.5847 7.30106H53.2098C53.9735 7.30106 54.5583 7.4568 54.9639 7.76773C55.3695 8.07893 55.5722 8.5288 55.5722 9.1176C55.5722 9.5176 55.4813 9.8672 55.2994 10.1656C55.1173 10.4643 54.8639 10.6936 54.5388 10.8531C54.214 11.0131 53.8402 11.0928 53.418 11.0928C53.1876 11.0928 52.9236 11.0651 52.6266 11.0093V13.3344H51.5847V7.30106Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M56.6015 7.30106H57.6431V12.4427H60.1389L60.0514 13.3344H56.6015V7.30106Z" fill="white" />
|
||||
<path
|
||||
d="M64.4223 12.2989C64.714 12.1032 64.9319 11.8339 65.0764 11.4907C65.221 11.1477 65.2932 10.7552 65.2932 10.3136C65.2932 9.88026 65.2292 9.49439 65.1015 9.15519C64.9735 8.81625 64.7695 8.54639 64.489 8.34479C64.2084 8.14346 63.8474 8.04266 63.4055 8.04266C62.9834 8.04266 62.625 8.14479 62.3306 8.34906C62.0362 8.55306 61.8154 8.82692 61.6682 9.16986C61.521 9.51306 61.4474 9.89145 61.4474 10.3053C61.4474 10.7413 61.5167 11.1317 61.6556 11.476C61.7943 11.8205 62.0068 12.0928 62.2932 12.2928C62.5791 12.4928 62.9332 12.5928 63.3556 12.5928C63.7751 12.5928 64.1306 12.4947 64.4223 12.2989ZM61.7223 13.0387C61.2807 12.7859 60.9431 12.4309 60.7098 11.9739C60.4764 11.5171 60.3599 10.9859 60.3599 10.3803C60.3599 9.74426 60.4839 9.18799 60.7327 8.71146C60.9813 8.23519 61.3396 7.86719 61.8076 7.60719C62.2756 7.34772 62.8277 7.21759 63.4639 7.21759C64.0722 7.21759 64.5959 7.34346 65.0348 7.59466C65.4735 7.84639 65.8084 8.19972 66.0388 8.65519C66.2695 9.11092 66.3847 9.63865 66.3847 10.2387C66.3847 10.8859 66.2591 11.4485 66.0076 11.9261C65.7562 12.4037 65.3981 12.772 64.9327 13.0301C64.4674 13.2885 63.921 13.4177 63.2932 13.4177C62.6876 13.4177 62.1639 13.2915 61.7223 13.0387Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M69.1807 10.1552C69.4333 10.1552 69.657 10.1061 69.8514 10.0072C70.0458 9.9088 70.1973 9.76986 70.3055 9.59066C70.4141 9.41146 70.4682 9.20373 70.4682 8.96773C70.4682 8.66506 70.3722 8.44346 70.1807 8.3032C69.989 8.16293 69.7098 8.0928 69.3431 8.0928H68.589V10.1552H69.1807ZM67.5474 7.30106H69.4349C70.1237 7.30106 70.6453 7.43866 70.9994 7.7136C71.3536 7.98853 71.5306 8.38186 71.5306 8.8928C71.5306 9.21226 71.4666 9.4936 71.3389 9.73653C71.2111 9.97973 71.0528 10.1776 70.864 10.3301C70.6749 10.4829 70.4807 10.5968 70.2807 10.672L72.1349 13.3344H70.9266L69.3557 10.9427H68.589V13.3344H67.5474V7.30106Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M72.9599 7.30106H76.5141L76.4141 8.15946H74.0015V9.82186H76.2765V10.6261H74.0015V12.4677H76.5431L76.4556 13.3344H72.9599V7.30106Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M80.3807 7.30106H81.4223V13.3344H80.3807V7.30106Z" fill="white" />
|
||||
<path d="M84.1765 8.172H82.3055L82.3973 7.30106H87.0933L86.9973 8.172H85.218V13.3344H84.1765V8.172Z" fill="white" />
|
||||
<path
|
||||
d="M94.3141 12.2989C94.6055 12.1032 94.8237 11.8339 94.9682 11.4907C95.1125 11.1477 95.1847 10.7552 95.1847 10.3136C95.1847 9.88026 95.1207 9.49439 94.9933 9.15519C94.8653 8.81625 94.661 8.54639 94.3807 8.34479C94.0999 8.14346 93.7389 8.04266 93.2973 8.04266C92.8749 8.04266 92.5167 8.14479 92.2223 8.34906C91.9277 8.55306 91.7069 8.82692 91.5599 9.16986C91.4125 9.51306 91.3389 9.89145 91.3389 10.3053C91.3389 10.7413 91.4082 11.1317 91.5474 11.476C91.6861 11.8205 91.8986 12.0928 92.1847 12.2928C92.4709 12.4928 92.825 12.5928 93.2474 12.5928C93.6666 12.5928 94.0223 12.4947 94.3141 12.2989ZM91.6141 13.0387C91.1722 12.7859 90.8349 12.4309 90.6015 11.9739C90.3682 11.5171 90.2514 10.9859 90.2514 10.3803C90.2514 9.74426 90.3757 9.18799 90.6245 8.71146C90.8727 8.23519 91.2311 7.86719 91.6994 7.60719C92.1674 7.34772 92.7194 7.21759 93.3557 7.21759C93.9639 7.21759 94.4874 7.34346 94.9266 7.59466C95.3653 7.84639 95.6999 8.19972 95.9306 8.65519C96.161 9.11092 96.2765 9.63865 96.2765 10.2387C96.2765 10.8859 96.1506 11.4485 95.8994 11.9261C95.6479 12.4037 95.2895 12.772 94.8245 13.0301C94.3591 13.2885 93.8125 13.4177 93.1847 13.4177C92.5791 13.4177 92.0557 13.2915 91.6141 13.0387Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M97.4389 7.30106H98.6349L101.619 11.976C101.592 11.5317 101.581 11.1219 101.581 10.7469V7.30106H102.547V13.3344H101.389L98.3599 8.58426C98.3903 9.12346 98.4055 9.60106 98.4055 10.0176V13.3344H97.4389V7.30106Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M26.9417 7.33329H15.8633C10.6446 7.33329 8.73584 9.24209 8.73584 14.4608V25.5393C8.73584 30.7579 10.6446 32.6666 15.8633 32.6666H26.9382C32.1569 32.6666 34.0692 30.7579 34.0692 25.5393V14.4608C34.0692 9.24209 32.1604 7.33329 26.9417 7.33329"
|
||||
fill="#C91C2E"
|
||||
/>
|
||||
<path
|
||||
d="M19.7881 22.0255H20.8041L20.2944 20.8404L19.7881 22.0255ZM19.5379 22.6229L19.2366 23.3125H18.5504L20.0097 20.0021H20.6025L22.0558 23.3125H21.3513L21.0537 22.6229H19.5379ZM30.5787 23.3102H31.2419V20.0021H30.5787V23.3102ZM27.9395 21.8887H29.1616V21.2859H27.9395V20.6078H29.7137V20.0042H27.2766V23.3121H29.7777V22.7088H27.9395V21.8887ZM25.3046 22.2796L24.5526 20.002H24.0043L23.2523 22.2796L22.5206 20.0036H21.8054L22.9598 23.314H23.516L24.2691 21.1395L25.0219 23.314H25.5832L26.7347 20.0036H26.0379L25.3046 22.2796ZM17.5382 21.8979C17.5382 22.4364 17.2707 22.7241 16.7851 22.7241C16.2969 22.7241 16.0281 22.4283 16.0281 21.875V20.004H15.356V21.8979C15.356 22.8296 15.8736 23.3638 16.776 23.3638C17.6872 23.3638 18.2099 22.8194 18.2099 21.8705V20.002H17.5382V21.8979ZM13.7526 20.0021H14.4243V23.3142H13.7526V21.9692H12.235V23.3142H11.563V20.0021H12.235V21.3383H13.7526V20.0021Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M21.4023 15.7851C19.0773 15.7851 17.1855 13.8933 17.1855 11.5683H17.7813C17.7813 13.5649 19.4055 15.1894 21.4023 15.1894C23.3991 15.1894 25.0234 13.5649 25.0234 11.5683H25.6191C25.6191 13.8933 23.7274 15.7851 21.4023 15.7851"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
117
src/components/base/buttons/button-utility.tsx
Normal file
117
src/components/base/buttons/button-utility.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps, FC, ReactNode } from "react";
|
||||
import { isValidElement } from "react";
|
||||
import type { Placement } from "react-aria";
|
||||
import type { ButtonProps as AriaButtonProps, LinkProps as AriaLinkProps } from "react-aria-components";
|
||||
import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
|
||||
import { Tooltip } from "@/components/base/tooltip/tooltip";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { isReactComponent } from "@/utils/is-react-component";
|
||||
|
||||
export const styles = {
|
||||
secondary:
|
||||
"bg-primary text-fg-quaternary shadow-xs-skeumorphic ring-1 ring-primary ring-inset hover:bg-primary_hover hover:text-fg-quaternary_hover disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||
tertiary: "text-fg-quaternary hover:bg-primary_hover hover:text-fg-quaternary_hover",
|
||||
};
|
||||
|
||||
/**
|
||||
* Common props shared between button and anchor variants
|
||||
*/
|
||||
export interface CommonProps {
|
||||
/** Disables the button and shows a disabled state */
|
||||
isDisabled?: boolean;
|
||||
/** The size variant of the button */
|
||||
size?: "xs" | "sm";
|
||||
/** The color variant of the button */
|
||||
color?: "secondary" | "tertiary";
|
||||
/** The icon to display in the button */
|
||||
icon?: FC<{ className?: string }> | ReactNode;
|
||||
/** The tooltip to display when hovering over the button */
|
||||
tooltip?: string;
|
||||
/** The placement of the tooltip */
|
||||
tooltipPlacement?: Placement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the button variant (non-link)
|
||||
*/
|
||||
export interface ButtonProps extends CommonProps, DetailedHTMLProps<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "slot">, HTMLButtonElement> {
|
||||
/** Slot name for react-aria component */
|
||||
slot?: AriaButtonProps["slot"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the link variant (anchor tag)
|
||||
*/
|
||||
interface LinkProps extends CommonProps, DetailedHTMLProps<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "color">, HTMLAnchorElement> {
|
||||
/** Options for the configured client side router. */
|
||||
routerOptions?: AriaLinkProps["routerOptions"];
|
||||
}
|
||||
|
||||
/** Union type of button and link props */
|
||||
export type Props = ButtonProps | LinkProps;
|
||||
|
||||
export const ButtonUtility = ({
|
||||
tooltip,
|
||||
className,
|
||||
isDisabled,
|
||||
icon: Icon,
|
||||
size = "sm",
|
||||
color = "secondary",
|
||||
tooltipPlacement = "top",
|
||||
...otherProps
|
||||
}: Props) => {
|
||||
const href = "href" in otherProps ? otherProps.href : undefined;
|
||||
const Component = href ? AriaLink : AriaButton;
|
||||
|
||||
let props = {};
|
||||
|
||||
if (href) {
|
||||
props = {
|
||||
...otherProps,
|
||||
|
||||
href: isDisabled ? undefined : href,
|
||||
|
||||
// Since anchor elements do not support the `disabled` attribute and state,
|
||||
// we need to specify `data-rac` and `data-disabled` in order to be able
|
||||
// to use the `disabled:` selector in classes.
|
||||
...(isDisabled ? { "data-rac": true, "data-disabled": true } : {}),
|
||||
};
|
||||
} else {
|
||||
props = {
|
||||
...otherProps,
|
||||
|
||||
type: otherProps.type || "button",
|
||||
isDisabled,
|
||||
};
|
||||
}
|
||||
|
||||
const content = (
|
||||
<Component
|
||||
aria-label={tooltip}
|
||||
{...props}
|
||||
className={cx(
|
||||
"group relative inline-flex h-max cursor-pointer items-center justify-center rounded-md p-1.5 outline-focus-ring transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:text-fg-disabled_subtle",
|
||||
styles[color],
|
||||
|
||||
// Icon styles
|
||||
"*:data-icon:pointer-events-none *:data-icon:shrink-0 *:data-icon:text-current *:data-icon:transition-inherit-all",
|
||||
size === "xs" ? "*:data-icon:size-4" : "*:data-icon:size-5",
|
||||
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isReactComponent(Icon) && <Icon data-icon />}
|
||||
{isValidElement(Icon) && Icon}
|
||||
</Component>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip title={tooltip} placement={tooltipPlacement} isDisabled={isDisabled} offset={size === "xs" ? 4 : 6}>
|
||||
{content}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
267
src/components/base/buttons/button.tsx
Normal file
267
src/components/base/buttons/button.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps, FC, ReactNode } from "react";
|
||||
import React, { isValidElement } from "react";
|
||||
import type { ButtonProps as AriaButtonProps, LinkProps as AriaLinkProps } from "react-aria-components";
|
||||
import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
import { isReactComponent } from "@/utils/is-react-component";
|
||||
|
||||
export const styles = sortCx({
|
||||
common: {
|
||||
root: [
|
||||
"group relative inline-flex h-max cursor-pointer items-center justify-center whitespace-nowrap outline-brand transition duration-100 ease-linear before:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
// When button is used within `InputGroup`
|
||||
"in-data-input-wrapper:shadow-xs in-data-input-wrapper:focus:!z-50 in-data-input-wrapper:in-data-leading:-mr-px in-data-input-wrapper:in-data-leading:rounded-r-none in-data-input-wrapper:in-data-leading:before:rounded-r-none in-data-input-wrapper:in-data-trailing:-ml-px in-data-input-wrapper:in-data-trailing:rounded-l-none in-data-input-wrapper:in-data-trailing:before:rounded-l-none",
|
||||
// Disabled styles
|
||||
"disabled:cursor-not-allowed disabled:text-fg-disabled",
|
||||
// Icon styles
|
||||
"disabled:*:data-icon:text-fg-disabled_subtle",
|
||||
// Same as `icon` but for SSR icons that cannot be passed to the client as functions.
|
||||
"*:data-icon:pointer-events-none *:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:transition-inherit-all",
|
||||
].join(" "),
|
||||
icon: "pointer-events-none size-5 shrink-0 transition-inherit-all",
|
||||
},
|
||||
sizes: {
|
||||
sm: {
|
||||
root: [
|
||||
"gap-1 rounded-lg px-3 py-2 text-sm font-semibold before:rounded-[7px] data-icon-only:p-2",
|
||||
"in-data-input-wrapper:px-3.5 in-data-input-wrapper:py-2.5 in-data-input-wrapper:data-icon-only:p-2.5",
|
||||
].join(" "),
|
||||
linkRoot: "gap-1",
|
||||
},
|
||||
md: {
|
||||
root: [
|
||||
"gap-1 rounded-lg px-3.5 py-2.5 text-sm font-semibold before:rounded-[7px] data-icon-only:p-2.5",
|
||||
"in-data-input-wrapper:gap-1.5 in-data-input-wrapper:px-4 in-data-input-wrapper:text-md in-data-input-wrapper:data-icon-only:p-3",
|
||||
].join(" "),
|
||||
linkRoot: "gap-1",
|
||||
},
|
||||
lg: {
|
||||
root: "gap-1.5 rounded-lg px-4 py-2.5 text-md font-semibold before:rounded-[7px] data-icon-only:p-3",
|
||||
linkRoot: "gap-1.5",
|
||||
},
|
||||
xl: {
|
||||
root: "gap-1.5 rounded-lg px-4.5 py-3 text-md font-semibold before:rounded-[7px] data-icon-only:p-3.5",
|
||||
linkRoot: "gap-1.5",
|
||||
},
|
||||
},
|
||||
|
||||
colors: {
|
||||
primary: {
|
||||
root: [
|
||||
"bg-brand-solid text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset hover:bg-brand-solid_hover data-loading:bg-brand-solid_hover",
|
||||
// Inner border gradient
|
||||
"before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
|
||||
// Disabled styles
|
||||
"disabled:bg-disabled disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||
// Icon styles
|
||||
"*:data-icon:text-button-primary-icon hover:*:data-icon:text-button-primary-icon_hover",
|
||||
].join(" "),
|
||||
},
|
||||
secondary: {
|
||||
root: [
|
||||
"bg-primary text-secondary shadow-xs-skeumorphic ring-1 ring-primary ring-inset hover:bg-primary_hover hover:text-secondary_hover data-loading:bg-primary_hover",
|
||||
// Disabled styles
|
||||
"disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||
// Icon styles
|
||||
"*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
|
||||
].join(" "),
|
||||
},
|
||||
tertiary: {
|
||||
root: [
|
||||
"text-tertiary hover:bg-primary_hover hover:text-tertiary_hover data-loading:bg-primary_hover",
|
||||
// Icon styles
|
||||
"*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
|
||||
].join(" "),
|
||||
},
|
||||
"link-gray": {
|
||||
root: [
|
||||
"justify-normal rounded p-0! text-tertiary hover:text-tertiary_hover",
|
||||
// Inner text underline
|
||||
"*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
|
||||
// Icon styles
|
||||
"*:data-icon:text-fg-quaternary hover:*:data-icon:text-fg-quaternary_hover",
|
||||
].join(" "),
|
||||
},
|
||||
"link-color": {
|
||||
root: [
|
||||
"justify-normal rounded p-0! text-brand-secondary hover:text-brand-secondary_hover",
|
||||
// Inner text underline
|
||||
"*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
|
||||
// Icon styles
|
||||
"*:data-icon:text-fg-brand-secondary_alt hover:*:data-icon:text-fg-brand-secondary_hover",
|
||||
].join(" "),
|
||||
},
|
||||
"primary-destructive": {
|
||||
root: [
|
||||
"bg-error-solid text-white shadow-xs-skeumorphic ring-1 ring-transparent outline-error ring-inset hover:bg-error-solid_hover data-loading:bg-error-solid_hover",
|
||||
// Inner border gradient
|
||||
"before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
|
||||
// Disabled styles
|
||||
"disabled:bg-disabled disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||
// Icon styles
|
||||
"*:data-icon:text-button-destructive-primary-icon hover:*:data-icon:text-button-destructive-primary-icon_hover",
|
||||
].join(" "),
|
||||
},
|
||||
"secondary-destructive": {
|
||||
root: [
|
||||
"bg-primary text-error-primary shadow-xs-skeumorphic ring-1 ring-error_subtle outline-error ring-inset hover:bg-error-primary hover:text-error-primary_hover data-loading:bg-error-primary",
|
||||
// Disabled styles
|
||||
"disabled:bg-primary disabled:shadow-xs disabled:ring-disabled_subtle",
|
||||
// Icon styles
|
||||
"*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
|
||||
].join(" "),
|
||||
},
|
||||
"tertiary-destructive": {
|
||||
root: [
|
||||
"text-error-primary outline-error hover:bg-error-primary hover:text-error-primary_hover data-loading:bg-error-primary",
|
||||
// Icon styles
|
||||
"*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
|
||||
].join(" "),
|
||||
},
|
||||
"link-destructive": {
|
||||
root: [
|
||||
"justify-normal rounded p-0! text-error-primary outline-error hover:text-error-primary_hover",
|
||||
// Inner text underline
|
||||
"*:data-text:underline *:data-text:decoration-transparent *:data-text:underline-offset-2 hover:*:data-text:decoration-current",
|
||||
// Icon styles
|
||||
"*:data-icon:text-fg-error-secondary hover:*:data-icon:text-fg-error-primary",
|
||||
].join(" "),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Common props shared between button and anchor variants
|
||||
*/
|
||||
export interface CommonProps {
|
||||
/** Disables the button and shows a disabled state */
|
||||
isDisabled?: boolean;
|
||||
/** Shows a loading spinner and disables the button */
|
||||
isLoading?: boolean;
|
||||
/** The size variant of the button */
|
||||
size?: keyof typeof styles.sizes;
|
||||
/** The color variant of the button */
|
||||
color?: keyof typeof styles.colors;
|
||||
/** Icon component or element to show before the text */
|
||||
iconLeading?: FC<{ className?: string }> | ReactNode;
|
||||
/** Icon component or element to show after the text */
|
||||
iconTrailing?: FC<{ className?: string }> | ReactNode;
|
||||
/** Removes horizontal padding from the text content */
|
||||
noTextPadding?: boolean;
|
||||
/** When true, keeps the text visible during loading state */
|
||||
showTextWhileLoading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the button variant (non-link)
|
||||
*/
|
||||
export interface ButtonProps extends CommonProps, DetailedHTMLProps<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "slot">, HTMLButtonElement> {
|
||||
/** Slot name for react-aria component */
|
||||
slot?: AriaButtonProps["slot"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the link variant (anchor tag)
|
||||
*/
|
||||
interface LinkProps extends CommonProps, DetailedHTMLProps<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "color">, HTMLAnchorElement> {
|
||||
/** Options for the configured client side router. */
|
||||
routerOptions?: AriaLinkProps["routerOptions"];
|
||||
}
|
||||
|
||||
/** Union type of button and link props */
|
||||
export type Props = ButtonProps | LinkProps;
|
||||
|
||||
export const Button = ({
|
||||
size = "sm",
|
||||
color = "primary",
|
||||
children,
|
||||
className,
|
||||
noTextPadding,
|
||||
iconLeading: IconLeading,
|
||||
iconTrailing: IconTrailing,
|
||||
isDisabled: disabled,
|
||||
isLoading: loading,
|
||||
showTextWhileLoading,
|
||||
...otherProps
|
||||
}: Props) => {
|
||||
const href = "href" in otherProps ? otherProps.href : undefined;
|
||||
const Component = href ? AriaLink : AriaButton;
|
||||
|
||||
const isIcon = (IconLeading || IconTrailing) && !children;
|
||||
const isLinkType = ["link-gray", "link-color", "link-destructive"].includes(color);
|
||||
|
||||
noTextPadding = isLinkType || noTextPadding;
|
||||
|
||||
let props = {};
|
||||
|
||||
if (href) {
|
||||
props = {
|
||||
...otherProps,
|
||||
|
||||
href: disabled ? undefined : href,
|
||||
};
|
||||
} else {
|
||||
props = {
|
||||
...otherProps,
|
||||
|
||||
type: otherProps.type || "button",
|
||||
isPending: loading,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
data-loading={loading ? true : undefined}
|
||||
data-icon-only={isIcon ? true : undefined}
|
||||
{...props}
|
||||
isDisabled={disabled}
|
||||
className={cx(
|
||||
styles.common.root,
|
||||
styles.sizes[size].root,
|
||||
styles.colors[color].root,
|
||||
isLinkType && styles.sizes[size].linkRoot,
|
||||
(loading || (href && (disabled || loading))) && "pointer-events-none",
|
||||
// If in `loading` state, hide everything except the loading icon (and text if `showTextWhileLoading` is true).
|
||||
loading && (showTextWhileLoading ? "[&>*:not([data-icon=loading]):not([data-text])]:hidden" : "[&>*:not([data-icon=loading])]:invisible"),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Leading icon */}
|
||||
{isValidElement(IconLeading) && IconLeading}
|
||||
{isReactComponent(IconLeading) && <IconLeading data-icon="leading" className={styles.common.icon} />}
|
||||
|
||||
{loading && (
|
||||
<svg
|
||||
fill="none"
|
||||
data-icon="loading"
|
||||
viewBox="0 0 20 20"
|
||||
className={cx(styles.common.icon, !showTextWhileLoading && "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2")}
|
||||
>
|
||||
{/* Background circle */}
|
||||
<circle className="stroke-current opacity-30" cx="10" cy="10" r="8" fill="none" strokeWidth="2" />
|
||||
{/* Spinning circle */}
|
||||
<circle
|
||||
className="origin-center animate-spin stroke-current"
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="8"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="12.5 50"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{children && (
|
||||
<span data-text className={cx("transition-inherit-all", !noTextPadding && "px-0.5")}>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Trailing icon */}
|
||||
{isValidElement(IconTrailing) && IconTrailing}
|
||||
{isReactComponent(IconTrailing) && <IconTrailing data-icon="trailing" className={styles.common.icon} />}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
40
src/components/base/buttons/close-button.tsx
Normal file
40
src/components/base/buttons/close-button.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { X as CloseIcon } from "@untitledui/icons";
|
||||
import { Button as AriaButton, type ButtonProps as AriaButtonProps } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const sizes = {
|
||||
xs: { root: "size-7", icon: "size-4" },
|
||||
sm: { root: "size-9", icon: "size-5" },
|
||||
md: { root: "size-10", icon: "size-5" },
|
||||
lg: { root: "size-11", icon: "size-6" },
|
||||
};
|
||||
|
||||
const themes = {
|
||||
light: "text-fg-quaternary hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 outline-focus-ring",
|
||||
dark: "text-fg-white/70 hover:text-fg-white hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-offset-2 outline-focus-ring",
|
||||
};
|
||||
|
||||
interface CloseButtonProps extends AriaButtonProps {
|
||||
theme?: "light" | "dark";
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const CloseButton = ({ label, className, size = "sm", theme = "light", ...otherProps }: CloseButtonProps) => {
|
||||
return (
|
||||
<AriaButton
|
||||
{...otherProps}
|
||||
aria-label={label || "Close"}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"flex cursor-pointer items-center justify-center rounded-lg p-2 transition duration-100 ease-linear focus:outline-hidden",
|
||||
sizes[size].root,
|
||||
themes[theme],
|
||||
typeof className === "function" ? className(state) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
<CloseIcon aria-hidden="true" className={cx("shrink-0 transition-inherit-all", sizes[size].icon)} />
|
||||
</AriaButton>
|
||||
);
|
||||
};
|
||||
149
src/components/base/buttons/social-button.tsx
Normal file
149
src/components/base/buttons/social-button.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps } from "react";
|
||||
import type { ButtonProps as AriaButtonProps, LinkProps as AriaLinkProps } from "react-aria-components";
|
||||
import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
import { AppleLogo, DribbleLogo, FacebookLogo, FigmaLogo, FigmaLogoOutlined, GoogleLogo, TwitterLogo } from "./social-logos";
|
||||
|
||||
export const styles = sortCx({
|
||||
common: {
|
||||
root: "group relative inline-flex h-max cursor-pointer items-center justify-center font-semibold whitespace-nowrap outline-focus-ring transition duration-100 ease-linear before:absolute focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:stroke-fg-disabled disabled:text-fg-disabled disabled:*:text-fg-disabled",
|
||||
icon: "pointer-events-none shrink-0 transition-inherit-all",
|
||||
},
|
||||
|
||||
sizes: {
|
||||
sm: {
|
||||
root: "gap-2 rounded-lg px-3 py-2 text-sm before:rounded-[7px] data-icon-only:p-2",
|
||||
},
|
||||
md: {
|
||||
root: "gap-2.5 rounded-lg px-3.5 py-2.5 text-sm before:rounded-[7px] data-icon-only:p-2.5",
|
||||
},
|
||||
lg: {
|
||||
root: "gap-3 rounded-lg px-4 py-2.5 text-md before:rounded-[7px] data-icon-only:p-2.5",
|
||||
},
|
||||
xl: {
|
||||
root: "gap-3.5 rounded-lg px-4.5 py-3 text-md before:rounded-[7px] data-icon-only:p-3.5",
|
||||
},
|
||||
"2xl": {
|
||||
root: "gap-4 rounded-[10px] px-5.5 py-4 text-lg before:rounded-[9px] data-icon-only:p-4",
|
||||
},
|
||||
},
|
||||
|
||||
colors: {
|
||||
gray: {
|
||||
root: "bg-primary text-secondary shadow-xs-skeumorphic ring-1 ring-primary ring-inset hover:bg-primary_hover hover:text-secondary_hover",
|
||||
icon: "text-fg-quaternary group-hover:text-fg-quaternary_hover",
|
||||
},
|
||||
black: {
|
||||
root: "bg-black text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
|
||||
icon: "",
|
||||
},
|
||||
|
||||
facebook: {
|
||||
root: "bg-[#1877F2] text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0% hover:bg-[#0C63D4]",
|
||||
icon: "",
|
||||
},
|
||||
|
||||
dribble: {
|
||||
root: "bg-[#EA4C89] text-white shadow-xs-skeumorphic ring-1 ring-transparent ring-inset before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0% hover:bg-[#E62872]",
|
||||
icon: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface CommonProps {
|
||||
social: "google" | "facebook" | "apple" | "twitter" | "figma" | "dribble";
|
||||
disabled?: boolean;
|
||||
theme?: "brand" | "color" | "gray";
|
||||
size?: keyof typeof styles.sizes;
|
||||
}
|
||||
|
||||
interface ButtonProps extends CommonProps, DetailedHTMLProps<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "slot">, HTMLButtonElement> {
|
||||
slot?: AriaButtonProps["slot"];
|
||||
}
|
||||
|
||||
interface LinkProps extends CommonProps, DetailedHTMLProps<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "color">, HTMLAnchorElement> {
|
||||
/** Options for the configured client side router. */
|
||||
routerOptions?: AriaLinkProps["routerOptions"];
|
||||
}
|
||||
|
||||
export type SocialButtonProps = ButtonProps | LinkProps;
|
||||
|
||||
export const SocialButton = ({ size = "lg", theme = "brand", social, className, children, disabled, ...otherProps }: SocialButtonProps) => {
|
||||
const href = "href" in otherProps ? otherProps.href : undefined;
|
||||
const Component = href ? AriaLink : AriaButton;
|
||||
|
||||
const isIconOnly = !children;
|
||||
|
||||
const socialToColor = {
|
||||
google: "gray",
|
||||
facebook: "facebook",
|
||||
apple: "black",
|
||||
twitter: "black",
|
||||
figma: "black",
|
||||
dribble: "dribble",
|
||||
} as const;
|
||||
|
||||
const colorStyles = theme === "brand" ? styles.colors[socialToColor[social]] : styles.colors.gray;
|
||||
|
||||
const logos = {
|
||||
google: GoogleLogo,
|
||||
facebook: FacebookLogo,
|
||||
apple: AppleLogo,
|
||||
twitter: TwitterLogo,
|
||||
figma: theme === "gray" ? FigmaLogoOutlined : FigmaLogo,
|
||||
dribble: DribbleLogo,
|
||||
};
|
||||
|
||||
const Logo = logos[social];
|
||||
|
||||
let props = {};
|
||||
|
||||
if (href) {
|
||||
props = {
|
||||
...otherProps,
|
||||
|
||||
href: disabled ? undefined : href,
|
||||
|
||||
// Since anchor elements do not support the `disabled` attribute and state,
|
||||
// we need to specify `data-rac` and `data-disabled` in order to be able
|
||||
// to use the `disabled:` selector in classes.
|
||||
...(disabled ? { "data-rac": true, "data-disabled": true } : {}),
|
||||
};
|
||||
} else {
|
||||
props = {
|
||||
...otherProps,
|
||||
|
||||
type: otherProps.type || "button",
|
||||
isDisabled: disabled,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
isDisabled={disabled}
|
||||
{...props}
|
||||
data-icon-only={isIconOnly ? true : undefined}
|
||||
className={cx(styles.common.root, styles.sizes[size].root, colorStyles.root, className)}
|
||||
>
|
||||
<Logo
|
||||
className={cx(
|
||||
styles.common.icon,
|
||||
theme === "gray"
|
||||
? colorStyles.icon
|
||||
: theme === "brand" && (social === "facebook" || social === "apple" || social === "twitter")
|
||||
? "text-white"
|
||||
: theme === "color" && (social === "apple" || social === "twitter")
|
||||
? "text-alpha-black"
|
||||
: "",
|
||||
)}
|
||||
colorful={
|
||||
(theme === "brand" && (social === "google" || social === "figma")) ||
|
||||
(theme === "color" && (social === "google" || social === "facebook" || social === "figma" || social === "dribble")) ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
113
src/components/base/buttons/social-logos.tsx
Normal file
113
src/components/base/buttons/social-logos.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export const GoogleLogo = ({ colorful, ...props }: SVGProps<SVGSVGElement> & { colorful?: boolean }) => {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M23.766 12.2764C23.766 11.4607 23.6999 10.6406 23.5588 9.83807H12.24V14.4591H18.7217C18.4528 15.9494 17.5885 17.2678 16.323 18.1056V21.1039H20.19C22.4608 19.0139 23.766 15.9274 23.766 12.2764Z"
|
||||
fill={colorful ? "#4285F4" : "currentColor"}
|
||||
/>
|
||||
<path
|
||||
d="M12.24 24.0008C15.4764 24.0008 18.2058 22.9382 20.1944 21.1039L16.3274 18.1055C15.2516 18.8375 13.8626 19.252 12.2444 19.252C9.11376 19.252 6.45934 17.1399 5.50693 14.3003H1.51648V17.3912C3.55359 21.4434 7.70278 24.0008 12.24 24.0008Z"
|
||||
fill={colorful ? "#34A853" : "currentColor"}
|
||||
/>
|
||||
<path
|
||||
d="M5.50253 14.3003C4.99987 12.8099 4.99987 11.1961 5.50253 9.70575V6.61481H1.51649C-0.18551 10.0056 -0.18551 14.0004 1.51649 17.3912L5.50253 14.3003Z"
|
||||
fill={colorful ? "#FBBC04" : "currentColor"}
|
||||
/>
|
||||
<path
|
||||
d="M12.24 4.74966C13.9508 4.7232 15.6043 5.36697 16.8433 6.54867L20.2694 3.12262C18.1 1.0855 15.2207 -0.034466 12.24 0.000808666C7.70277 0.000808666 3.55359 2.55822 1.51648 6.61481L5.50252 9.70575C6.45052 6.86173 9.10935 4.74966 12.24 4.74966Z"
|
||||
fill={colorful ? "#EA4335" : "currentColor"}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const FigmaLogo = ({ colorful, ...props }: SVGProps<SVGSVGElement> & { colorful?: boolean }) => {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M8.00006 24.0001C10.2081 24.0001 12.0001 22.208 12.0001 20V16H8.00006C5.79205 16 4 17.792 4 20C4 22.208 5.79205 24.0001 8.00006 24.0001Z"
|
||||
fill={colorful ? "#24CB71" : "currentColor"}
|
||||
/>
|
||||
<path d="M4 12C4 9.79203 5.79205 8 8.00006 8H12.0001V16H8.00006C5.79205 16.0001 4 14.208 4 12Z" fill={colorful ? "#874FFF" : "currentColor"} />
|
||||
<path
|
||||
d="M4 4.00003C4 1.79203 5.79205 0 8.00006 0H12.0001V7.99997H8.00006C5.79205 7.99997 4 6.20803 4 4.00003Z"
|
||||
fill={colorful ? "#FF3737" : "currentColor"}
|
||||
/>
|
||||
<path
|
||||
d="M12 0H16.0001C18.2081 0 20.0001 1.79203 20.0001 4.00003C20.0001 6.20803 18.2081 7.99997 16.0001 7.99997H12V0Z"
|
||||
fill={colorful ? "#FF7237" : "currentColor"}
|
||||
/>
|
||||
<path
|
||||
d="M20.0001 12C20.0001 14.208 18.2081 16.0001 16.0001 16.0001C13.792 16.0001 12 14.208 12 12C12 9.79203 13.792 8 16.0001 8C18.2081 8 20.0001 9.79203 20.0001 12Z"
|
||||
fill={colorful ? "#00B6FF" : "currentColor"}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const FigmaLogoOutlined = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.25 2C7.51349 2 6.81155 2.28629 6.29747 2.78895C5.78414 3.29087 5.5 3.96677 5.5 4.66667C5.5 5.36657 5.78414 6.04247 6.29747 6.54438C6.81155 7.04705 7.51349 7.33333 8.25 7.33333H11V2H8.25ZM13 2V7.33333H15.75C16.1142 7.33333 16.4744 7.26316 16.8097 7.12736C17.145 6.99157 17.4482 6.79311 17.7025 6.54438C17.9569 6.29571 18.1574 6.00171 18.2938 5.67977C18.4301 5.35788 18.5 5.0137 18.5 4.66667C18.5 4.31964 18.4301 3.97545 18.2938 3.65356C18.1574 3.33162 17.9569 3.03763 17.7025 2.78895C17.4482 2.54022 17.145 2.34177 16.8097 2.20598C16.4744 2.07017 16.1142 2 15.75 2H13ZM18.6884 8.33334C18.8324 8.22191 18.9702 8.10211 19.1008 7.9744C19.5429 7.54211 19.8948 7.02769 20.1353 6.45991C20.3759 5.89208 20.5 5.28266 20.5 4.66667C20.5 4.05067 20.3759 3.44126 20.1353 2.87342C19.8948 2.30564 19.5429 1.79122 19.1008 1.35894C18.6587 0.926696 18.1351 0.584984 17.5605 0.352241C16.9858 0.119512 16.3707 0 15.75 0H8.25C6.99738 0 5.79167 0.486331 4.89923 1.35894C4.00603 2.23228 3.5 3.42165 3.5 4.66667C3.5 5.91169 4.00603 7.10105 4.89923 7.9744C5.03021 8.10247 5.16794 8.22222 5.31158 8.33333C5.16794 8.44445 5.03021 8.5642 4.89923 8.69227C4.00603 9.56562 3.5 10.755 3.5 12C3.5 13.245 4.00603 14.4344 4.89923 15.3077C5.03022 15.4358 5.16795 15.5556 5.31159 15.6667C5.16795 15.7778 5.03022 15.8975 4.89923 16.0256C4.00603 16.899 3.5 18.0883 3.5 19.3333C3.5 20.5784 4.00603 21.7677 4.89923 22.6411C5.79167 23.5137 6.99738 24 8.25 24C9.5026 24 10.7083 23.5137 11.6008 22.6411C12.494 21.7677 13 20.5784 13 19.3333V15.8051C13.2922 16.0089 13.6073 16.1799 13.9395 16.3144C14.5142 16.5472 15.1293 16.6667 15.75 16.6667C16.3707 16.6667 16.9858 16.5472 17.5605 16.3144C18.1351 16.0817 18.6587 15.74 19.1008 15.3077C19.5429 14.8754 19.8948 14.361 20.1353 13.7932C20.3759 13.2254 20.5 12.616 20.5 12C20.5 11.384 20.3759 10.7746 20.1353 10.2068C19.8948 9.63898 19.5429 9.12456 19.1008 8.69227C18.9702 8.56456 18.8324 8.44476 18.6884 8.33334ZM11 14.6667V9.33333H8.25C7.51349 9.33333 6.81155 9.61962 6.29747 10.1223C5.78414 10.6242 5.5 11.3001 5.5 12C5.5 12.6999 5.78414 13.3758 6.29747 13.8777C6.81155 14.3804 7.51349 14.6667 8.25 14.6667H11ZM11 16.6667H8.25C7.51349 16.6667 6.81155 16.953 6.29747 17.4556C5.78414 17.9575 5.5 18.6334 5.5 19.3333C5.5 20.0332 5.78414 20.7091 6.29747 21.2111C6.81155 21.7137 7.51349 22 8.25 22C8.98651 22 9.6884 21.7137 10.2025 21.2111C10.7159 20.7091 11 20.0332 11 19.3333V16.6667ZM15.75 9.33333C15.3858 9.33333 15.0256 9.4035 14.6903 9.53931C14.355 9.6751 14.0518 9.87356 13.7975 10.1223C13.5431 10.371 13.3426 10.665 13.2062 10.9869C13.0699 11.3088 13 11.653 13 12C13 12.347 13.0699 12.6912 13.2062 13.0131C13.3426 13.335 13.5431 13.629 13.7975 13.8777C14.0518 14.1264 14.355 14.3249 14.6903 14.4607C15.0256 14.5965 15.3858 14.6667 15.75 14.6667C16.1142 14.6667 16.4744 14.5965 16.8097 14.4607C17.145 14.3249 17.4482 14.1264 17.7025 13.8777C17.9569 13.629 18.1574 13.335 18.2938 13.0131C18.4301 12.6912 18.5 12.347 18.5 12C18.5 11.653 18.4301 11.3088 18.2938 10.9869C18.1574 10.665 17.9569 10.371 17.7025 10.1223C17.4482 9.87356 17.145 9.6751 16.8097 9.53931C16.4744 9.4035 16.1142 9.33333 15.75 9.33333Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const DribbleLogo = ({ colorful, ...props }: SVGProps<SVGSVGElement> & { colorful?: boolean }) => {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M12 23.625C18.4203 23.625 23.625 18.4203 23.625 12C23.625 5.57969 18.4203 0.375 12 0.375C5.57969 0.375 0.375 5.57969 0.375 12C0.375 18.4203 5.57969 23.625 12 23.625Z"
|
||||
fill={colorful ? "#EA4C89" : "none"}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 0C5.37527 0 0 5.37527 0 12C0 18.6248 5.37527 24 12 24C18.6117 24 24 18.6248 24 12C24 5.37527 18.6117 0 12 0ZM19.9262 5.53145C21.3579 7.27549 22.217 9.50107 22.243 11.9089C21.9046 11.8439 18.5206 11.154 15.1106 11.5835C15.0325 11.4143 14.9675 11.2321 14.8894 11.0499C14.6811 10.5554 14.4469 10.0477 14.2126 9.56618C17.9869 8.0304 19.705 5.81779 19.9262 5.53145ZM12 1.77007C14.603 1.77007 16.9848 2.74621 18.7939 4.34707C18.6117 4.60738 17.0629 6.67679 13.4186 8.04338C11.7397 4.95878 9.87855 2.43384 9.5922 2.04338C10.3601 1.86117 11.1671 1.77007 12 1.77007ZM7.63995 2.73319C7.91325 3.09761 9.73538 5.63558 11.4404 8.65508C6.65076 9.9306 2.42083 9.90458 1.96529 9.90458C2.62907 6.72885 4.77657 4.08676 7.63995 2.73319ZM1.74404 12.0131C1.74404 11.9089 1.74404 11.8048 1.74404 11.7007C2.18655 11.7136 7.15835 11.7787 12.2733 10.243C12.5727 10.8156 12.846 11.4013 13.1063 11.9869C12.9761 12.026 12.8329 12.0651 12.7028 12.1041C7.41865 13.8091 4.60738 18.4685 4.3731 18.859C2.7462 17.0499 1.74404 14.6421 1.74404 12.0131ZM12 22.256C9.6312 22.256 7.44469 21.449 5.71367 20.0954C5.89588 19.718 7.97827 15.7094 13.757 13.692C13.783 13.679 13.7961 13.679 13.8221 13.666C15.2668 17.4013 15.8525 20.5379 16.0087 21.436C14.7722 21.9696 13.4186 22.256 12 22.256ZM17.7136 20.4989C17.6096 19.8742 17.0629 16.8807 15.7223 13.1974C18.9371 12.6898 21.7484 13.5228 22.0998 13.6399C21.6573 16.4902 20.0173 18.9501 17.7136 20.4989Z"
|
||||
fill={colorful ? "#C32361" : "currentColor"}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const FacebookLogo = ({ colorful, ...props }: SVGProps<SVGSVGElement> & { colorful?: boolean }) => {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 17.9895 4.3882 22.954 10.125 23.8542V15.4688H7.07812V12H10.125V9.35625C10.125 6.34875 11.9166 4.6875 14.6576 4.6875C15.9701 4.6875 17.3438 4.92188 17.3438 4.92188V7.875H15.8306C14.34 7.875 13.875 8.80008 13.875 9.75V12H17.2031L16.6711 15.4688H13.875V23.8542C19.6118 22.954 24 17.9895 24 12Z"
|
||||
fill={colorful ? "#1877F2" : "currentColor"}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppleLogo = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M20.8426 17.1449C20.5099 17.9135 20.1161 18.6211 19.6598 19.2715C19.0379 20.1583 18.5286 20.7721 18.1362 21.113C17.5278 21.6724 16.876 21.959 16.178 21.9753C15.6769 21.9753 15.0726 21.8327 14.3691 21.5434C13.6634 21.2555 13.0148 21.113 12.4218 21.113C11.7998 21.113 11.1328 21.2555 10.4193 21.5434C9.70475 21.8327 9.1291 21.9834 8.68898 21.9984C8.01963 22.0269 7.35246 21.7322 6.6865 21.113C6.26145 20.7422 5.7298 20.1067 5.09291 19.2063C4.40957 18.2449 3.84778 17.13 3.40766 15.8589C2.9363 14.486 2.70001 13.1565 2.70001 11.8694C2.70001 10.3951 3.01859 9.12345 3.65671 8.05784C4.15821 7.20191 4.82539 6.52672 5.66041 6.03105C6.49543 5.53539 7.39768 5.2828 8.36931 5.26664C8.90096 5.26664 9.59815 5.43109 10.4645 5.75429C11.3285 6.07858 11.8832 6.24303 12.1264 6.24303C12.3083 6.24303 12.9245 6.05074 13.9692 5.66738C14.9571 5.31186 15.7909 5.16466 16.474 5.22264C18.3249 5.37202 19.7155 6.10167 20.6403 7.41619C18.9849 8.4192 18.1661 9.82403 18.1824 11.6262C18.1973 13.03 18.7065 14.1981 19.7074 15.1256C20.1609 15.5561 20.6675 15.8888 21.231 16.1251C21.1088 16.4795 20.9798 16.819 20.8426 17.1449ZM16.5976 0.440369C16.5976 1.54062 16.1956 2.56792 15.3944 3.51878C14.4275 4.64917 13.258 5.30236 11.9898 5.19929C11.9737 5.06729 11.9643 4.92837 11.9643 4.78239C11.9643 3.72615 12.4241 2.59576 13.2407 1.67152C13.6483 1.20356 14.1668 0.814453 14.7955 0.504058C15.4229 0.198295 16.0164 0.0292007 16.5745 0.000244141C16.5908 0.147331 16.5976 0.294426 16.5976 0.440355V0.440369Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TwitterLogo = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.9455 23L10.396 15.0901L3.44886 23H0.509766L9.09209 13.2311L0.509766 1H8.05571L13.286 8.45502L19.8393 1H22.7784L14.5943 10.3165L23.4914 23H15.9455ZM19.2185 20.77H17.2398L4.71811 3.23H6.6971L11.7121 10.2532L12.5793 11.4719L19.2185 20.77Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
118
src/components/base/checkbox/checkbox.tsx
Normal file
118
src/components/base/checkbox/checkbox.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { ReactNode, Ref } from "react";
|
||||
import { Checkbox as AriaCheckbox, type CheckboxProps as AriaCheckboxProps } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export interface CheckboxBaseProps {
|
||||
size?: "sm" | "md";
|
||||
className?: string;
|
||||
isFocusVisible?: boolean;
|
||||
isSelected?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isIndeterminate?: boolean;
|
||||
}
|
||||
|
||||
export const CheckboxBase = ({ className, isSelected, isDisabled, isIndeterminate, size = "sm", isFocusVisible = false }: CheckboxBaseProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"relative flex size-4 shrink-0 cursor-pointer appearance-none items-center justify-center rounded bg-primary ring-1 ring-primary ring-inset",
|
||||
size === "md" && "size-5 rounded-md",
|
||||
(isSelected || isIndeterminate) && "bg-brand-solid ring-bg-brand-solid",
|
||||
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
|
||||
isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
className={cx(
|
||||
"pointer-events-none absolute h-3 w-2.5 text-fg-white opacity-0 transition-inherit-all",
|
||||
size === "md" && "size-3.5",
|
||||
isIndeterminate && "opacity-100",
|
||||
isDisabled && "text-fg-disabled_subtle",
|
||||
)}
|
||||
>
|
||||
<path d="M2.91675 7H11.0834" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
className={cx(
|
||||
"pointer-events-none absolute size-3 text-fg-white opacity-0 transition-inherit-all",
|
||||
size === "md" && "size-3.5",
|
||||
isSelected && !isIndeterminate && "opacity-100",
|
||||
isDisabled && "text-fg-disabled_subtle",
|
||||
)}
|
||||
>
|
||||
<path d="M11.6666 3.5L5.24992 9.91667L2.33325 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
CheckboxBase.displayName = "CheckboxBase";
|
||||
|
||||
interface CheckboxProps extends AriaCheckboxProps {
|
||||
ref?: Ref<HTMLLabelElement>;
|
||||
size?: "sm" | "md";
|
||||
label?: ReactNode;
|
||||
hint?: ReactNode;
|
||||
}
|
||||
|
||||
export const Checkbox = ({ label, hint, size = "sm", className, ...ariaCheckboxProps }: CheckboxProps) => {
|
||||
const sizes = {
|
||||
sm: {
|
||||
root: "gap-2",
|
||||
textWrapper: "",
|
||||
label: "text-sm font-medium",
|
||||
hint: "text-sm",
|
||||
},
|
||||
md: {
|
||||
root: "gap-3",
|
||||
textWrapper: "gap-0.5",
|
||||
label: "text-md font-medium",
|
||||
hint: "text-md",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<AriaCheckbox
|
||||
{...ariaCheckboxProps}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"flex items-start",
|
||||
state.isDisabled && "cursor-not-allowed",
|
||||
sizes[size].root,
|
||||
typeof className === "function" ? className(state) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isSelected, isIndeterminate, isDisabled, isFocusVisible }) => (
|
||||
<>
|
||||
<CheckboxBase
|
||||
size={size}
|
||||
isSelected={isSelected}
|
||||
isIndeterminate={isIndeterminate}
|
||||
isDisabled={isDisabled}
|
||||
isFocusVisible={isFocusVisible}
|
||||
className={label || hint ? "mt-0.5" : ""}
|
||||
/>
|
||||
{(label || hint) && (
|
||||
<div className={cx("inline-flex flex-col", sizes[size].textWrapper)}>
|
||||
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
||||
{hint && (
|
||||
<span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
||||
{hint}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AriaCheckbox>
|
||||
);
|
||||
};
|
||||
Checkbox.displayName = "Checkbox";
|
||||
161
src/components/base/dropdown/dropdown.tsx
Normal file
161
src/components/base/dropdown/dropdown.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { FC, RefAttributes } from "react";
|
||||
import { DotsVertical } from "@untitledui/icons";
|
||||
import type {
|
||||
ButtonProps as AriaButtonProps,
|
||||
MenuItemProps as AriaMenuItemProps,
|
||||
MenuProps as AriaMenuProps,
|
||||
PopoverProps as AriaPopoverProps,
|
||||
SeparatorProps as AriaSeparatorProps,
|
||||
} from "react-aria-components";
|
||||
import {
|
||||
Button as AriaButton,
|
||||
Header as AriaHeader,
|
||||
Menu as AriaMenu,
|
||||
MenuItem as AriaMenuItem,
|
||||
MenuSection as AriaMenuSection,
|
||||
MenuTrigger as AriaMenuTrigger,
|
||||
Popover as AriaPopover,
|
||||
Separator as AriaSeparator,
|
||||
} from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface DropdownItemProps extends AriaMenuItemProps {
|
||||
/** The label of the item to be displayed. */
|
||||
label?: string;
|
||||
/** An addon to be displayed on the right side of the item. */
|
||||
addon?: string;
|
||||
/** If true, the item will not have any styles. */
|
||||
unstyled?: boolean;
|
||||
/** An icon to be displayed on the left side of the item. */
|
||||
icon?: FC<{ className?: string }>;
|
||||
}
|
||||
|
||||
const DropdownItem = ({ label, children, addon, icon: Icon, unstyled, ...props }: DropdownItemProps) => {
|
||||
if (unstyled) {
|
||||
return <AriaMenuItem id={label} textValue={label} {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AriaMenuItem
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"group block cursor-pointer px-1.5 py-px outline-hidden",
|
||||
state.isDisabled && "cursor-not-allowed",
|
||||
typeof props.className === "function" ? props.className(state) : props.className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{(state) => (
|
||||
<div
|
||||
className={cx(
|
||||
"relative flex items-center rounded-md px-2.5 py-2 outline-focus-ring transition duration-100 ease-linear",
|
||||
!state.isDisabled && "group-hover:bg-primary_hover",
|
||||
state.isFocused && "bg-primary_hover",
|
||||
state.isFocusVisible && "outline-2 -outline-offset-2",
|
||||
)}
|
||||
>
|
||||
{Icon && (
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
className={cx("mr-2 size-4 shrink-0 stroke-[2.25px]", state.isDisabled ? "text-fg-disabled" : "text-fg-quaternary")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={cx(
|
||||
"grow truncate text-sm font-semibold",
|
||||
state.isDisabled ? "text-disabled" : "text-secondary",
|
||||
state.isFocused && "text-secondary_hover",
|
||||
)}
|
||||
>
|
||||
{label || (typeof children === "function" ? children(state) : children)}
|
||||
</span>
|
||||
|
||||
{addon && (
|
||||
<span
|
||||
className={cx(
|
||||
"ml-3 shrink-0 rounded px-1 py-px text-xs font-medium ring-1 ring-secondary ring-inset",
|
||||
state.isDisabled ? "text-disabled" : "text-quaternary",
|
||||
)}
|
||||
>
|
||||
{addon}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</AriaMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
interface DropdownMenuProps<T extends object> extends AriaMenuProps<T> {}
|
||||
|
||||
const DropdownMenu = <T extends object>(props: DropdownMenuProps<T>) => {
|
||||
return (
|
||||
<AriaMenu
|
||||
disallowEmptySelection
|
||||
selectionMode="single"
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx("h-min overflow-y-auto py-1 outline-hidden select-none", typeof props.className === "function" ? props.className(state) : props.className)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface DropdownPopoverProps extends AriaPopoverProps {}
|
||||
|
||||
const DropdownPopover = (props: DropdownPopoverProps) => {
|
||||
return (
|
||||
<AriaPopover
|
||||
placement="bottom right"
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"w-62 origin-(--trigger-anchor-point) overflow-auto rounded-lg bg-primary shadow-lg ring-1 ring-secondary_alt will-change-transform",
|
||||
state.isEntering &&
|
||||
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||
state.isExiting &&
|
||||
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||
typeof props.className === "function" ? props.className(state) : props.className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</AriaPopover>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownSeparator = (props: AriaSeparatorProps) => {
|
||||
return <AriaSeparator {...props} className={cx("my-1 h-px w-full bg-border-secondary", props.className)} />;
|
||||
};
|
||||
|
||||
const DropdownDotsButton = (props: AriaButtonProps & RefAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<AriaButton
|
||||
{...props}
|
||||
aria-label="Open menu"
|
||||
className={(state) =>
|
||||
cx(
|
||||
"cursor-pointer rounded-md text-fg-quaternary outline-focus-ring transition duration-100 ease-linear",
|
||||
(state.isPressed || state.isHovered) && "text-fg-quaternary_hover",
|
||||
(state.isPressed || state.isFocusVisible) && "outline-2 outline-offset-2",
|
||||
typeof props.className === "function" ? props.className(state) : props.className,
|
||||
)
|
||||
}
|
||||
>
|
||||
<DotsVertical className="size-5 transition-inherit-all" />
|
||||
</AriaButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const Dropdown = {
|
||||
Root: AriaMenuTrigger,
|
||||
Popover: DropdownPopover,
|
||||
Menu: DropdownMenu,
|
||||
Section: AriaMenuSection,
|
||||
SectionHeader: AriaHeader,
|
||||
Item: DropdownItem,
|
||||
Separator: DropdownSeparator,
|
||||
DotsButton: DropdownDotsButton,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { DetailedReactHTMLElement, HTMLAttributes, ReactNode } from "react";
|
||||
import React, { cloneElement, useRef } from "react";
|
||||
import { filterDOMProps } from "@react-aria/utils";
|
||||
|
||||
interface FileTriggerProps {
|
||||
/**
|
||||
* Specifies what mime type of files are allowed.
|
||||
*/
|
||||
acceptedFileTypes?: Array<string>;
|
||||
/**
|
||||
* Whether multiple files can be selected.
|
||||
*/
|
||||
allowsMultiple?: boolean;
|
||||
/**
|
||||
* Specifies the use of a media capture mechanism to capture the media on the spot.
|
||||
*/
|
||||
defaultCamera?: "user" | "environment";
|
||||
/**
|
||||
* Handler when a user selects a file.
|
||||
*/
|
||||
onSelect?: (files: FileList | null) => void;
|
||||
/**
|
||||
* The children of the component.
|
||||
*/
|
||||
children: ReactNode;
|
||||
/**
|
||||
* Enables the selection of directories instead of individual files.
|
||||
*/
|
||||
acceptDirectory?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A FileTrigger allows a user to access the file system with any pressable React Aria or React Spectrum component, or custom components built with usePress.
|
||||
*/
|
||||
export const FileTrigger = (props: FileTriggerProps) => {
|
||||
const { children, onSelect, acceptedFileTypes, allowsMultiple, defaultCamera, acceptDirectory, ...rest } = props;
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const domProps = filterDOMProps(rest);
|
||||
|
||||
// Make sure that only one child is passed to the component.
|
||||
const clonableElement = React.Children.only(children);
|
||||
|
||||
// Clone the child element and add an `onClick` handler to open the file dialog.
|
||||
const mainElement = cloneElement(clonableElement as DetailedReactHTMLElement<HTMLAttributes<HTMLElement>, HTMLElement>, {
|
||||
onClick: () => {
|
||||
if (inputRef.current?.value) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
inputRef.current?.click();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{mainElement}
|
||||
<input
|
||||
{...domProps}
|
||||
type="file"
|
||||
ref={inputRef}
|
||||
style={{ display: "none" }}
|
||||
accept={acceptedFileTypes?.toString()}
|
||||
onChange={(e) => onSelect?.(e.target.files)}
|
||||
capture={defaultCamera}
|
||||
multiple={allowsMultiple}
|
||||
// @ts-expect-error
|
||||
webkitdirectory={acceptDirectory ? "" : undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
8
src/components/base/form/form.tsx
Normal file
8
src/components/base/form/form.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { ComponentPropsWithRef } from "react";
|
||||
import { Form as AriaForm } from "react-aria-components";
|
||||
|
||||
export const Form = (props: ComponentPropsWithRef<typeof AriaForm>) => {
|
||||
return <AriaForm {...props} />;
|
||||
};
|
||||
|
||||
Form.displayName = "Form";
|
||||
31
src/components/base/input/hint-text.tsx
Normal file
31
src/components/base/input/hint-text.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { ReactNode, Ref } from "react";
|
||||
import type { TextProps as AriaTextProps } from "react-aria-components";
|
||||
import { Text as AriaText } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface HintTextProps extends AriaTextProps {
|
||||
/** Indicates that the hint text is an error message. */
|
||||
isInvalid?: boolean;
|
||||
ref?: Ref<HTMLElement>;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const HintText = ({ isInvalid, className, ...props }: HintTextProps) => {
|
||||
return (
|
||||
<AriaText
|
||||
{...props}
|
||||
slot={isInvalid ? "errorMessage" : "description"}
|
||||
className={cx(
|
||||
"text-sm text-tertiary",
|
||||
|
||||
// Invalid state
|
||||
isInvalid && "text-error-primary",
|
||||
"group-invalid:text-error-primary",
|
||||
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
HintText.displayName = "HintText";
|
||||
131
src/components/base/input/input-group.tsx
Normal file
131
src/components/base/input/input-group.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { type HTMLAttributes, type ReactNode } from "react";
|
||||
import { HintText } from "@/components/base/input/hint-text";
|
||||
import type { InputBaseProps } from "@/components/base/input/input";
|
||||
import { TextField } from "@/components/base/input/input";
|
||||
import { Label } from "@/components/base/input/label";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
|
||||
interface InputPrefixProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/** The position of the prefix. */
|
||||
position?: "leading" | "trailing";
|
||||
/** The size of the prefix. */
|
||||
size?: "sm" | "md";
|
||||
/** Indicates that the prefix is disabled. */
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const InputPrefix = ({ isDisabled, children, ...props }: InputPrefixProps) => (
|
||||
<span
|
||||
{...props}
|
||||
className={cx(
|
||||
"flex text-md text-tertiary shadow-xs ring-1 ring-border-primary ring-inset",
|
||||
// Styles when the prefix is within an `InputGroup`
|
||||
"in-data-input-wrapper:in-data-leading:-mr-px in-data-input-wrapper:in-data-leading:rounded-l-lg",
|
||||
"in-data-input-wrapper:in-data-trailing:-ml-px in-data-input-wrapper:in-data-trailing:rounded-r-lg",
|
||||
// Size styles based on size when within an `InputGroup`
|
||||
"in-data-input-wrapper:in-data-[input-size=md]:py-2.5 in-data-input-wrapper:in-data-[input-size=md]:pr-3 in-data-input-wrapper:in-data-[input-size=md]:pl-3.5 in-data-input-wrapper:in-data-[input-size=sm]:px-3 in-data-input-wrapper:in-data-[input-size=sm]:py-2",
|
||||
// Disabled styles
|
||||
isDisabled && "border-disabled bg-disabled_subtle text-tertiary",
|
||||
"in-data-input-wrapper:group-disabled:bg-disabled_subtle in-data-input-wrapper:group-disabled:text-disabled in-data-input-wrapper:group-disabled:ring-border-disabled",
|
||||
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
// `${string}ClassName` is used to omit any className prop that ends with a `ClassName` suffix
|
||||
interface InputGroupProps extends Omit<InputBaseProps, "type" | "icon" | "placeholder" | "tooltip" | "shortcut" | `${string}ClassName`> {
|
||||
/** A prefix text that is displayed in the same box as the input.*/
|
||||
prefix?: string;
|
||||
/** A leading addon that is displayed with visual separation from the input. */
|
||||
leadingAddon?: ReactNode;
|
||||
/** A trailing addon that is displayed with visual separation from the input. */
|
||||
trailingAddon?: ReactNode;
|
||||
/** The class name to apply to the input group. */
|
||||
className?: string;
|
||||
/** The children of the input group (i.e `<InputBase />`) */
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const InputGroup = ({ size = "sm", prefix, leadingAddon, trailingAddon, label, hint, children, ...props }: InputGroupProps) => {
|
||||
const hasLeading = !!leadingAddon;
|
||||
const hasTrailing = !!trailingAddon;
|
||||
|
||||
const paddings = sortCx({
|
||||
sm: {
|
||||
input: cx(
|
||||
// Apply padding styles when select element is passed as a child
|
||||
hasLeading && "group-has-[&>select]:px-2.5 group-has-[&>select]:pl-2.5",
|
||||
hasTrailing && (prefix ? "group-has-[&>select]:pr-6 group-has-[&>select]:pl-0" : "group-has-[&>select]:pr-6 group-has-[&>select]:pl-3"),
|
||||
),
|
||||
leadingText: "pl-3",
|
||||
},
|
||||
md: {
|
||||
input: cx(
|
||||
// Apply padding styles when select element is passed as a child
|
||||
hasLeading && "group-has-[&>select]:px-3 group-has-[&>select]:pl-3",
|
||||
hasTrailing && (prefix ? "group-has-[&>select]:pr-6 group-has-[&>select]:pl-0" : "group-has-[&>select]:pr-6 group-has-[&>select]:pl-3"),
|
||||
),
|
||||
leadingText: "pl-3.5",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TextField
|
||||
size={size}
|
||||
aria-label={label || undefined}
|
||||
inputClassName={cx(paddings[size].input)}
|
||||
tooltipClassName={cx(hasTrailing && !hasLeading && "group-has-[&>select]:right-0")}
|
||||
wrapperClassName={cx(
|
||||
"z-10",
|
||||
// Apply styles based on the presence of leading or trailing elements
|
||||
hasLeading && "rounded-l-none",
|
||||
hasTrailing && "rounded-r-none",
|
||||
// When select element is passed as a child
|
||||
"group-has-[&>select]:bg-transparent group-has-[&>select]:shadow-none group-has-[&>select]:ring-0 group-has-[&>select]:focus-within:ring-0",
|
||||
// In `Input` component, there is "group-disabled" class so here we need to use "group-disabled:group-has-[&>select]" to avoid conflict
|
||||
"group-disabled:group-has-[&>select]:bg-transparent",
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{({ isDisabled, isInvalid, isRequired }) => (
|
||||
<>
|
||||
{label && <Label isRequired={isRequired}>{label}</Label>}
|
||||
|
||||
<div
|
||||
data-input-size={size}
|
||||
className={cx(
|
||||
"group relative flex h-max w-full flex-row justify-center rounded-lg bg-primary transition-all duration-100 ease-linear",
|
||||
|
||||
// Only apply focus ring when child is select and input is focused
|
||||
"has-[&>select]:shadow-xs has-[&>select]:ring-1 has-[&>select]:ring-border-primary has-[&>select]:ring-inset has-[&>select]:has-[input:focus]:ring-2 has-[&>select]:has-[input:focus]:ring-border-brand",
|
||||
|
||||
isDisabled && "cursor-not-allowed has-[&>select]:bg-disabled_subtle has-[&>select]:ring-border-disabled",
|
||||
isInvalid && "has-[&>select]:ring-border-error_subtle has-[&>select]:has-[input:focus]:ring-border-error",
|
||||
)}
|
||||
>
|
||||
{leadingAddon && <section data-leading={hasLeading || undefined}>{leadingAddon}</section>}
|
||||
|
||||
{prefix && (
|
||||
<span className={cx("my-auto grow pr-2", paddings[size].leadingText)}>
|
||||
<p className={cx("text-md text-tertiary", isDisabled && "text-disabled")}>{prefix}</p>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{children}
|
||||
|
||||
{trailingAddon && <section data-trailing={hasTrailing || undefined}>{trailingAddon}</section>}
|
||||
</div>
|
||||
|
||||
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||
</>
|
||||
)}
|
||||
</TextField>
|
||||
);
|
||||
};
|
||||
|
||||
InputGroup.Prefix = InputPrefix;
|
||||
|
||||
InputGroup.displayName = "InputGroup";
|
||||
121
src/components/base/input/input-payment.tsx
Normal file
121
src/components/base/input/input-payment.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useControlledState } from "@react-stately/utils";
|
||||
import { HintText } from "@/components/base/input/hint-text";
|
||||
import type { InputBaseProps } from "@/components/base/input/input";
|
||||
import { InputBase, TextField } from "@/components/base/input/input";
|
||||
import { Label } from "@/components/base/input/label";
|
||||
import { AmexIcon, DiscoverIcon, MastercardIcon, UnionPayIcon, VisaIcon } from "@/components/foundations/payment-icons";
|
||||
|
||||
const cardTypes = [
|
||||
{
|
||||
name: "Visa",
|
||||
pattern: /^4[0-9]{3,}$/, // Visa card numbers start with 4 and are 13 or 16 digits long
|
||||
card: "visa",
|
||||
icon: VisaIcon,
|
||||
},
|
||||
{
|
||||
name: "MasterCard",
|
||||
pattern: /^5[1-5][0-9]{2,}$/, // MasterCard numbers start with 51-55 and are 16 digits long
|
||||
card: "mastercard",
|
||||
icon: MastercardIcon,
|
||||
},
|
||||
{
|
||||
name: "American Express",
|
||||
pattern: /^3[47][0-9]{2,}$/, // American Express numbers start with 34 or 37 and are 15 digits long
|
||||
card: "amex",
|
||||
icon: AmexIcon,
|
||||
},
|
||||
{
|
||||
name: "Discover",
|
||||
pattern: /^6(?:011|5[0-9]{2}|4[4-9][0-9])[0-9]{12}$/, // Discover card numbers start with 6011 or 65 and are 16 digits long
|
||||
card: "discover",
|
||||
icon: DiscoverIcon,
|
||||
},
|
||||
{
|
||||
name: "UnionPay",
|
||||
pattern: /^(62|88)[0-9]{14,17}$/, // UnionPay card numbers start with 62 or 88 and are between 15-19 digits long
|
||||
card: "unionpay",
|
||||
icon: UnionPayIcon,
|
||||
},
|
||||
{
|
||||
name: "Unknown",
|
||||
pattern: /.*/, // Fallback pattern for unknown cards
|
||||
card: "unknown",
|
||||
icon: MastercardIcon,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Detect the card type based on the card number.
|
||||
* @param number The card number to detect the type for.
|
||||
* @returns The matching card type object.
|
||||
*/
|
||||
const detectCardType = (number: string) => {
|
||||
// Remove all spaces
|
||||
const sanitizedNumber = number.replace(/\D/g, "");
|
||||
|
||||
// Find the matching card type
|
||||
const card = cardTypes.find((cardType) => cardType.pattern.test(sanitizedNumber));
|
||||
|
||||
return card || cardTypes[cardTypes.length - 1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Format the card number in groups of 4 digits (i.e. 1234 5678 9012 3456).
|
||||
*/
|
||||
export const formatCardNumber = (number: string) => {
|
||||
// Remove non-numeric characters
|
||||
const cleaned = number.replace(/\D/g, "");
|
||||
|
||||
// Format the card number in groups of 4 digits
|
||||
const match = cleaned.match(/\d{1,4}/g);
|
||||
|
||||
if (match) {
|
||||
return match.join(" ");
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
interface PaymentInputProps extends Omit<InputBaseProps, "icon"> {}
|
||||
|
||||
export const PaymentInput = ({ onChange, value, defaultValue, className, maxLength = 19, label, hint, ...props }: PaymentInputProps) => {
|
||||
const [cardNumber, setCardNumber] = useControlledState(value, defaultValue || "", (value) => {
|
||||
// Remove all non-numeric characters
|
||||
value = value.replace(/\D/g, "");
|
||||
|
||||
onChange?.(value || "");
|
||||
});
|
||||
|
||||
const card = detectCardType(cardNumber);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
aria-label={!label ? props?.placeholder : undefined}
|
||||
{...props}
|
||||
className={className}
|
||||
inputMode="numeric"
|
||||
maxLength={maxLength}
|
||||
value={formatCardNumber(cardNumber)}
|
||||
onChange={setCardNumber}
|
||||
>
|
||||
{({ isDisabled, isInvalid, isRequired }) => (
|
||||
<>
|
||||
{label && <Label isRequired={isRequired}>{label}</Label>}
|
||||
|
||||
<InputBase
|
||||
{...props}
|
||||
isDisabled={isDisabled}
|
||||
isInvalid={isInvalid}
|
||||
icon={card.icon}
|
||||
inputClassName="pl-13"
|
||||
iconClassName="left-2.5 h-6 w-8.5"
|
||||
/>
|
||||
|
||||
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||
</>
|
||||
)}
|
||||
</TextField>
|
||||
);
|
||||
};
|
||||
|
||||
PaymentInput.displayName = "PaymentInput";
|
||||
269
src/components/base/input/input.tsx
Normal file
269
src/components/base/input/input.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { type ComponentType, type HTMLAttributes, type ReactNode, type Ref, createContext, useContext } from "react";
|
||||
import { HelpCircle, InfoCircle } from "@untitledui/icons";
|
||||
import type { InputProps as AriaInputProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components";
|
||||
import { Group as AriaGroup, Input as AriaInput, TextField as AriaTextField } from "react-aria-components";
|
||||
import { HintText } from "@/components/base/input/hint-text";
|
||||
import { Label } from "@/components/base/input/label";
|
||||
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
|
||||
export interface InputBaseProps extends TextFieldProps {
|
||||
/** Tooltip message on hover. */
|
||||
tooltip?: string;
|
||||
/**
|
||||
* Input size.
|
||||
* @default "sm"
|
||||
*/
|
||||
size?: "sm" | "md";
|
||||
/** Placeholder text. */
|
||||
placeholder?: string;
|
||||
/** Class name for the icon. */
|
||||
iconClassName?: string;
|
||||
/** Class name for the input. */
|
||||
inputClassName?: string;
|
||||
/** Class name for the input wrapper. */
|
||||
wrapperClassName?: string;
|
||||
/** Class name for the tooltip. */
|
||||
tooltipClassName?: string;
|
||||
/** Keyboard shortcut to display. */
|
||||
shortcut?: string | boolean;
|
||||
ref?: Ref<HTMLInputElement>;
|
||||
groupRef?: Ref<HTMLDivElement>;
|
||||
/** Icon component to display on the left side of the input. */
|
||||
icon?: ComponentType<HTMLAttributes<HTMLOrSVGElement>>;
|
||||
}
|
||||
|
||||
export const InputBase = ({
|
||||
ref,
|
||||
tooltip,
|
||||
shortcut,
|
||||
groupRef,
|
||||
size = "sm",
|
||||
isInvalid,
|
||||
isDisabled,
|
||||
icon: Icon,
|
||||
placeholder,
|
||||
wrapperClassName,
|
||||
tooltipClassName,
|
||||
inputClassName,
|
||||
iconClassName,
|
||||
// Omit this prop to avoid invalid HTML attribute warning
|
||||
isRequired: _isRequired,
|
||||
...inputProps
|
||||
}: Omit<InputBaseProps, "label" | "hint">) => {
|
||||
// Check if the input has a leading icon or tooltip
|
||||
const hasTrailingIcon = tooltip || isInvalid;
|
||||
const hasLeadingIcon = Icon;
|
||||
|
||||
// If the input is inside a `TextFieldContext`, use its context to simplify applying styles
|
||||
const context = useContext(TextFieldContext);
|
||||
|
||||
const inputSize = context?.size || size;
|
||||
|
||||
const sizes = sortCx({
|
||||
sm: {
|
||||
root: cx("px-3 py-2", hasTrailingIcon && "pr-9", hasLeadingIcon && "pl-10"),
|
||||
iconLeading: "left-3",
|
||||
iconTrailing: "right-3",
|
||||
shortcut: "pr-2.5",
|
||||
},
|
||||
md: {
|
||||
root: cx("px-3.5 py-2.5", hasTrailingIcon && "pr-9.5", hasLeadingIcon && "pl-10.5"),
|
||||
iconLeading: "left-3.5",
|
||||
iconTrailing: "right-3.5",
|
||||
shortcut: "pr-3",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AriaGroup
|
||||
{...{ isDisabled, isInvalid }}
|
||||
ref={groupRef}
|
||||
className={({ isFocusWithin, isDisabled, isInvalid }) =>
|
||||
cx(
|
||||
"relative flex w-full flex-row place-content-center place-items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary transition-shadow duration-100 ease-linear ring-inset",
|
||||
|
||||
isFocusWithin && !isDisabled && "ring-2 ring-brand",
|
||||
|
||||
// Disabled state styles
|
||||
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
|
||||
"group-disabled:cursor-not-allowed group-disabled:bg-disabled_subtle group-disabled:ring-disabled",
|
||||
|
||||
// Invalid state styles
|
||||
isInvalid && "ring-error_subtle",
|
||||
"group-invalid:ring-error_subtle",
|
||||
|
||||
// Invalid state with focus-within styles
|
||||
isInvalid && isFocusWithin && "ring-2 ring-error",
|
||||
isFocusWithin && "group-invalid:ring-2 group-invalid:ring-error",
|
||||
|
||||
context?.wrapperClassName,
|
||||
wrapperClassName,
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Leading icon and Payment icon */}
|
||||
{Icon && (
|
||||
<Icon
|
||||
className={cx(
|
||||
"pointer-events-none absolute size-5 text-fg-quaternary",
|
||||
isDisabled && "text-fg-disabled",
|
||||
sizes[inputSize].iconLeading,
|
||||
context?.iconClassName,
|
||||
iconClassName,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Input field */}
|
||||
<AriaInput
|
||||
{...(inputProps as AriaInputProps)}
|
||||
ref={ref}
|
||||
placeholder={placeholder}
|
||||
className={cx(
|
||||
"m-0 w-full bg-transparent text-md text-primary ring-0 outline-hidden placeholder:text-placeholder autofill:rounded-lg autofill:text-primary",
|
||||
isDisabled && "cursor-not-allowed text-disabled",
|
||||
sizes[inputSize].root,
|
||||
context?.inputClassName,
|
||||
inputClassName,
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Tooltip and help icon */}
|
||||
{tooltip && !isInvalid && (
|
||||
<Tooltip title={tooltip} placement="top">
|
||||
<TooltipTrigger
|
||||
className={cx(
|
||||
"absolute cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover",
|
||||
sizes[inputSize].iconTrailing,
|
||||
context?.tooltipClassName,
|
||||
tooltipClassName,
|
||||
)}
|
||||
>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Invalid icon */}
|
||||
{isInvalid && (
|
||||
<InfoCircle
|
||||
className={cx(
|
||||
"pointer-events-none absolute size-4 text-fg-error-secondary",
|
||||
sizes[inputSize].iconTrailing,
|
||||
context?.tooltipClassName,
|
||||
tooltipClassName,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Shortcut */}
|
||||
{shortcut && (
|
||||
<div
|
||||
className={cx(
|
||||
"pointer-events-none absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
|
||||
sizes[inputSize].shortcut,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cx(
|
||||
"pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
|
||||
isDisabled && "bg-transparent text-disabled",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{typeof shortcut === "string" ? shortcut : "⌘K"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</AriaGroup>
|
||||
);
|
||||
};
|
||||
|
||||
InputBase.displayName = "InputBase";
|
||||
|
||||
interface BaseProps {
|
||||
/** Label text for the input */
|
||||
label?: string;
|
||||
/** Helper text displayed below the input */
|
||||
hint?: ReactNode;
|
||||
}
|
||||
|
||||
interface TextFieldProps
|
||||
extends BaseProps,
|
||||
AriaTextFieldProps,
|
||||
Pick<InputBaseProps, "size" | "wrapperClassName" | "inputClassName" | "iconClassName" | "tooltipClassName"> {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const TextFieldContext = createContext<TextFieldProps>({});
|
||||
|
||||
export const TextField = ({ className, ...props }: TextFieldProps) => {
|
||||
return (
|
||||
<TextFieldContext.Provider value={props}>
|
||||
<AriaTextField
|
||||
{...props}
|
||||
data-input-wrapper
|
||||
className={(state) =>
|
||||
cx("group flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
|
||||
}
|
||||
/>
|
||||
</TextFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
TextField.displayName = "TextField";
|
||||
|
||||
interface InputProps extends InputBaseProps, BaseProps {
|
||||
/** Whether to hide required indicator from label */
|
||||
hideRequiredIndicator?: boolean;
|
||||
}
|
||||
|
||||
export const Input = ({
|
||||
size = "sm",
|
||||
placeholder,
|
||||
icon: Icon,
|
||||
label,
|
||||
hint,
|
||||
shortcut,
|
||||
hideRequiredIndicator,
|
||||
className,
|
||||
ref,
|
||||
groupRef,
|
||||
tooltip,
|
||||
iconClassName,
|
||||
inputClassName,
|
||||
wrapperClassName,
|
||||
tooltipClassName,
|
||||
...props
|
||||
}: InputProps) => {
|
||||
return (
|
||||
<TextField aria-label={!label ? placeholder : undefined} {...props} className={className}>
|
||||
{({ isRequired, isInvalid }) => (
|
||||
<>
|
||||
{label && <Label isRequired={hideRequiredIndicator ? !hideRequiredIndicator : isRequired}>{label}</Label>}
|
||||
|
||||
<InputBase
|
||||
{...{
|
||||
ref,
|
||||
groupRef,
|
||||
size,
|
||||
placeholder,
|
||||
icon: Icon,
|
||||
shortcut,
|
||||
iconClassName,
|
||||
inputClassName,
|
||||
wrapperClassName,
|
||||
tooltipClassName,
|
||||
tooltip,
|
||||
}}
|
||||
/>
|
||||
|
||||
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||
</>
|
||||
)}
|
||||
</TextField>
|
||||
);
|
||||
};
|
||||
|
||||
Input.displayName = "Input";
|
||||
48
src/components/base/input/label.tsx
Normal file
48
src/components/base/input/label.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { ReactNode, Ref } from "react";
|
||||
import { HelpCircle } from "@untitledui/icons";
|
||||
import type { LabelProps as AriaLabelProps } from "react-aria-components";
|
||||
import { Label as AriaLabel } from "react-aria-components";
|
||||
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface LabelProps extends AriaLabelProps {
|
||||
children: ReactNode;
|
||||
isRequired?: boolean;
|
||||
tooltip?: string;
|
||||
tooltipDescription?: string;
|
||||
ref?: Ref<HTMLLabelElement>;
|
||||
}
|
||||
|
||||
export const Label = ({ isRequired, tooltip, tooltipDescription, className, ...props }: LabelProps) => {
|
||||
return (
|
||||
<AriaLabel
|
||||
// Used for conditionally hiding/showing the label element via CSS:
|
||||
// <Input label="Visible only on mobile" className="lg:**:data-label:hidden" />
|
||||
// or
|
||||
// <Input label="Visible only on mobile" className="lg:label:hidden" />
|
||||
data-label="true"
|
||||
{...props}
|
||||
className={cx("flex cursor-default items-center gap-0.5 text-sm font-medium text-secondary", className)}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
<span className={cx("hidden text-brand-tertiary", isRequired && "block", typeof isRequired === "undefined" && "group-required:block")}>*</span>
|
||||
|
||||
{tooltip && (
|
||||
<Tooltip title={tooltip} description={tooltipDescription} placement="top">
|
||||
<TooltipTrigger
|
||||
// `TooltipTrigger` inherits the disabled state from the parent form field
|
||||
// but we don't that. We want the tooltip be enabled even if the parent
|
||||
// field is disabled.
|
||||
isDisabled={false}
|
||||
className="cursor-pointer text-fg-quaternary transition duration-200 hover:text-fg-quaternary_hover focus:text-fg-quaternary_hover"
|
||||
>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
)}
|
||||
</AriaLabel>
|
||||
);
|
||||
};
|
||||
|
||||
Label.displayName = "Label";
|
||||
152
src/components/base/pin-input/pin-input.tsx
Normal file
152
src/components/base/pin-input/pin-input.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { ComponentPropsWithRef } from "react";
|
||||
import { createContext, useContext, useId } from "react";
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
type PinInputContextType = {
|
||||
size: "sm" | "md" | "lg";
|
||||
disabled: boolean;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const PinInputContext = createContext<PinInputContextType>({
|
||||
size: "sm",
|
||||
id: "",
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
export const usePinInputContext = () => {
|
||||
const context = useContext(PinInputContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("The 'usePinInputContext' hook must be used within a '<PinInput />'");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
interface RootProps extends ComponentPropsWithRef<"div"> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Root = ({ className, size = "md", disabled = false, ...props }: RootProps) => {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<PinInputContext.Provider value={{ size, disabled, id }}>
|
||||
<div role="group" className={cx("flex h-max flex-col gap-1.5", className)} {...props} />
|
||||
</PinInputContext.Provider>
|
||||
);
|
||||
};
|
||||
Root.displayName = "Root";
|
||||
|
||||
type GroupProps = ComponentPropsWithRef<typeof OTPInput> & {
|
||||
width?: number;
|
||||
inputClassName?: string;
|
||||
};
|
||||
|
||||
const Group = ({ inputClassName, containerClassName, width, maxLength = 4, ...props }: GroupProps) => {
|
||||
const { id, size, disabled } = usePinInputContext();
|
||||
|
||||
const heights = {
|
||||
sm: "h-16.5",
|
||||
md: "h-20.5",
|
||||
lg: "h-24.5",
|
||||
};
|
||||
|
||||
return (
|
||||
<OTPInput
|
||||
{...props}
|
||||
size={width}
|
||||
maxLength={maxLength}
|
||||
disabled={disabled}
|
||||
id={"pin-input-" + id}
|
||||
aria-label="Enter your pin"
|
||||
aria-labelledby={"pin-input-label-" + id}
|
||||
aria-describedby={"pin-input-description-" + id}
|
||||
containerClassName={cx("flex flex-row gap-3", size === "sm" && "gap-2", heights[size], containerClassName)}
|
||||
className={cx("w-full! disabled:cursor-not-allowed", inputClassName)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Group.displayName = "Group";
|
||||
|
||||
const sizes = {
|
||||
sm: "size-16 px-2 py-0.5 text-display-lg font-medium",
|
||||
md: "size-20 px-2 py-2.5 text-display-lg font-medium",
|
||||
lg: "size-24 px-2 py-3 text-display-xl font-medium",
|
||||
};
|
||||
|
||||
const Slot = ({ index, className, ...props }: ComponentPropsWithRef<"div"> & { index: number }) => {
|
||||
const { size, disabled } = usePinInputContext();
|
||||
const { slots, isFocused } = useContext(OTPInputContext);
|
||||
const slot = slots[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
aria-label={"Enter digit " + (index + 1) + " of " + slots.length}
|
||||
className={cx(
|
||||
"relative flex items-center justify-center rounded-xl bg-primary text-center text-placeholder_subtle shadow-xs ring-1 ring-primary transition-[box-shadow,background-color] duration-100 ease-linear ring-inset",
|
||||
sizes[size],
|
||||
isFocused && slot?.isActive && "ring-2 ring-brand outline-2 outline-offset-2 outline-brand",
|
||||
slot?.char && "text-brand-tertiary_alt ring-2 ring-brand",
|
||||
disabled && "bg-disabled_subtle text-fg-disabled_subtle ring-disabled",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{slot?.char ? slot.char : slot?.hasFakeCaret ? <FakeCaret size={size} /> : 0}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Slot.displayName = "Slot";
|
||||
|
||||
const FakeCaret = ({ size = "md" }: { size?: "sm" | "md" | "lg" }) => {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"pointer-events-none h-[1em] w-0.5 animate-caret-blink bg-fg-brand-primary",
|
||||
size === "lg" ? "text-display-xl font-medium" : "text-display-lg font-medium",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Separator = (props: ComponentPropsWithRef<"p">) => {
|
||||
return (
|
||||
<div role="separator" {...props} className={cx("text-center text-display-xl font-medium text-placeholder_subtle", props.className)}>
|
||||
-
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Separator.displayName = "Separator";
|
||||
|
||||
const Label = ({ className, ...props }: ComponentPropsWithRef<"label">) => {
|
||||
const { id } = usePinInputContext();
|
||||
|
||||
return <label {...props} htmlFor={"pin-input-" + id} id={"pin-input-label-" + id} className={cx("text-sm font-medium text-secondary", className)} />;
|
||||
};
|
||||
Label.displayName = "Label";
|
||||
|
||||
const Description = ({ className, ...props }: ComponentPropsWithRef<"p">) => {
|
||||
const { id } = usePinInputContext();
|
||||
|
||||
return <p {...props} id={"pin-input-description-" + id} role="description" className={cx("text-sm text-tertiary", className)} />;
|
||||
};
|
||||
Description.displayName = "Description";
|
||||
|
||||
const PinInput = Root as typeof Root & {
|
||||
Slot: typeof Slot;
|
||||
Label: typeof Label;
|
||||
Group: typeof Group;
|
||||
Separator: typeof Separator;
|
||||
Description: typeof Description;
|
||||
};
|
||||
PinInput.Slot = Slot;
|
||||
PinInput.Label = Label;
|
||||
PinInput.Group = Group;
|
||||
PinInput.Separator = Separator;
|
||||
PinInput.Description = Description;
|
||||
|
||||
export { PinInput };
|
||||
174
src/components/base/progress-indicators/progress-circles.tsx
Normal file
174
src/components/base/progress-indicators/progress-circles.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { cx as clx, sortCx } from "@/utils/cx";
|
||||
|
||||
interface ProgressBarProps {
|
||||
value: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
size: "xxs" | "xs" | "sm" | "md" | "lg";
|
||||
label?: string;
|
||||
valueFormatter?: (value: number, valueInPercentage: number) => string | number;
|
||||
}
|
||||
|
||||
const sizes = sortCx({
|
||||
xxs: {
|
||||
strokeWidth: 6,
|
||||
radius: 29,
|
||||
valueClass: "text-sm font-semibold text-primary",
|
||||
labelClass: "text-xs font-medium text-tertiary",
|
||||
halfCircleTextPosition: "absolute bottom-0.5 text-center",
|
||||
},
|
||||
xs: {
|
||||
strokeWidth: 16,
|
||||
radius: 72,
|
||||
valueClass: "text-display-xs font-semibold text-primary",
|
||||
labelClass: "text-xs font-medium text-tertiary",
|
||||
halfCircleTextPosition: "absolute bottom-0.5 text-center",
|
||||
},
|
||||
sm: {
|
||||
strokeWidth: 20,
|
||||
radius: 90,
|
||||
valueClass: "text-display-sm font-semibold text-primary",
|
||||
labelClass: "text-xs font-medium text-tertiary",
|
||||
halfCircleTextPosition: "absolute bottom-1 text-center",
|
||||
},
|
||||
md: {
|
||||
strokeWidth: 24,
|
||||
radius: 108,
|
||||
valueClass: "text-display-md font-semibold text-primary",
|
||||
labelClass: "text-sm font-medium text-tertiary",
|
||||
halfCircleTextPosition: "absolute bottom-1 text-center",
|
||||
},
|
||||
lg: {
|
||||
strokeWidth: 28,
|
||||
radius: 126,
|
||||
valueClass: "text-display-lg font-semibold text-primary",
|
||||
labelClass: "text-sm font-medium text-tertiary",
|
||||
halfCircleTextPosition: "absolute bottom-0 text-center",
|
||||
},
|
||||
});
|
||||
|
||||
export const ProgressBarCircle = ({ value, min = 0, max = 100, size, label, valueFormatter }: ProgressBarProps) => {
|
||||
const percentage = Math.round(((value - min) * 100) / (max - min));
|
||||
|
||||
const sizeConfig = sizes[size];
|
||||
|
||||
const { strokeWidth, radius, valueClass, labelClass } = sizeConfig;
|
||||
|
||||
const diameter = 2 * (radius + strokeWidth / 2);
|
||||
const width = diameter;
|
||||
const height = diameter;
|
||||
const viewBox = `0 0 ${width} ${height}`;
|
||||
const cx = diameter / 2;
|
||||
const cy = diameter / 2;
|
||||
|
||||
const textPosition = label ? "absolute text-center" : "absolute text-primary";
|
||||
const strokeDashoffset = 100 - percentage;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
||||
<svg className="-rotate-90" width={width} height={height} viewBox={viewBox}>
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
className="stroke-bg-quaternary"
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={radius}
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
pathLength="100"
|
||||
strokeDasharray="100"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
{/* Foreground circle */}
|
||||
<circle
|
||||
className="stroke-fg-brand-primary"
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={radius}
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
pathLength="100"
|
||||
strokeDasharray="100"
|
||||
strokeLinecap="round"
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
/>
|
||||
</svg>
|
||||
{label && size !== "xxs" ? (
|
||||
<div className="absolute text-center">
|
||||
<div className={labelClass}>{label}</div>
|
||||
<div className={valueClass}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className={clx(textPosition, valueClass)}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{label && size === "xxs" && <div className={labelClass}>{label}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProgressBarHalfCircle = ({ value, min = 0, max = 100, size, label, valueFormatter }: ProgressBarProps) => {
|
||||
const percentage = Math.round(((value - min) * 100) / (max - min));
|
||||
|
||||
const sizeConfig = sizes[size];
|
||||
|
||||
const { strokeWidth, radius, valueClass, labelClass, halfCircleTextPosition } = sizeConfig;
|
||||
|
||||
const width = 2 * (radius + strokeWidth / 2);
|
||||
const height = radius + strokeWidth;
|
||||
const viewBox = `0 0 ${width} ${height}`;
|
||||
const cx = "50%";
|
||||
const cy = radius + strokeWidth / 2;
|
||||
|
||||
const strokeDashoffset = -50 - (100 - percentage) / 2;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
||||
<svg width={width} height={height} viewBox={viewBox}>
|
||||
{/* Background half-circle */}
|
||||
<circle
|
||||
className="stroke-bg-quaternary"
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={radius}
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
pathLength="100"
|
||||
strokeDasharray="100"
|
||||
strokeDashoffset="-50"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
{/* Foreground half-circle */}
|
||||
<circle
|
||||
className="origin-center -scale-x-100 stroke-fg-brand-primary"
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={radius}
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
pathLength="100"
|
||||
strokeDasharray="100"
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{label && size !== "xxs" ? (
|
||||
<div className={halfCircleTextPosition}>
|
||||
<div className={labelClass}>{label}</div>
|
||||
<div className={valueClass}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className={clx(halfCircleTextPosition, valueClass)}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{label && size === "xxs" && <div className={labelClass}>{label}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
121
src/components/base/progress-indicators/progress-indicators.tsx
Normal file
121
src/components/base/progress-indicators/progress-indicators.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export interface ProgressBarProps {
|
||||
/**
|
||||
* The current value of the progress bar.
|
||||
*/
|
||||
value: number;
|
||||
/**
|
||||
* The minimum value of the progress bar.
|
||||
* @default 0
|
||||
*/
|
||||
min?: number;
|
||||
/**
|
||||
* The maximum value of the progress bar.
|
||||
* @default 100
|
||||
*/
|
||||
max?: number;
|
||||
/**
|
||||
* Optional additional CSS class names for the progress bar container.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Optional additional CSS class names for the progress bar indicator element.
|
||||
*/
|
||||
progressClassName?: string;
|
||||
/**
|
||||
* Optional function to format the displayed value.
|
||||
* It receives the raw value and the calculated percentage.
|
||||
*/
|
||||
valueFormatter?: (value: number, valueInPercentage: number) => string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic progress bar component.
|
||||
*/
|
||||
export const ProgressBarBase = ({ value, min = 0, max = 100, className, progressClassName }: ProgressBarProps) => {
|
||||
const percentage = ((value - min) * 100) / (max - min);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-valuenow={value}
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
className={cx("h-2 w-full overflow-hidden rounded-md bg-quaternary", className)}
|
||||
>
|
||||
<div
|
||||
// Use transform instead of width to avoid layout thrashing (and for smoother animation)
|
||||
style={{ transform: `translateX(-${100 - percentage}%)` }}
|
||||
className={cx("size-full rounded-md bg-fg-brand-primary transition duration-75 ease-linear", progressClassName)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ProgressBarLabelPosition = "right" | "bottom" | "top-floating" | "bottom-floating";
|
||||
|
||||
export interface ProgressIndicatorWithTextProps extends ProgressBarProps {
|
||||
/**
|
||||
* Specifies the layout of the text relative to the progress bar.
|
||||
* - `right`: Text is displayed to the right of the progress bar.
|
||||
* - `bottom`: Text is displayed below the progress bar, aligned to the right.
|
||||
* - `top-floating`: Text is displayed in a floating tooltip above the progress indicator.
|
||||
* - `bottom-floating`: Text is displayed in a floating tooltip below the progress indicator.
|
||||
*/
|
||||
labelPosition?: ProgressBarLabelPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* A progress bar component that displays the value text in various configurable layouts.
|
||||
*/
|
||||
export const ProgressBar = ({ value, min = 0, max = 100, valueFormatter, labelPosition, className, progressClassName }: ProgressIndicatorWithTextProps) => {
|
||||
const percentage = ((value - min) * 100) / (max - min);
|
||||
const formattedValue = valueFormatter ? valueFormatter(value, percentage) : `${percentage.toFixed(0)}%`; // Default to rounded percentage
|
||||
|
||||
const baseProgressBar = <ProgressBarBase min={min} max={max} value={value} className={className} progressClassName={progressClassName} />;
|
||||
|
||||
switch (labelPosition) {
|
||||
case "right":
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{baseProgressBar}
|
||||
<span className="shrink-0 text-sm font-medium text-secondary tabular-nums">{formattedValue}</span>
|
||||
</div>
|
||||
);
|
||||
case "bottom":
|
||||
return (
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{baseProgressBar}
|
||||
<span className="text-sm font-medium text-secondary tabular-nums">{formattedValue}</span>
|
||||
</div>
|
||||
);
|
||||
case "top-floating":
|
||||
return (
|
||||
<div className="relative flex flex-col items-end gap-2">
|
||||
{baseProgressBar}
|
||||
<div
|
||||
style={{ left: `${percentage}%` }}
|
||||
className="absolute -top-2 -translate-x-1/2 -translate-y-full rounded-lg bg-primary_alt px-3 py-2 shadow-lg ring-1 ring-secondary_alt"
|
||||
>
|
||||
<div className="text-xs font-semibold text-secondary tabular-nums">{formattedValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "bottom-floating":
|
||||
return (
|
||||
<div className="relative flex flex-col items-end gap-2">
|
||||
{baseProgressBar}
|
||||
<div
|
||||
style={{ left: `${percentage}%` }}
|
||||
className="absolute -bottom-2 -translate-x-1/2 translate-y-full rounded-lg bg-primary_alt px-3 py-2 shadow-lg ring-1 ring-secondary_alt"
|
||||
>
|
||||
<div className="text-xs font-semibold text-secondary">{formattedValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
// Fallback or default case, could render the basic progress bar or throw an error
|
||||
return baseProgressBar;
|
||||
}
|
||||
};
|
||||
27
src/components/base/progress-indicators/simple-circle.tsx
Normal file
27
src/components/base/progress-indicators/simple-circle.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
export const CircleProgressBar = (props: { value: number; min?: 0; max?: 100 }) => {
|
||||
const { value, min = 0, max = 100 } = props;
|
||||
const percentage = ((value - min) * 100) / (max - min);
|
||||
|
||||
return (
|
||||
<div role="progressbar" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className="relative flex w-max items-center justify-center">
|
||||
<span className="absolute text-sm font-medium text-primary">{percentage}%</span>
|
||||
<svg className="size-16 -rotate-90" viewBox="0 0 60 60">
|
||||
<circle className="stroke-bg-quaternary" cx="30" cy="30" r="26" fill="none" strokeWidth="6" />
|
||||
<circle
|
||||
className="stroke-fg-brand-primary"
|
||||
style={{
|
||||
strokeDashoffset: `calc(100 - ${percentage})`,
|
||||
}}
|
||||
cx="30"
|
||||
cy="30"
|
||||
r="26"
|
||||
fill="none"
|
||||
strokeWidth="6"
|
||||
strokeDasharray="100"
|
||||
pathLength="100"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
127
src/components/base/radio-buttons/radio-buttons.tsx
Normal file
127
src/components/base/radio-buttons/radio-buttons.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { type ReactNode, type Ref, createContext, useContext } from "react";
|
||||
import {
|
||||
Radio as AriaRadio,
|
||||
RadioGroup as AriaRadioGroup,
|
||||
type RadioGroupProps as AriaRadioGroupProps,
|
||||
type RadioProps as AriaRadioProps,
|
||||
} from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export interface RadioGroupContextType {
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
const RadioGroupContext = createContext<RadioGroupContextType | null>(null);
|
||||
|
||||
export interface RadioButtonBaseProps {
|
||||
size?: "sm" | "md";
|
||||
className?: string;
|
||||
isFocusVisible?: boolean;
|
||||
isSelected?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const RadioButtonBase = ({ className, isFocusVisible, isSelected, isDisabled, size = "sm" }: RadioButtonBaseProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"flex size-4 min-h-4 min-w-4 cursor-pointer appearance-none items-center justify-center rounded-full bg-primary ring-1 ring-primary ring-inset",
|
||||
size === "md" && "size-5 min-h-5 min-w-5",
|
||||
isSelected && !isDisabled && "bg-brand-solid ring-bg-brand-solid",
|
||||
isDisabled && "cursor-not-allowed border-disabled bg-disabled_subtle",
|
||||
isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
"size-1.5 rounded-full bg-fg-white opacity-0 transition-inherit-all",
|
||||
size === "md" && "size-2",
|
||||
isDisabled && "bg-fg-disabled_subtle",
|
||||
isSelected && "opacity-100",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
RadioButtonBase.displayName = "RadioButtonBase";
|
||||
|
||||
interface RadioButtonProps extends AriaRadioProps {
|
||||
size?: "sm" | "md";
|
||||
label?: ReactNode;
|
||||
hint?: ReactNode;
|
||||
ref?: Ref<HTMLLabelElement>;
|
||||
}
|
||||
|
||||
export const RadioButton = ({ label, hint, className, size = "sm", ...ariaRadioProps }: RadioButtonProps) => {
|
||||
const context = useContext(RadioGroupContext);
|
||||
|
||||
size = context?.size ?? size;
|
||||
|
||||
const sizes = {
|
||||
sm: {
|
||||
root: "gap-2",
|
||||
textWrapper: "",
|
||||
label: "text-sm font-medium",
|
||||
hint: "text-sm",
|
||||
},
|
||||
md: {
|
||||
root: "gap-3",
|
||||
textWrapper: "gap-0.5",
|
||||
label: "text-md font-medium",
|
||||
hint: "text-md",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<AriaRadio
|
||||
{...ariaRadioProps}
|
||||
className={(renderProps) =>
|
||||
cx(
|
||||
"flex items-start",
|
||||
renderProps.isDisabled && "cursor-not-allowed",
|
||||
sizes[size].root,
|
||||
typeof className === "function" ? className(renderProps) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isSelected, isDisabled, isFocusVisible }) => (
|
||||
<>
|
||||
<RadioButtonBase
|
||||
size={size}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
isFocusVisible={isFocusVisible}
|
||||
className={label || hint ? "mt-0.5" : ""}
|
||||
/>
|
||||
{(label || hint) && (
|
||||
<div className={cx("inline-flex flex-col", sizes[size].textWrapper)}>
|
||||
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
||||
{hint && (
|
||||
<span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
||||
{hint}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AriaRadio>
|
||||
);
|
||||
};
|
||||
RadioButton.displayName = "RadioButton";
|
||||
|
||||
interface RadioGroupProps extends RadioGroupContextType, AriaRadioGroupProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RadioGroup = ({ children, className, size = "sm", ...props }: RadioGroupProps) => {
|
||||
return (
|
||||
<RadioGroupContext.Provider value={{ size }}>
|
||||
<AriaRadioGroup {...props} className={cx("flex flex-col gap-4", className)}>
|
||||
{children}
|
||||
</AriaRadioGroup>
|
||||
</RadioGroupContext.Provider>
|
||||
);
|
||||
};
|
||||
150
src/components/base/select/combobox.tsx
Normal file
150
src/components/base/select/combobox.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { FocusEventHandler, PointerEventHandler, RefAttributes, RefObject } from "react";
|
||||
import { useCallback, useContext, useRef, useState } from "react";
|
||||
import { SearchLg as SearchIcon } from "@untitledui/icons";
|
||||
import type { ComboBoxProps as AriaComboBoxProps, GroupProps as AriaGroupProps, ListBoxProps as AriaListBoxProps } from "react-aria-components";
|
||||
import { ComboBox as AriaComboBox, Group as AriaGroup, Input as AriaInput, ListBox as AriaListBox, ComboBoxStateContext } from "react-aria-components";
|
||||
import { HintText } from "@/components/base/input/hint-text";
|
||||
import { Label } from "@/components/base/input/label";
|
||||
import { Popover } from "@/components/base/select/popover";
|
||||
import { type CommonProps, SelectContext, type SelectItemType, sizes } from "@/components/base/select/select";
|
||||
import { useResizeObserver } from "@/hooks/use-resize-observer";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface ComboBoxProps extends Omit<AriaComboBoxProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
|
||||
shortcut?: boolean;
|
||||
items?: SelectItemType[];
|
||||
popoverClassName?: string;
|
||||
shortcutClassName?: string;
|
||||
children: AriaListBoxProps<SelectItemType>["children"];
|
||||
}
|
||||
|
||||
interface ComboBoxValueProps extends AriaGroupProps {
|
||||
size: "sm" | "md";
|
||||
shortcut: boolean;
|
||||
placeholder?: string;
|
||||
shortcutClassName?: string;
|
||||
onFocus?: FocusEventHandler;
|
||||
onPointerEnter?: PointerEventHandler;
|
||||
ref?: RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
const ComboBoxValue = ({ size, shortcut, placeholder, shortcutClassName, ...otherProps }: ComboBoxValueProps) => {
|
||||
const state = useContext(ComboBoxStateContext);
|
||||
|
||||
const value = state?.selectedItem?.value || null;
|
||||
const inputValue = state?.inputValue || null;
|
||||
|
||||
const first = inputValue?.split(value?.supportingText)?.[0] || "";
|
||||
const last = inputValue?.split(first)[1];
|
||||
|
||||
return (
|
||||
<AriaGroup
|
||||
{...otherProps}
|
||||
className={({ isFocusWithin, isDisabled }) =>
|
||||
cx(
|
||||
"relative flex w-full items-center gap-2 rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition-shadow duration-100 ease-linear ring-inset",
|
||||
isDisabled && "cursor-not-allowed bg-disabled_subtle",
|
||||
isFocusWithin && "ring-2 ring-brand",
|
||||
sizes[size].root,
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isDisabled }) => (
|
||||
<>
|
||||
<SearchIcon className="pointer-events-none size-5 shrink-0 text-fg-quaternary" />
|
||||
|
||||
<div className="relative flex w-full items-center gap-2">
|
||||
{inputValue && (
|
||||
<span className="absolute top-1/2 z-0 inline-flex w-full -translate-y-1/2 gap-2 truncate" aria-hidden="true">
|
||||
<p className={cx("text-md font-medium text-primary", isDisabled && "text-disabled")}>{first}</p>
|
||||
{last && <p className={cx("-ml-0.75 text-md text-tertiary", isDisabled && "text-disabled")}>{last}</p>}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<AriaInput
|
||||
placeholder={placeholder}
|
||||
className="z-10 w-full appearance-none bg-transparent text-md text-transparent caret-alpha-black/90 placeholder:text-placeholder focus:outline-hidden disabled:cursor-not-allowed disabled:text-disabled disabled:placeholder:text-disabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{shortcut && (
|
||||
<div
|
||||
className={cx(
|
||||
"absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
|
||||
isDisabled && "to-bg-disabled_subtle",
|
||||
sizes[size].shortcut,
|
||||
shortcutClassName,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cx(
|
||||
"pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
|
||||
isDisabled && "bg-transparent text-disabled",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
⌘K
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AriaGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComboBox = ({ placeholder = "Search", shortcut = true, size = "sm", children, items, shortcutClassName, ...otherProps }: ComboBoxProps) => {
|
||||
const placeholderRef = useRef<HTMLDivElement>(null);
|
||||
const [popoverWidth, setPopoverWidth] = useState("");
|
||||
|
||||
// Resize observer for popover width
|
||||
const onResize = useCallback(() => {
|
||||
if (!placeholderRef.current) return;
|
||||
|
||||
const divRect = placeholderRef.current?.getBoundingClientRect();
|
||||
|
||||
setPopoverWidth(divRect.width + "px");
|
||||
}, [placeholderRef, setPopoverWidth]);
|
||||
|
||||
useResizeObserver({
|
||||
ref: placeholderRef,
|
||||
box: "border-box",
|
||||
onResize,
|
||||
});
|
||||
|
||||
return (
|
||||
<SelectContext.Provider value={{ size }}>
|
||||
<AriaComboBox menuTrigger="focus" {...otherProps}>
|
||||
{(state) => (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{otherProps.label && (
|
||||
<Label isRequired={state.isRequired} tooltip={otherProps.tooltip}>
|
||||
{otherProps.label}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
<ComboBoxValue
|
||||
ref={placeholderRef}
|
||||
placeholder={placeholder}
|
||||
shortcut={shortcut}
|
||||
shortcutClassName={shortcutClassName}
|
||||
size={size}
|
||||
// This is a workaround to correctly calculating the trigger width
|
||||
// while using ResizeObserver wasn't 100% reliable.
|
||||
onFocus={onResize}
|
||||
onPointerEnter={onResize}
|
||||
/>
|
||||
|
||||
<Popover size={size} triggerRef={placeholderRef} style={{ width: popoverWidth }} className={otherProps.popoverClassName}>
|
||||
<AriaListBox items={items} className="size-full outline-hidden">
|
||||
{children}
|
||||
</AriaListBox>
|
||||
</Popover>
|
||||
|
||||
{otherProps.hint && <HintText isInvalid={state.isInvalid}>{otherProps.hint}</HintText>}
|
||||
</div>
|
||||
)}
|
||||
</AriaComboBox>
|
||||
</SelectContext.Provider>
|
||||
);
|
||||
};
|
||||
361
src/components/base/select/multi-select.tsx
Normal file
361
src/components/base/select/multi-select.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
import type { FocusEventHandler, KeyboardEvent, PointerEventHandler, RefAttributes, RefObject } from "react";
|
||||
import { createContext, useCallback, useContext, useRef, useState } from "react";
|
||||
import { SearchLg } from "@untitledui/icons";
|
||||
import { FocusScope, useFilter, useFocusManager } from "react-aria";
|
||||
import type { ComboBoxProps as AriaComboBoxProps, GroupProps as AriaGroupProps, ListBoxProps as AriaListBoxProps, Key } from "react-aria-components";
|
||||
import { ComboBox as AriaComboBox, Group as AriaGroup, Input as AriaInput, ListBox as AriaListBox, ComboBoxStateContext } from "react-aria-components";
|
||||
import type { ListData } from "react-stately";
|
||||
import { useListData } from "react-stately";
|
||||
import { Avatar } from "@/components/base/avatar/avatar";
|
||||
import type { IconComponentType } from "@/components/base/badges/badge-types";
|
||||
import { HintText } from "@/components/base/input/hint-text";
|
||||
import { Label } from "@/components/base/input/label";
|
||||
import { Popover } from "@/components/base/select/popover";
|
||||
import { type SelectItemType, sizes } from "@/components/base/select/select";
|
||||
import { TagCloseX } from "@/components/base/tags/base-components/tag-close-x";
|
||||
import { useResizeObserver } from "@/hooks/use-resize-observer";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { SelectItem } from "./select-item";
|
||||
|
||||
interface ComboBoxValueProps extends AriaGroupProps {
|
||||
size: "sm" | "md";
|
||||
shortcut?: boolean;
|
||||
isDisabled?: boolean;
|
||||
placeholder?: string;
|
||||
shortcutClassName?: string;
|
||||
placeholderIcon?: IconComponentType | null;
|
||||
ref?: RefObject<HTMLDivElement | null>;
|
||||
onFocus?: FocusEventHandler;
|
||||
onPointerEnter?: PointerEventHandler;
|
||||
}
|
||||
|
||||
const ComboboxContext = createContext<{
|
||||
size: "sm" | "md";
|
||||
selectedKeys: Key[];
|
||||
selectedItems: ListData<SelectItemType>;
|
||||
onRemove: (keys: Set<Key>) => void;
|
||||
onInputChange: (value: string) => void;
|
||||
}>({
|
||||
size: "sm",
|
||||
selectedKeys: [],
|
||||
selectedItems: {} as ListData<SelectItemType>,
|
||||
onRemove: () => {},
|
||||
onInputChange: () => {},
|
||||
});
|
||||
|
||||
interface MultiSelectProps extends Omit<AriaComboBoxProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement> {
|
||||
hint?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
size?: "sm" | "md";
|
||||
placeholder?: string;
|
||||
shortcut?: boolean;
|
||||
items?: SelectItemType[];
|
||||
popoverClassName?: string;
|
||||
shortcutClassName?: string;
|
||||
selectedItems: ListData<SelectItemType>;
|
||||
placeholderIcon?: IconComponentType | null;
|
||||
children: AriaListBoxProps<SelectItemType>["children"];
|
||||
onItemCleared?: (key: Key) => void;
|
||||
onItemInserted?: (key: Key) => void;
|
||||
}
|
||||
|
||||
export const MultiSelectBase = ({
|
||||
items,
|
||||
children,
|
||||
size = "sm",
|
||||
selectedItems,
|
||||
onItemCleared,
|
||||
onItemInserted,
|
||||
shortcut,
|
||||
placeholder = "Search",
|
||||
// Omit these props to avoid conflicts with the `Select` component
|
||||
name: _name,
|
||||
className: _className,
|
||||
...props
|
||||
}: MultiSelectProps) => {
|
||||
const { contains } = useFilter({ sensitivity: "base" });
|
||||
const selectedKeys = selectedItems.items.map((item) => item.id);
|
||||
|
||||
const filter = useCallback(
|
||||
(item: SelectItemType, filterText: string) => {
|
||||
return !selectedKeys.includes(item.id) && contains(item.label || item.supportingText || "", filterText);
|
||||
},
|
||||
[contains, selectedKeys],
|
||||
);
|
||||
|
||||
const accessibleList = useListData({
|
||||
initialItems: items,
|
||||
filter,
|
||||
});
|
||||
|
||||
const onRemove = useCallback(
|
||||
(keys: Set<Key>) => {
|
||||
const key = keys.values().next().value;
|
||||
|
||||
if (!key) return;
|
||||
|
||||
selectedItems.remove(key);
|
||||
onItemCleared?.(key);
|
||||
},
|
||||
[selectedItems, onItemCleared],
|
||||
);
|
||||
|
||||
const onSelectionChange = (id: Key | null) => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = accessibleList.getItem(id);
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedKeys.includes(id as string)) {
|
||||
selectedItems.append(item);
|
||||
onItemInserted?.(id);
|
||||
}
|
||||
|
||||
accessibleList.setFilterText("");
|
||||
};
|
||||
|
||||
const onInputChange = (value: string) => {
|
||||
accessibleList.setFilterText(value);
|
||||
};
|
||||
|
||||
const placeholderRef = useRef<HTMLDivElement>(null);
|
||||
const [popoverWidth, setPopoverWidth] = useState("");
|
||||
|
||||
// Resize observer for popover width
|
||||
const onResize = useCallback(() => {
|
||||
if (!placeholderRef.current) return;
|
||||
let divRect = placeholderRef.current?.getBoundingClientRect();
|
||||
setPopoverWidth(divRect.width + "px");
|
||||
}, [placeholderRef, setPopoverWidth]);
|
||||
|
||||
useResizeObserver({
|
||||
ref: placeholderRef,
|
||||
onResize: onResize,
|
||||
box: "border-box",
|
||||
});
|
||||
|
||||
return (
|
||||
<ComboboxContext.Provider
|
||||
value={{
|
||||
size,
|
||||
selectedKeys,
|
||||
selectedItems,
|
||||
onInputChange,
|
||||
onRemove,
|
||||
}}
|
||||
>
|
||||
<AriaComboBox
|
||||
allowsEmptyCollection
|
||||
menuTrigger="focus"
|
||||
items={accessibleList.items}
|
||||
onInputChange={onInputChange}
|
||||
inputValue={accessibleList.filterText}
|
||||
// This keeps the combobox popover open and the input value unchanged when an item is selected.
|
||||
selectedKey={null}
|
||||
onSelectionChange={onSelectionChange}
|
||||
{...props}
|
||||
>
|
||||
{(state) => (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{props.label && (
|
||||
<Label isRequired={state.isRequired} tooltip={props.tooltip}>
|
||||
{props.label}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
<MultiSelectTagsValue
|
||||
size={size}
|
||||
shortcut={shortcut}
|
||||
ref={placeholderRef}
|
||||
placeholder={placeholder}
|
||||
// This is a workaround to correctly calculating the trigger width
|
||||
// while using ResizeObserver wasn't 100% reliable.
|
||||
onFocus={onResize}
|
||||
onPointerEnter={onResize}
|
||||
/>
|
||||
|
||||
<Popover size={"md"} triggerRef={placeholderRef} style={{ width: popoverWidth }} className={props?.popoverClassName}>
|
||||
<AriaListBox selectionMode="multiple" className="size-full outline-hidden">
|
||||
{children}
|
||||
</AriaListBox>
|
||||
</Popover>
|
||||
|
||||
{props.hint && <HintText isInvalid={state.isInvalid}>{props.hint}</HintText>}
|
||||
</div>
|
||||
)}
|
||||
</AriaComboBox>
|
||||
</ComboboxContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const InnerMultiSelect = ({ isDisabled, shortcut, shortcutClassName, placeholder }: Omit<MultiSelectProps, "selectedItems" | "children">) => {
|
||||
const focusManager = useFocusManager();
|
||||
const comboBoxContext = useContext(ComboboxContext);
|
||||
const comboBoxStateContext = useContext(ComboBoxStateContext);
|
||||
|
||||
const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
const isCaretAtStart = event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0;
|
||||
|
||||
if (!isCaretAtStart && event.currentTarget.value !== "") {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case "Backspace":
|
||||
case "ArrowLeft":
|
||||
focusManager?.focusPrevious({ wrap: false, tabbable: false });
|
||||
break;
|
||||
case "ArrowRight":
|
||||
focusManager?.focusNext({ wrap: false, tabbable: false });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure dropdown opens on click even if input is already focused
|
||||
const handleInputMouseDown = (_event: React.MouseEvent<HTMLInputElement>) => {
|
||||
if (comboBoxStateContext && !comboBoxStateContext.isOpen) {
|
||||
comboBoxStateContext.open();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagKeyDown = (event: KeyboardEvent<HTMLButtonElement>, value: Key) => {
|
||||
// Do nothing when tab is clicked to move focus from the tag to the input element.
|
||||
if (event.key === "Tab") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const isFirstTag = comboBoxContext?.selectedItems?.items?.[0]?.id === value;
|
||||
|
||||
switch (event.key) {
|
||||
case " ":
|
||||
case "Enter":
|
||||
case "Backspace":
|
||||
if (isFirstTag) {
|
||||
focusManager?.focusNext({ wrap: false, tabbable: false });
|
||||
} else {
|
||||
focusManager?.focusPrevious({ wrap: false, tabbable: false });
|
||||
}
|
||||
|
||||
comboBoxContext.onRemove(new Set([value]));
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
focusManager?.focusPrevious({ wrap: false, tabbable: false });
|
||||
break;
|
||||
case "ArrowRight":
|
||||
focusManager?.focusNext({ wrap: false, tabbable: false });
|
||||
break;
|
||||
case "Escape":
|
||||
comboBoxStateContext?.close();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const isSelectionEmpty = comboBoxContext?.selectedItems?.items?.length === 0;
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-1 flex-row flex-wrap items-center justify-start gap-1.5">
|
||||
{!isSelectionEmpty &&
|
||||
comboBoxContext?.selectedItems?.items?.map((value) => (
|
||||
<span key={value.id} className="flex items-center rounded-md bg-primary py-0.5 pr-1 pl-1.25 ring-1 ring-primary ring-inset">
|
||||
<Avatar size="xxs" alt={value?.label} src={value?.avatarUrl} />
|
||||
|
||||
<p className="ml-1.25 truncate text-sm font-medium whitespace-nowrap text-secondary select-none">{value?.label}</p>
|
||||
|
||||
<TagCloseX
|
||||
size="md"
|
||||
isDisabled={isDisabled}
|
||||
className="ml-0.75"
|
||||
// For workaround, onKeyDown is added to the button
|
||||
onKeyDown={(event) => handleTagKeyDown(event, value.id)}
|
||||
onPress={() => comboBoxContext.onRemove(new Set([value.id]))}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
|
||||
<div className={cx("relative flex min-w-[20%] flex-1 flex-row items-center", !isSelectionEmpty && "ml-0.5", shortcut && "min-w-[30%]")}>
|
||||
<AriaInput
|
||||
placeholder={placeholder}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onMouseDown={handleInputMouseDown}
|
||||
className="w-full flex-[1_0_0] appearance-none bg-transparent text-md text-ellipsis text-primary caret-alpha-black/90 outline-none placeholder:text-placeholder focus:outline-hidden disabled:cursor-not-allowed disabled:text-disabled disabled:placeholder:text-disabled"
|
||||
/>
|
||||
|
||||
{shortcut && (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={cx(
|
||||
"absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
|
||||
shortcutClassName,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cx(
|
||||
"pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
|
||||
isDisabled && "bg-transparent text-disabled",
|
||||
)}
|
||||
>
|
||||
⌘K
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MultiSelectTagsValue = ({
|
||||
size,
|
||||
shortcut,
|
||||
placeholder,
|
||||
shortcutClassName,
|
||||
placeholderIcon: Icon = SearchLg,
|
||||
// Omit this prop to avoid invalid HTML attribute warning
|
||||
isDisabled: _isDisabled,
|
||||
...otherProps
|
||||
}: ComboBoxValueProps) => {
|
||||
return (
|
||||
<AriaGroup
|
||||
{...otherProps}
|
||||
className={({ isFocusWithin, isDisabled }) =>
|
||||
cx(
|
||||
"relative flex w-full items-center gap-2 rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset",
|
||||
isDisabled && "cursor-not-allowed bg-disabled_subtle",
|
||||
isFocusWithin && "ring-2 ring-brand",
|
||||
sizes[size].root,
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isDisabled }) => (
|
||||
<>
|
||||
{Icon && <Icon className="pointer-events-none size-5 text-fg-quaternary" />}
|
||||
<FocusScope contain={false} autoFocus={false} restoreFocus={false}>
|
||||
<InnerMultiSelect
|
||||
isDisabled={isDisabled}
|
||||
size={size}
|
||||
shortcut={shortcut}
|
||||
shortcutClassName={shortcutClassName}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</FocusScope>
|
||||
</>
|
||||
)}
|
||||
</AriaGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const MultiSelect = MultiSelectBase as typeof MultiSelectBase & {
|
||||
Item: typeof SelectItem;
|
||||
};
|
||||
|
||||
MultiSelect.Item = SelectItem;
|
||||
|
||||
export { MultiSelect as MultiSelect };
|
||||
32
src/components/base/select/popover.tsx
Normal file
32
src/components/base/select/popover.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { RefAttributes } from "react";
|
||||
import type { PopoverProps as AriaPopoverProps } from "react-aria-components";
|
||||
import { Popover as AriaPopover } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface PopoverProps extends AriaPopoverProps, RefAttributes<HTMLElement> {
|
||||
size: "sm" | "md";
|
||||
}
|
||||
|
||||
export const Popover = (props: PopoverProps) => {
|
||||
return (
|
||||
<AriaPopover
|
||||
placement="bottom"
|
||||
containerPadding={0}
|
||||
offset={4}
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"max-h-64! w-(--trigger-width) origin-(--trigger-anchor-point) overflow-x-hidden overflow-y-auto rounded-lg bg-primary py-1 shadow-lg ring-1 ring-secondary_alt outline-hidden will-change-transform",
|
||||
|
||||
state.isEntering &&
|
||||
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
||||
state.isExiting &&
|
||||
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
||||
props.size === "md" && "max-h-80!",
|
||||
|
||||
typeof props.className === "function" ? props.className(state) : props.className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
95
src/components/base/select/select-item.tsx
Normal file
95
src/components/base/select/select-item.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { isValidElement, useContext } from "react";
|
||||
import { Check } from "@untitledui/icons";
|
||||
import type { ListBoxItemProps as AriaListBoxItemProps } from "react-aria-components";
|
||||
import { ListBoxItem as AriaListBoxItem, Text as AriaText } from "react-aria-components";
|
||||
import { Avatar } from "@/components/base/avatar/avatar";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { isReactComponent } from "@/utils/is-react-component";
|
||||
import type { SelectItemType } from "./select";
|
||||
import { SelectContext } from "./select";
|
||||
|
||||
const sizes = {
|
||||
sm: "p-2 pr-2.5",
|
||||
md: "p-2.5 pl-2",
|
||||
};
|
||||
|
||||
interface SelectItemProps extends Omit<AriaListBoxItemProps<SelectItemType>, "id">, SelectItemType {}
|
||||
|
||||
export const SelectItem = ({ label, id, value, avatarUrl, supportingText, isDisabled, icon: Icon, className, children, ...props }: SelectItemProps) => {
|
||||
const { size } = useContext(SelectContext);
|
||||
|
||||
const labelOrChildren = label || (typeof children === "string" ? children : "");
|
||||
const textValue = supportingText ? labelOrChildren + " " + supportingText : labelOrChildren;
|
||||
|
||||
return (
|
||||
<AriaListBoxItem
|
||||
id={id}
|
||||
value={
|
||||
value ?? {
|
||||
id,
|
||||
label: labelOrChildren,
|
||||
avatarUrl,
|
||||
supportingText,
|
||||
isDisabled,
|
||||
icon: Icon,
|
||||
}
|
||||
}
|
||||
textValue={textValue}
|
||||
isDisabled={isDisabled}
|
||||
{...props}
|
||||
className={(state) => cx("w-full px-1.5 py-px outline-hidden", typeof className === "function" ? className(state) : className)}
|
||||
>
|
||||
{(state) => (
|
||||
<div
|
||||
className={cx(
|
||||
"flex cursor-pointer items-center gap-2 rounded-md outline-hidden select-none",
|
||||
state.isSelected && "bg-active",
|
||||
state.isDisabled && "cursor-not-allowed",
|
||||
state.isFocused && "bg-primary_hover",
|
||||
state.isFocusVisible && "ring-2 ring-focus-ring ring-inset",
|
||||
|
||||
// Icon styles
|
||||
"*:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:text-fg-quaternary",
|
||||
state.isDisabled && "*:data-icon:text-fg-disabled",
|
||||
|
||||
sizes[size],
|
||||
)}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<Avatar aria-hidden="true" size="xs" src={avatarUrl} alt={label} />
|
||||
) : isReactComponent(Icon) ? (
|
||||
<Icon data-icon aria-hidden="true" />
|
||||
) : isValidElement(Icon) ? (
|
||||
Icon
|
||||
) : null}
|
||||
|
||||
<div className="flex w-full min-w-0 flex-1 flex-wrap gap-x-2">
|
||||
<AriaText
|
||||
slot="label"
|
||||
className={cx("truncate text-md font-medium whitespace-nowrap text-primary", state.isDisabled && "text-disabled")}
|
||||
>
|
||||
{label || (typeof children === "function" ? children(state) : children)}
|
||||
</AriaText>
|
||||
|
||||
{supportingText && (
|
||||
<AriaText slot="description" className={cx("text-md whitespace-nowrap text-tertiary", state.isDisabled && "text-disabled")}>
|
||||
{supportingText}
|
||||
</AriaText>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{state.isSelected && (
|
||||
<Check
|
||||
aria-hidden="true"
|
||||
className={cx(
|
||||
"ml-auto text-fg-brand-primary",
|
||||
size === "sm" ? "size-4 stroke-[2.5px]" : "size-5",
|
||||
state.isDisabled && "text-fg-disabled",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</AriaListBoxItem>
|
||||
);
|
||||
};
|
||||
67
src/components/base/select/select-native.tsx
Normal file
67
src/components/base/select/select-native.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { type SelectHTMLAttributes, useId } from "react";
|
||||
import { ChevronDown } from "@untitledui/icons";
|
||||
import { HintText } from "@/components/base/input/hint-text";
|
||||
import { Label } from "@/components/base/input/label";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface NativeSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
label?: string;
|
||||
hint?: string;
|
||||
selectClassName?: string;
|
||||
options: { label: string; value: string; disabled?: boolean }[];
|
||||
}
|
||||
|
||||
export const NativeSelect = ({ label, hint, options, className, selectClassName, ...props }: NativeSelectProps) => {
|
||||
const id = useId();
|
||||
const selectId = `select-native-${id}`;
|
||||
const hintId = `select-native-hint-${id}`;
|
||||
|
||||
return (
|
||||
<div className={cx("w-full in-data-input-wrapper:w-max", className)}>
|
||||
{label && (
|
||||
<Label htmlFor={selectId} id={selectId} className="mb-1.5">
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
<div className="relative grid w-full items-center">
|
||||
<select
|
||||
{...props}
|
||||
id={selectId}
|
||||
aria-describedby={hintId}
|
||||
aria-labelledby={selectId}
|
||||
className={cx(
|
||||
"appearance-none rounded-lg bg-primary px-3.5 py-2.5 text-md font-medium text-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset placeholder:text-fg-quaternary focus-visible:ring-2 focus-visible:ring-brand disabled:cursor-not-allowed disabled:bg-disabled_subtle disabled:text-disabled",
|
||||
// Styles when the select is within an `InputGroup`
|
||||
"in-data-input-wrapper:flex in-data-input-wrapper:h-full in-data-input-wrapper:gap-1 in-data-input-wrapper:bg-inherit in-data-input-wrapper:px-3 in-data-input-wrapper:py-2 in-data-input-wrapper:font-normal in-data-input-wrapper:text-tertiary in-data-input-wrapper:shadow-none in-data-input-wrapper:ring-transparent",
|
||||
// Styles for the select when `TextField` is disabled
|
||||
"in-data-input-wrapper:group-disabled:pointer-events-none in-data-input-wrapper:group-disabled:cursor-not-allowed in-data-input-wrapper:group-disabled:bg-transparent in-data-input-wrapper:group-disabled:text-disabled",
|
||||
// Common styles for sizes and border radius within `InputGroup`
|
||||
"in-data-input-wrapper:in-data-leading:rounded-r-none in-data-input-wrapper:in-data-trailing:rounded-l-none in-data-input-wrapper:in-data-[input-size=md]:py-2.5 in-data-input-wrapper:in-data-leading:in-data-[input-size=md]:pl-3.5 in-data-input-wrapper:in-data-[input-size=sm]:py-2 in-data-input-wrapper:in-data-[input-size=sm]:pl-3",
|
||||
// For "leading" dropdown within `InputGroup`
|
||||
"in-data-input-wrapper:in-data-leading:in-data-[input-size=md]:pr-4.5 in-data-input-wrapper:in-data-leading:in-data-[input-size=sm]:pr-4.5",
|
||||
// For "trailing" dropdown within `InputGroup`
|
||||
"in-data-input-wrapper:in-data-trailing:in-data-[input-size=md]:pr-8 in-data-input-wrapper:in-data-trailing:in-data-[input-size=sm]:pr-7.5",
|
||||
selectClassName,
|
||||
)}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute right-3.5 size-5 text-fg-quaternary in-data-input-wrapper:right-0 in-data-input-wrapper:size-4 in-data-input-wrapper:stroke-[2.625px] in-data-input-wrapper:in-data-trailing:in-data-[input-size=sm]:right-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hint && (
|
||||
<HintText className="mt-2" id={hintId}>
|
||||
{hint}
|
||||
</HintText>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
144
src/components/base/select/select.tsx
Normal file
144
src/components/base/select/select.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import type { FC, ReactNode, Ref, RefAttributes } from "react";
|
||||
import { createContext, isValidElement } from "react";
|
||||
import { ChevronDown } from "@untitledui/icons";
|
||||
import type { SelectProps as AriaSelectProps } from "react-aria-components";
|
||||
import { Button as AriaButton, ListBox as AriaListBox, Select as AriaSelect, SelectValue as AriaSelectValue } from "react-aria-components";
|
||||
import { Avatar } from "@/components/base/avatar/avatar";
|
||||
import { HintText } from "@/components/base/input/hint-text";
|
||||
import { Label } from "@/components/base/input/label";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { isReactComponent } from "@/utils/is-react-component";
|
||||
import { ComboBox } from "./combobox";
|
||||
import { Popover } from "./popover";
|
||||
import { SelectItem } from "./select-item";
|
||||
|
||||
export type SelectItemType = {
|
||||
id: string;
|
||||
label?: string;
|
||||
avatarUrl?: string;
|
||||
isDisabled?: boolean;
|
||||
supportingText?: string;
|
||||
icon?: FC | ReactNode;
|
||||
};
|
||||
|
||||
export interface CommonProps {
|
||||
hint?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
size?: "sm" | "md";
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface SelectProps extends Omit<AriaSelectProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
|
||||
items?: SelectItemType[];
|
||||
popoverClassName?: string;
|
||||
placeholderIcon?: FC | ReactNode;
|
||||
children: ReactNode | ((item: SelectItemType) => ReactNode);
|
||||
}
|
||||
|
||||
interface SelectValueProps {
|
||||
isOpen: boolean;
|
||||
size: "sm" | "md";
|
||||
isFocused: boolean;
|
||||
isDisabled: boolean;
|
||||
placeholder?: string;
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
placeholderIcon?: FC | ReactNode;
|
||||
}
|
||||
|
||||
export const sizes = {
|
||||
sm: { root: "py-2 px-3", shortcut: "pr-2.5" },
|
||||
md: { root: "py-2.5 px-3.5", shortcut: "pr-3" },
|
||||
};
|
||||
|
||||
const SelectValue = ({ isOpen, isFocused, isDisabled, size, placeholder, placeholderIcon, ref }: SelectValueProps) => {
|
||||
return (
|
||||
<AriaButton
|
||||
ref={ref}
|
||||
className={cx(
|
||||
"relative flex w-full cursor-pointer items-center rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset",
|
||||
(isFocused || isOpen) && "ring-2 ring-brand",
|
||||
isDisabled && "cursor-not-allowed bg-disabled_subtle text-disabled",
|
||||
)}
|
||||
>
|
||||
<AriaSelectValue<SelectItemType>
|
||||
className={cx(
|
||||
"flex h-max w-full items-center justify-start gap-2 truncate text-left align-middle",
|
||||
|
||||
// Icon styles
|
||||
"*:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:text-fg-quaternary in-disabled:*:data-icon:text-fg-disabled",
|
||||
|
||||
sizes[size].root,
|
||||
)}
|
||||
>
|
||||
{(state) => {
|
||||
const Icon = state.selectedItem?.icon || placeholderIcon;
|
||||
return (
|
||||
<>
|
||||
{state.selectedItem?.avatarUrl ? (
|
||||
<Avatar size="xs" src={state.selectedItem.avatarUrl} alt={state.selectedItem.label} />
|
||||
) : isReactComponent(Icon) ? (
|
||||
<Icon data-icon aria-hidden="true" />
|
||||
) : isValidElement(Icon) ? (
|
||||
Icon
|
||||
) : null}
|
||||
|
||||
{state.selectedItem ? (
|
||||
<section className="flex w-full gap-2 truncate">
|
||||
<p className="truncate text-md font-medium text-primary">{state.selectedItem?.label}</p>
|
||||
{state.selectedItem?.supportingText && <p className="text-md text-tertiary">{state.selectedItem?.supportingText}</p>}
|
||||
</section>
|
||||
) : (
|
||||
<p className={cx("text-md text-placeholder", isDisabled && "text-disabled")}>{placeholder}</p>
|
||||
)}
|
||||
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className={cx("ml-auto shrink-0 text-fg-quaternary", size === "sm" ? "size-4 stroke-[2.5px]" : "size-5")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</AriaSelectValue>
|
||||
</AriaButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectContext = createContext<{ size: "sm" | "md" }>({ size: "sm" });
|
||||
|
||||
const Select = ({ placeholder = "Select", placeholderIcon, size = "sm", children, items, label, hint, tooltip, className, ...rest }: SelectProps) => {
|
||||
return (
|
||||
<SelectContext.Provider value={{ size }}>
|
||||
<AriaSelect {...rest} className={(state) => cx("flex flex-col gap-1.5", typeof className === "function" ? className(state) : className)}>
|
||||
{(state) => (
|
||||
<>
|
||||
{label && (
|
||||
<Label isRequired={state.isRequired} tooltip={tooltip}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
<SelectValue {...state} {...{ size, placeholder }} placeholderIcon={placeholderIcon} />
|
||||
|
||||
<Popover size={size} className={rest.popoverClassName}>
|
||||
<AriaListBox items={items} className="size-full outline-hidden">
|
||||
{children}
|
||||
</AriaListBox>
|
||||
</Popover>
|
||||
|
||||
{hint && <HintText isInvalid={state.isInvalid}>{hint}</HintText>}
|
||||
</>
|
||||
)}
|
||||
</AriaSelect>
|
||||
</SelectContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const _Select = Select as typeof Select & {
|
||||
ComboBox: typeof ComboBox;
|
||||
Item: typeof SelectItem;
|
||||
};
|
||||
_Select.ComboBox = ComboBox;
|
||||
_Select.Item = SelectItem;
|
||||
|
||||
export { _Select as Select };
|
||||
73
src/components/base/slider/slider.tsx
Normal file
73
src/components/base/slider/slider.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { SliderProps as AriaSliderProps } from "react-aria-components";
|
||||
import {
|
||||
Label as AriaLabel,
|
||||
Slider as AriaSlider,
|
||||
SliderOutput as AriaSliderOutput,
|
||||
SliderThumb as AriaSliderThumb,
|
||||
SliderTrack as AriaSliderTrack,
|
||||
} from "react-aria-components";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
|
||||
const styles = sortCx({
|
||||
default: "hidden",
|
||||
bottom: "absolute top-2 left-1/2 -translate-x-1/2 translate-y-full text-md font-medium text-primary",
|
||||
"top-floating":
|
||||
"absolute -top-2 left-1/2 -translate-x-1/2 -translate-y-full rounded-lg bg-primary px-3 py-2 text-xs font-semibold text-secondary shadow-lg ring-1 ring-secondary_alt",
|
||||
});
|
||||
|
||||
interface SliderProps extends AriaSliderProps {
|
||||
labelPosition?: keyof typeof styles;
|
||||
labelFormatter?: (value: number) => string;
|
||||
}
|
||||
|
||||
export const Slider = ({ labelPosition = "default", minValue = 0, maxValue = 100, labelFormatter, formatOptions, ...rest }: SliderProps) => {
|
||||
// Format thumb value as percentage by default.
|
||||
const defaultFormatOptions: Intl.NumberFormatOptions = {
|
||||
style: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<AriaSlider {...rest} {...{ minValue, maxValue }} formatOptions={formatOptions ?? defaultFormatOptions}>
|
||||
<AriaLabel />
|
||||
<AriaSliderTrack className="relative h-6 w-full">
|
||||
{({ state: { values, getThumbValue, getThumbPercent, getFormattedValue } }) => {
|
||||
const left = values.length === 1 ? 0 : getThumbPercent(0);
|
||||
const width = values.length === 1 ? getThumbPercent(0) : getThumbPercent(1) - left;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="absolute top-1/2 h-2 w-full -translate-y-1/2 rounded-full bg-quaternary" />
|
||||
<span
|
||||
className="absolute top-1/2 h-2 w-full -translate-y-1/2 rounded-full bg-brand-solid"
|
||||
style={{
|
||||
left: `${left * 100}%`,
|
||||
width: `${width * 100}%`,
|
||||
}}
|
||||
/>
|
||||
{values.map((_, index) => {
|
||||
return (
|
||||
<AriaSliderThumb
|
||||
key={index}
|
||||
index={index}
|
||||
className={({ isFocusVisible, isDragging }) =>
|
||||
cx(
|
||||
"top-1/2 box-border size-6 cursor-grab rounded-full bg-slider-handle-bg shadow-md ring-2 ring-slider-handle-border ring-inset",
|
||||
isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
|
||||
isDragging && "cursor-grabbing",
|
||||
)
|
||||
}
|
||||
>
|
||||
<AriaSliderOutput className={cx("whitespace-nowrap", styles[labelPosition])}>
|
||||
{labelFormatter ? labelFormatter(getThumbValue(index)) : getFormattedValue(getThumbValue(index) / 100)}
|
||||
</AriaSliderOutput>
|
||||
</AriaSliderThumb>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</AriaSliderTrack>
|
||||
</AriaSlider>
|
||||
);
|
||||
};
|
||||
43
src/components/base/tags/base-components/tag-checkbox.tsx
Normal file
43
src/components/base/tags/base-components/tag-checkbox.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface TagCheckboxProps {
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
isFocused?: boolean;
|
||||
isSelected?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const TagCheckbox = ({ className, isFocused, isSelected, isDisabled, size = "sm" }: TagCheckboxProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"flex cursor-pointer appearance-none items-center justify-center rounded bg-primary ring-1 ring-primary ring-inset",
|
||||
size === "sm" && "size-3.5",
|
||||
size === "md" && "size-4",
|
||||
size === "lg" && "size-4.5",
|
||||
isSelected && "bg-brand-solid ring-bg-brand-solid",
|
||||
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-disabled",
|
||||
isFocused && "outline-2 outline-offset-2 outline-focus-ring",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
className={cx(
|
||||
"pointer-events-none absolute text-fg-white opacity-0 transition-inherit-all",
|
||||
size === "sm" && "size-2.5",
|
||||
size === "md" && "size-3",
|
||||
size === "lg" && "size-3.5",
|
||||
isSelected && "opacity-100",
|
||||
isDisabled && "text-fg-disabled_subtle",
|
||||
)}
|
||||
>
|
||||
<path d="M11.6666 3.5L5.24992 9.91667L2.33325 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
TagCheckbox.displayName = "TagCheckbox";
|
||||
32
src/components/base/tags/base-components/tag-close-x.tsx
Normal file
32
src/components/base/tags/base-components/tag-close-x.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { RefAttributes } from "react";
|
||||
import { XClose } from "@untitledui/icons";
|
||||
import { Button as AriaButton, type ButtonProps as AriaButtonProps } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface TagCloseXProps extends AriaButtonProps, RefAttributes<HTMLButtonElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
sm: { root: "p-0.5", icon: "size-2.5" },
|
||||
md: { root: "p-0.5", icon: "size-3" },
|
||||
lg: { root: "p-0.75", icon: "size-3.5" },
|
||||
};
|
||||
|
||||
export const TagCloseX = ({ size = "md", className, ...otherProps }: TagCloseXProps) => {
|
||||
return (
|
||||
<AriaButton
|
||||
slot="remove"
|
||||
aria-label="Remove this tag"
|
||||
className={cx(
|
||||
"flex cursor-pointer rounded-[3px] text-fg-quaternary outline-transparent transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring disabled:cursor-not-allowed",
|
||||
styles[size].root,
|
||||
className,
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<XClose className={cx("transition-inherit-all", styles[size].icon)} strokeWidth="3" />
|
||||
</AriaButton>
|
||||
);
|
||||
};
|
||||
161
src/components/base/tags/tags.tsx
Normal file
161
src/components/base/tags/tags.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { type PropsWithChildren, type RefAttributes, createContext, useContext } from "react";
|
||||
import {
|
||||
Tag as AriaTag,
|
||||
TagGroup as AriaTagGroup,
|
||||
type TagGroupProps as AriaTagGroupProps,
|
||||
TagList as AriaTagList,
|
||||
type TagProps as AriaTagProps,
|
||||
} from "react-aria-components";
|
||||
import { Avatar } from "@/components/base/avatar/avatar";
|
||||
import { Dot } from "@/components/foundations/dot-icon";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { TagCheckbox } from "./base-components/tag-checkbox";
|
||||
import { TagCloseX } from "./base-components/tag-close-x";
|
||||
|
||||
export interface TagItem {
|
||||
id: string;
|
||||
label: string;
|
||||
count?: number;
|
||||
avatarSrc?: string;
|
||||
avatarContrastBorder?: boolean;
|
||||
dot?: boolean;
|
||||
dotClassName?: string;
|
||||
isDisabled?: boolean;
|
||||
onClose?: (id: string) => void;
|
||||
}
|
||||
|
||||
const TagGroupContext = createContext<{
|
||||
selectionMode: "none" | "single" | "multiple";
|
||||
size: "sm" | "md" | "lg";
|
||||
}>({
|
||||
selectionMode: "none",
|
||||
size: "sm",
|
||||
});
|
||||
|
||||
interface TagGroupProps extends AriaTagGroupProps, RefAttributes<HTMLDivElement> {
|
||||
label: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export const TagGroup = ({ label, selectionMode = "none", size = "sm", children, ...otherProps }: TagGroupProps) => {
|
||||
return (
|
||||
<TagGroupContext.Provider value={{ selectionMode, size }}>
|
||||
<AriaTagGroup aria-label={label} selectionMode={selectionMode} disallowEmptySelection={selectionMode === "single"} {...otherProps}>
|
||||
{children}
|
||||
</AriaTagGroup>
|
||||
</TagGroupContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const TagList = AriaTagList;
|
||||
|
||||
const styles = {
|
||||
sm: {
|
||||
root: {
|
||||
base: "px-2 py-0.75 text-xs font-medium",
|
||||
withCheckbox: "pl-1.25",
|
||||
withAvatar: "pl-1",
|
||||
withDot: "pl-1.5",
|
||||
withCount: "pr-1",
|
||||
withClose: "pr-1",
|
||||
},
|
||||
content: "gap-1",
|
||||
count: "px-1 text-xs font-medium",
|
||||
},
|
||||
md: {
|
||||
root: {
|
||||
base: "px-2.25 py-0.5 text-sm font-medium",
|
||||
withCheckbox: "pl-1",
|
||||
withAvatar: "pl-1.25",
|
||||
withDot: "pl-1.75",
|
||||
withCount: "pr-0.75",
|
||||
withClose: "pr-1",
|
||||
},
|
||||
content: "gap-1.25",
|
||||
count: "px-1.25 text-xs font-medium",
|
||||
},
|
||||
lg: {
|
||||
root: {
|
||||
base: "px-2.5 py-1 text-sm font-medium",
|
||||
withCheckbox: "pl-1.25",
|
||||
withAvatar: "pl-1.75",
|
||||
withDot: "pl-2.25",
|
||||
withCount: "pr-1",
|
||||
withClose: "pr-1",
|
||||
},
|
||||
content: "gap-1.5",
|
||||
count: "px-1.5 text-sm font-medium",
|
||||
},
|
||||
};
|
||||
|
||||
interface TagProps extends AriaTagProps, RefAttributes<object>, Omit<TagItem, "label" | "id"> {}
|
||||
|
||||
export const Tag = ({
|
||||
id,
|
||||
avatarSrc,
|
||||
avatarContrastBorder,
|
||||
dot,
|
||||
dotClassName,
|
||||
isDisabled,
|
||||
count,
|
||||
className,
|
||||
children,
|
||||
onClose,
|
||||
}: PropsWithChildren<TagProps>) => {
|
||||
const context = useContext(TagGroupContext);
|
||||
|
||||
const leadingContent = avatarSrc ? (
|
||||
<Avatar size="xxs" src={avatarSrc} alt="Avatar" contrastBorder={avatarContrastBorder} />
|
||||
) : dot ? (
|
||||
<Dot className={cx("text-fg-success-secondary", dotClassName)} size="sm" />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<AriaTag
|
||||
id={id}
|
||||
isDisabled={isDisabled}
|
||||
textValue={typeof children === "string" ? children : undefined}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"flex cursor-default items-center gap-0.75 rounded-md bg-primary text-secondary ring-1 ring-primary ring-inset focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring",
|
||||
styles[context.size].root.base,
|
||||
|
||||
// With avatar
|
||||
avatarSrc && styles[context.size].root.withAvatar,
|
||||
// With X button
|
||||
(onClose || state.allowsRemoving) && styles[context.size].root.withClose,
|
||||
// With dot
|
||||
dot && styles[context.size].root.withDot,
|
||||
// With count
|
||||
typeof count === "number" && styles[context.size].root.withCount,
|
||||
// With checkbox
|
||||
context.selectionMode !== "none" && styles[context.size].root.withCheckbox,
|
||||
// Disabled
|
||||
isDisabled && "cursor-not-allowed",
|
||||
|
||||
typeof className === "function" ? className(state) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isSelected, isDisabled, allowsRemoving }) => (
|
||||
<>
|
||||
<div className={cx("flex items-center gap-1", styles[context.size].content)}>
|
||||
{context.selectionMode !== "none" && <TagCheckbox size={context.size} isSelected={isSelected} isDisabled={isDisabled} />}
|
||||
|
||||
{leadingContent}
|
||||
|
||||
{children}
|
||||
|
||||
{typeof count === "number" && (
|
||||
<span className={cx("flex items-center justify-center rounded-[3px] bg-tertiary text-center", styles[context.size].count)}>
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(onClose || allowsRemoving) && <TagCloseX size={context.size} onPress={() => id && onClose?.(id.toString())} />}
|
||||
</>
|
||||
)}
|
||||
</AriaTag>
|
||||
);
|
||||
};
|
||||
109
src/components/base/textarea/textarea.tsx
Normal file
109
src/components/base/textarea/textarea.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { ReactNode, Ref } from "react";
|
||||
import React from "react";
|
||||
import type { TextAreaProps as AriaTextAreaProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components";
|
||||
import { TextArea as AriaTextArea, TextField as AriaTextField } from "react-aria-components";
|
||||
import { HintText } from "@/components/base/input/hint-text";
|
||||
import { Label } from "@/components/base/input/label";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
// Creates a data URL for an SVG resize handle with a given color.
|
||||
const getResizeHandleBg = (color: string) => {
|
||||
return `url(data:image/svg+xml;base64,${btoa(`<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 2L2 10" stroke="${color}" stroke-linecap="round"/><path d="M11 7L7 11" stroke="${color}" stroke-linecap="round"/></svg>`)})`;
|
||||
};
|
||||
|
||||
interface TextAreaBaseProps extends AriaTextAreaProps {
|
||||
ref?: Ref<HTMLTextAreaElement>;
|
||||
}
|
||||
|
||||
export const TextAreaBase = ({ className, ...props }: TextAreaBaseProps) => {
|
||||
return (
|
||||
<AriaTextArea
|
||||
{...props}
|
||||
style={
|
||||
{
|
||||
"--resize-handle-bg": getResizeHandleBg("#D5D7DA"),
|
||||
"--resize-handle-bg-dark": getResizeHandleBg("#373A41"),
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={(state) =>
|
||||
cx(
|
||||
"w-full scroll-py-3 rounded-lg bg-primary px-3.5 py-3 text-md text-primary shadow-xs ring-1 ring-primary transition duration-100 ease-linear ring-inset placeholder:text-placeholder autofill:rounded-lg autofill:text-primary focus:outline-hidden",
|
||||
|
||||
// Resize handle
|
||||
"[&::-webkit-resizer]:bg-(image:--resize-handle-bg) [&::-webkit-resizer]:bg-contain dark:[&::-webkit-resizer]:bg-(image:--resize-handle-bg-dark)",
|
||||
|
||||
state.isFocused && !state.isDisabled && "ring-2 ring-brand",
|
||||
state.isDisabled && "cursor-not-allowed bg-disabled_subtle text-disabled ring-disabled",
|
||||
state.isInvalid && "ring-error_subtle",
|
||||
state.isInvalid && state.isFocused && "ring-2 ring-error",
|
||||
|
||||
typeof className === "function" ? className(state) : className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TextAreaBase.displayName = "TextAreaBase";
|
||||
|
||||
interface TextFieldProps extends AriaTextFieldProps {
|
||||
/** Label text for the textarea */
|
||||
label?: string;
|
||||
/** Helper text displayed below the textarea */
|
||||
hint?: ReactNode;
|
||||
/** Tooltip message displayed after the label. */
|
||||
tooltip?: string;
|
||||
/** Class name for the textarea wrapper */
|
||||
textAreaClassName?: TextAreaBaseProps["className"];
|
||||
/** Ref for the textarea wrapper */
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
/** Ref for the textarea */
|
||||
textAreaRef?: TextAreaBaseProps["ref"];
|
||||
/** Whether to hide required indicator from label. */
|
||||
hideRequiredIndicator?: boolean;
|
||||
/** Placeholder text. */
|
||||
placeholder?: string;
|
||||
/** Visible height of textarea in rows . */
|
||||
rows?: number;
|
||||
/** Visible width of textarea in columns. */
|
||||
cols?: number;
|
||||
}
|
||||
|
||||
export const TextArea = ({
|
||||
label,
|
||||
hint,
|
||||
tooltip,
|
||||
textAreaRef,
|
||||
hideRequiredIndicator,
|
||||
textAreaClassName,
|
||||
placeholder,
|
||||
className,
|
||||
rows,
|
||||
cols,
|
||||
...props
|
||||
}: TextFieldProps) => {
|
||||
return (
|
||||
<AriaTextField
|
||||
{...props}
|
||||
className={(state) =>
|
||||
cx("group flex h-max w-full flex-col items-start justify-start gap-1.5", typeof className === "function" ? className(state) : className)
|
||||
}
|
||||
>
|
||||
{({ isInvalid, isRequired }) => (
|
||||
<>
|
||||
{label && (
|
||||
<Label isRequired={hideRequiredIndicator ? !hideRequiredIndicator : isRequired} tooltip={tooltip}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
<TextAreaBase placeholder={placeholder} className={textAreaClassName} ref={textAreaRef} rows={rows} cols={cols} />
|
||||
|
||||
{hint && <HintText isInvalid={isInvalid}>{hint}</HintText>}
|
||||
</>
|
||||
)}
|
||||
</AriaTextField>
|
||||
);
|
||||
};
|
||||
|
||||
TextArea.displayName = "TextArea";
|
||||
138
src/components/base/toggle/toggle.tsx
Normal file
138
src/components/base/toggle/toggle.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { SwitchProps as AriaSwitchProps } from "react-aria-components";
|
||||
import { Switch as AriaSwitch } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface ToggleBaseProps {
|
||||
size?: "sm" | "md";
|
||||
slim?: boolean;
|
||||
className?: string;
|
||||
isHovered?: boolean;
|
||||
isFocusVisible?: boolean;
|
||||
isSelected?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const ToggleBase = ({ className, isHovered, isDisabled, isFocusVisible, isSelected, slim, size = "sm" }: ToggleBaseProps) => {
|
||||
const styles = {
|
||||
default: {
|
||||
sm: {
|
||||
root: "h-5 w-9 p-0.5",
|
||||
switch: cx("size-4", isSelected && "translate-x-4"),
|
||||
},
|
||||
md: {
|
||||
root: "h-6 w-11 p-0.5",
|
||||
switch: cx("size-5", isSelected && "translate-x-5"),
|
||||
},
|
||||
},
|
||||
slim: {
|
||||
sm: {
|
||||
root: "h-4 w-8",
|
||||
switch: cx("size-4", isSelected && "translate-x-4"),
|
||||
},
|
||||
md: {
|
||||
root: "h-5 w-10",
|
||||
switch: cx("size-5", isSelected && "translate-x-5"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const classes = slim ? styles.slim[size] : styles.default[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"cursor-pointer rounded-full bg-tertiary outline-focus-ring transition duration-150 ease-linear",
|
||||
isSelected && "bg-brand-solid",
|
||||
isSelected && isHovered && "bg-brand-solid_hover",
|
||||
isDisabled && "cursor-not-allowed bg-disabled",
|
||||
isFocusVisible && "outline-2 outline-offset-2",
|
||||
|
||||
slim && "ring-1 ring-secondary ring-inset",
|
||||
slim && isSelected && "ring-transparent",
|
||||
classes.root,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
transition: "transform 0.15s ease-in-out, translate 0.15s ease-in-out, border-color 0.1s linear, background-color 0.1s linear",
|
||||
}}
|
||||
className={cx(
|
||||
"rounded-full bg-fg-white shadow-sm",
|
||||
isDisabled && "bg-toggle-button-fg_disabled",
|
||||
|
||||
slim && "shadow-xs",
|
||||
slim && "border border-toggle-border",
|
||||
slim && isSelected && "border-toggle-slim-border_pressed",
|
||||
slim && isSelected && isHovered && "border-toggle-slim-border_pressed-hover",
|
||||
|
||||
classes.switch,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ToggleProps extends AriaSwitchProps {
|
||||
size?: "sm" | "md";
|
||||
label?: string;
|
||||
hint?: ReactNode;
|
||||
slim?: boolean;
|
||||
}
|
||||
|
||||
export const Toggle = ({ label, hint, className, size = "sm", slim, ...ariaSwitchProps }: ToggleProps) => {
|
||||
const sizes = {
|
||||
sm: {
|
||||
root: "gap-2",
|
||||
textWrapper: "",
|
||||
label: "text-sm font-medium",
|
||||
hint: "text-sm",
|
||||
},
|
||||
md: {
|
||||
root: "gap-3",
|
||||
textWrapper: "gap-0.5",
|
||||
label: "text-md font-medium",
|
||||
hint: "text-md",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<AriaSwitch
|
||||
{...ariaSwitchProps}
|
||||
className={(renderProps) =>
|
||||
cx(
|
||||
"flex w-max items-start",
|
||||
renderProps.isDisabled && "cursor-not-allowed",
|
||||
sizes[size].root,
|
||||
typeof className === "function" ? className(renderProps) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isSelected, isDisabled, isFocusVisible, isHovered }) => (
|
||||
<>
|
||||
<ToggleBase
|
||||
slim={slim}
|
||||
size={size}
|
||||
isHovered={isHovered}
|
||||
isDisabled={isDisabled}
|
||||
isFocusVisible={isFocusVisible}
|
||||
isSelected={isSelected}
|
||||
className={slim ? "mt-0.5" : ""}
|
||||
/>
|
||||
|
||||
{(label || hint) && (
|
||||
<div className={cx("flex flex-col", sizes[size].textWrapper)}>
|
||||
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
||||
{hint && (
|
||||
<span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
||||
{hint}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AriaSwitch>
|
||||
);
|
||||
};
|
||||
107
src/components/base/tooltip/tooltip.tsx
Normal file
107
src/components/base/tooltip/tooltip.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type {
|
||||
ButtonProps as AriaButtonProps,
|
||||
TooltipProps as AriaTooltipProps,
|
||||
TooltipTriggerComponentProps as AriaTooltipTriggerComponentProps,
|
||||
} from "react-aria-components";
|
||||
import { Button as AriaButton, OverlayArrow as AriaOverlayArrow, Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface TooltipProps extends AriaTooltipTriggerComponentProps, Omit<AriaTooltipProps, "children"> {
|
||||
/**
|
||||
* The title of the tooltip.
|
||||
*/
|
||||
title: ReactNode;
|
||||
/**
|
||||
* The description of the tooltip.
|
||||
*/
|
||||
description?: ReactNode;
|
||||
/**
|
||||
* Whether to show the arrow on the tooltip.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
arrow?: boolean;
|
||||
/**
|
||||
* Delay in milliseconds before the tooltip is shown.
|
||||
*
|
||||
* @default 300
|
||||
*/
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const Tooltip = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
arrow = false,
|
||||
delay = 300,
|
||||
closeDelay = 0,
|
||||
trigger,
|
||||
isDisabled,
|
||||
isOpen,
|
||||
defaultOpen,
|
||||
offset = 6,
|
||||
crossOffset,
|
||||
placement = "top",
|
||||
onOpenChange,
|
||||
...tooltipProps
|
||||
}: TooltipProps) => {
|
||||
const isTopOrBottomLeft = ["top left", "top end", "bottom left", "bottom end"].includes(placement);
|
||||
const isTopOrBottomRight = ["top right", "top start", "bottom right", "bottom start"].includes(placement);
|
||||
// Set negative cross offset for left and right placement to visually balance the tooltip.
|
||||
const calculatedCrossOffset = isTopOrBottomLeft ? -12 : isTopOrBottomRight ? 12 : 0;
|
||||
|
||||
return (
|
||||
<AriaTooltipTrigger {...{ trigger, delay, closeDelay, isDisabled, isOpen, defaultOpen, onOpenChange }}>
|
||||
{children}
|
||||
|
||||
<AriaTooltip
|
||||
{...tooltipProps}
|
||||
offset={offset}
|
||||
placement={placement}
|
||||
crossOffset={crossOffset ?? calculatedCrossOffset}
|
||||
className={({ isEntering, isExiting }) => cx(isEntering && "ease-out animate-in", isExiting && "ease-in animate-out")}
|
||||
>
|
||||
{({ isEntering, isExiting }) => (
|
||||
<div
|
||||
className={cx(
|
||||
"z-50 flex max-w-xs origin-(--trigger-anchor-point) flex-col items-start gap-1 rounded-lg bg-primary-solid px-3 shadow-lg will-change-transform",
|
||||
description ? "py-3" : "py-2",
|
||||
|
||||
isEntering &&
|
||||
"ease-out animate-in fade-in zoom-in-95 in-placement-left:slide-in-from-right-0.5 in-placement-right:slide-in-from-left-0.5 in-placement-top:slide-in-from-bottom-0.5 in-placement-bottom:slide-in-from-top-0.5",
|
||||
isExiting &&
|
||||
"ease-in animate-out fade-out zoom-out-95 in-placement-left:slide-out-to-right-0.5 in-placement-right:slide-out-to-left-0.5 in-placement-top:slide-out-to-bottom-0.5 in-placement-bottom:slide-out-to-top-0.5",
|
||||
)}
|
||||
>
|
||||
<span className="text-xs font-semibold text-white">{title}</span>
|
||||
|
||||
{description && <span className="text-xs font-medium text-tooltip-supporting-text">{description}</span>}
|
||||
|
||||
{arrow && (
|
||||
<AriaOverlayArrow>
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
className="size-2.5 fill-bg-primary-solid in-placement-left:-rotate-90 in-placement-right:rotate-90 in-placement-top:rotate-0 in-placement-bottom:rotate-180"
|
||||
>
|
||||
<path d="M0,0 L35.858,35.858 Q50,50 64.142,35.858 L100,0 Z" />
|
||||
</svg>
|
||||
</AriaOverlayArrow>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</AriaTooltip>
|
||||
</AriaTooltipTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
interface TooltipTriggerProps extends AriaButtonProps {}
|
||||
|
||||
export const TooltipTrigger = ({ children, className, ...buttonProps }: TooltipTriggerProps) => {
|
||||
return (
|
||||
<AriaButton {...buttonProps} className={(values) => cx("h-max w-max outline-hidden", typeof className === "function" ? className(values) : className)}>
|
||||
{children}
|
||||
</AriaButton>
|
||||
);
|
||||
};
|
||||
22
src/components/foundations/dot-icon.tsx
Normal file
22
src/components/foundations/dot-icon.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
const sizes = {
|
||||
sm: {
|
||||
wh: 8,
|
||||
c: 4,
|
||||
r: 2.5,
|
||||
},
|
||||
md: {
|
||||
wh: 10,
|
||||
c: 5,
|
||||
r: 4,
|
||||
},
|
||||
};
|
||||
|
||||
export const Dot = ({ size = "md", ...props }: HTMLAttributes<HTMLOrSVGElement> & { size?: "sm" | "md" }) => {
|
||||
return (
|
||||
<svg width={sizes[size].wh} height={sizes[size].wh} viewBox={`0 0 ${sizes[size].wh} ${sizes[size].wh}`} fill="none" {...props}>
|
||||
<circle cx={sizes[size].c} cy={sizes[size].c} r={sizes[size].r} fill="currentColor" stroke="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
154
src/components/foundations/featured-icon/featured-icon.tsx
Normal file
154
src/components/foundations/featured-icon/featured-icon.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import type { FC, ReactNode, Ref } from "react";
|
||||
import { isValidElement } from "react";
|
||||
import { cx, sortCx } from "@/utils/cx";
|
||||
import { isReactComponent } from "@/utils/is-react-component";
|
||||
|
||||
const iconsSizes = {
|
||||
sm: "*:data-icon:size-4",
|
||||
md: "*:data-icon:size-5",
|
||||
lg: "*:data-icon:size-6",
|
||||
xl: "*:data-icon:size-7",
|
||||
};
|
||||
|
||||
const styles = sortCx({
|
||||
light: {
|
||||
base: "rounded-full",
|
||||
sizes: {
|
||||
sm: "size-8",
|
||||
md: "size-10",
|
||||
lg: "size-12",
|
||||
xl: "size-14",
|
||||
},
|
||||
colors: {
|
||||
brand: "bg-brand-secondary text-featured-icon-light-fg-brand",
|
||||
gray: "bg-tertiary text-featured-icon-light-fg-gray",
|
||||
error: "bg-error-secondary text-featured-icon-light-fg-error",
|
||||
warning: "bg-warning-secondary text-featured-icon-light-fg-warning",
|
||||
success: "bg-success-secondary text-featured-icon-light-fg-success",
|
||||
},
|
||||
},
|
||||
|
||||
gradient: {
|
||||
base: "rounded-full text-fg-white before:absolute before:inset-0 before:size-full before:rounded-full before:border before:mask-b-from-0% after:absolute after:block after:rounded-full",
|
||||
sizes: {
|
||||
sm: "size-8 after:size-6 *:data-icon:size-4",
|
||||
md: "size-10 after:size-7 *:data-icon:size-4",
|
||||
lg: "size-12 after:size-8 *:data-icon:size-5",
|
||||
xl: "size-14 after:size-10 *:data-icon:size-5",
|
||||
},
|
||||
colors: {
|
||||
brand: "before:border-utility-brand-200 before:bg-utility-brand-50 after:bg-brand-solid",
|
||||
gray: "before:border-utility-gray-200 before:bg-utility-gray-50 after:bg-secondary-solid",
|
||||
error: "before:border-utility-error-200 before:bg-utility-error-50 after:bg-error-solid",
|
||||
warning: "before:border-utility-warning-200 before:bg-utility-warning-50 after:bg-warning-solid",
|
||||
success: "before:border-utility-success-200 before:bg-utility-success-50 after:bg-success-solid",
|
||||
},
|
||||
},
|
||||
|
||||
dark: {
|
||||
base: "text-fg-white shadow-xs-skeumorphic before:absolute before:inset-px before:border before:border-white/12 before:mask-b-from-0%",
|
||||
sizes: {
|
||||
sm: "size-8 rounded-md before:rounded-[5px]",
|
||||
md: "size-10 rounded-lg before:rounded-[7px]",
|
||||
lg: "size-12 rounded-[10px] before:rounded-[9px]",
|
||||
xl: "size-14 rounded-xl before:rounded-[11px]",
|
||||
},
|
||||
colors: {
|
||||
brand: "bg-brand-solid before:border-utility-brand-200/12",
|
||||
gray: "bg-secondary-solid before:border-utility-gray-200/12",
|
||||
error: "bg-error-solid before:border-utility-error-200/12",
|
||||
warning: "bg-warning-solid before:border-utility-warning-200/12",
|
||||
success: "bg-success-solid before:border-utility-success-200/12",
|
||||
},
|
||||
},
|
||||
|
||||
modern: {
|
||||
base: "bg-primary shadow-xs-skeumorphic ring-1 ring-inset",
|
||||
sizes: {
|
||||
sm: "size-8 rounded-md",
|
||||
md: "size-10 rounded-lg",
|
||||
lg: "size-12 rounded-[10px]",
|
||||
xl: "size-14 rounded-xl",
|
||||
},
|
||||
colors: {
|
||||
brand: "",
|
||||
gray: "text-fg-secondary ring-primary",
|
||||
error: "",
|
||||
warning: "",
|
||||
success: "",
|
||||
},
|
||||
},
|
||||
"modern-neue": {
|
||||
base: [
|
||||
"bg-primary_alt ring-1 ring-inset before:absolute before:inset-1",
|
||||
// Shadow
|
||||
"before:shadow-[0px_1px_2px_0px_rgba(0,0,0,0.1),0px_3px_3px_0px_rgba(0,0,0,0.09),1px_8px_5px_0px_rgba(0,0,0,0.05),2px_21px_6px_0px_rgba(0,0,0,0),0px_0px_0px_1px_rgba(0,0,0,0.08),1px_13px_5px_0px_rgba(0,0,0,0.01),0px_-2px_2px_0px_rgba(0,0,0,0.13)_inset] before:ring-1 before:ring-secondary_alt",
|
||||
].join(" "),
|
||||
sizes: {
|
||||
sm: "size-8 rounded-[8px] before:rounded-[4px]",
|
||||
md: "size-10 rounded-[10px] before:rounded-[6px]",
|
||||
lg: "size-12 rounded-[12px] before:rounded-[8px]",
|
||||
xl: "size-14 rounded-[14px] before:rounded-[10px]",
|
||||
},
|
||||
colors: {
|
||||
brand: "",
|
||||
gray: "text-fg-secondary ring-primary",
|
||||
error: "",
|
||||
warning: "",
|
||||
success: "",
|
||||
},
|
||||
},
|
||||
|
||||
outline: {
|
||||
base: "before:absolute before:rounded-full before:border-2 after:absolute after:rounded-full after:border-2",
|
||||
sizes: {
|
||||
sm: "size-4 before:size-6 after:size-8.5",
|
||||
md: "size-5 before:size-7 after:size-9.5",
|
||||
lg: "size-6 before:size-8 after:size-10.5",
|
||||
xl: "size-7 before:size-9 after:size-11.5",
|
||||
},
|
||||
colors: {
|
||||
brand: "text-fg-brand-primary before:border-fg-brand-primary/30 after:border-fg-brand-primary/10",
|
||||
gray: "text-fg-tertiary before:border-fg-tertiary/30 after:border-fg-tertiary/10",
|
||||
error: "text-fg-error-primary before:border-fg-error-primary/30 after:border-fg-error-primary/10",
|
||||
warning: "text-fg-warning-primary before:border-fg-warning-primary/30 after:border-fg-warning-primary/10",
|
||||
success: "text-fg-success-primary before:border-fg-success-primary/30 after:border-fg-success-primary/10",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface FeaturedIconProps {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
icon?: FC<{ className?: string }> | ReactNode;
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
color: "brand" | "gray" | "success" | "warning" | "error";
|
||||
theme?: "light" | "gradient" | "dark" | "outline" | "modern" | "modern-neue";
|
||||
}
|
||||
|
||||
export const FeaturedIcon = (props: FeaturedIconProps) => {
|
||||
const { size = "sm", theme: variant = "light", color = "brand", icon: Icon, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...otherProps}
|
||||
data-featured-icon
|
||||
className={cx(
|
||||
"relative flex shrink-0 items-center justify-center",
|
||||
|
||||
iconsSizes[size],
|
||||
styles[variant].base,
|
||||
styles[variant].sizes[size],
|
||||
styles[variant].colors[color],
|
||||
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{isReactComponent(Icon) && <Icon data-icon className="z-1" />}
|
||||
{isValidElement(Icon) && <div className="z-1">{Icon}</div>}
|
||||
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
170
src/components/foundations/logo/untitledui-logo-minimal.tsx
Normal file
170
src/components/foundations/logo/untitledui-logo-minimal.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import type { SVGProps } from "react";
|
||||
import { useId } from "react";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export const UntitledLogoMinimal = (props: SVGProps<SVGSVGElement>) => {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 38 38" fill="none" {...props} className={cx("size-8 origin-center scale-[1.2]", props.className)}>
|
||||
<g filter={`url(#filter0-${id}`}>
|
||||
<g clipPath={`url(#clip0-${id}`}>
|
||||
<path
|
||||
d="M3 14.8C3 10.3196 3 8.07937 3.87195 6.36808C4.63893 4.86278 5.86278 3.63893 7.36808 2.87195C9.07937 2 11.3196 2 15.8 2H22.2C26.6804 2 28.9206 2 30.6319 2.87195C32.1372 3.63893 33.3611 4.86278 34.1281 6.36808C35 8.07937 35 10.3196 35 14.8V21.2C35 25.6804 35 27.9206 34.1281 29.6319C33.3611 31.1372 32.1372 32.3611 30.6319 33.1281C28.9206 34 26.6804 34 22.2 34H15.8C11.3196 34 9.07937 34 7.36808 33.1281C5.86278 32.3611 4.63893 31.1372 3.87195 29.6319C3 27.9206 3 25.6804 3 21.2V14.8Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M3 14.8C3 10.3196 3 8.07937 3.87195 6.36808C4.63893 4.86278 5.86278 3.63893 7.36808 2.87195C9.07937 2 11.3196 2 15.8 2H22.2C26.6804 2 28.9206 2 30.6319 2.87195C32.1372 3.63893 33.3611 4.86278 34.1281 6.36808C35 8.07937 35 10.3196 35 14.8V21.2C35 25.6804 35 27.9206 34.1281 29.6319C33.3611 31.1372 32.1372 32.3611 30.6319 33.1281C28.9206 34 26.6804 34 22.2 34H15.8C11.3196 34 9.07937 34 7.36808 33.1281C5.86278 32.3611 4.63893 31.1372 3.87195 29.6319C3 27.9206 3 25.6804 3 21.2V14.8Z"
|
||||
fill={`url(#paint0_linear-${id}`}
|
||||
fillOpacity="0.2"
|
||||
/>
|
||||
<g opacity="0.14" clipPath={`url(#clip1-${id}`}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18.9612 2H19.0388V3.96123C20.8929 3.96625 22.6625 4.33069 24.2816 4.98855V2H24.3592V5.02038C25.7339 5.58859 26.9986 6.36882 28.1126 7.32031H29.602V2H29.6796V7.32031H35V7.39798H29.6796V8.88728C30.6311 10.0013 31.4113 11.266 31.9796 12.6406H35V12.7183H32.0114C32.6693 14.3373 33.0337 16.1069 33.0388 17.9609H35V18.0386H33.0388C33.0338 19.8927 32.6694 21.6622 32.0116 23.2812H35V23.3589H31.9798C31.4115 24.7337 30.6312 25.9986 29.6796 27.1128V28.6016H35V28.6792H29.6796V34H29.602V28.6792H28.1132C26.999 29.6309 25.7341 30.4113 24.3592 30.9797V34H24.2816V31.0115C22.6625 31.6693 20.8929 32.0338 19.0388 32.0388V34H18.9612V32.0388C17.1071 32.0338 15.3375 31.6693 13.7184 31.0115V34H13.6408V30.9797C12.2659 30.4113 11.001 29.6309 9.88678 28.6792H8.39804V34H8.32037V28.6792H3V28.6016H8.32037V27.1128C7.36877 25.9986 6.58847 24.7337 6.02023 23.3589H3V23.2812H5.9884C5.3306 21.6622 4.96621 19.8927 4.96122 18.0386H3V17.9609H4.96122C4.96627 16.1069 5.33073 14.3373 5.9886 12.7183H3V12.6406H6.02044C6.58866 11.266 7.36889 10.0013 8.32037 8.88728V7.39798H3V7.32031H8.32037V2H8.39804V7.32031H9.88736C11.0014 6.36882 12.2661 5.58859 13.6408 5.02038V2H13.7184V4.98855C15.3375 4.33069 17.1071 3.96626 18.9612 3.96123V2ZM18.9612 4.0389C17.1062 4.04396 15.3364 4.41075 13.7184 5.07245V7.32031H18.9612V4.0389ZM13.6408 5.10449C12.3137 5.65662 11.0902 6.40763 10.0074 7.32031H13.6408V5.10449ZM9.79719 7.39798H8.39804V8.79711C8.8311 8.29865 9.29872 7.83103 9.79719 7.39798ZM8.39804 8.91598C8.86452 8.37206 9.37213 7.86446 9.91606 7.39798H13.6408V12.6406H8.39804V8.91598ZM8.32037 9.00733C7.4077 10.0901 6.65669 11.3136 6.10454 12.6406H8.32037V9.00733ZM6.0725 12.7183C5.41078 14.3362 5.04397 16.106 5.03889 17.9609H8.32037V12.7183H6.0725ZM5.03889 18.0386C5.04391 19.8935 5.41065 21.6633 6.0723 23.2812H8.32037V18.0386H5.03889ZM6.10434 23.3589C6.6565 24.6861 7.40759 25.9098 8.32037 26.9927V23.3589H6.10434ZM8.39804 27.2029V28.6016H9.79662C9.29837 28.1686 8.83093 27.7012 8.39804 27.2029ZM9.91548 28.6016C9.37178 28.1352 8.86436 27.6278 8.39804 27.0841V23.3589H13.6408V28.6016H9.91548ZM10.0068 28.6792C11.0898 29.5921 12.3135 30.3433 13.6408 30.8955V28.6792H10.0068ZM13.7184 30.9276C15.3364 31.5893 17.1062 31.9561 18.9612 31.9611V28.6792H13.7184V30.9276ZM19.0388 31.9611C20.8937 31.9561 22.6636 31.5893 24.2816 30.9276V28.6792H19.0388V31.9611ZM24.3592 30.8955C25.6865 30.3433 26.9102 29.5921 27.9932 28.6792H24.3592V30.8955ZM28.2034 28.6016H29.602V27.2029C29.1691 27.7012 28.7016 28.1686 28.2034 28.6016ZM29.602 27.0841C29.1356 27.6278 28.6282 28.1352 28.0845 28.6016H24.3592V23.3589H29.602V27.0841ZM29.6796 26.9927C30.5924 25.9098 31.3435 24.6861 31.8957 23.3589H29.6796V26.9927ZM31.9277 23.2812C32.5894 21.6633 32.9561 19.8935 32.9611 18.0386H29.6796V23.2812H31.9277ZM32.9611 17.9609C32.956 16.1061 32.5892 14.3362 31.9275 12.7183H29.6796V17.9609H32.9611ZM31.8955 12.6406C31.3433 11.3136 30.5923 10.0901 29.6796 9.00733V12.6406H31.8955ZM29.602 8.79711V7.39798H28.2028C28.7013 7.83103 29.1689 8.29865 29.602 8.79711ZM28.0839 7.39798C28.6279 7.86446 29.1355 8.37206 29.602 8.91598V12.6406H24.3592V7.39798H28.0839ZM27.9926 7.32031C26.9098 6.40763 25.6863 5.65662 24.3592 5.10449V7.32031H27.9926ZM24.2816 5.07245C22.6636 4.41074 20.8937 4.04395 19.0388 4.0389V7.32031H24.2816V5.07245ZM13.7184 7.39798H18.9612V12.6406H13.7184V7.39798ZM24.2816 7.39798H19.0388V12.6406H24.2816V7.39798ZM13.6408 23.2812H8.39804V18.0386H13.6408V23.2812ZM13.7184 23.3589V28.6016H18.9612V23.3589H13.7184ZM18.9612 23.2812H13.7184V18.0386H18.9612V23.2812ZM19.0388 23.3589V28.6016H24.2816V23.3589H19.0388ZM24.2816 23.2812H19.0388V18.0386H24.2816V23.2812ZM29.602 23.2812H24.3592V18.0386H29.602V23.2812ZM13.7184 12.7183H18.9612V17.9609H13.7184V12.7183ZM8.39804 12.7183L13.6408 12.7183V17.9609H8.39804V12.7183ZM24.2816 12.7183H19.0388V17.9609H24.2816V12.7183ZM24.3592 17.9609V12.7183L29.602 12.7183V17.9609H24.3592Z"
|
||||
fill="#0A0D12"
|
||||
/>
|
||||
</g>
|
||||
<g filter={`url(#filter1_dd-${id}`}>
|
||||
<rect x="11" y="10" width="16" height="16" rx="8" fill={`url(#paint1_linear-${id}`} />
|
||||
<rect x="11" y="10" width="16" height="16" rx="8" fill={`url(#paint2_radial-${id}`} fillOpacity="0.08" />
|
||||
<rect x="11" y="10" width="16" height="16" rx="8" fill={`url(#paint3_radial-${id}`} fillOpacity="0.18" />
|
||||
<rect x="11" y="10" width="16" height="16" rx="8" fill={`url(#paint4_radial-${id}`} fillOpacity="0.05" />
|
||||
<path
|
||||
d="M23.8 14.0414C23.8 15.3898 21.651 14.5297 19 14.5297C16.349 14.5297 14.2 15.3898 14.2 14.0414C14.2 12.693 16.349 11.6 19 11.6C21.651 11.6 23.8 12.693 23.8 14.0414Z"
|
||||
fill={`url(#paint5_linear-${id}`}
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
d="M3.1 14.8C3.1 12.5581 3.10008 10.8828 3.20866 9.55376C3.31715 8.22593 3.53345 7.25268 3.96105 6.41348C4.71845 4.92699 5.92699 3.71845 7.41348 2.96105C8.25268 2.53345 9.22593 2.31715 10.5538 2.20866C11.8828 2.10008 13.5581 2.1 15.8 2.1H22.2C24.4419 2.1 26.1172 2.10008 27.4462 2.20866C28.7741 2.31715 29.7473 2.53345 30.5865 2.96105C32.073 3.71845 33.2816 4.92699 34.039 6.41348C34.4665 7.25268 34.6828 8.22593 34.7913 9.55376C34.8999 10.8828 34.9 12.5581 34.9 14.8V21.2C34.9 23.4419 34.8999 25.1172 34.7913 26.4462C34.6828 27.7741 34.4665 28.7473 34.039 29.5865C33.2816 31.073 32.073 32.2816 30.5865 33.039C29.7473 33.4665 28.7741 33.6828 27.4462 33.7913C26.1172 33.8999 24.4419 33.9 22.2 33.9H15.8C13.5581 33.9 11.8828 33.8999 10.5538 33.7913C9.22593 33.6828 8.25268 33.4665 7.41348 33.039C5.92699 32.2816 4.71845 31.073 3.96105 29.5865C3.53345 28.7473 3.31715 27.7741 3.20866 26.4462C3.10008 25.1172 3.1 23.4419 3.1 21.2V14.8Z"
|
||||
stroke="#0A0D12"
|
||||
strokeOpacity="0.12"
|
||||
strokeWidth="0.2"
|
||||
/>
|
||||
</g>
|
||||
<image
|
||||
href="data:image/webp;base64,UklGRoYHAABXRUJQVlA4WAoAAAAQAAAAnwAATwAAQUxQSIcFAAABD9D/iAjY3/9PbSp7ed+ZTNJJKWUSCpsenYbSDay1rPukpGxgrV3WtfCsu16XSSmc9NgTuLa+Qa+7e1h3d3ff/em6RvQ/jjUcsJXcxqnp73y0Dd1iS99JJiIk8ActCkLbexBTRwGAiRepAc0DWAxNoPI469jGE+uApSJKwSaxwH/SDuvFRgSL+iF90f0/uOinRfp2jRkl3Nff8aS4scgmTK8WSJAVeVMRv8MPmIiL0AL18jbT0iUQ4DLPPF/UJ4mbkQ4c4NfrQGJI7ocYCmQLUqQGLS9i1ZjJ9YD0wfkBRF/XQbJCuVeo1XGMePjwSf2n48dncwVJ5gjukhRfrTlAVTy4rpscDVXOijJypxQS6iZgBm9Q/kbbeQC8WMyky2I1RSACacETMY7ld3aOWZWw7g04bJcNNfCX6IsL4cz9WT+HbkzgHlngSQGIuajVbyZkyV32WwfZg2CY/K69d78dyr19p3jzASrzrYmkoHQM/2cGci2NjEfmfsjszTI6mhxAeqmb/h72AaqBsGWdCrha2VmpQtyCx/EEGm/dvEnAt/gBqYPCDEh3R6bFAQoJcUvQRIdkuD2Zk4H+uHUhGxHpl7F/DNHVZnf7DX/F7JqyqhhOZ4Vy4aVATT89pT6Oio4sfs5ULH9Ur0ZpYPL1Eq+om9+wDFkhQ0IDF7EBFfA8yWGAgWQ8lWnm0cnzknwjxU6eQkAfVsiZBxhw0hjPFwx7uwcLkAZYD2lOyDqorXqpgjlI9zdQX61m7fly+X4FlyixZi0OifzLtlA26N2N9+N5d8NP3f3h7AG4EfiMPaLDdgJOUEz0LipV00L4ouO+0F5826ZwaV2BSUWQ03dWSTzXqKZVC9pM6caAo/Xxu9V2Y5hwD0eAaPXq6ndNlvNQYS4NcAJpT6UHVigUPfSX5tJxATt1ZVukUft1iDcX/swp1q9mPN+Jn8hNz22sgVH/Hj6e+N/5DPU6IkcEMIhhCYffRA+HtZbBngZ4epF4vzCAu6OSk+D6xA2piTzCJVPIGnseVYeMjMgTPejNPc6yYypzXj4G8AFZeiu3+MlN8E5Pzm+e6XoV043lWOqKp7XjesRhra133Li79Z2UoDiwizOksgRVaMAsj3o/INvNRZ3HSA/vuYkPsoNbBgIopPsHfiPSdJY2Szy752RKphatmd/pACuOi57f3tVk30vJ3DAMf07h1UMzknO2Ez8B24dV7YxgK87zMIKdB+w8BCp59/G29K5GW5NM97V3NwBS9BcIN/91ETsQNwRQcWhAQloc1ngPoAcX3t3GgqXP8f88Sj3S07Ub09YQWbJ1MHRgEpl10ayV96mWyfsGaBONZoORvD9zPei0Ad6XtGZytQ6WDxJN61//aXV1BosiPeNS69b2B+XS+dc66dgPjB9mfyNvfPFhuUQRyOu1pHN+Dxw7u7YMP7Gb0DGL4nRcJQDEufQ+UMcP6F6a3RX/ZtPJN8P/yb/g4YsEpOOEk72GY2VNq7AlCJ/n96M3Rq6J5sXXNmrgfHjypfv3WSO/pOuElp+ckuHDNSLmsfmHdmSj9j+Nmosz8FeFuu1+0QMOQlMnTxfNAkTfoe0Rijtpq38Mpj+W9b3yUeHlfcazjLbQQhds194PAPDZwgj71n+VRxDXHscni/Wky7ywckRU4/JqDpVmuOed3eBwCb10sD55/TfECEJNPJPYeG6UuvcrtswWvQOGXDft/1xrxR7Az/JQWtisbGGbQrgbQjx2rJRUh0am0G/Yj0Cu/NQy7f8JtQVbh+ZCgIt7gOEADvJFKwEr7/GrgQql3CM/urtBzm9nMnCj53WEWr3/5BJROomYBUwsRXInvP5O3DqZfVoD9WRpEsUH4yx6BI4JaaMQZUYZ8F7y0KBvD/PTDSR/CgBWUDgg2AEAAFATAJ0BKqAAUAA+KRKHQqGhCj4CcgwBQlnAM4spE3mv/5foxj7CIWswa6AqkXFI+Yh0P/x5D3Ezvirfp0TGLFgI7x9F5fO5IHYoatU6Evly836v+YqU0oO2cfVypfHDsDw29g1p0iydYsDT1vJNxjinCDWybXmWiyxS9UEu/eRrG49iyvR67gMIAazf0vPgtrbCHQlTv5rJV7PPFdfirZtz+MAA/v5sJNKJKeJ/hzR4FAhZT+MvOLxH42j6POGLQ5xx2In90EX/MqvckpWMe6weKHop6T9BUAz/qmeY0d8DTiGxEk9YU598XXHiiNGKd3N9bWu4tL9X/OTRVy1Rn7PiG3QoRIX3J93WJsqnemrvJRVh5OFlmDx8B2NZglxXEALZRATlzsBOth3ETpiwt4j0QGUWf9bqg+8o+N9xVYJQdwytiTyuepp1FCA/u6R5xx93RhFIuyDILKBC2Y5InXCxD6GMe2LENm7ZJ/grDJ4/Sw87hS1FBNG9/Q83pgBC8DNlOmf4pz//jBR6YIzM9rp4182sAr4cYbiEejZB40FUN1LWRAjTwjz+qDQMg6IT9yo01SmHMkGkr4vQZayROK4PIIkRRSAlJALZq89W30VJUep6YrggAAA="
|
||||
x="0"
|
||||
y="19"
|
||||
width="38"
|
||||
height="19"
|
||||
transform="scale(0.84) translate(0, -1.5)"
|
||||
className="origin-center"
|
||||
preserveAspectRatio="xMidYMax slice"
|
||||
clipPath={`url(#imageClip-${id})`}
|
||||
/>
|
||||
|
||||
<defs>
|
||||
<clipPath id={`imageClip-${id}`}>
|
||||
<path d="M 0 19 L 38 19 L 38 28.88 A 9.12 9.12 0 0 1 28.88 38 L 9.12 38 A 9.12 9.12 0 0 1 0 28.88 Z" />
|
||||
</clipPath>
|
||||
<filter id={`filter0-${id}`} x="0" y="0" width="38" height="38" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.06 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.1 0" />
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feMorphology radius="0.5" operator="erode" in="SourceAlpha" result="effect3_dropShadow" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="0.5" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.13 0" />
|
||||
<feBlend mode="normal" in2="effect2_dropShadow" result="effect3_dropShadow" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow" result="shape" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="-0.5" />
|
||||
<feGaussianBlur stdDeviation="0.25" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.1 0" />
|
||||
<feBlend mode="normal" in2="shape" result="effect4_innerShadow" />
|
||||
</filter>
|
||||
<filter id={`filter1_dd-${id}`} x="8" y="8" width="22" height="22" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.06 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0392157 0 0 0 0 0.0509804 0 0 0 0 0.0705882 0 0 0 0.1 0" />
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape" />
|
||||
</filter>
|
||||
<filter id={`filter2_b-${id}`} x="-2" y="13" width="42" height="26" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="2.5" />
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur" result="shape" />
|
||||
</filter>
|
||||
<linearGradient id={`paint0_linear-${id}`} x1="19" y1="2" x2="19" y2="34" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" />
|
||||
<stop offset="1" stopColor="#0A0D12" />
|
||||
</linearGradient>
|
||||
<linearGradient id={`paint1_linear-${id}`} x1="15" y1="26" x2="23" y2="10" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#53389E" />
|
||||
<stop offset="1" stopColor="#6941C6" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id={`paint2_radial-${id}`}
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(19 10) rotate(90) scale(12)"
|
||||
>
|
||||
<stop stopColor="white" stopOpacity="0" />
|
||||
<stop offset="0.5" stopColor="white" stopOpacity="0" />
|
||||
<stop offset="0.99" stopColor="white" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`paint3_radial-${id}`}
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(19 18) rotate(90) scale(8)"
|
||||
>
|
||||
<stop offset="0.746599" stopColor="white" stopOpacity="0" />
|
||||
<stop offset="1" stopColor="white" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={`paint4_radial-${id}`}
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(19 14.6) rotate(90) scale(7)"
|
||||
>
|
||||
<stop stopColor="white" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient id={`paint5_linear-${id}`} x1="19" y1="11.6" x2="19" y2="14.8" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.1" />
|
||||
</linearGradient>
|
||||
<clipPath id={`clip0-${id}`}>
|
||||
<path
|
||||
d="M3 14.8C3 10.3196 3 8.07937 3.87195 6.36808C4.63893 4.86278 5.86278 3.63893 7.36808 2.87195C9.07937 2 11.3196 2 15.8 2H22.2C26.6804 2 28.9206 2 30.6319 2.87195C32.1372 3.63893 33.3611 4.86278 34.1281 6.36808C35 8.07937 35 10.3196 35 14.8V21.2C35 25.6804 35 27.9206 34.1281 29.6319C33.3611 31.1372 32.1372 32.3611 30.6319 33.1281C28.9206 34 26.6804 34 22.2 34H15.8C11.3196 34 9.07937 34 7.36808 33.1281C5.86278 32.3611 4.63893 31.1372 3.87195 29.6319C3 27.9206 3 25.6804 3 21.2V14.8Z"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id={`clip1-${id}`}>
|
||||
<rect width="32" height="32" fill="white" transform="translate(3 2)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
58
src/components/foundations/logo/untitledui-logo.tsx
Normal file
58
src/components/foundations/logo/untitledui-logo.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { HTMLAttributes } from "react";
|
||||
import { cx } from "@/utils/cx";
|
||||
import { UntitledLogoMinimal } from "./untitledui-logo-minimal";
|
||||
|
||||
export const UntitledLogo = (props: HTMLAttributes<HTMLOrSVGElement>) => {
|
||||
return (
|
||||
<div {...props} className={cx("flex h-8 w-max items-center justify-start overflow-visible", props.className)}>
|
||||
{/* Minimal logo */}
|
||||
<UntitledLogoMinimal className="aspect-square h-full w-auto shrink-0" />
|
||||
|
||||
{/* Gap that adjusts to the height of the container */}
|
||||
<div className="aspect-[0.3] h-full" />
|
||||
|
||||
{/* Logomark */}
|
||||
<svg viewBox="0 0 97 32" fill="none" className="aspect-[3] h-full shrink-0">
|
||||
<path
|
||||
d="M33.9101 10.2372C34.2321 10.5355 34.6179 10.6847 35.0678 10.6847C35.5176 10.6847 35.9011 10.5355 36.2183 10.2372C36.5403 9.9342 36.7013 9.57199 36.7013 9.15058C36.7013 8.73392 36.5403 8.37644 36.2183 8.07814C35.9011 7.77511 35.5176 7.6236 35.0678 7.6236C34.6179 7.6236 34.2321 7.77511 33.9101 8.07814C33.5928 8.37644 33.4342 8.73392 33.4342 9.15058C33.4342 9.57199 33.5928 9.9342 33.9101 10.2372Z"
|
||||
className="fill-fg-primary"
|
||||
/>
|
||||
<path
|
||||
d="M11.2997 20.6847C11.8063 19.8892 12.0597 18.9612 12.0597 17.9006V8.45456H8.98438V17.6378C8.98438 18.1918 8.86127 18.6842 8.61506 19.1151C8.37358 19.5459 8.0303 19.8845 7.58523 20.1307C7.14489 20.3769 6.62642 20.5 6.02983 20.5C5.43797 20.5 4.91951 20.3769 4.47443 20.1307C4.02936 19.8845 3.68371 19.5459 3.4375 19.1151C3.19602 18.6842 3.07528 18.1918 3.07528 17.6378V8.45456H0V17.9006C0 18.9612 0.250947 19.8892 0.752841 20.6847C1.25473 21.4801 1.95786 22.1004 2.86222 22.5455C3.76657 22.9858 4.82244 23.206 6.02983 23.206C7.23248 23.206 8.28599 22.9858 9.19034 22.5455C10.0947 22.1004 10.7978 21.4801 11.2997 20.6847Z"
|
||||
className="fill-fg-primary"
|
||||
/>
|
||||
<path
|
||||
d="M18.3589 12.51C17.7907 12.8793 17.3859 13.3812 17.1444 14.0156H17.0165V12.0909H14.133V23H17.1586V16.6932C17.1633 16.2244 17.2509 15.8244 17.4214 15.4929C17.5966 15.1567 17.838 14.9011 18.1458 14.7259C18.4583 14.5507 18.8182 14.4631 19.2254 14.4631C19.8314 14.4631 20.3073 14.6525 20.6529 15.0313C20.9986 15.4053 21.169 15.9262 21.1643 16.5938V23H24.1898V16.054C24.1898 15.2065 24.0336 14.4773 23.7211 13.8665C23.4086 13.251 22.9706 12.7775 22.4072 12.446C21.8437 12.1146 21.1832 11.9489 20.4256 11.9489C19.616 11.9489 18.9271 12.1359 18.3589 12.51Z"
|
||||
className="fill-fg-primary"
|
||||
/>
|
||||
<path
|
||||
d="M27.3463 21.821C27.0433 21.3523 26.8941 20.7604 26.8989 20.0455V14.3637H25.4074V12.0909H26.8989V9.47729H29.9244V12.0909H31.977V14.3637H29.9244V19.6477C29.9244 19.9271 29.967 20.1449 30.0523 20.3012C30.1375 20.4527 30.2559 20.5592 30.4074 20.6208C30.5636 20.6823 30.7436 20.7131 30.9472 20.7131C31.0892 20.7131 31.2313 20.7012 31.3733 20.6776C31.5153 20.6492 31.6242 20.6279 31.7 20.6137L32.1759 22.8651C32.0243 22.9124 31.8113 22.9669 31.5366 23.0284C31.262 23.0947 30.9282 23.135 30.5352 23.1492C29.8061 23.1776 29.1669 23.0805 28.6176 22.858C28.0731 22.6354 27.6493 22.2898 27.3463 21.821Z"
|
||||
className="fill-fg-primary"
|
||||
/>
|
||||
<path
|
||||
d="M39.769 21.821C39.4659 21.3523 39.3168 20.7604 39.3215 20.0455V14.3637H37.83V12.0909H39.3215V9.47729H42.3471V12.0909H44.3996V14.3637H42.3471V19.6477C42.3471 19.9271 42.3897 20.1449 42.4749 20.3012C42.5602 20.4527 42.6785 20.5592 42.83 20.6208C42.9863 20.6823 43.1662 20.7131 43.3698 20.7131C43.5119 20.7131 43.6539 20.7012 43.796 20.6776C43.938 20.6492 44.0469 20.6279 44.1227 20.6137L44.5985 22.8651C44.447 22.9124 44.2339 22.9669 43.9593 23.0284C43.6847 23.0947 43.3509 23.135 42.9579 23.1492C42.2287 23.1776 41.5895 23.0805 41.0403 22.858C40.4958 22.6354 40.072 22.2898 39.769 21.821Z"
|
||||
className="fill-fg-primary"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M56.2257 23.2131C55.1035 23.2131 54.1376 22.9858 53.328 22.5313C52.5231 22.072 51.9028 21.4233 51.4672 20.5852C51.0316 19.7424 50.8138 18.7458 50.8138 17.5952C50.8138 16.473 51.0316 15.4882 51.4672 14.6406C51.9028 13.7931 52.516 13.1326 53.3067 12.6591C54.1021 12.1856 55.0349 11.9489 56.105 11.9489C56.8247 11.9489 57.4946 12.0649 58.1149 12.2969C58.7399 12.5242 59.2844 12.8674 59.7484 13.3267C60.2172 13.786 60.5818 14.3637 60.8422 15.0597C61.1026 15.751 61.2328 16.5606 61.2328 17.4887V18.3196H53.8038V18.3267C53.8038 18.8665 53.9033 19.3329 54.1021 19.7259C54.3057 20.1189 54.5922 20.4219 54.9615 20.635C55.3308 20.848 55.7688 20.9546 56.2754 20.9546C56.6116 20.9546 56.9194 20.9072 57.1987 20.8125C57.4781 20.7178 57.7172 20.5758 57.916 20.3864C58.1149 20.197 58.2664 19.965 58.3706 19.6904L61.1689 19.875C61.0268 20.5474 60.7357 21.1345 60.2953 21.6364C59.8597 22.1335 59.2963 22.5218 58.605 22.8012C57.9184 23.0758 57.1253 23.2131 56.2257 23.2131ZM54.1092 15.3722C53.9258 15.6954 53.8249 16.0529 53.8067 16.4446H58.3848C58.3848 16.009 58.2901 15.6231 58.1007 15.2869C57.9113 14.9508 57.6485 14.688 57.3124 14.4986C56.9809 14.3045 56.595 14.2074 56.1547 14.2074C55.6954 14.2074 55.2882 14.3139 54.9331 14.527C54.5827 14.7353 54.3081 15.0171 54.1092 15.3722Z"
|
||||
className="fill-fg-primary"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M64.5757 22.5384C65.2481 22.9645 65.9985 23.1776 66.8271 23.1776C67.4143 23.1776 67.9114 23.0805 68.3186 22.8864C68.7305 22.6923 69.0643 22.4484 69.32 22.1548C69.5804 21.8566 69.7817 21.5559 69.9237 21.2529H70.0516V23H73.0345V8.45456H70.0161V13.9233H69.9237C69.7911 13.6298 69.597 13.3315 69.3413 13.0284C69.0904 12.7207 68.7589 12.465 68.347 12.2614C67.9398 12.053 67.4308 11.9489 66.82 11.9489C66.0198 11.9489 65.2836 12.1572 64.6112 12.5739C63.9436 12.9858 63.4086 13.6084 63.0061 14.4418C62.6036 15.2704 62.4024 16.3097 62.4024 17.5597C62.4024 18.7765 62.5965 19.804 62.9848 20.6421C63.3778 21.4754 63.9081 22.1075 64.5757 22.5384ZM69.0217 20.3722C68.6856 20.6373 68.2736 20.7699 67.7859 20.7699C67.2888 20.7699 66.8698 20.635 66.5288 20.3651C66.1927 20.0905 65.9346 19.7117 65.7547 19.2287C65.5795 18.741 65.4919 18.1799 65.4919 17.5455C65.4919 16.9157 65.5795 16.3618 65.7547 15.8835C65.9299 15.4053 66.1879 15.0313 66.5288 14.7614C66.8698 14.4915 67.2888 14.3566 67.7859 14.3566C68.2736 14.3566 68.6879 14.4868 69.0288 14.7472C69.3698 15.0076 69.6302 15.3769 69.8101 15.8551C69.99 16.3334 70.08 16.8968 70.08 17.5455C70.08 18.1941 69.9876 18.76 69.803 19.2429C69.6231 19.7259 69.3626 20.1023 69.0217 20.3722Z"
|
||||
className="fill-fg-primary"
|
||||
/>
|
||||
<path
|
||||
d="M88.0229 19.1151C88.2691 18.6842 88.3922 18.1918 88.3922 17.6378V8.45456H91.4675V17.9006C91.4675 18.9612 91.2142 19.8892 90.7075 20.6847C90.2056 21.4801 89.5025 22.1004 88.5982 22.5455C87.6938 22.9858 86.6403 23.206 85.4376 23.206C84.2303 23.206 83.1744 22.9858 82.27 22.5455C81.3657 22.1004 80.6625 21.4801 80.1607 20.6847C79.6588 19.8892 79.4078 18.9612 79.4078 17.9006V8.45456H82.4831V17.6378C82.4831 18.1918 82.6038 18.6842 82.8453 19.1151C83.0915 19.5459 83.4372 19.8845 83.8822 20.1307C84.3273 20.3769 84.8458 20.5 85.4376 20.5C86.0342 20.5 86.5527 20.3769 86.993 20.1307C87.4381 19.8845 87.7814 19.5459 88.0229 19.1151Z"
|
||||
className="fill-fg-primary"
|
||||
/>
|
||||
<path d="M33.5479 12.0909V23H36.5734V12.0909H33.5479Z" className="fill-fg-primary" />
|
||||
<path d="M49.2305 23V8.45456H46.2049V23H49.2305Z" className="fill-fg-primary" />
|
||||
<path d="M96.6729 23V8.45456H93.5977V23H96.6729Z" className="fill-fg-primary" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
17
src/components/foundations/payment-icons/amex-icon.tsx
Normal file
17
src/components/foundations/payment-icons/amex-icon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const AmexIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||
<path d="M0 4C0 1.79086 1.79086 0 4 0H30C32.2091 0 34 1.79086 34 4V20C34 22.2091 32.2091 24 30 24H4C1.79086 24 0 22.2091 0 20V4Z" fill="#1F72CD" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.09517 8.5L2.91406 15.7467H6.7223L7.19441 14.5913H8.27355L8.74566 15.7467H12.9375V14.8649L13.311 15.7467H15.4793L15.8528 14.8462V15.7467H24.5706L25.6307 14.6213L26.6232 15.7467L31.1009 15.7561L27.9097 12.1436L31.1009 8.5H26.6927L25.6608 9.60463L24.6995 8.5H15.2156L14.4013 10.3704L13.5678 8.5H9.7675V9.35186L9.34474 8.5H6.09517ZM6.83205 9.52905H8.68836L10.7984 14.4431V9.52905H12.8319L14.4617 13.0524L15.9637 9.52905H17.987V14.7291H16.7559L16.7458 10.6544L14.9509 14.7291H13.8495L12.0446 10.6544V14.7291H9.51179L9.03162 13.5633H6.43745L5.95827 14.728H4.60123L6.83205 9.52905ZM24.1196 9.52905H19.1134V14.726H24.0421L25.6307 13.0036L27.1618 14.726H28.7624L26.436 12.1426L28.7624 9.52905H27.2313L25.6507 11.2316L24.1196 9.52905ZM7.73508 10.4089L6.8804 12.4856H8.58876L7.73508 10.4089ZM20.3497 11.555V10.6057V10.6048H23.4734L24.8364 12.1229L23.413 13.6493H20.3497V12.613H23.0808V11.555H20.3497Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default AmexIcon;
|
||||
25
src/components/foundations/payment-icons/apple-pay-icon.tsx
Normal file
25
src/components/foundations/payment-icons/apple-pay-icon.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const ApplePayIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
className="stroke-border-secondary"
|
||||
strokeWidth="0.75"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.44921 8.34316C9.16382 8.69506 8.70721 8.97261 8.2506 8.93296C8.19353 8.45715 8.41707 7.95161 8.67867 7.63936C8.96406 7.27755 9.46348 7.01983 9.86777 7C9.91533 7.49563 9.72983 7.98135 9.44921 8.34316ZM9.86297 9.02712C9.46071 9.003 9.09366 9.15319 8.79718 9.2745C8.60639 9.35256 8.44483 9.41867 8.32191 9.41867C8.18397 9.41867 8.01574 9.34903 7.82685 9.27084L7.82685 9.27084C7.57935 9.16838 7.29638 9.05124 6.99964 9.05686C6.31948 9.06677 5.68688 9.46823 5.33967 10.1076C4.62621 11.3863 5.15417 13.2796 5.84384 14.3205C6.18155 14.8359 6.58584 15.4009 7.11855 15.3811C7.35291 15.3719 7.5215 15.2973 7.69597 15.2202C7.89683 15.1314 8.10549 15.0391 8.43131 15.0391C8.74582 15.0391 8.94536 15.129 9.1369 15.2152C9.31903 15.2973 9.49393 15.376 9.75358 15.3712C10.3053 15.3613 10.6525 14.8557 10.9902 14.3403C11.3547 13.7871 11.5148 13.2471 11.5391 13.1652L11.542 13.1557C11.5414 13.1551 11.5369 13.153 11.5289 13.1492C11.4071 13.0911 10.476 12.6469 10.467 11.4557C10.4581 10.4559 11.2056 9.94935 11.3233 9.86961L11.3233 9.8696C11.3304 9.86476 11.3353 9.86149 11.3374 9.85978C10.8618 9.12625 10.1198 9.04695 9.86297 9.02712ZM13.6824 15.3167V7.5898H16.4649C17.9013 7.5898 18.9049 8.62071 18.9049 10.1274C18.9049 11.6341 17.8822 12.675 16.4268 12.675H14.8334V15.3167H13.6824ZM14.8333 8.60088H16.1603C17.1592 8.60088 17.7299 9.15599 17.7299 10.1324C17.7299 11.1088 17.1592 11.6688 16.1556 11.6688H14.8333V8.60088ZM22.7053 14.3898C22.4009 14.9945 21.7302 15.3761 21.0072 15.3761C19.9371 15.3761 19.1903 14.712 19.1903 13.7108C19.1903 12.7196 19.9133 12.1496 21.2498 12.0653L22.6862 11.9761V11.5499C22.6862 10.9204 22.2915 10.5784 21.5875 10.5784C21.0072 10.5784 20.5839 10.8907 20.4983 11.3665H19.4614C19.4947 10.3653 20.3984 9.63675 21.6208 9.63675C22.9383 9.63675 23.7945 10.3554 23.7945 11.4706V15.3167H22.729V14.3898H22.7053ZM21.3163 14.4592C20.7027 14.4592 20.3127 14.1519 20.3127 13.6811C20.3127 13.1954 20.6885 12.9129 21.4067 12.8683L22.6861 12.784V13.2202C22.6861 13.9438 22.0964 14.4592 21.3163 14.4592ZM27.3284 15.619C26.867 16.9721 26.3391 17.4181 25.2166 17.4181C25.131 17.4181 24.8456 17.4082 24.779 17.3884V16.4616C24.8503 16.4715 25.0263 16.4814 25.1167 16.4814C25.6256 16.4814 25.911 16.2584 26.087 15.6785L26.1916 15.3365L24.2415 9.7111H25.4449L26.8004 14.2759H26.8242L28.1798 9.7111H29.3499L27.3284 15.619Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplePayIcon;
|
||||
32
src/components/foundations/payment-icons/discover-icon.tsx
Normal file
32
src/components/foundations/payment-icons/discover-icon.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const DiscoverIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
className="stroke-border-secondary"
|
||||
strokeWidth="0.75"
|
||||
/>
|
||||
<path d="M14 23L33 17.25V20C33 21.6569 31.6569 23 30 23H14Z" fill="#FD6020" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M29.3937 9.11084C30.439 9.11084 31.0139 9.59438 31.0139 10.5077C31.0662 11.2062 30.5958 11.7972 29.9686 11.9046L31.3797 13.8925H30.2822L29.0801 11.9584H28.9756V13.8925H28.0871V9.11084H29.3937ZM28.9756 11.3137H29.2369C29.8118 11.3137 30.0731 11.045 30.0731 10.5615C30.0731 10.1317 29.8118 9.86304 29.2369 9.86304H28.9756V11.3137ZM25.0034 13.8925H27.5122V13.0866H25.8919V11.7972H27.4599V10.9913H25.8919V9.91674H27.5122V9.11084H25.0034V13.8925ZM22.3902 12.3345L21.1881 9.11084H20.2474L22.1812 14H22.6515L24.5853 9.11084H23.6446L22.3902 12.3345ZM11.7805 11.5286C11.7805 12.8717 12.8258 14 14.1324 14C14.5505 14 14.9164 13.8925 15.2822 13.7314V12.6568C15.0209 12.9792 14.655 13.1941 14.2369 13.1941C13.4007 13.1941 12.7212 12.5494 12.7212 11.6897V11.5823C12.669 10.7227 13.3484 9.97048 14.1847 9.91675C14.6028 9.91675 15.0209 10.1317 15.2822 10.454V9.37948C14.9686 9.16458 14.5505 9.11085 14.1847 9.11085C12.8258 9.0034 11.7805 10.1317 11.7805 11.5286ZM10.1603 10.9376C9.63762 10.7227 9.48082 10.6152 9.48082 10.3466C9.53309 10.0242 9.79441 9.75557 10.108 9.8093C10.3693 9.8093 10.6306 9.97048 10.8397 10.1854L11.3101 9.54066C10.9442 9.2183 10.4739 9.00339 10.0035 9.00339C9.27176 8.94967 8.64459 9.54066 8.59232 10.2928V10.3466C8.59232 10.9913 8.85365 11.3674 9.68988 11.636C9.89894 11.6897 10.108 11.7972 10.3171 11.9046C10.4739 12.0121 10.5784 12.1733 10.5784 12.3882C10.5784 12.7643 10.2648 13.0866 9.95121 13.0866H9.89894C9.48082 13.0866 9.11497 12.818 8.95818 12.4419L8.38326 13.0329C8.69685 13.6239 9.32403 13.9463 9.95121 13.9463C10.7874 14 11.4669 13.3553 11.5191 12.4956V12.3345C11.4669 11.6897 11.2056 11.3674 10.1603 10.9376ZM7.12892 13.8925H8.01742V9.11084H7.12892V13.8925ZM3 9.11086H4.30662H4.56794C5.8223 9.16458 6.81532 10.2391 6.76306 11.5286C6.76306 12.227 6.44947 12.8717 5.92682 13.3553C5.45644 13.7314 4.88153 13.9463 4.30662 13.8926H3V9.11086ZM4.14983 13.0866C4.56794 13.1404 5.03833 12.9792 5.35191 12.7105C5.6655 12.3882 5.8223 11.9584 5.8223 11.4748C5.8223 11.045 5.6655 10.6152 5.35191 10.2928C5.03833 10.0242 4.56794 9.86302 4.14983 9.91674H3.8885V13.0866H4.14983Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.9481 9C16.6415 9 15.5439 10.0745 15.5439 11.4714C15.5439 12.8146 16.5892 13.9429 17.9481 13.9966C19.307 14.0503 20.3523 12.9221 20.4046 11.5252C20.3523 10.1283 19.307 9 17.9481 9V9Z"
|
||||
fill="#FD6020"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscoverIcon;
|
||||
8
src/components/foundations/payment-icons/index.tsx
Normal file
8
src/components/foundations/payment-icons/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export { default as AmexIcon } from "./amex-icon";
|
||||
export { default as ApplePayIcon } from "./apple-pay-icon";
|
||||
export { default as DiscoverIcon } from "./discover-icon";
|
||||
export { default as MastercardIcon } from "./mastercard-icon";
|
||||
export { default as VisaIcon } from "./visa-icon";
|
||||
export { default as PayPalIcon } from "./paypal-icon";
|
||||
export { default as StripeIcon } from "./stripe-icon";
|
||||
export { default as UnionPayIcon } from "./union-pay-icon";
|
||||
37
src/components/foundations/payment-icons/mastercard-icon.tsx
Normal file
37
src/components/foundations/payment-icons/mastercard-icon.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const MastercardIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
className="stroke-border-secondary"
|
||||
strokeWidth="0.75"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.179 16.8294C15.9949 17.8275 14.459 18.43 12.7807 18.43C9.03582 18.43 6 15.4303 6 11.73C6 8.02966 9.03582 5.02997 12.7807 5.02997C14.459 5.02997 15.9949 5.63247 17.179 6.63051C18.363 5.63247 19.8989 5.02997 21.5773 5.02997C25.3221 5.02997 28.358 8.02966 28.358 11.73C28.358 15.4303 25.3221 18.43 21.5773 18.43C19.8989 18.43 18.363 17.8275 17.179 16.8294Z"
|
||||
fill="#ED0006"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.1787 16.8294C18.6366 15.6005 19.5611 13.7719 19.5611 11.73C19.5611 9.68801 18.6366 7.85941 17.1787 6.63051C18.3628 5.63247 19.8987 5.02997 21.577 5.02997C25.3219 5.02997 28.3577 8.02966 28.3577 11.73C28.3577 15.4303 25.3219 18.43 21.577 18.43C19.8987 18.43 18.3628 17.8275 17.1787 16.8294Z"
|
||||
fill="#F9A000"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.1793 16.8294C18.6372 15.6005 19.5616 13.7719 19.5616 11.73C19.5616 9.68805 18.6372 7.85946 17.1793 6.63055C15.7213 7.85946 14.7969 9.68805 14.7969 11.73C14.7969 13.7719 15.7213 15.6005 17.1793 16.8294Z"
|
||||
fill="#FF5E00"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default MastercardIcon;
|
||||
43
src/components/foundations/payment-icons/paypal-icon.tsx
Normal file
43
src/components/foundations/payment-icons/paypal-icon.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const PayPalIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
className="stroke-border-secondary"
|
||||
strokeWidth="0.75"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.614 18.4483L14.8347 16.9992L14.3431 16.9873H11.9951L13.6268 6.2937C13.6319 6.26132 13.6484 6.23126 13.6724 6.20987C13.6965 6.18849 13.7272 6.17676 13.7594 6.17676H17.7184C19.0328 6.17676 19.9398 6.45939 20.4133 7.01734C20.6353 7.27908 20.7767 7.55267 20.8452 7.85364C20.9169 8.16951 20.9181 8.54685 20.8481 9.00715L20.843 9.04063V9.33561L21.0651 9.46563C21.252 9.56815 21.4006 9.68546 21.5145 9.81975C21.7044 10.0436 21.8272 10.3281 21.8791 10.6652C21.9328 11.012 21.9151 11.4248 21.8272 11.892C21.7259 12.4295 21.5622 12.8976 21.341 13.2805C21.1376 13.6334 20.8785 13.9262 20.5708 14.153C20.277 14.3686 19.928 14.5322 19.5333 14.6369C19.1509 14.7398 18.7149 14.7917 18.2367 14.7917H17.9286C17.7083 14.7917 17.4943 14.8737 17.3263 15.0207C17.1579 15.1708 17.0465 15.3758 17.0123 15.6L16.989 15.7305L16.599 18.2848L16.5814 18.3785C16.5767 18.4082 16.5686 18.423 16.5568 18.433C16.5463 18.4422 16.5311 18.4483 16.5164 18.4483H14.614Z"
|
||||
fill="#28356A"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21.2761 9.07458C21.2644 9.15267 21.2508 9.23246 21.2356 9.31445C20.7136 12.0851 18.9273 13.0422 16.646 13.0422H15.4845C15.2055 13.0422 14.9703 13.2516 14.9269 13.536L14.1638 18.5393C14.1356 18.7261 14.2748 18.8944 14.4571 18.8944H16.5173C16.7612 18.8944 16.9684 18.7112 17.0069 18.4626L17.0271 18.3544L17.415 15.8102L17.4399 15.6707C17.4779 15.4211 17.6856 15.2378 17.9295 15.2378H18.2376C20.2336 15.2378 21.7961 14.4003 22.2528 11.9765C22.4435 10.964 22.3448 10.1185 21.84 9.52389C21.6873 9.34464 21.4977 9.1958 21.2761 9.07458Z"
|
||||
fill="#298FC2"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M20.7298 8.84956C20.65 8.82549 20.5677 8.80374 20.4833 8.78407C20.3984 8.76488 20.3115 8.7479 20.222 8.73299C19.9089 8.68069 19.5656 8.65588 19.1981 8.65588H16.0951C16.0186 8.65588 15.946 8.67372 15.8811 8.70598C15.7379 8.7771 15.6316 8.91714 15.6058 9.08857L14.9457 13.4101L14.9268 13.5361C14.9701 13.2516 15.2053 13.0423 15.4843 13.0423H16.6459C18.9271 13.0423 20.7134 12.0847 21.2354 9.31451C21.2511 9.23252 21.2642 9.15273 21.2759 9.07464C21.1438 9.00218 21.0008 8.94023 20.8467 8.88744C20.8087 8.87437 20.7694 8.86178 20.7298 8.84956Z"
|
||||
fill="#22284F"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.6056 9.08862C15.6314 8.91718 15.7377 8.77715 15.8809 8.70652C15.9462 8.67414 16.0184 8.6563 16.0948 8.6563H19.1979C19.5654 8.6563 19.9086 8.68123 20.2218 8.73353C20.3113 8.74831 20.3982 8.76542 20.4831 8.7846C20.5675 8.80415 20.6498 8.82603 20.7296 8.84998C20.7692 8.8622 20.8085 8.8749 20.8469 8.88749C21.0009 8.94028 21.1441 9.00272 21.2761 9.07469C21.4315 8.05082 21.2748 7.3537 20.7393 6.72245C20.1488 6.0274 19.0831 5.72998 17.7194 5.72998H13.7603C13.4817 5.72998 13.2441 5.9393 13.2011 6.22426L11.5521 17.0279C11.5196 17.2416 11.679 17.4344 11.8876 17.4344H14.3318L15.6056 9.08862Z"
|
||||
fill="#28356A"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayPalIcon;
|
||||
25
src/components/foundations/payment-icons/stripe-icon.tsx
Normal file
25
src/components/foundations/payment-icons/stripe-icon.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const StripeIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
className="stroke-border-secondary"
|
||||
strokeWidth="0.75"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18.2684 8.14192L16.5413 8.52349V7.08202L18.2684 6.70752V8.14192ZM21.8602 8.94038C21.1858 8.94038 20.7523 9.26542 20.5115 9.49153L20.422 9.05344H18.9082V17.2924L20.6285 16.9179L20.6354 14.9183C20.8831 15.102 21.2478 15.3634 21.8533 15.3634C23.085 15.3634 24.2066 14.3459 24.2066 12.106C24.1997 10.0568 23.0643 8.94038 21.8602 8.94038ZM21.4473 13.8089C21.0413 13.8089 20.8005 13.6605 20.6353 13.4768L20.6285 10.8553C20.8074 10.6504 21.0551 10.509 21.4473 10.509C22.0735 10.509 22.507 11.2298 22.507 12.1554C22.507 13.1023 22.0803 13.8089 21.4473 13.8089ZM29.6289 12.1766C29.6289 10.3677 28.7756 8.94038 27.1448 8.94038C25.5072 8.94038 24.5163 10.3677 24.5163 12.1625C24.5163 14.2894 25.6861 15.3634 27.365 15.3634C28.1839 15.3634 28.8031 15.1726 29.271 14.9041V13.4909C28.8031 13.7312 28.2664 13.8795 27.5852 13.8795C26.9178 13.8795 26.326 13.6393 26.2503 12.8055H29.6151C29.6151 12.7666 29.6176 12.6782 29.6204 12.5763L29.6204 12.5761C29.6243 12.4377 29.6289 12.2743 29.6289 12.1766ZM26.2296 11.5054C26.2296 10.7069 26.7044 10.3748 27.1379 10.3748C27.5576 10.3748 28.0049 10.7069 28.0049 11.5054H26.2296ZM16.5412 9.06052H18.2683V15.2433H16.5412V9.06052ZM14.5803 9.06051L14.6904 9.5834C15.0963 8.82026 15.9014 8.97572 16.1216 9.06051V10.6857C15.9083 10.608 15.2202 10.509 14.8142 11.0531V15.2433H13.094V9.06051H14.5803ZM11.2498 7.52717L9.57081 7.8946L9.56392 13.5545C9.56392 14.6003 10.3277 15.3705 11.3461 15.3705C11.9103 15.3705 12.3232 15.2645 12.5503 15.1373V13.7029C12.3301 13.7947 11.2429 14.1198 11.2429 13.074V10.5656H12.5503V9.0605H11.2429L11.2498 7.52717ZM7.1832 10.4737C6.8185 10.4737 6.59831 10.5797 6.59831 10.8553C6.59831 11.1562 6.97726 11.2885 7.4474 11.4527C8.21383 11.7204 9.22258 12.0728 9.22685 13.3779C9.22685 14.6427 8.24287 15.3705 6.81162 15.3705C6.21986 15.3705 5.57304 15.2504 4.93311 14.9677V13.286C5.51112 13.611 6.2405 13.8513 6.81162 13.8513C7.19696 13.8513 7.4722 13.7453 7.4722 13.4203C7.4722 13.087 7.0614 12.9346 6.56547 12.7507C5.81018 12.4706 4.85742 12.1173 4.85742 10.9401C4.85742 9.6894 5.78636 8.9404 7.1832 8.9404C7.75432 8.9404 8.31856 9.03225 8.88968 9.26543V10.926C8.36673 10.6362 7.70615 10.4737 7.1832 10.4737Z"
|
||||
fill="#6461FC"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default StripeIcon;
|
||||
35
src/components/foundations/payment-icons/union-pay-icon.tsx
Normal file
35
src/components/foundations/payment-icons/union-pay-icon.tsx
Normal file
File diff suppressed because one or more lines are too long
25
src/components/foundations/payment-icons/visa-icon.tsx
Normal file
25
src/components/foundations/payment-icons/visa-icon.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
const VisaIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg width="34" height="24" viewBox="0 0 34 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M0.5 4C0.5 2.067 2.067 0.5 4 0.5H30C31.933 0.5 33.5 2.067 33.5 4V20C33.5 21.933 31.933 23.5 30 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4Z"
|
||||
className="stroke-border-secondary"
|
||||
strokeWidth="0.75"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.7501 15.8582H8.69031L7.14576 9.79235C7.07245 9.51332 6.91679 9.26664 6.68782 9.15038C6.11639 8.85821 5.48672 8.62568 4.7998 8.50841V8.27487H8.11789C8.57583 8.27487 8.91929 8.62568 8.97653 9.0331L9.77793 13.4086L11.8367 8.27487H13.8392L10.7501 15.8582ZM14.984 15.8582H13.0388L14.6406 8.27487H16.5858L14.984 15.8582ZM19.1025 10.3757C19.1597 9.96725 19.5032 9.73372 19.9039 9.73372C20.5336 9.67508 21.2195 9.79235 21.7919 10.0835L22.1354 8.45079C21.5629 8.21725 20.9333 8.09998 20.3619 8.09998C18.4738 8.09998 17.1 9.15038 17.1 10.6082C17.1 11.7173 18.0731 12.2996 18.7601 12.6504C19.5032 13.0002 19.7894 13.2337 19.7322 13.5835C19.7322 14.1082 19.1597 14.3418 18.5883 14.3418C17.9014 14.3418 17.2145 14.1669 16.5858 13.8747L16.2424 15.5084C16.9293 15.7996 17.6724 15.9169 18.3594 15.9169C20.4763 15.9745 21.7919 14.9251 21.7919 13.35C21.7919 11.3664 19.1025 11.2502 19.1025 10.3757ZM28.5998 15.8582L27.0553 8.27487H25.3962C25.0528 8.27487 24.7093 8.50841 24.5948 8.85821L21.7347 15.8582H23.7372L24.1369 14.7502H26.5973L26.8263 15.8582H28.5998ZM25.6824 10.3171L26.2539 13.1751H24.6521L25.6824 10.3171Z"
|
||||
fill="#172B85"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisaIcon;
|
||||
142
src/components/foundations/rating-badge.tsx
Normal file
142
src/components/foundations/rating-badge.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { type HTMLAttributes } from "react";
|
||||
import { RatingStars } from "@/components/foundations/rating-stars";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export const Wreath = (props: HTMLAttributes<HTMLOrSVGElement>) => (
|
||||
<svg width="36" height="81" viewBox="0 0 36 81" fill="none" {...props} className={cx("text-fg-primary", props.className)}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M34.188 79.123C21.8844 77.4193 12.9273 67.7396 8.84084 54.5087C7.16207 49.0327 6.91909 42.9593 7.50445 36.6094C8.58681 25.2702 13.7888 15.4245 21.3764 8.24482C21.4095 8.21163 21.4206 8.20057 21.4316 8.23376C21.4537 8.26695 21.52 8.30013 21.5531 8.32226C21.5973 8.34439 21.6083 8.32226 21.5752 8.37757C13.5237 15.7563 8.35488 27.4938 7.79161 39.3529C6.91909 56.2898 15.1362 71.2907 27.4509 76.4569C29.5162 77.3308 31.6809 77.9946 33.934 78.3375C34.0886 78.5367 34.177 78.8132 34.188 79.123Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.51737 50.8359C4.52243 52.0306 6.12388 53.2033 7.84683 52.7497C7.93518 52.8825 8.38801 53.5462 8.45428 53.6458C8.53159 53.7675 8.67517 53.8892 8.78561 53.7896C8.85188 53.7453 8.9071 53.6458 8.84084 53.4909C8.58681 53.1812 8.34383 52.8603 8.05667 52.5285C7.54863 49.8402 6.80864 48.0481 5.26241 46.2338C3.4732 44.1541 1.6398 43.2248 0.502216 42.8376C0.336548 42.7934 0.181926 42.7491 0.0162576 42.6938C-0.0941877 43.7337 0.380727 45.2493 0.756241 46.1121C1.48518 47.8822 2.28039 49.4309 3.51737 50.8359Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18.317 12.9686C18.1735 13.0239 17.3341 13.2451 17.2347 13.0792C17.2015 13.0349 17.2457 12.869 17.323 12.8579C17.6323 12.8247 17.9747 12.8026 18.2729 12.7362C18.814 11.0326 20.3161 10.2914 21.6967 10.0038C23.3533 9.6719 25.1757 10.1808 27.1306 10.4573C27.23 10.4795 27.3404 10.4905 27.4288 10.4905C27.3956 10.5569 27.3515 10.6012 27.3073 10.6675C26.49 11.7849 25.2088 12.8137 23.9608 13.3226C23.1214 13.6766 22.1164 13.8757 21.1334 13.8425C20.0952 13.7983 19.2227 13.5659 18.317 12.9686Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21.5531 8.3112C21.6194 8.33332 21.6194 8.3112 21.7408 8.24482C21.9728 8.02357 22.1716 7.89082 22.4035 7.68063C23.7289 7.69169 24.9879 7.29344 26.1807 6.49693C26.7109 6.14293 27.241 5.71148 27.6828 5.19154C28.7431 4.05209 29.5935 2.38164 29.9801 0.854996C30.0022 0.766495 30.0132 0.711182 30.0242 0.622681C29.9248 0.666931 29.8365 0.711183 29.7371 0.755433C27.5945 1.61832 25.5181 2.01657 23.7951 3.68703C22.6244 4.80435 22.017 6.15399 22.2599 7.45938C21.9949 7.66957 21.7188 7.86869 21.4647 8.11207C21.3543 8.21163 21.4868 8.3112 21.5531 8.3112Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.7557 20.1039C13.6121 20.1814 12.8058 20.6018 12.6843 20.4579C12.6291 20.4026 12.6512 20.2256 12.7285 20.1924C13.0267 20.0818 13.3801 20.0044 13.6783 19.8716C13.7888 18.6658 14.4625 17.5374 15.7989 16.763C17.8421 15.6789 20.1615 15.8559 22.3483 15.6678C22.4587 15.6568 22.5581 15.6568 22.6575 15.6457C22.6244 15.7121 22.5913 15.7674 22.5692 15.8338C21.8402 17.0949 20.6585 18.4556 19.4546 19.23C18.6484 19.7721 17.6544 20.1814 16.6604 20.3805C15.6111 20.5907 14.7165 20.469 13.7557 20.1039Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.0535 38.7113C3.71618 40.2047 4.90898 41.842 6.65402 41.9526C6.70924 42.1075 7.02953 42.8266 7.07371 42.9372C7.10685 43.0921 7.21729 43.258 7.36087 43.1916C7.42714 43.1695 7.4934 43.081 7.48236 42.9261C7.31669 42.5279 7.10685 42.2181 6.90804 41.8088C7.06267 39.0653 6.90804 37.0629 5.85881 34.8062C4.666 32.2064 3.03141 30.713 2.02636 29.9829C1.90487 29.8722 1.76129 29.7837 1.61771 29.6952C1.38578 30.7572 1.46309 32.4056 1.60667 33.357C1.86069 35.2818 2.26934 36.9744 3.0535 38.7113Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.00838 27.6929C5.29554 29.2859 6.01344 30.89 7.60385 31.5869C7.62594 31.7529 7.80265 32.5273 7.82474 32.6489C7.82474 32.8149 7.87996 33.0029 8.03458 32.9808C8.07876 32.9698 8.18921 32.9255 8.20025 32.7596C8.13399 32.3392 8.0125 31.8635 7.93519 31.4099C8.46532 29.474 8.75248 27.704 8.66412 25.8123C8.52054 23.1462 7.52654 20.8341 5.95821 18.9756C5.83672 18.8428 5.74837 18.699 5.63792 18.5773C5.19614 19.4955 4.90898 21.0664 4.83167 22.0067C4.666 23.9095 4.67705 25.8233 5.00838 27.6929Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.01755 17.637C8.97337 19.2742 9.37097 21.1106 10.7405 22.1727C10.7295 22.3386 10.6963 23.1351 10.6963 23.2457C10.6742 23.4006 10.6853 23.5887 10.8399 23.6219C10.8951 23.6219 10.9945 23.6108 11.0277 23.4559C11.0608 23.0466 11.0718 22.416 11.0939 21.9625C12.2646 19.9712 12.8942 17.8029 12.9163 15.3249C12.9604 12.4597 12.2536 10.6343 11.6903 9.48383C11.613 9.32896 11.5357 9.17408 11.4584 9.01921C10.8399 9.77146 10.2545 11.2096 9.98947 12.0946C9.45933 13.8868 9.07277 15.7563 9.01755 17.637Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.092 8.45501C14.7386 9.99271 14.838 12.2495 15.9867 13.1787C15.9425 13.3779 15.8431 14.0638 15.832 14.2186C15.7878 14.3846 15.7547 14.5394 15.8872 14.6058C15.9425 14.6279 15.9866 14.628 16.0529 14.4841C16.1302 14.0527 16.1523 13.6987 16.2186 13.2562C17.8532 11.5083 18.7809 9.78252 19.3 7.42619C19.8191 4.95923 19.7418 3.00114 19.5209 1.61832C19.4988 1.45238 19.4546 1.28644 19.4105 1.10944C17.9747 2.25995 16.9586 3.77553 16.0971 5.50129C15.6332 6.44162 15.2688 7.40406 15.092 8.45501Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.0889 29.5182C9.95633 29.6399 9.22739 30.3369 9.05068 30.2484C9.0065 30.2041 8.97337 29.9718 9.05068 29.9275C9.32679 29.7063 9.68022 29.5404 9.95633 29.3302C9.86798 28.0026 10.1662 26.653 11.37 25.3919C13.2255 23.5887 15.6884 22.9802 17.9305 22.051C18.0299 22.0067 18.1293 21.9514 18.2508 21.9071C18.2287 21.9846 18.2176 22.0731 18.1845 22.1505C17.7427 23.6993 16.8592 25.5025 15.7878 26.7194C15.092 27.538 14.1533 28.3235 13.1703 28.8434C12.1432 29.3965 11.1712 29.5735 10.0889 29.5182Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.54263 40.3264C8.44323 40.5034 7.85787 41.4548 7.67012 41.4105C7.5928 41.4105 7.51549 41.1782 7.57072 41.0897C7.81369 40.7689 8.12294 40.5145 8.33279 40.1826C7.95727 38.9657 8.13399 37.2067 9.03964 35.5252C10.4754 33.0693 12.5739 31.8746 14.6061 30.1599C14.6944 30.0714 14.7828 29.9939 14.8822 29.8944C14.8932 29.9718 14.9043 30.0603 14.8932 30.1599C14.8491 31.8414 14.4073 33.9543 13.6231 35.5474C13.1151 36.6204 12.364 37.6935 11.5026 38.5896C10.608 39.4856 9.65813 40.0056 8.54263 40.3264Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.55873 50.9244C9.48142 51.1346 9.08382 52.263 8.88501 52.3072C8.81875 52.2961 8.68621 52.1302 8.74144 52.0085C8.90711 51.6324 9.09486 51.2009 9.24948 50.8138C8.56472 50.1611 8.33279 48.3468 8.88501 46.444C9.80171 43.6562 11.8449 41.5654 13.4906 39.2533C13.5679 39.1427 13.6452 39.0321 13.7225 38.9214C13.7446 38.9989 13.7557 39.0874 13.7777 39.1759C14.0649 40.8021 14.0428 42.9593 13.601 44.7183C13.3028 45.8909 12.7837 47.1852 12.11 48.2915C11.4142 49.4641 10.5859 50.2717 9.55873 50.9244Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.0958 63.3256C8.34383 64.1442 10.5748 64.7637 12.11 63.757C12.2315 63.8455 12.8721 64.2327 12.9494 64.2991C13.0598 64.4097 13.2145 64.4871 13.3139 64.3323C13.3691 64.2659 13.4022 64.1553 13.2918 64.0336C12.9715 63.8123 12.6181 63.6685 12.2757 63.4473C11.105 61.0135 9.84589 59.3652 7.9131 58.1483C5.70419 56.7323 3.85975 56.7101 2.67799 56.7101C2.51232 56.7101 2.34665 56.7212 2.18098 56.7212C2.46814 59.1218 5.43912 62.2968 7.0958 63.3256Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.2255 61.4449C13.2145 61.5998 13.1372 62.2857 13.0267 62.7061C12.9936 62.8277 12.9825 63.0048 12.8169 62.9384C12.7506 62.9273 12.6843 62.7835 12.6843 62.7171C12.7616 62.3078 12.8279 61.8432 12.861 61.4339C12.099 60.9471 11.5026 59.3541 11.3811 57.396C11.5136 54.4423 12.839 51.7651 13.8992 48.9774C13.9324 48.8557 13.9765 48.7229 14.0207 48.5902C14.0649 48.6565 14.087 48.7229 14.1312 48.7893C14.838 50.2496 15.3792 52.2962 15.4234 54.1436C15.4676 55.3494 15.2908 56.7544 14.9374 58.0598C14.5619 59.3983 14.0097 60.4493 13.2255 61.4449Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.8551 73.4257C15.2908 73.7134 17.2347 73.647 18.4496 72.0982C18.7588 72.1425 19.2558 72.2199 19.5761 72.242C19.6645 72.2531 19.7307 72.1867 19.7639 72.0761C19.786 71.9987 19.7639 71.8991 19.6203 71.8217C19.2558 71.7442 18.8914 71.7442 18.5048 71.6778C16.8923 69.7861 15.3682 68.7241 13.1703 68.3259C10.6301 67.8612 8.47637 68.5139 7.32774 68.9896C7.17311 69.0449 7.00744 69.1113 6.86387 69.1888C7.27251 70.1069 8.46532 71.0473 9.21635 71.5008C10.6521 72.4522 12.1211 73.1381 13.8551 73.4257Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18.9908 69.4321C19.0128 69.5981 19.0349 70.2618 19.0349 70.7043C19.0239 70.826 19.057 70.992 18.8914 71.003C18.814 71.003 18.7257 70.9035 18.7257 70.826C18.6926 70.4167 18.7146 69.9742 18.6484 69.5538C17.798 69.2883 16.6714 67.994 16.1523 66.2129C15.5559 63.3588 16.2075 60.4161 16.5168 57.396C16.5389 57.2633 16.5278 57.1305 16.5389 56.9867C16.5941 57.042 16.6604 57.0973 16.7156 57.1526C17.7538 58.2921 18.792 60.051 19.289 61.7768C19.6313 62.9052 19.8081 64.2991 19.8081 65.6376C19.786 67.0426 19.4988 68.2263 18.9908 69.4321Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M27.0312 75.24C27.1085 75.3617 27.2079 75.9148 27.3183 76.3352C27.3515 76.4348 27.4177 76.5896 27.2631 76.645C27.1858 76.6781 27.0864 76.6118 27.0643 76.5454C26.9207 76.1914 26.9207 75.8153 26.744 75.4613C25.9157 75.6604 24.4799 74.6094 23.5411 73.2377C22.2379 70.8703 22.0943 68.0936 21.5862 65.2726C21.5752 65.1398 21.531 65.0182 21.5089 64.8854C21.5752 64.8965 21.6414 64.9407 21.7077 64.9628C22.9557 65.5934 24.3805 66.7107 25.2972 68.0825C25.9267 68.9675 26.5121 70.1955 26.8434 71.4234C27.1858 72.7177 27.1968 73.9457 27.0312 75.24Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M22.514 80.5058C23.9829 80.3067 25.8825 79.6319 26.7219 77.7512C27.0201 77.6627 27.5392 77.6074 27.8485 77.5189C27.9368 77.5078 27.981 77.4083 27.9921 77.2866C27.9921 77.2202 27.9479 77.1207 27.7933 77.0985C27.4288 77.1538 27.0643 77.2645 26.6557 77.3087C24.5241 76.0476 22.8011 75.4723 20.5591 75.7931C17.9857 76.1582 16.1081 77.4525 15.1031 78.2822C14.9595 78.4039 14.8159 78.5145 14.6834 78.6362C15.3019 79.3996 16.6935 79.9084 17.5218 80.1297C19.1454 80.5833 20.7358 80.7603 22.514 80.5058Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface RatingBadgeProps extends HTMLAttributes<HTMLDivElement> {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
rating?: number;
|
||||
theme?: "light" | "dark";
|
||||
}
|
||||
|
||||
export const RatingBadge = ({ title = "Best Design Tool", subtitle = "2,000+ reviews", rating, theme = "dark", className, ...props }: RatingBadgeProps) => {
|
||||
return (
|
||||
<div {...props} className={cx("flex items-center -space-x-0.5", className)}>
|
||||
<Wreath className={cx("shrink-0", theme === "light" && "text-fg-white")} />
|
||||
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<RatingStars rating={rating} className="gap-0.5" starClassName="size-4" />
|
||||
|
||||
<div className="text-center">
|
||||
<p className={cx("text-sm font-semibold", theme === "light" ? "text-primary_on-brand" : "text-primary")}>{title}</p>
|
||||
<p className={cx("text-xs font-medium", theme === "light" ? "text-secondary_on-brand" : "text-secondary")}>{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Wreath className={cx("shrink-0 -scale-x-100", theme === "light" && "text-fg-white")} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
75
src/components/foundations/rating-stars.tsx
Normal file
75
src/components/foundations/rating-stars.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { HTMLAttributes, SVGProps } from "react";
|
||||
import { useId } from "react";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export const getStarProgress = (starPosition: number, rating: number, maxRating: number = 5) => {
|
||||
// Ensure rating is between 0 and 5
|
||||
const clampedRating = Math.min(Math.max(rating, 0), maxRating);
|
||||
|
||||
const diff = clampedRating - starPosition;
|
||||
|
||||
if (diff >= 1) return 100;
|
||||
if (diff <= 0) return 0;
|
||||
|
||||
return Math.round(diff * 100);
|
||||
};
|
||||
|
||||
interface StarIconProps extends SVGProps<SVGSVGElement> {
|
||||
/**
|
||||
* The progress of the star icon. It should be a number between 0 and 100.
|
||||
*
|
||||
* @default 100
|
||||
*/
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
export const StarIcon = ({ progress = 100, ...props }: StarIconProps) => {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" {...props} className={cx("size-5 text-warning-400", props.className)}>
|
||||
<path
|
||||
d="M9.53834 1.60996C9.70914 1.19932 10.2909 1.19932 10.4617 1.60996L12.5278 6.57744C12.5998 6.75056 12.7626 6.86885 12.9495 6.88383L18.3123 7.31376C18.7556 7.3493 18.9354 7.90256 18.5976 8.19189L14.5117 11.6919C14.3693 11.8139 14.3071 12.0053 14.3506 12.1876L15.5989 17.4208C15.7021 17.8534 15.2315 18.1954 14.8519 17.9635L10.2606 15.1592C10.1006 15.0615 9.89938 15.0615 9.73937 15.1592L5.14806 17.9635C4.76851 18.1954 4.29788 17.8534 4.40108 17.4208L5.64939 12.1876C5.69289 12.0053 5.6307 11.8139 5.48831 11.6919L1.40241 8.19189C1.06464 7.90256 1.24441 7.3493 1.68773 7.31376L7.05054 6.88383C7.23744 6.86885 7.40024 6.75056 7.47225 6.57744L9.53834 1.60996Z"
|
||||
className="fill-bg-tertiary"
|
||||
/>
|
||||
<g clipPath={`url(#clip-${id})`}>
|
||||
<path
|
||||
d="M9.53834 1.60996C9.70914 1.19932 10.2909 1.19932 10.4617 1.60996L12.5278 6.57744C12.5998 6.75056 12.7626 6.86885 12.9495 6.88383L18.3123 7.31376C18.7556 7.3493 18.9354 7.90256 18.5976 8.19189L14.5117 11.6919C14.3693 11.8139 14.3071 12.0053 14.3506 12.1876L15.5989 17.4208C15.7021 17.8534 15.2315 18.1954 14.8519 17.9635L10.2606 15.1592C10.1006 15.0615 9.89938 15.0615 9.73937 15.1592L5.14806 17.9635C4.76851 18.1954 4.29788 17.8534 4.40108 17.4208L5.64939 12.1876C5.69289 12.0053 5.6307 11.8139 5.48831 11.6919L1.40241 8.19189C1.06464 7.90256 1.24441 7.3493 1.68773 7.31376L7.05054 6.88383C7.23744 6.86885 7.40024 6.75056 7.47225 6.57744L9.53834 1.60996Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id={`clip-${id}`}>
|
||||
<rect width={`${progress}%`} height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
interface RatingStarsProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* The rating to display.
|
||||
*
|
||||
* @default 5
|
||||
*/
|
||||
rating?: number;
|
||||
/**
|
||||
* The number of stars to display.
|
||||
*/
|
||||
stars?: number;
|
||||
/**
|
||||
* The class name of the star icon.
|
||||
*/
|
||||
starClassName?: string;
|
||||
}
|
||||
|
||||
export const RatingStars = ({ rating = 5, stars = 5, starClassName, ...props }: RatingStarsProps) => {
|
||||
return (
|
||||
<div {...props} className={cx("flex", props.className)}>
|
||||
{Array.from({ length: stars }).map((_, index) => (
|
||||
<StarIcon key={index} progress={getStarProgress(index, rating, stars)} className={starClassName} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
18
src/components/foundations/social-icons/angel-list.tsx
Normal file
18
src/components/foundations/social-icons/angel-list.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
interface Props extends SVGProps<SVGSVGElement> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const AngelList = ({ size = 24, ...props }: Props) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M17.7705 10.0969C18.319 8.56875 19.8986 4.14844 19.8986 2.73281C19.8986 1.48594 19.1627 0.440625 17.8502 0.440625C15.7596 0.440625 13.8846 6.61406 13.2986 8.08594C12.844 6.75 10.7158 0 8.84083 0C7.38301 0 6.69864 1.07344 6.69864 2.42344C6.69864 4.07812 8.30176 8.36719 8.88301 10.0172C8.5877 9.90937 8.26895 9.81562 7.94551 9.81562C6.84864 9.81562 5.68145 11.1797 5.68145 12.2812C5.68145 12.6984 5.91114 13.2844 6.05645 13.6734C4.32676 14.1422 3.66114 15.2953 3.66114 17.0344C3.65645 20.4187 6.8627 24 11.3721 24C16.9033 24 20.344 19.8469 20.344 14.4891C20.344 12.4688 20.0205 10.6453 17.7705 10.0969V10.0969ZM16.1111 5.0625C16.2986 4.48594 17.1002 2.04844 17.8502 2.04844C18.2533 2.04844 18.3611 2.46563 18.3611 2.79844C18.3611 3.69375 16.5518 8.63906 16.1533 9.73594L14.5596 9.45469L16.1111 5.0625V5.0625ZM8.17051 2.26406C8.17051 1.70625 8.8502 0.121875 10.3408 4.47188L11.9627 9.17344C11.2315 9.1125 10.6643 9.03281 10.3033 9.23906C9.79239 7.88906 8.17051 3.62812 8.17051 2.26406V2.26406ZM8.0627 11.4375C9.43614 11.4375 11.208 15.8719 11.208 16.4719C11.208 16.7109 10.9783 17.0062 10.7111 17.0062C9.73145 17.0062 7.10645 13.4016 7.10645 12.4266C7.11114 12.0656 7.70176 11.4375 8.0627 11.4375V11.4375ZM16.7018 20.1703C15.3377 21.6703 13.594 22.4484 11.5596 22.4484C8.7752 22.4484 6.57676 20.9203 5.51739 18.3094C4.71583 16.275 5.69551 15.1078 6.48301 15.1078C7.01739 15.1078 9.02833 17.9344 9.02833 18.5344C9.02833 18.7641 8.66739 18.9234 8.47989 18.9234C7.7252 18.9234 7.42989 18.1969 6.08458 16.5141C4.69239 17.9062 7.04551 20.5875 8.81739 20.5875C10.0408 20.5875 10.8377 19.4531 10.5986 18.6188C10.7721 18.6188 10.9877 18.6328 11.1471 18.5906C11.1986 19.8609 11.5736 21.375 13.1018 21.4828C13.1018 21.4406 13.1955 21.15 13.1955 21.1359C13.1955 20.3203 12.6986 19.6078 12.6986 18.7781C12.6986 17.4516 13.7158 16.1672 14.7471 15.4172C15.1221 15.1359 15.5768 14.9625 16.0174 14.8031C16.4721 14.6297 16.9549 14.4281 17.3018 14.0812C17.2502 13.5562 17.0346 13.0922 16.5096 13.0922C15.2111 13.0922 10.8565 13.2797 10.8565 11.2313C10.8565 10.9172 10.8611 10.6172 11.6721 10.6172C13.1861 10.6172 17.0299 10.9922 18.1549 11.9813C19.0033 12.7359 19.294 17.2875 16.7018 20.1703V20.1703ZM12.0799 14.2641C12.5346 14.4094 13.0033 14.4516 13.4721 14.5453C13.1252 14.7984 12.8158 15.1078 12.5205 15.4406C12.3893 15.0422 12.2299 14.6531 12.0799 14.2641V14.2641Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default AngelList;
|
||||
18
src/components/foundations/social-icons/apple.tsx
Normal file
18
src/components/foundations/social-icons/apple.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
interface Props extends SVGProps<SVGSVGElement> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const Apple = ({ size = 24, ...props }: Props) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M20.8428 17.1449C20.5101 17.9135 20.1163 18.6211 19.66 19.2715C19.0381 20.1583 18.5288 20.7721 18.1364 21.113C17.528 21.6724 16.8762 21.959 16.1782 21.9753C15.6771 21.9753 15.0728 21.8327 14.3693 21.5434C13.6636 21.2555 13.015 21.113 12.422 21.113C11.8 21.113 11.133 21.2555 10.4195 21.5434C9.70493 21.8327 9.12928 21.9834 8.68916 21.9984C8.01981 22.0269 7.35264 21.7322 6.68668 21.113C6.26164 20.7422 5.72999 20.1067 5.09309 19.2063C4.40976 18.2449 3.84796 17.13 3.40784 15.8589C2.93648 14.486 2.7002 13.1565 2.7002 11.8694C2.7002 10.3951 3.01878 9.12345 3.65689 8.05784C4.1584 7.20191 4.82557 6.52672 5.66059 6.03105C6.49562 5.53539 7.39786 5.2828 8.36949 5.26664C8.90114 5.26664 9.59833 5.43109 10.4647 5.75429C11.3287 6.07858 11.8834 6.24303 12.1266 6.24303C12.3085 6.24303 12.9247 6.05074 13.9694 5.66738C14.9573 5.31186 15.7911 5.16466 16.4742 5.22264C18.3251 5.37202 19.7157 6.10167 20.6405 7.41619C18.9851 8.4192 18.1662 9.82403 18.1825 11.6262C18.1975 13.03 18.7067 14.1981 19.7075 15.1256C20.1611 15.5561 20.6676 15.8888 21.2312 16.1251C21.109 16.4795 20.98 16.819 20.8428 17.1449V17.1449ZM16.5978 0.440369C16.5978 1.54062 16.1958 2.56792 15.3946 3.51878C14.4277 4.64917 13.2582 5.30236 11.99 5.19929C11.9738 5.06729 11.9645 4.92837 11.9645 4.78239C11.9645 3.72615 12.4243 2.59576 13.2408 1.67152C13.6485 1.20356 14.167 0.814453 14.7957 0.504058C15.4231 0.198295 16.0166 0.0292007 16.5747 0.000244141C16.591 0.147331 16.5978 0.294426 16.5978 0.440355V0.440369Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Apple;
|
||||
22
src/components/foundations/social-icons/clubhouse.tsx
Normal file
22
src/components/foundations/social-icons/clubhouse.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
interface Props extends SVGProps<SVGSVGElement> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const Clubhouse = ({ size = 24, ...props }: Props) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M1.74167 17.875C0.783333 17.875 0 18.6583 0 19.6167C0 20.575 0.783333 21.3583 1.74167 21.3583C2.7 21.3583 3.48333 20.575 3.48333 19.6167C3.48333 18.6583 2.70833 17.875 1.74167 17.875Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M23.1833 2.66663L12.2166 6.16663V2.70829L0.391602 6.48329V16.5083L10.5916 13.25V16.6916L23.9999 12.4166L20.3666 8.86663L23.1833 2.66663ZM10.5916 11.5333L2.02494 14.2666V7.67496L10.5916 4.94163V11.5333ZM20.9666 11.675L12.2166 14.4666V7.88329L20.2749 5.30829L18.4249 9.20829L20.9666 11.675Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Clubhouse;
|
||||
28
src/components/foundations/social-icons/discord.tsx
Normal file
28
src/components/foundations/social-icons/discord.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
interface Props extends SVGProps<SVGSVGElement> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const Discord = ({ size = 24, ...props }: Props) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M7.50197 13.0057C7.50197 14.0114 8.28047 14.8343 9.14548 14.8343C10.0105 14.8343 10.789 14.0114 10.789 13.0057C10.789 12 10.0105 11.1771 9.14548 11.1771C8.28047 11.1771 7.50197 12 7.50197 13.0057Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M13.211 13.0057C13.211 14.0114 13.9895 14.8343 14.8545 14.8343C15.7195 14.8343 16.498 14.0114 16.498 13.0057C16.498 12 15.7195 11.1771 14.8545 11.1771C13.9895 11.1771 13.211 12 13.211 13.0057Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.560538 2.80805C0 3.90817 0 5.3483 0 8.22857V15.7714C0 18.6517 0 20.0918 0.560538 21.192C1.0536 22.1596 1.84036 22.9464 2.80805 23.4395C3.90817 24 5.3483 24 8.22857 24H15.7714C18.6517 24 20.0918 24 21.192 23.4395C22.1596 22.9464 22.9464 22.1596 23.4395 21.192C24 20.0918 24 18.6517 24 15.7714V8.22857C24 5.3483 24 3.90817 23.4395 2.80805C22.9464 1.84036 22.1596 1.0536 21.192 0.560538C20.0918 0 18.6517 0 15.7714 0H8.22857C5.3483 0 3.90817 0 2.80805 0.560538C1.84036 1.0536 1.0536 1.84036 0.560538 2.80805ZM14.595 5.14286C16.2385 5.23429 17.7955 5.78286 19.0931 6.88C20.5636 9.80572 21.3421 13.0971 21.4286 16.48C20.1311 17.9429 18.3145 18.8571 16.4115 18.8571C16.4115 18.8571 15.806 18.1257 15.3735 17.4857C16.498 17.2114 17.536 16.5714 18.228 15.5657C17.6225 15.9314 17.017 16.2971 16.4115 16.5714C15.633 16.9371 14.8545 17.12 14.076 17.3029C13.384 17.3943 12.692 17.4857 12 17.4857C11.308 17.4857 10.616 17.3943 9.92398 17.3029C9.14548 17.12 8.36697 16.9371 7.58847 16.5714C6.98296 16.2971 6.37746 15.9314 5.77195 15.5657C6.46396 16.5714 7.50197 17.2114 8.62647 17.4857C8.19397 18.1257 7.58847 18.8571 7.58847 18.8571C5.68545 18.8571 3.86894 17.9429 2.57143 16.48C2.65793 13.0971 3.43644 9.80572 4.90695 6.88C6.20446 5.78286 7.76147 5.23429 9.40498 5.14286L9.66448 5.41714C8.19397 5.78286 6.89646 6.51429 5.68545 7.52C7.15596 6.69714 8.79948 6.14857 10.5295 5.96571C11.0485 5.87429 11.481 5.87429 12 5.87429C12.519 5.87429 12.9515 5.87429 13.4705 5.96571C15.2005 6.14857 16.844 6.69714 18.3145 7.52C17.1035 6.51429 15.806 5.78286 14.3355 5.41714L14.595 5.14286Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Discord;
|
||||
20
src/components/foundations/social-icons/dribbble.tsx
Normal file
20
src/components/foundations/social-icons/dribbble.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
interface Props extends SVGProps<SVGSVGElement> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const Dribbble = ({ size = 24, ...props }: Props) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 0C5.37527 0 0 5.37527 0 12C0 18.6248 5.37527 24 12 24C18.6117 24 24 18.6248 24 12C24 5.37527 18.6117 0 12 0ZM19.9262 5.53145C21.3579 7.27549 22.217 9.50107 22.243 11.9089C21.9046 11.8438 18.5206 11.154 15.1106 11.5835C15.0325 11.4143 14.9675 11.2321 14.8894 11.0499C14.6811 10.5554 14.4469 10.0477 14.2126 9.56617C17.9869 8.0304 19.705 5.81779 19.9262 5.53145ZM12 1.77007C14.603 1.77007 16.9848 2.74621 18.7939 4.34708C18.6117 4.60738 17.0629 6.67679 13.4186 8.04337C11.7397 4.95878 9.87855 2.43384 9.5922 2.04338C10.3601 1.86117 11.1671 1.77007 12 1.77007ZM7.63995 2.73319C7.91325 3.09761 9.73538 5.63558 11.4404 8.65508C6.65076 9.9306 2.42083 9.90458 1.96529 9.90458C2.62906 6.72885 4.77657 4.08676 7.63995 2.73319ZM1.74404 12.0131C1.74404 11.9089 1.74404 11.8048 1.74404 11.7007C2.18655 11.7136 7.15835 11.7787 12.2733 10.243C12.5727 10.8156 12.846 11.4013 13.1063 11.987C12.9761 12.026 12.8329 12.0651 12.7028 12.1041C7.41865 13.8091 4.60738 18.4685 4.3731 18.859C2.7462 17.0499 1.74404 14.6421 1.74404 12.0131ZM12 22.256C9.6312 22.256 7.44469 21.449 5.71366 20.0954C5.89588 19.718 7.97827 15.7094 13.757 13.692C13.783 13.679 13.7961 13.679 13.8221 13.666C15.2668 17.4013 15.8525 20.5379 16.0087 21.436C14.7722 21.9696 13.4186 22.256 12 22.256ZM17.7137 20.4989C17.6096 19.8742 17.0629 16.8807 15.7223 13.1974C18.9371 12.6898 21.7484 13.5228 22.0998 13.6399C21.6573 16.4902 20.0174 18.9501 17.7137 20.4989Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dribbble;
|
||||
18
src/components/foundations/social-icons/facebook.tsx
Normal file
18
src/components/foundations/social-icons/facebook.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
interface Props extends SVGProps<SVGSVGElement> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const Facebook = ({ size = 24, ...props }: Props) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
d="M24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 17.9895 4.3882 22.954 10.125 23.8542V15.4688H7.07812V12H10.125V9.35625C10.125 6.34875 11.9166 4.6875 14.6576 4.6875C15.9701 4.6875 17.3438 4.92188 17.3438 4.92188V7.875H15.8306C14.34 7.875 13.875 8.80008 13.875 9.75V12H17.2031L16.6711 15.4688H13.875V23.8542C19.6118 22.954 24 17.9895 24 12Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Facebook;
|
||||
20
src/components/foundations/social-icons/figma.tsx
Normal file
20
src/components/foundations/social-icons/figma.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
interface Props extends SVGProps<SVGSVGElement> {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const Figma = ({ size = 24, ...props }: Props) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.25 2C7.51349 2 6.81155 2.28629 6.29747 2.78895C5.78414 3.29087 5.5 3.96677 5.5 4.66667C5.5 5.36657 5.78414 6.04247 6.29747 6.54438C6.81155 7.04705 7.51349 7.33333 8.25 7.33333H11V2H8.25ZM13 2V7.33333H15.75C16.1142 7.33333 16.4744 7.26316 16.8097 7.12736C17.145 6.99157 17.4482 6.79311 17.7025 6.54438C17.9569 6.29571 18.1574 6.00171 18.2938 5.67977C18.4301 5.35788 18.5 5.0137 18.5 4.66667C18.5 4.31964 18.4301 3.97545 18.2938 3.65356C18.1574 3.33162 17.9569 3.03763 17.7025 2.78895C17.4482 2.54022 17.145 2.34177 16.8097 2.20598C16.4744 2.07017 16.1142 2 15.75 2H13ZM18.6884 8.33334C18.8324 8.22191 18.9702 8.10211 19.1008 7.9744C19.5429 7.54211 19.8948 7.02769 20.1353 6.45991C20.3759 5.89208 20.5 5.28266 20.5 4.66667C20.5 4.05067 20.3759 3.44126 20.1353 2.87342C19.8948 2.30564 19.5429 1.79122 19.1008 1.35894C18.6587 0.926696 18.1351 0.584984 17.5605 0.352241C16.9858 0.119512 16.3707 0 15.75 0H8.25C6.99738 0 5.79167 0.486331 4.89923 1.35894C4.00603 2.23228 3.5 3.42165 3.5 4.66667C3.5 5.91169 4.00603 7.10105 4.89923 7.9744C5.03021 8.10247 5.16794 8.22222 5.31158 8.33333C5.16794 8.44445 5.03021 8.5642 4.89923 8.69227C4.00603 9.56562 3.5 10.755 3.5 12C3.5 13.245 4.00603 14.4344 4.89923 15.3077C5.03022 15.4358 5.16795 15.5556 5.31159 15.6667C5.16795 15.7778 5.03022 15.8975 4.89923 16.0256C4.00603 16.899 3.5 18.0883 3.5 19.3333C3.5 20.5784 4.00603 21.7677 4.89923 22.6411C5.79167 23.5137 6.99738 24 8.25 24C9.5026 24 10.7083 23.5137 11.6008 22.6411C12.494 21.7677 13 20.5784 13 19.3333V15.8051C13.2922 16.0089 13.6073 16.1799 13.9395 16.3144C14.5142 16.5472 15.1293 16.6667 15.75 16.6667C16.3707 16.6667 16.9858 16.5472 17.5605 16.3144C18.1351 16.0817 18.6587 15.74 19.1008 15.3077C19.5429 14.8754 19.8948 14.361 20.1353 13.7932C20.3759 13.2254 20.5 12.616 20.5 12C20.5 11.384 20.3759 10.7746 20.1353 10.2068C19.8948 9.63898 19.5429 9.12456 19.1008 8.69227C18.9702 8.56456 18.8324 8.44476 18.6884 8.33334ZM11 14.6667V9.33333H8.25C7.51349 9.33333 6.81155 9.61962 6.29747 10.1223C5.78414 10.6242 5.5 11.3001 5.5 12C5.5 12.6999 5.78414 13.3758 6.29747 13.8777C6.81155 14.3804 7.51349 14.6667 8.25 14.6667H11ZM11 16.6667H8.25C7.51349 16.6667 6.81155 16.953 6.29747 17.4556C5.78414 17.9575 5.5 18.6334 5.5 19.3333C5.5 20.0332 5.78414 20.7091 6.29747 21.2111C6.81155 21.7137 7.51349 22 8.25 22C8.98651 22 9.6884 21.7137 10.2025 21.2111C10.7159 20.7091 11 20.0332 11 19.3333V16.6667ZM15.75 9.33333C15.3858 9.33333 15.0256 9.4035 14.6903 9.53931C14.355 9.6751 14.0518 9.87356 13.7975 10.1223C13.5431 10.371 13.3426 10.665 13.2062 10.9869C13.0699 11.3088 13 11.653 13 12C13 12.347 13.0699 12.6912 13.2062 13.0131C13.3426 13.335 13.5431 13.629 13.7975 13.8777C14.0518 14.1264 14.355 14.3249 14.6903 14.4607C15.0256 14.5965 15.3858 14.6667 15.75 14.6667C16.1142 14.6667 16.4744 14.5965 16.8097 14.4607C17.145 14.3249 17.4482 14.1264 17.7025 13.8777C17.9569 13.629 18.1574 13.335 18.2938 13.0131C18.4301 12.6912 18.5 12.347 18.5 12C18.5 11.653 18.4301 11.3088 18.2938 10.9869C18.1574 10.665 17.9569 10.371 17.7025 10.1223C17.4482 9.87356 17.145 9.6751 16.8097 9.53931C16.4744 9.4035 16.1142 9.33333 15.75 9.33333Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Figma;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user