mirror of
https://gitea.gofwd.group/dstrawsb/ballistic-builder.git
synced 2025-12-06 02:36:44 -05:00
stuff...?
This commit is contained in:
37
app/components/Header.tsx
Normal file
37
app/components/Header.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Box, Flex, Link, Heading } from "@chakra-ui/react";
|
||||||
|
import NextLink from "next/link";
|
||||||
|
|
||||||
|
const Header: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Box as="header" bg="primary" color="white" px="6" py="4" shadow="md">
|
||||||
|
<Flex justify="space-between" align="center" maxW="5xl" mx="auto">
|
||||||
|
<Heading as="h1" size="lg">
|
||||||
|
<NextLink href="/" passHref>
|
||||||
|
<Link color="white" _hover={{ textDecoration: "none" }}>
|
||||||
|
Ballistic Builder
|
||||||
|
</Link>
|
||||||
|
</NextLink>
|
||||||
|
</Heading>
|
||||||
|
<Flex as="nav" gap="6">
|
||||||
|
<NextLink href="/builder" passHref>
|
||||||
|
<Link color="white" _hover={{ textDecoration: "underline" }}>
|
||||||
|
Builder
|
||||||
|
</Link>
|
||||||
|
</NextLink>
|
||||||
|
<NextLink href="/products" passHref>
|
||||||
|
<Link color="white" _hover={{ textDecoration: "underline" }}>
|
||||||
|
Products
|
||||||
|
</Link>
|
||||||
|
</NextLink>
|
||||||
|
<NextLink href="/auth/signin" passHref>
|
||||||
|
<Link color="white" _hover={{ textDecoration: "underline" }}>
|
||||||
|
Sign In
|
||||||
|
</Link>
|
||||||
|
</NextLink>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
@@ -47,7 +47,7 @@ export default function Home() {
|
|||||||
<div className="bg-white shadow-md p-6 rounded">
|
<div className="bg-white shadow-md p-6 rounded">
|
||||||
<h4 className="text-xl font-bold mb-2">Compatibility Checker</h4>
|
<h4 className="text-xl font-bold mb-2">Compatibility Checker</h4>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
Ensure every part works perfectly together.
|
Ensure every component works perfectly together.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white shadow-md p-6 rounded">
|
<div className="bg-white shadow-md p-6 rounded">
|
||||||
@@ -64,7 +64,7 @@ export default function Home() {
|
|||||||
<div className="container mx-auto px-6 text-center">
|
<div className="container mx-auto px-6 text-center">
|
||||||
<h3 className="text-3xl font-bold mb-6">About Us</h3>
|
<h3 className="text-3xl font-bold mb-6">About Us</h3>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
Firearm Builder is your go-to platform for customizing, building,
|
Ballistic Builder is your go-to platform for customizing, building,
|
||||||
and exploring firearm parts. Designed for enthusiasts by
|
and exploring firearm parts. Designed for enthusiasts by
|
||||||
enthusiasts, we make firearm building easy and accessible.
|
enthusiasts, we make firearm building easy and accessible.
|
||||||
</p>
|
</p>
|
||||||
@@ -90,6 +90,8 @@ export default function Home() {
|
|||||||
<footer className="bg-gray-800 text-white py-4">
|
<footer className="bg-gray-800 text-white py-4">
|
||||||
<div className="container mx-auto px-6 text-center">
|
<div className="container mx-auto px-6 text-center">
|
||||||
<p>© {new Date().getFullYear()} Firearm Builder. All rights reserved.</p>
|
<p>© {new Date().getFullYear()} Firearm Builder. All rights reserved.</p>
|
||||||
|
<p>Built By Forward Group</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>)
|
</div>)
|
||||||
|
|||||||
1245
package-lock.json
generated
1245
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,11 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@chakra-ui/react": "^3.1.2",
|
||||||
|
"@emotion/react": "^11.13.5",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
|
"@headlessui/react": "^2.2.0",
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"@mui/icons-material": "^6.1.7",
|
"@mui/icons-material": "^6.1.7",
|
||||||
"@mui/joy": "^5.0.0-beta.48",
|
"@mui/joy": "^5.0.0-beta.48",
|
||||||
"@mui/material": "^6.1.7",
|
"@mui/material": "^6.1.7",
|
||||||
@@ -22,9 +25,11 @@
|
|||||||
"drizzle-orm": "^0.36.3",
|
"drizzle-orm": "^0.36.3",
|
||||||
"fontsource-roboto": "^4.0.0",
|
"fontsource-roboto": "^4.0.0",
|
||||||
"next": "15.0.3",
|
"next": "15.0.3",
|
||||||
|
"next-themes": "^0.4.3",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0",
|
||||||
|
"react-icons": "^5.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.17.6",
|
"@types/node": "^20.17.6",
|
||||||
|
|||||||
47
src/components/ui/accordion.tsx
Normal file
47
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Accordion, HStack } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuChevronDown } from "react-icons/lu"
|
||||||
|
|
||||||
|
interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps {
|
||||||
|
indicatorPlacement?: "start" | "end"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccordionItemTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
AccordionItemTriggerProps
|
||||||
|
>(function AccordionItemTrigger(props, ref) {
|
||||||
|
const { children, indicatorPlacement = "end", ...rest } = props
|
||||||
|
return (
|
||||||
|
<Accordion.ItemTrigger {...rest} ref={ref}>
|
||||||
|
{indicatorPlacement === "start" && (
|
||||||
|
<Accordion.ItemIndicator rotate={{ base: "-90deg", _open: "0deg" }}>
|
||||||
|
<LuChevronDown />
|
||||||
|
</Accordion.ItemIndicator>
|
||||||
|
)}
|
||||||
|
<HStack gap="4" flex="1" textAlign="start" width="full">
|
||||||
|
{children}
|
||||||
|
</HStack>
|
||||||
|
{indicatorPlacement === "end" && (
|
||||||
|
<Accordion.ItemIndicator>
|
||||||
|
<LuChevronDown />
|
||||||
|
</Accordion.ItemIndicator>
|
||||||
|
)}
|
||||||
|
</Accordion.ItemTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
interface AccordionItemContentProps extends Accordion.ItemContentProps {}
|
||||||
|
|
||||||
|
export const AccordionItemContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
AccordionItemContentProps
|
||||||
|
>(function AccordionItemContent(props, ref) {
|
||||||
|
return (
|
||||||
|
<Accordion.ItemContent>
|
||||||
|
<Accordion.ItemBody {...props} ref={ref} />
|
||||||
|
</Accordion.ItemContent>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AccordionRoot = Accordion.Root
|
||||||
|
export const AccordionItem = Accordion.Item
|
||||||
40
src/components/ui/action-bar.tsx
Normal file
40
src/components/ui/action-bar.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { ActionBar, Portal } from "@chakra-ui/react"
|
||||||
|
import { CloseButton } from "./close-button"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface ActionBarContentProps extends ActionBar.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ActionBarContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ActionBarContentProps
|
||||||
|
>(function ActionBarContent(props, ref) {
|
||||||
|
const { children, portalled = true, portalRef, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ActionBar.Positioner>
|
||||||
|
<ActionBar.Content ref={ref} {...rest} asChild={false}>
|
||||||
|
{children}
|
||||||
|
</ActionBar.Content>
|
||||||
|
</ActionBar.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ActionBarCloseTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ActionBar.CloseTriggerProps
|
||||||
|
>(function ActionBarCloseTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<ActionBar.CloseTrigger {...props} asChild ref={ref}>
|
||||||
|
<CloseButton size="sm" />
|
||||||
|
</ActionBar.CloseTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ActionBarRoot = ActionBar.Root
|
||||||
|
export const ActionBarSelectionTrigger = ActionBar.SelectionTrigger
|
||||||
|
export const ActionBarSeparator = ActionBar.Separator
|
||||||
51
src/components/ui/alert.tsx
Normal file
51
src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Alert as ChakraAlert } from "@chakra-ui/react"
|
||||||
|
import { CloseButton } from "./close-button"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface AlertProps extends Omit<ChakraAlert.RootProps, "title"> {
|
||||||
|
startElement?: React.ReactNode
|
||||||
|
endElement?: React.ReactNode
|
||||||
|
title?: React.ReactNode
|
||||||
|
icon?: React.ReactElement
|
||||||
|
closable?: boolean
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||||
|
function Alert(props, ref) {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
closable,
|
||||||
|
onClose,
|
||||||
|
startElement,
|
||||||
|
endElement,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
return (
|
||||||
|
<ChakraAlert.Root ref={ref} {...rest}>
|
||||||
|
{startElement || <ChakraAlert.Indicator>{icon}</ChakraAlert.Indicator>}
|
||||||
|
{children ? (
|
||||||
|
<ChakraAlert.Content>
|
||||||
|
<ChakraAlert.Title>{title}</ChakraAlert.Title>
|
||||||
|
<ChakraAlert.Description>{children}</ChakraAlert.Description>
|
||||||
|
</ChakraAlert.Content>
|
||||||
|
) : (
|
||||||
|
<ChakraAlert.Title flex="1">{title}</ChakraAlert.Title>
|
||||||
|
)}
|
||||||
|
{endElement}
|
||||||
|
{closable && (
|
||||||
|
<CloseButton
|
||||||
|
size="sm"
|
||||||
|
pos="relative"
|
||||||
|
top="-2"
|
||||||
|
insetEnd="-2"
|
||||||
|
alignSelf="flex-start"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ChakraAlert.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
74
src/components/ui/avatar.tsx
Normal file
74
src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { GroupProps, SlotRecipeProps } from "@chakra-ui/react"
|
||||||
|
import { Avatar as ChakraAvatar, Group } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>
|
||||||
|
|
||||||
|
export interface AvatarProps extends ChakraAvatar.RootProps {
|
||||||
|
name?: string
|
||||||
|
src?: string
|
||||||
|
srcSet?: string
|
||||||
|
loading?: ImageProps["loading"]
|
||||||
|
icon?: React.ReactElement
|
||||||
|
fallback?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
||||||
|
function Avatar(props, ref) {
|
||||||
|
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
|
||||||
|
props
|
||||||
|
return (
|
||||||
|
<ChakraAvatar.Root ref={ref} {...rest}>
|
||||||
|
<AvatarFallback name={name} icon={icon}>
|
||||||
|
{fallback}
|
||||||
|
</AvatarFallback>
|
||||||
|
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
|
||||||
|
{children}
|
||||||
|
</ChakraAvatar.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface AvatarFallbackProps extends ChakraAvatar.FallbackProps {
|
||||||
|
name?: string
|
||||||
|
icon?: React.ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef<HTMLDivElement, AvatarFallbackProps>(
|
||||||
|
function AvatarFallback(props, ref) {
|
||||||
|
const { name, icon, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraAvatar.Fallback ref={ref} {...rest}>
|
||||||
|
{children}
|
||||||
|
{name != null && children == null && <>{getInitials(name)}</>}
|
||||||
|
{name == null && children == null && (
|
||||||
|
<ChakraAvatar.Icon asChild={!!icon}>{icon}</ChakraAvatar.Icon>
|
||||||
|
)}
|
||||||
|
</ChakraAvatar.Fallback>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function getInitials(name: string) {
|
||||||
|
const names = name.trim().split(" ")
|
||||||
|
const firstName = names[0] != null ? names[0] : ""
|
||||||
|
const lastName = names.length > 1 ? names[names.length - 1] : ""
|
||||||
|
return firstName && lastName
|
||||||
|
? `${firstName.charAt(0)}${lastName.charAt(0)}`
|
||||||
|
: firstName.charAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvatarGroupProps extends GroupProps, SlotRecipeProps<"avatar"> {}
|
||||||
|
|
||||||
|
export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
||||||
|
function AvatarGroup(props, ref) {
|
||||||
|
const { size, variant, borderless, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraAvatar.PropsProvider value={{ size, variant, borderless }}>
|
||||||
|
<Group gap="0" spaceX="-3" ref={ref} {...rest} />
|
||||||
|
</ChakraAvatar.PropsProvider>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
31
src/components/ui/blockquote.tsx
Normal file
31
src/components/ui/blockquote.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Blockquote as ChakraBlockquote } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface BlockquoteProps extends ChakraBlockquote.RootProps {
|
||||||
|
cite?: React.ReactNode
|
||||||
|
citeUrl?: string
|
||||||
|
icon?: React.ReactNode
|
||||||
|
showDash?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Blockquote = React.forwardRef<HTMLDivElement, BlockquoteProps>(
|
||||||
|
function Blockquote(props, ref) {
|
||||||
|
const { children, cite, citeUrl, showDash, icon, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraBlockquote.Root ref={ref} {...rest}>
|
||||||
|
{icon}
|
||||||
|
<ChakraBlockquote.Content cite={citeUrl}>
|
||||||
|
{children}
|
||||||
|
</ChakraBlockquote.Content>
|
||||||
|
{cite && (
|
||||||
|
<ChakraBlockquote.Caption>
|
||||||
|
{showDash ? <>—</> : null} <cite>{cite}</cite>
|
||||||
|
</ChakraBlockquote.Caption>
|
||||||
|
)}
|
||||||
|
</ChakraBlockquote.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const BlockquoteIcon = ChakraBlockquote.Icon
|
||||||
40
src/components/ui/breadcrumb.tsx
Normal file
40
src/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Breadcrumb, type SystemStyleObject } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface BreadcrumbRootProps extends Breadcrumb.RootProps {
|
||||||
|
separator?: React.ReactNode
|
||||||
|
separatorGap?: SystemStyleObject["gap"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BreadcrumbRoot = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
BreadcrumbRootProps
|
||||||
|
>(function BreadcrumbRoot(props, ref) {
|
||||||
|
const { separator, separatorGap, children, ...rest } = props
|
||||||
|
|
||||||
|
const validChildren = React.Children.toArray(children).filter(
|
||||||
|
React.isValidElement,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Breadcrumb.Root ref={ref} {...rest}>
|
||||||
|
<Breadcrumb.List gap={separatorGap}>
|
||||||
|
{validChildren.map((child, index) => {
|
||||||
|
const last = index === validChildren.length - 1
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<Breadcrumb.Item>{child}</Breadcrumb.Item>
|
||||||
|
{!last && (
|
||||||
|
<Breadcrumb.Separator>{separator}</Breadcrumb.Separator>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Breadcrumb.List>
|
||||||
|
</Breadcrumb.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const BreadcrumbLink = Breadcrumb.Link
|
||||||
|
export const BreadcrumbCurrentLink = Breadcrumb.CurrentLink
|
||||||
|
export const BreadcrumbEllipsis = Breadcrumb.Ellipsis
|
||||||
40
src/components/ui/button.tsx
Normal file
40
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
AbsoluteCenter,
|
||||||
|
Button as ChakraButton,
|
||||||
|
Span,
|
||||||
|
Spinner,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface ButtonLoadingProps {
|
||||||
|
loading?: boolean
|
||||||
|
loadingText?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {}
|
||||||
|
|
||||||
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function Button(props, ref) {
|
||||||
|
const { loading, disabled, loadingText, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}>
|
||||||
|
{loading && !loadingText ? (
|
||||||
|
<>
|
||||||
|
<AbsoluteCenter display="inline-flex">
|
||||||
|
<Spinner size="inherit" color="inherit" />
|
||||||
|
</AbsoluteCenter>
|
||||||
|
<Span opacity={0}>{children}</Span>
|
||||||
|
</>
|
||||||
|
) : loading && loadingText ? (
|
||||||
|
<>
|
||||||
|
<Spinner size="inherit" color="inherit" />
|
||||||
|
{loadingText}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</ChakraButton>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
58
src/components/ui/checkbox-card.tsx
Normal file
58
src/components/ui/checkbox-card.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { CheckboxCard as ChakraCheckboxCard } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface CheckboxCardProps extends ChakraCheckboxCard.RootProps {
|
||||||
|
icon?: React.ReactElement
|
||||||
|
label?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
addon?: React.ReactNode
|
||||||
|
indicator?: React.ReactNode | null
|
||||||
|
indicatorPlacement?: "start" | "end" | "inside"
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CheckboxCard = React.forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
CheckboxCardProps
|
||||||
|
>(function CheckboxCard(props, ref) {
|
||||||
|
const {
|
||||||
|
inputProps,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
addon,
|
||||||
|
indicator = <ChakraCheckboxCard.Indicator />,
|
||||||
|
indicatorPlacement = "end",
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const hasContent = label || description || icon
|
||||||
|
const ContentWrapper = indicator ? ChakraCheckboxCard.Content : React.Fragment
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraCheckboxCard.Root {...rest}>
|
||||||
|
<ChakraCheckboxCard.HiddenInput ref={ref} {...inputProps} />
|
||||||
|
<ChakraCheckboxCard.Control>
|
||||||
|
{indicatorPlacement === "start" && indicator}
|
||||||
|
{hasContent && (
|
||||||
|
<ContentWrapper>
|
||||||
|
{icon}
|
||||||
|
{label && (
|
||||||
|
<ChakraCheckboxCard.Label>{label}</ChakraCheckboxCard.Label>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<ChakraCheckboxCard.Description>
|
||||||
|
{description}
|
||||||
|
</ChakraCheckboxCard.Description>
|
||||||
|
)}
|
||||||
|
{indicatorPlacement === "inside" && indicator}
|
||||||
|
</ContentWrapper>
|
||||||
|
)}
|
||||||
|
{indicatorPlacement === "end" && indicator}
|
||||||
|
</ChakraCheckboxCard.Control>
|
||||||
|
{addon && <ChakraCheckboxCard.Addon>{addon}</ChakraCheckboxCard.Addon>}
|
||||||
|
</ChakraCheckboxCard.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CheckboxCardIndicator = ChakraCheckboxCard.Indicator
|
||||||
25
src/components/ui/checkbox.tsx
Normal file
25
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface CheckboxProps extends ChakraCheckbox.RootProps {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
rootRef?: React.Ref<HTMLLabelElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||||
|
function Checkbox(props, ref) {
|
||||||
|
const { icon, children, inputProps, rootRef, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraCheckbox.Root ref={rootRef} {...rest}>
|
||||||
|
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
|
||||||
|
<ChakraCheckbox.Control>
|
||||||
|
{icon || <ChakraCheckbox.Indicator />}
|
||||||
|
</ChakraCheckbox.Control>
|
||||||
|
{children != null && (
|
||||||
|
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
|
||||||
|
)}
|
||||||
|
</ChakraCheckbox.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
108
src/components/ui/clipboard.tsx
Normal file
108
src/components/ui/clipboard.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import type { ButtonProps, InputProps } from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Clipboard as ChakraClipboard,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuCheck, LuClipboard, LuLink } from "react-icons/lu"
|
||||||
|
|
||||||
|
const ClipboardIcon = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraClipboard.IndicatorProps
|
||||||
|
>(function ClipboardIcon(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraClipboard.Indicator copied={<LuCheck />} {...props} ref={ref}>
|
||||||
|
<LuClipboard />
|
||||||
|
</ChakraClipboard.Indicator>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const ClipboardCopyText = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraClipboard.IndicatorProps
|
||||||
|
>(function ClipboardCopyText(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraClipboard.Indicator copied="Copied" {...props} ref={ref}>
|
||||||
|
Copy
|
||||||
|
</ChakraClipboard.Indicator>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ClipboardLabel = React.forwardRef<
|
||||||
|
HTMLLabelElement,
|
||||||
|
ChakraClipboard.LabelProps
|
||||||
|
>(function ClipboardLabel(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraClipboard.Label
|
||||||
|
textStyle="sm"
|
||||||
|
fontWeight="medium"
|
||||||
|
display="inline-block"
|
||||||
|
mb="1"
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ClipboardButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function ClipboardButton(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraClipboard.Trigger asChild>
|
||||||
|
<Button ref={ref} size="sm" variant="surface" {...props}>
|
||||||
|
<ClipboardIcon />
|
||||||
|
<ClipboardCopyText />
|
||||||
|
</Button>
|
||||||
|
</ChakraClipboard.Trigger>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ClipboardLink = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function ClipboardLink(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraClipboard.Trigger asChild>
|
||||||
|
<Button
|
||||||
|
unstyled
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
display="inline-flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap="2"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<LuLink />
|
||||||
|
<ClipboardCopyText />
|
||||||
|
</Button>
|
||||||
|
</ChakraClipboard.Trigger>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ClipboardIconButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ButtonProps
|
||||||
|
>(function ClipboardIconButton(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraClipboard.Trigger asChild>
|
||||||
|
<IconButton ref={ref} size="xs" variant="subtle" {...props}>
|
||||||
|
<ClipboardIcon />
|
||||||
|
<ClipboardCopyText srOnly />
|
||||||
|
</IconButton>
|
||||||
|
</ChakraClipboard.Trigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ClipboardInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
function ClipboardInputElement(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraClipboard.Input asChild>
|
||||||
|
<Input ref={ref} {...props} />
|
||||||
|
</ChakraClipboard.Input>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ClipboardRoot = ChakraClipboard.Root
|
||||||
17
src/components/ui/close-button.tsx
Normal file
17
src/components/ui/close-button.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { ButtonProps as ChakraCloseButtonProps } from "@chakra-ui/react"
|
||||||
|
import { IconButton as ChakraIconButton } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuX } from "react-icons/lu"
|
||||||
|
|
||||||
|
export interface CloseButtonProps extends ChakraCloseButtonProps {}
|
||||||
|
|
||||||
|
export const CloseButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
CloseButtonProps
|
||||||
|
>(function CloseButton(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
|
||||||
|
{props.children ?? <LuX />}
|
||||||
|
</ChakraIconButton>
|
||||||
|
)
|
||||||
|
})
|
||||||
67
src/components/ui/color-mode.tsx
Normal file
67
src/components/ui/color-mode.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { IconButtonProps } from "@chakra-ui/react"
|
||||||
|
import { ClientOnly, IconButton, Skeleton } from "@chakra-ui/react"
|
||||||
|
import { ThemeProvider, useTheme } from "next-themes"
|
||||||
|
import type { ThemeProviderProps } from "next-themes"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuMoon, LuSun } from "react-icons/lu"
|
||||||
|
|
||||||
|
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||||
|
|
||||||
|
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColorMode() {
|
||||||
|
const { resolvedTheme, setTheme } = useTheme()
|
||||||
|
const toggleColorMode = () => {
|
||||||
|
setTheme(resolvedTheme === "light" ? "dark" : "light")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
colorMode: resolvedTheme,
|
||||||
|
setColorMode: setTheme,
|
||||||
|
toggleColorMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColorModeValue<T>(light: T, dark: T) {
|
||||||
|
const { colorMode } = useColorMode()
|
||||||
|
return colorMode === "light" ? light : dark
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ColorModeIcon() {
|
||||||
|
const { colorMode } = useColorMode()
|
||||||
|
return colorMode === "light" ? <LuSun /> : <LuMoon />
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||||
|
|
||||||
|
export const ColorModeButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ColorModeButtonProps
|
||||||
|
>(function ColorModeButton(props, ref) {
|
||||||
|
const { toggleColorMode } = useColorMode()
|
||||||
|
return (
|
||||||
|
<ClientOnly fallback={<Skeleton boxSize="8" />}>
|
||||||
|
<IconButton
|
||||||
|
onClick={toggleColorMode}
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="Toggle color mode"
|
||||||
|
size="sm"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
css={{
|
||||||
|
_icon: {
|
||||||
|
width: "5",
|
||||||
|
height: "5",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorModeIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ClientOnly>
|
||||||
|
)
|
||||||
|
})
|
||||||
30
src/components/ui/data-list.tsx
Normal file
30
src/components/ui/data-list.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { DataList as ChakraDataList } from "@chakra-ui/react"
|
||||||
|
import { InfoTip } from "./toggle-tip"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export const DataListRoot = ChakraDataList.Root
|
||||||
|
|
||||||
|
interface ItemProps extends ChakraDataList.ItemProps {
|
||||||
|
label: React.ReactNode
|
||||||
|
value: React.ReactNode
|
||||||
|
info?: React.ReactNode
|
||||||
|
grow?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataListItem = React.forwardRef<HTMLDivElement, ItemProps>(
|
||||||
|
function DataListItem(props, ref) {
|
||||||
|
const { label, info, value, children, grow, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraDataList.Item ref={ref} {...rest}>
|
||||||
|
<ChakraDataList.ItemLabel flex={grow ? "1" : undefined}>
|
||||||
|
{label}
|
||||||
|
{info && <InfoTip>{info}</InfoTip>}
|
||||||
|
</ChakraDataList.ItemLabel>
|
||||||
|
<ChakraDataList.ItemValue flex={grow ? "1" : undefined}>
|
||||||
|
{value}
|
||||||
|
</ChakraDataList.ItemValue>
|
||||||
|
{children}
|
||||||
|
</ChakraDataList.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
62
src/components/ui/dialog.tsx
Normal file
62
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
|
||||||
|
import { CloseButton } from "./close-button"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface DialogContentProps extends ChakraDialog.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
backdrop?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DialogContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
DialogContentProps
|
||||||
|
>(function DialogContent(props, ref) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
portalled = true,
|
||||||
|
portalRef,
|
||||||
|
backdrop = true,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
{backdrop && <ChakraDialog.Backdrop />}
|
||||||
|
<ChakraDialog.Positioner>
|
||||||
|
<ChakraDialog.Content ref={ref} {...rest} asChild={false}>
|
||||||
|
{children}
|
||||||
|
</ChakraDialog.Content>
|
||||||
|
</ChakraDialog.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DialogCloseTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraDialog.CloseTriggerProps
|
||||||
|
>(function DialogCloseTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraDialog.CloseTrigger
|
||||||
|
position="absolute"
|
||||||
|
top="2"
|
||||||
|
insetEnd="2"
|
||||||
|
{...props}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<CloseButton size="sm" ref={ref}>
|
||||||
|
{props.children}
|
||||||
|
</CloseButton>
|
||||||
|
</ChakraDialog.CloseTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DialogRoot = ChakraDialog.Root
|
||||||
|
export const DialogFooter = ChakraDialog.Footer
|
||||||
|
export const DialogHeader = ChakraDialog.Header
|
||||||
|
export const DialogBody = ChakraDialog.Body
|
||||||
|
export const DialogBackdrop = ChakraDialog.Backdrop
|
||||||
|
export const DialogTitle = ChakraDialog.Title
|
||||||
|
export const DialogDescription = ChakraDialog.Description
|
||||||
|
export const DialogTrigger = ChakraDialog.Trigger
|
||||||
|
export const DialogActionTrigger = ChakraDialog.ActionTrigger
|
||||||
52
src/components/ui/drawer.tsx
Normal file
52
src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react"
|
||||||
|
import { CloseButton } from "./close-button"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface DrawerContentProps extends ChakraDrawer.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
offset?: ChakraDrawer.ContentProps["padding"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DrawerContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
DrawerContentProps
|
||||||
|
>(function DrawerContent(props, ref) {
|
||||||
|
const { children, portalled = true, portalRef, offset, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraDrawer.Positioner padding={offset}>
|
||||||
|
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
|
||||||
|
{children}
|
||||||
|
</ChakraDrawer.Content>
|
||||||
|
</ChakraDrawer.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DrawerCloseTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraDrawer.CloseTriggerProps
|
||||||
|
>(function DrawerCloseTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraDrawer.CloseTrigger
|
||||||
|
position="absolute"
|
||||||
|
top="2"
|
||||||
|
insetEnd="2"
|
||||||
|
{...props}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<CloseButton size="sm" ref={ref} />
|
||||||
|
</ChakraDrawer.CloseTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DrawerTrigger = ChakraDrawer.Trigger
|
||||||
|
export const DrawerRoot = ChakraDrawer.Root
|
||||||
|
export const DrawerFooter = ChakraDrawer.Footer
|
||||||
|
export const DrawerHeader = ChakraDrawer.Header
|
||||||
|
export const DrawerBody = ChakraDrawer.Body
|
||||||
|
export const DrawerBackdrop = ChakraDrawer.Backdrop
|
||||||
|
export const DrawerDescription = ChakraDrawer.Description
|
||||||
|
export const DrawerTitle = ChakraDrawer.Title
|
||||||
|
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger
|
||||||
34
src/components/ui/empty-state.tsx
Normal file
34
src/components/ui/empty-state.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { EmptyState as ChakraEmptyState, VStack } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface EmptyStateProps extends ChakraEmptyState.RootProps {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
icon?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
||||||
|
function EmptyState(props, ref) {
|
||||||
|
const { title, description, icon, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraEmptyState.Root ref={ref} {...rest}>
|
||||||
|
<ChakraEmptyState.Content>
|
||||||
|
{icon && (
|
||||||
|
<ChakraEmptyState.Indicator>{icon}</ChakraEmptyState.Indicator>
|
||||||
|
)}
|
||||||
|
{description ? (
|
||||||
|
<VStack textAlign="center">
|
||||||
|
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
|
||||||
|
<ChakraEmptyState.Description>
|
||||||
|
{description}
|
||||||
|
</ChakraEmptyState.Description>
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</ChakraEmptyState.Content>
|
||||||
|
</ChakraEmptyState.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
33
src/components/ui/field.tsx
Normal file
33
src/components/ui/field.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Field as ChakraField } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
|
||||||
|
label?: React.ReactNode
|
||||||
|
helperText?: React.ReactNode
|
||||||
|
errorText?: React.ReactNode
|
||||||
|
optionalText?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
||||||
|
function Field(props, ref) {
|
||||||
|
const { label, children, helperText, errorText, optionalText, ...rest } =
|
||||||
|
props
|
||||||
|
return (
|
||||||
|
<ChakraField.Root ref={ref} {...rest}>
|
||||||
|
{label && (
|
||||||
|
<ChakraField.Label>
|
||||||
|
{label}
|
||||||
|
<ChakraField.RequiredIndicator fallback={optionalText} />
|
||||||
|
</ChakraField.Label>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
{helperText && (
|
||||||
|
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
|
||||||
|
)}
|
||||||
|
{errorText && (
|
||||||
|
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
|
||||||
|
)}
|
||||||
|
</ChakraField.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
170
src/components/ui/file-button.tsx
Normal file
170
src/components/ui/file-button.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { ButtonProps, RecipeProps } from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FileUpload as ChakraFileUpload,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Span,
|
||||||
|
Text,
|
||||||
|
useFileUploadContext,
|
||||||
|
useRecipe,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuFile, LuUpload, LuX } from "react-icons/lu"
|
||||||
|
|
||||||
|
export interface FileUploadRootProps extends ChakraFileUpload.RootProps {
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileUploadRoot = React.forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
FileUploadRootProps
|
||||||
|
>(function FileUploadRoot(props, ref) {
|
||||||
|
const { children, inputProps, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraFileUpload.Root {...rest}>
|
||||||
|
<ChakraFileUpload.HiddenInput ref={ref} {...inputProps} />
|
||||||
|
{children}
|
||||||
|
</ChakraFileUpload.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface FileUploadDropzoneProps
|
||||||
|
extends ChakraFileUpload.DropzoneProps {
|
||||||
|
label: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileUploadDropzone = React.forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
FileUploadDropzoneProps
|
||||||
|
>(function FileUploadDropzone(props, ref) {
|
||||||
|
const { children, label, description, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraFileUpload.Dropzone ref={ref} {...rest}>
|
||||||
|
<Icon fontSize="xl" color="fg.muted">
|
||||||
|
<LuUpload />
|
||||||
|
</Icon>
|
||||||
|
<ChakraFileUpload.DropzoneContent>
|
||||||
|
<div>{label}</div>
|
||||||
|
{description && <Text color="fg.muted">{description}</Text>}
|
||||||
|
</ChakraFileUpload.DropzoneContent>
|
||||||
|
{children}
|
||||||
|
</ChakraFileUpload.Dropzone>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
interface VisibilityProps {
|
||||||
|
showSize?: boolean
|
||||||
|
clearable?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileUploadItemProps extends VisibilityProps {
|
||||||
|
file: File
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileUploadItem = React.forwardRef<HTMLLIElement, FileUploadItemProps>(
|
||||||
|
function FileUploadItem(props, ref) {
|
||||||
|
const { file, showSize, clearable } = props
|
||||||
|
return (
|
||||||
|
<ChakraFileUpload.Item file={file} ref={ref}>
|
||||||
|
<ChakraFileUpload.ItemPreview asChild>
|
||||||
|
<Icon fontSize="lg" color="fg.muted">
|
||||||
|
<LuFile />
|
||||||
|
</Icon>
|
||||||
|
</ChakraFileUpload.ItemPreview>
|
||||||
|
|
||||||
|
{showSize ? (
|
||||||
|
<ChakraFileUpload.ItemContent>
|
||||||
|
<ChakraFileUpload.ItemName />
|
||||||
|
<ChakraFileUpload.ItemSizeText />
|
||||||
|
</ChakraFileUpload.ItemContent>
|
||||||
|
) : (
|
||||||
|
<ChakraFileUpload.ItemName flex="1" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{clearable && (
|
||||||
|
<ChakraFileUpload.ItemDeleteTrigger asChild>
|
||||||
|
<IconButton variant="ghost" color="fg.muted" size="xs">
|
||||||
|
<LuX />
|
||||||
|
</IconButton>
|
||||||
|
</ChakraFileUpload.ItemDeleteTrigger>
|
||||||
|
)}
|
||||||
|
</ChakraFileUpload.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface FileUploadListProps
|
||||||
|
extends VisibilityProps,
|
||||||
|
ChakraFileUpload.ItemGroupProps {
|
||||||
|
files?: File[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileUploadList = React.forwardRef<
|
||||||
|
HTMLUListElement,
|
||||||
|
FileUploadListProps
|
||||||
|
>(function FileUploadList(props, ref) {
|
||||||
|
const { showSize, clearable, files, ...rest } = props
|
||||||
|
|
||||||
|
const fileUpload = useFileUploadContext()
|
||||||
|
const acceptedFiles = files ?? fileUpload.acceptedFiles
|
||||||
|
|
||||||
|
if (acceptedFiles.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraFileUpload.ItemGroup ref={ref} {...rest}>
|
||||||
|
{acceptedFiles.map((file) => (
|
||||||
|
<FileUploadItem
|
||||||
|
key={file.name}
|
||||||
|
file={file}
|
||||||
|
showSize={showSize}
|
||||||
|
clearable={clearable}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ChakraFileUpload.ItemGroup>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
type Assign<T, U> = Omit<T, keyof U> & U
|
||||||
|
|
||||||
|
interface FileInputProps extends Assign<ButtonProps, RecipeProps<"input">> {
|
||||||
|
placeholder?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileInput = React.forwardRef<HTMLButtonElement, FileInputProps>(
|
||||||
|
function FileInput(props, ref) {
|
||||||
|
const inputRecipe = useRecipe({ key: "input" })
|
||||||
|
const [recipeProps, restProps] = inputRecipe.splitVariantProps(props)
|
||||||
|
const { placeholder = "Select file(s)", ...rest } = restProps
|
||||||
|
return (
|
||||||
|
<ChakraFileUpload.Trigger asChild>
|
||||||
|
<Button
|
||||||
|
unstyled
|
||||||
|
py="0"
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
css={[inputRecipe(recipeProps), props.css]}
|
||||||
|
>
|
||||||
|
<ChakraFileUpload.Context>
|
||||||
|
{({ acceptedFiles }) => {
|
||||||
|
if (acceptedFiles.length === 1) {
|
||||||
|
return <span>{acceptedFiles[0].name}</span>
|
||||||
|
}
|
||||||
|
if (acceptedFiles.length > 1) {
|
||||||
|
return <span>{acceptedFiles.length} files</span>
|
||||||
|
}
|
||||||
|
return <Span color="fg.subtle">{placeholder}</Span>
|
||||||
|
}}
|
||||||
|
</ChakraFileUpload.Context>
|
||||||
|
</Button>
|
||||||
|
</ChakraFileUpload.Trigger>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const FileUploadLabel = ChakraFileUpload.Label
|
||||||
|
export const FileUploadClearTrigger = ChakraFileUpload.ClearTrigger
|
||||||
|
export const FileUploadTrigger = ChakraFileUpload.Trigger
|
||||||
36
src/components/ui/hover-card.tsx
Normal file
36
src/components/ui/hover-card.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { HoverCard, Portal } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface HoverCardContentProps extends HoverCard.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HoverCardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
HoverCardContentProps
|
||||||
|
>(function HoverCardContent(props, ref) {
|
||||||
|
const { portalled = true, portalRef, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<HoverCard.Positioner>
|
||||||
|
<HoverCard.Content ref={ref} {...rest} />
|
||||||
|
</HoverCard.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const HoverCardArrow = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
HoverCard.ArrowProps
|
||||||
|
>(function HoverCardArrow(props, ref) {
|
||||||
|
return (
|
||||||
|
<HoverCard.Arrow ref={ref} {...props}>
|
||||||
|
<HoverCard.ArrowTip />
|
||||||
|
</HoverCard.Arrow>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const HoverCardRoot = HoverCard.Root
|
||||||
|
export const HoverCardTrigger = HoverCard.Trigger
|
||||||
50
src/components/ui/input-group.tsx
Normal file
50
src/components/ui/input-group.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { BoxProps, InputElementProps } from "@chakra-ui/react"
|
||||||
|
import { Group, InputElement } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface InputGroupProps extends BoxProps {
|
||||||
|
startElementProps?: InputElementProps
|
||||||
|
endElementProps?: InputElementProps
|
||||||
|
startElement?: React.ReactNode
|
||||||
|
endElement?: React.ReactNode
|
||||||
|
children: React.ReactElement
|
||||||
|
startOffset?: InputElementProps["paddingStart"]
|
||||||
|
endOffset?: InputElementProps["paddingEnd"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
||||||
|
function InputGroup(props, ref) {
|
||||||
|
const {
|
||||||
|
startElement,
|
||||||
|
startElementProps,
|
||||||
|
endElement,
|
||||||
|
endElementProps,
|
||||||
|
children,
|
||||||
|
startOffset = "6px",
|
||||||
|
endOffset = "6px",
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group ref={ref} {...rest}>
|
||||||
|
{startElement && (
|
||||||
|
<InputElement pointerEvents="none" {...startElementProps}>
|
||||||
|
{startElement}
|
||||||
|
</InputElement>
|
||||||
|
)}
|
||||||
|
{React.cloneElement(children, {
|
||||||
|
...(startElement && {
|
||||||
|
ps: `calc(var(--input-height) - ${startOffset})`,
|
||||||
|
}),
|
||||||
|
...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }),
|
||||||
|
...children.props,
|
||||||
|
})}
|
||||||
|
{endElement && (
|
||||||
|
<InputElement placement="end" {...endElementProps}>
|
||||||
|
{endElement}
|
||||||
|
</InputElement>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
12
src/components/ui/link-button.tsx
Normal file
12
src/components/ui/link-button.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { HTMLChakraProps, RecipeProps } from "@chakra-ui/react"
|
||||||
|
import { createRecipeContext } from "@chakra-ui/react"
|
||||||
|
|
||||||
|
export interface LinkButtonProps
|
||||||
|
extends HTMLChakraProps<"a", RecipeProps<"button">> {}
|
||||||
|
|
||||||
|
const { withContext } = createRecipeContext({ key: "button" })
|
||||||
|
|
||||||
|
// Replace "a" with your framework's link component
|
||||||
|
export const LinkButton = withContext<HTMLAnchorElement, LinkButtonProps>("a")
|
||||||
110
src/components/ui/menu.tsx
Normal file
110
src/components/ui/menu.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { AbsoluteCenter, Menu as ChakraMenu, Portal } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuCheck, LuChevronRight } from "react-icons/lu"
|
||||||
|
|
||||||
|
interface MenuContentProps extends ChakraMenu.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(
|
||||||
|
function MenuContent(props, ref) {
|
||||||
|
const { portalled = true, portalRef, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraMenu.Positioner>
|
||||||
|
<ChakraMenu.Content ref={ref} {...rest} />
|
||||||
|
</ChakraMenu.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const MenuArrow = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraMenu.ArrowProps
|
||||||
|
>(function MenuArrow(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraMenu.Arrow ref={ref} {...props}>
|
||||||
|
<ChakraMenu.ArrowTip />
|
||||||
|
</ChakraMenu.Arrow>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const MenuCheckboxItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraMenu.CheckboxItemProps
|
||||||
|
>(function MenuCheckboxItem(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraMenu.CheckboxItem ref={ref} {...props}>
|
||||||
|
<ChakraMenu.ItemIndicator hidden={false}>
|
||||||
|
<LuCheck />
|
||||||
|
</ChakraMenu.ItemIndicator>
|
||||||
|
{props.children}
|
||||||
|
</ChakraMenu.CheckboxItem>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const MenuRadioItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraMenu.RadioItemProps
|
||||||
|
>(function MenuRadioItem(props, ref) {
|
||||||
|
const { children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraMenu.RadioItem ps="8" ref={ref} {...rest}>
|
||||||
|
<AbsoluteCenter axis="horizontal" left="4" asChild>
|
||||||
|
<ChakraMenu.ItemIndicator>
|
||||||
|
<LuCheck />
|
||||||
|
</ChakraMenu.ItemIndicator>
|
||||||
|
</AbsoluteCenter>
|
||||||
|
<ChakraMenu.ItemText>{children}</ChakraMenu.ItemText>
|
||||||
|
</ChakraMenu.RadioItem>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const MenuItemGroup = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraMenu.ItemGroupProps
|
||||||
|
>(function MenuItemGroup(props, ref) {
|
||||||
|
const { title, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraMenu.ItemGroup ref={ref} {...rest}>
|
||||||
|
{title && (
|
||||||
|
<ChakraMenu.ItemGroupLabel userSelect="none">
|
||||||
|
{title}
|
||||||
|
</ChakraMenu.ItemGroupLabel>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</ChakraMenu.ItemGroup>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface MenuTriggerItemProps extends ChakraMenu.ItemProps {
|
||||||
|
startIcon?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuTriggerItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
MenuTriggerItemProps
|
||||||
|
>(function MenuTriggerItem(props, ref) {
|
||||||
|
const { startIcon, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraMenu.TriggerItem ref={ref} {...rest}>
|
||||||
|
{startIcon}
|
||||||
|
{children}
|
||||||
|
<LuChevronRight />
|
||||||
|
</ChakraMenu.TriggerItem>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup
|
||||||
|
export const MenuContextTrigger = ChakraMenu.ContextTrigger
|
||||||
|
export const MenuRoot = ChakraMenu.Root
|
||||||
|
export const MenuSeparator = ChakraMenu.Separator
|
||||||
|
|
||||||
|
export const MenuItem = ChakraMenu.Item
|
||||||
|
export const MenuItemText = ChakraMenu.ItemText
|
||||||
|
export const MenuItemCommand = ChakraMenu.ItemCommand
|
||||||
|
export const MenuTrigger = ChakraMenu.Trigger
|
||||||
57
src/components/ui/native-select.tsx
Normal file
57
src/components/ui/native-select.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { NativeSelect as Select } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface NativeSelectRootProps extends Select.RootProps {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NativeSelectRoot = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
NativeSelectRootProps
|
||||||
|
>(function NativeSelect(props, ref) {
|
||||||
|
const { icon, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Select.Root ref={ref} {...rest}>
|
||||||
|
{children}
|
||||||
|
<Select.Indicator>{icon}</Select.Indicator>
|
||||||
|
</Select.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
interface NativeSelectItem {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NativeSelectField extends Select.FieldProps {
|
||||||
|
items?: Array<string | NativeSelectItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NativeSelectField = React.forwardRef<
|
||||||
|
HTMLSelectElement,
|
||||||
|
NativeSelectField
|
||||||
|
>(function NativeSelectField(props, ref) {
|
||||||
|
const { items: itemsProp, children, ...rest } = props
|
||||||
|
|
||||||
|
const items = React.useMemo(
|
||||||
|
() =>
|
||||||
|
itemsProp?.map((item) =>
|
||||||
|
typeof item === "string" ? { label: item, value: item } : item,
|
||||||
|
),
|
||||||
|
[itemsProp],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select.Field ref={ref} {...rest}>
|
||||||
|
{children}
|
||||||
|
{items?.map((item) => (
|
||||||
|
<option key={item.value} value={item.value} disabled={item.disabled}>
|
||||||
|
{item.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select.Field>
|
||||||
|
)
|
||||||
|
})
|
||||||
24
src/components/ui/number-input.tsx
Normal file
24
src/components/ui/number-input.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { NumberInput as ChakraNumberInput } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface NumberInputProps extends ChakraNumberInput.RootProps {}
|
||||||
|
|
||||||
|
export const NumberInputRoot = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
NumberInputProps
|
||||||
|
>(function NumberInput(props, ref) {
|
||||||
|
const { children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraNumberInput.Root ref={ref} variant="outline" {...rest}>
|
||||||
|
{children}
|
||||||
|
<ChakraNumberInput.Control>
|
||||||
|
<ChakraNumberInput.IncrementTrigger />
|
||||||
|
<ChakraNumberInput.DecrementTrigger />
|
||||||
|
</ChakraNumberInput.Control>
|
||||||
|
</ChakraNumberInput.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const NumberInputField = ChakraNumberInput.Input
|
||||||
|
export const NumberInputScruber = ChakraNumberInput.Scrubber
|
||||||
|
export const NumberInputLabel = ChakraNumberInput.Label
|
||||||
208
src/components/ui/pagination.tsx
Normal file
208
src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { ButtonProps, TextProps } from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Pagination as ChakraPagination,
|
||||||
|
IconButton,
|
||||||
|
Text,
|
||||||
|
createContext,
|
||||||
|
usePaginationContext,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
HiChevronLeft,
|
||||||
|
HiChevronRight,
|
||||||
|
HiMiniEllipsisHorizontal,
|
||||||
|
} from "react-icons/hi2"
|
||||||
|
import { LinkButton } from "./link-button"
|
||||||
|
|
||||||
|
interface ButtonVariantMap {
|
||||||
|
current: ButtonProps["variant"]
|
||||||
|
default: ButtonProps["variant"]
|
||||||
|
ellipsis: ButtonProps["variant"]
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationVariant = "outline" | "solid" | "subtle"
|
||||||
|
|
||||||
|
interface ButtonVariantContext {
|
||||||
|
size: ButtonProps["size"]
|
||||||
|
variantMap: ButtonVariantMap
|
||||||
|
getHref?: (page: number) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
const [RootPropsProvider, useRootProps] = createContext<ButtonVariantContext>({
|
||||||
|
name: "RootPropsProvider",
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface PaginationRootProps
|
||||||
|
extends Omit<ChakraPagination.RootProps, "type"> {
|
||||||
|
size?: ButtonProps["size"]
|
||||||
|
variant?: PaginationVariant
|
||||||
|
getHref?: (page: number) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantMap: Record<PaginationVariant, ButtonVariantMap> = {
|
||||||
|
outline: { default: "ghost", ellipsis: "plain", current: "outline" },
|
||||||
|
solid: { default: "outline", ellipsis: "outline", current: "solid" },
|
||||||
|
subtle: { default: "ghost", ellipsis: "plain", current: "subtle" },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaginationRoot = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
PaginationRootProps
|
||||||
|
>(function PaginationRoot(props, ref) {
|
||||||
|
const { size = "sm", variant = "outline", getHref, ...rest } = props
|
||||||
|
return (
|
||||||
|
<RootPropsProvider
|
||||||
|
value={{ size, variantMap: variantMap[variant], getHref }}
|
||||||
|
>
|
||||||
|
<ChakraPagination.Root
|
||||||
|
ref={ref}
|
||||||
|
type={getHref ? "link" : "button"}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</RootPropsProvider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PaginationEllipsis = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraPagination.EllipsisProps
|
||||||
|
>(function PaginationEllipsis(props, ref) {
|
||||||
|
const { size, variantMap } = useRootProps()
|
||||||
|
return (
|
||||||
|
<ChakraPagination.Ellipsis ref={ref} {...props} asChild>
|
||||||
|
<Button as="span" variant={variantMap.ellipsis} size={size}>
|
||||||
|
<HiMiniEllipsisHorizontal />
|
||||||
|
</Button>
|
||||||
|
</ChakraPagination.Ellipsis>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PaginationItem = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraPagination.ItemProps
|
||||||
|
>(function PaginationItem(props, ref) {
|
||||||
|
const { page } = usePaginationContext()
|
||||||
|
const { size, variantMap, getHref } = useRootProps()
|
||||||
|
|
||||||
|
const current = page === props.value
|
||||||
|
const variant = current ? variantMap.current : variantMap.default
|
||||||
|
|
||||||
|
if (getHref) {
|
||||||
|
return (
|
||||||
|
<LinkButton href={getHref(props.value)} variant={variant} size={size}>
|
||||||
|
{props.value}
|
||||||
|
</LinkButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraPagination.Item ref={ref} {...props} asChild>
|
||||||
|
<Button variant={variant} size={size}>
|
||||||
|
{props.value}
|
||||||
|
</Button>
|
||||||
|
</ChakraPagination.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PaginationPrevTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraPagination.PrevTriggerProps
|
||||||
|
>(function PaginationPrevTrigger(props, ref) {
|
||||||
|
const { size, variantMap, getHref } = useRootProps()
|
||||||
|
const { previousPage } = usePaginationContext()
|
||||||
|
|
||||||
|
if (getHref) {
|
||||||
|
return (
|
||||||
|
<LinkButton
|
||||||
|
href={previousPage != null ? getHref(previousPage) : undefined}
|
||||||
|
variant={variantMap.default}
|
||||||
|
size={size}
|
||||||
|
>
|
||||||
|
<HiChevronLeft />
|
||||||
|
</LinkButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraPagination.PrevTrigger ref={ref} asChild {...props}>
|
||||||
|
<IconButton variant={variantMap.default} size={size}>
|
||||||
|
<HiChevronLeft />
|
||||||
|
</IconButton>
|
||||||
|
</ChakraPagination.PrevTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PaginationNextTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraPagination.NextTriggerProps
|
||||||
|
>(function PaginationNextTrigger(props, ref) {
|
||||||
|
const { size, variantMap, getHref } = useRootProps()
|
||||||
|
const { nextPage } = usePaginationContext()
|
||||||
|
|
||||||
|
if (getHref) {
|
||||||
|
return (
|
||||||
|
<LinkButton
|
||||||
|
href={nextPage != null ? getHref(nextPage) : undefined}
|
||||||
|
variant={variantMap.default}
|
||||||
|
size={size}
|
||||||
|
>
|
||||||
|
<HiChevronRight />
|
||||||
|
</LinkButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraPagination.NextTrigger ref={ref} asChild {...props}>
|
||||||
|
<IconButton variant={variantMap.default} size={size}>
|
||||||
|
<HiChevronRight />
|
||||||
|
</IconButton>
|
||||||
|
</ChakraPagination.NextTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PaginationItems = (props: React.HTMLAttributes<HTMLElement>) => {
|
||||||
|
return (
|
||||||
|
<ChakraPagination.Context>
|
||||||
|
{({ pages }) =>
|
||||||
|
pages.map((page, index) => {
|
||||||
|
return page.type === "ellipsis" ? (
|
||||||
|
<PaginationEllipsis key={index} index={index} {...props} />
|
||||||
|
) : (
|
||||||
|
<PaginationItem
|
||||||
|
key={index}
|
||||||
|
type="page"
|
||||||
|
value={page.value}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ChakraPagination.Context>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageTextProps extends TextProps {
|
||||||
|
format?: "short" | "compact" | "long"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaginationPageText = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
PageTextProps
|
||||||
|
>(function PaginationPageText(props, ref) {
|
||||||
|
const { format = "compact", ...rest } = props
|
||||||
|
const { page, totalPages, pageRange, count } = usePaginationContext()
|
||||||
|
const content = React.useMemo(() => {
|
||||||
|
if (format === "short") return `${page} / ${totalPages}`
|
||||||
|
if (format === "compact") return `${page} of ${totalPages}`
|
||||||
|
return `${pageRange.start + 1} - ${pageRange.end} of ${count}`
|
||||||
|
}, [format, page, totalPages, pageRange, count])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text fontWeight="medium" ref={ref} {...rest}>
|
||||||
|
{content}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
})
|
||||||
148
src/components/ui/password-input.tsx
Normal file
148
src/components/ui/password-input.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ButtonProps,
|
||||||
|
GroupProps,
|
||||||
|
InputProps,
|
||||||
|
StackProps,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
HStack,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Stack,
|
||||||
|
mergeRefs,
|
||||||
|
useControllableState,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuEye, LuEyeOff } from "react-icons/lu"
|
||||||
|
import { InputGroup } from "./input-group"
|
||||||
|
|
||||||
|
export interface PasswordVisibilityProps {
|
||||||
|
defaultVisible?: boolean
|
||||||
|
visible?: boolean
|
||||||
|
onVisibleChange?: (visible: boolean) => void
|
||||||
|
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordInputProps
|
||||||
|
extends InputProps,
|
||||||
|
PasswordVisibilityProps {
|
||||||
|
rootProps?: GroupProps
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordInput = React.forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
PasswordInputProps
|
||||||
|
>(function PasswordInput(props, ref) {
|
||||||
|
const {
|
||||||
|
rootProps,
|
||||||
|
defaultVisible,
|
||||||
|
visible: visibleProp,
|
||||||
|
onVisibleChange,
|
||||||
|
visibilityIcon = { on: <LuEye />, off: <LuEyeOff /> },
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const [visible, setVisible] = useControllableState({
|
||||||
|
value: visibleProp,
|
||||||
|
defaultValue: defaultVisible || false,
|
||||||
|
onChange: onVisibleChange,
|
||||||
|
})
|
||||||
|
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroup
|
||||||
|
width="full"
|
||||||
|
endElement={
|
||||||
|
<VisibilityTrigger
|
||||||
|
disabled={rest.disabled}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
if (rest.disabled) return
|
||||||
|
if (e.button !== 0) return
|
||||||
|
e.preventDefault()
|
||||||
|
setVisible(!visible)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{visible ? visibilityIcon.off : visibilityIcon.on}
|
||||||
|
</VisibilityTrigger>
|
||||||
|
}
|
||||||
|
{...rootProps}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...rest}
|
||||||
|
ref={mergeRefs(ref, inputRef)}
|
||||||
|
type={visible ? "text" : "password"}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const VisibilityTrigger = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function VisibilityTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
tabIndex={-1}
|
||||||
|
ref={ref}
|
||||||
|
me="-2"
|
||||||
|
aspectRatio="square"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
height="calc(100% - {spacing.2})"
|
||||||
|
aria-label="Toggle password visibility"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface PasswordStrengthMeterProps extends StackProps {
|
||||||
|
max?: number
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordStrengthMeter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
PasswordStrengthMeterProps
|
||||||
|
>(function PasswordStrengthMeter(props, ref) {
|
||||||
|
const { max = 4, value, ...rest } = props
|
||||||
|
|
||||||
|
const percent = (value / max) * 100
|
||||||
|
const { label, colorPalette } = getColorPalette(percent)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack align="flex-end" gap="1" ref={ref} {...rest}>
|
||||||
|
<HStack width="full" ref={ref} {...rest}>
|
||||||
|
{Array.from({ length: max }).map((_, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
height="1"
|
||||||
|
flex="1"
|
||||||
|
rounded="sm"
|
||||||
|
data-selected={index < value ? "" : undefined}
|
||||||
|
layerStyle="fill.subtle"
|
||||||
|
colorPalette="gray"
|
||||||
|
_selected={{
|
||||||
|
colorPalette,
|
||||||
|
layerStyle: "fill.solid",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
{label && <HStack textStyle="xs">{label}</HStack>}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function getColorPalette(percent: number) {
|
||||||
|
switch (true) {
|
||||||
|
case percent < 33:
|
||||||
|
return { label: "Low", colorPalette: "red" }
|
||||||
|
case percent < 66:
|
||||||
|
return { label: "Medium", colorPalette: "orange" }
|
||||||
|
default:
|
||||||
|
return { label: "High", colorPalette: "green" }
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/components/ui/pin-input.tsx
Normal file
27
src/components/ui/pin-input.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { PinInput as ChakraPinInput, Group } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface PinInputProps extends ChakraPinInput.RootProps {
|
||||||
|
rootRef?: React.Ref<HTMLDivElement>
|
||||||
|
count?: number
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
attached?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
|
||||||
|
function PinInput(props, ref) {
|
||||||
|
const { count = 4, inputProps, rootRef, attached, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraPinInput.Root ref={rootRef} {...rest}>
|
||||||
|
<ChakraPinInput.HiddenInput ref={ref} {...inputProps} />
|
||||||
|
<ChakraPinInput.Control>
|
||||||
|
<Group attached={attached}>
|
||||||
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
|
<ChakraPinInput.Input key={index} index={index} />
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</ChakraPinInput.Control>
|
||||||
|
</ChakraPinInput.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
59
src/components/ui/popover.tsx
Normal file
59
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Popover as ChakraPopover, Portal } from "@chakra-ui/react"
|
||||||
|
import { CloseButton } from "./close-button"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface PopoverContentProps extends ChakraPopover.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PopoverContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
PopoverContentProps
|
||||||
|
>(function PopoverContent(props, ref) {
|
||||||
|
const { portalled = true, portalRef, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraPopover.Positioner>
|
||||||
|
<ChakraPopover.Content ref={ref} {...rest} />
|
||||||
|
</ChakraPopover.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PopoverArrow = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraPopover.ArrowProps
|
||||||
|
>(function PopoverArrow(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraPopover.Arrow {...props} ref={ref}>
|
||||||
|
<ChakraPopover.ArrowTip />
|
||||||
|
</ChakraPopover.Arrow>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PopoverCloseTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraPopover.CloseTriggerProps
|
||||||
|
>(function PopoverCloseTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraPopover.CloseTrigger
|
||||||
|
position="absolute"
|
||||||
|
top="1"
|
||||||
|
insetEnd="1"
|
||||||
|
{...props}
|
||||||
|
asChild
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<CloseButton size="sm" />
|
||||||
|
</ChakraPopover.CloseTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PopoverTitle = ChakraPopover.Title
|
||||||
|
export const PopoverDescription = ChakraPopover.Description
|
||||||
|
export const PopoverFooter = ChakraPopover.Footer
|
||||||
|
export const PopoverHeader = ChakraPopover.Header
|
||||||
|
export const PopoverRoot = ChakraPopover.Root
|
||||||
|
export const PopoverBody = ChakraPopover.Body
|
||||||
|
export const PopoverTrigger = ChakraPopover.Trigger
|
||||||
37
src/components/ui/progress-circle.tsx
Normal file
37
src/components/ui/progress-circle.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { SystemStyleObject } from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
AbsoluteCenter,
|
||||||
|
ProgressCircle as ChakraProgressCircle,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface ProgressCircleRingProps extends ChakraProgressCircle.CircleProps {
|
||||||
|
trackColor?: SystemStyleObject["stroke"]
|
||||||
|
cap?: SystemStyleObject["strokeLinecap"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgressCircleRing = React.forwardRef<
|
||||||
|
SVGSVGElement,
|
||||||
|
ProgressCircleRingProps
|
||||||
|
>(function ProgressCircleRing(props, ref) {
|
||||||
|
const { trackColor, cap, color, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraProgressCircle.Circle {...rest} ref={ref}>
|
||||||
|
<ChakraProgressCircle.Track stroke={trackColor} />
|
||||||
|
<ChakraProgressCircle.Range stroke={color} strokeLinecap={cap} />
|
||||||
|
</ChakraProgressCircle.Circle>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ProgressCircleValueText = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraProgressCircle.ValueTextProps
|
||||||
|
>(function ProgressCircleValueText(props, ref) {
|
||||||
|
return (
|
||||||
|
<AbsoluteCenter>
|
||||||
|
<ChakraProgressCircle.ValueText {...props} ref={ref} />
|
||||||
|
</AbsoluteCenter>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ProgressCircleRoot = ChakraProgressCircle.Root
|
||||||
34
src/components/ui/progress.tsx
Normal file
34
src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Progress as ChakraProgress } from "@chakra-ui/react"
|
||||||
|
import { InfoTip } from "./toggle-tip"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export const ProgressBar = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraProgress.TrackProps
|
||||||
|
>(function ProgressBar(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraProgress.Track {...props} ref={ref}>
|
||||||
|
<ChakraProgress.Range />
|
||||||
|
</ChakraProgress.Track>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface ProgressLabelProps extends ChakraProgress.LabelProps {
|
||||||
|
info?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgressLabel = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ProgressLabelProps
|
||||||
|
>(function ProgressLabel(props, ref) {
|
||||||
|
const { children, info, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraProgress.Label {...rest} ref={ref}>
|
||||||
|
{children}
|
||||||
|
{info && <InfoTip>{info}</InfoTip>}
|
||||||
|
</ChakraProgress.Label>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ProgressRoot = ChakraProgress.Root
|
||||||
|
export const ProgressValueText = ChakraProgress.ValueText
|
||||||
264
src/components/ui/prose.tsx
Normal file
264
src/components/ui/prose.tsx
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { chakra } from "@chakra-ui/react"
|
||||||
|
|
||||||
|
export const Prose = chakra("div", {
|
||||||
|
base: {
|
||||||
|
color: "fg.muted",
|
||||||
|
maxWidth: "65ch",
|
||||||
|
fontSize: "sm",
|
||||||
|
lineHeight: "1.7em",
|
||||||
|
"& p": {
|
||||||
|
marginTop: "1em",
|
||||||
|
marginBottom: "1em",
|
||||||
|
},
|
||||||
|
"& blockquote": {
|
||||||
|
marginTop: "1.285em",
|
||||||
|
marginBottom: "1.285em",
|
||||||
|
paddingInline: "1.285em",
|
||||||
|
borderInlineStartWidth: "0.25em",
|
||||||
|
},
|
||||||
|
"& a": {
|
||||||
|
color: "fg",
|
||||||
|
textDecoration: "underline",
|
||||||
|
textUnderlineOffset: "3px",
|
||||||
|
textDecorationThickness: "2px",
|
||||||
|
textDecorationColor: "border.muted",
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
"& strong": {
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
"& a strong": {
|
||||||
|
color: "inherit",
|
||||||
|
},
|
||||||
|
"& h1": {
|
||||||
|
fontSize: "2.15em",
|
||||||
|
letterSpacing: "-0.02em",
|
||||||
|
marginTop: "0",
|
||||||
|
marginBottom: "0.8em",
|
||||||
|
lineHeight: "1.2em",
|
||||||
|
},
|
||||||
|
"& h2": {
|
||||||
|
fontSize: "1.4em",
|
||||||
|
letterSpacing: "-0.02em",
|
||||||
|
marginTop: "1.6em",
|
||||||
|
marginBottom: "0.8em",
|
||||||
|
lineHeight: "1.4em",
|
||||||
|
},
|
||||||
|
"& h3": {
|
||||||
|
fontSize: "1.285em",
|
||||||
|
letterSpacing: "-0.01em",
|
||||||
|
marginTop: "1.5em",
|
||||||
|
marginBottom: "0.4em",
|
||||||
|
lineHeight: "1.5em",
|
||||||
|
},
|
||||||
|
"& h4": {
|
||||||
|
marginTop: "1.4em",
|
||||||
|
marginBottom: "0.5em",
|
||||||
|
letterSpacing: "-0.01em",
|
||||||
|
lineHeight: "1.5em",
|
||||||
|
},
|
||||||
|
"& img": {
|
||||||
|
marginTop: "1.7em",
|
||||||
|
marginBottom: "1.7em",
|
||||||
|
borderRadius: "lg",
|
||||||
|
boxShadow: "inset",
|
||||||
|
},
|
||||||
|
"& picture": {
|
||||||
|
marginTop: "1.7em",
|
||||||
|
marginBottom: "1.7em",
|
||||||
|
},
|
||||||
|
"& picture > img": {
|
||||||
|
marginTop: "0",
|
||||||
|
marginBottom: "0",
|
||||||
|
},
|
||||||
|
"& video": {
|
||||||
|
marginTop: "1.7em",
|
||||||
|
marginBottom: "1.7em",
|
||||||
|
},
|
||||||
|
"& kbd": {
|
||||||
|
fontSize: "0.85em",
|
||||||
|
borderRadius: "xs",
|
||||||
|
paddingTop: "0.15em",
|
||||||
|
paddingBottom: "0.15em",
|
||||||
|
paddingInlineEnd: "0.35em",
|
||||||
|
paddingInlineStart: "0.35em",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
color: "fg.muted",
|
||||||
|
"--shadow": "colors.border",
|
||||||
|
boxShadow: "0 0 0 1px var(--shadow),0 1px 0 1px var(--shadow)",
|
||||||
|
},
|
||||||
|
"& code": {
|
||||||
|
fontSize: "0.925em",
|
||||||
|
letterSpacing: "-0.01em",
|
||||||
|
borderRadius: "md",
|
||||||
|
borderWidth: "1px",
|
||||||
|
padding: "0.25em",
|
||||||
|
},
|
||||||
|
"& pre code": {
|
||||||
|
fontSize: "inherit",
|
||||||
|
letterSpacing: "inherit",
|
||||||
|
borderWidth: "inherit",
|
||||||
|
padding: "0",
|
||||||
|
},
|
||||||
|
"& h2 code": {
|
||||||
|
fontSize: "0.9em",
|
||||||
|
},
|
||||||
|
"& h3 code": {
|
||||||
|
fontSize: "0.8em",
|
||||||
|
},
|
||||||
|
"& pre": {
|
||||||
|
backgroundColor: "bg.subtle",
|
||||||
|
marginTop: "1.6em",
|
||||||
|
marginBottom: "1.6em",
|
||||||
|
borderRadius: "md",
|
||||||
|
fontSize: "0.9em",
|
||||||
|
paddingTop: "0.65em",
|
||||||
|
paddingBottom: "0.65em",
|
||||||
|
paddingInlineEnd: "1em",
|
||||||
|
paddingInlineStart: "1em",
|
||||||
|
overflowX: "auto",
|
||||||
|
fontWeight: "400",
|
||||||
|
},
|
||||||
|
"& ol": {
|
||||||
|
marginTop: "1em",
|
||||||
|
marginBottom: "1em",
|
||||||
|
paddingInlineStart: "1.5em",
|
||||||
|
},
|
||||||
|
"& ul": {
|
||||||
|
marginTop: "1em",
|
||||||
|
marginBottom: "1em",
|
||||||
|
paddingInlineStart: "1.5em",
|
||||||
|
},
|
||||||
|
"& li": {
|
||||||
|
marginTop: "0.285em",
|
||||||
|
marginBottom: "0.285em",
|
||||||
|
},
|
||||||
|
"& ol > li": {
|
||||||
|
paddingInlineStart: "0.4em",
|
||||||
|
listStyleType: "decimal",
|
||||||
|
"&::marker": {
|
||||||
|
color: "fg.muted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& ul > li": {
|
||||||
|
paddingInlineStart: "0.4em",
|
||||||
|
listStyleType: "disc",
|
||||||
|
"&::marker": {
|
||||||
|
color: "fg.muted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& > ul > li p": {
|
||||||
|
marginTop: "0.5em",
|
||||||
|
marginBottom: "0.5em",
|
||||||
|
},
|
||||||
|
"& > ul > li > p:first-of-type": {
|
||||||
|
marginTop: "1em",
|
||||||
|
},
|
||||||
|
"& > ul > li > p:last-of-type": {
|
||||||
|
marginBottom: "1em",
|
||||||
|
},
|
||||||
|
"& > ol > li > p:first-of-type": {
|
||||||
|
marginTop: "1em",
|
||||||
|
},
|
||||||
|
"& > ol > li > p:last-of-type": {
|
||||||
|
marginBottom: "1em",
|
||||||
|
},
|
||||||
|
"& ul ul, ul ol, ol ul, ol ol": {
|
||||||
|
marginTop: "0.5em",
|
||||||
|
marginBottom: "0.5em",
|
||||||
|
},
|
||||||
|
"& dl": {
|
||||||
|
marginTop: "1em",
|
||||||
|
marginBottom: "1em",
|
||||||
|
},
|
||||||
|
"& dt": {
|
||||||
|
fontWeight: "600",
|
||||||
|
marginTop: "1em",
|
||||||
|
},
|
||||||
|
"& dd": {
|
||||||
|
marginTop: "0.285em",
|
||||||
|
paddingInlineStart: "1.5em",
|
||||||
|
},
|
||||||
|
"& hr": {
|
||||||
|
marginTop: "2.25em",
|
||||||
|
marginBottom: "2.25em",
|
||||||
|
},
|
||||||
|
"& :is(h1,h2,h3,h4,h5,hr) + *": {
|
||||||
|
marginTop: "0",
|
||||||
|
},
|
||||||
|
"& table": {
|
||||||
|
width: "100%",
|
||||||
|
tableLayout: "auto",
|
||||||
|
textAlign: "start",
|
||||||
|
lineHeight: "1.5em",
|
||||||
|
marginTop: "2em",
|
||||||
|
marginBottom: "2em",
|
||||||
|
},
|
||||||
|
"& thead": {
|
||||||
|
borderBottomWidth: "1px",
|
||||||
|
color: "fg",
|
||||||
|
},
|
||||||
|
"& tbody tr": {
|
||||||
|
borderBottomWidth: "1px",
|
||||||
|
borderBottomColor: "border",
|
||||||
|
},
|
||||||
|
"& thead th": {
|
||||||
|
paddingInlineEnd: "1em",
|
||||||
|
paddingBottom: "0.65em",
|
||||||
|
paddingInlineStart: "1em",
|
||||||
|
fontWeight: "medium",
|
||||||
|
textAlign: "start",
|
||||||
|
},
|
||||||
|
"& thead th:first-of-type": {
|
||||||
|
paddingInlineStart: "0",
|
||||||
|
},
|
||||||
|
"& thead th:last-of-type": {
|
||||||
|
paddingInlineEnd: "0",
|
||||||
|
},
|
||||||
|
"& tbody td, tfoot td": {
|
||||||
|
paddingTop: "0.65em",
|
||||||
|
paddingInlineEnd: "1em",
|
||||||
|
paddingBottom: "0.65em",
|
||||||
|
paddingInlineStart: "1em",
|
||||||
|
},
|
||||||
|
"& tbody td:first-of-type, tfoot td:first-of-type": {
|
||||||
|
paddingInlineStart: "0",
|
||||||
|
},
|
||||||
|
"& tbody td:last-of-type, tfoot td:last-of-type": {
|
||||||
|
paddingInlineEnd: "0",
|
||||||
|
},
|
||||||
|
"& figure": {
|
||||||
|
marginTop: "1.625em",
|
||||||
|
marginBottom: "1.625em",
|
||||||
|
},
|
||||||
|
"& figure > *": {
|
||||||
|
marginTop: "0",
|
||||||
|
marginBottom: "0",
|
||||||
|
},
|
||||||
|
"& figcaption": {
|
||||||
|
fontSize: "0.85em",
|
||||||
|
lineHeight: "1.25em",
|
||||||
|
marginTop: "0.85em",
|
||||||
|
color: "fg.muted",
|
||||||
|
},
|
||||||
|
"& h1, h2, h3, h4": {
|
||||||
|
color: "fg",
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
md: {
|
||||||
|
fontSize: "sm",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
fontSize: "md",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "md",
|
||||||
|
},
|
||||||
|
})
|
||||||
15
src/components/ui/provider.tsx
Normal file
15
src/components/ui/provider.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
ColorModeProvider,
|
||||||
|
type ColorModeProviderProps,
|
||||||
|
} from "./color-mode"
|
||||||
|
|
||||||
|
export function Provider(props: ColorModeProviderProps) {
|
||||||
|
return (
|
||||||
|
<ChakraProvider value={defaultSystem}>
|
||||||
|
<ColorModeProvider {...props} />
|
||||||
|
</ChakraProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
58
src/components/ui/radio-card.tsx
Normal file
58
src/components/ui/radio-card.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { RadioCard } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface RadioCardItemProps extends RadioCard.ItemProps {
|
||||||
|
icon?: React.ReactElement
|
||||||
|
label?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
addon?: React.ReactNode
|
||||||
|
indicator?: React.ReactNode | null
|
||||||
|
indicatorPlacement?: "start" | "end" | "inside"
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadioCardItem = React.forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
RadioCardItemProps
|
||||||
|
>(function RadioCardItem(props, ref) {
|
||||||
|
const {
|
||||||
|
inputProps,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
addon,
|
||||||
|
icon,
|
||||||
|
indicator = <RadioCard.ItemIndicator />,
|
||||||
|
indicatorPlacement = "end",
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const hasContent = label || description || icon
|
||||||
|
const ContentWrapper = indicator ? RadioCard.ItemContent : React.Fragment
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioCard.Item {...rest}>
|
||||||
|
<RadioCard.ItemHiddenInput ref={ref} {...inputProps} />
|
||||||
|
<RadioCard.ItemControl>
|
||||||
|
{indicatorPlacement === "start" && indicator}
|
||||||
|
{hasContent && (
|
||||||
|
<ContentWrapper>
|
||||||
|
{icon}
|
||||||
|
{label && <RadioCard.ItemText>{label}</RadioCard.ItemText>}
|
||||||
|
{description && (
|
||||||
|
<RadioCard.ItemDescription>
|
||||||
|
{description}
|
||||||
|
</RadioCard.ItemDescription>
|
||||||
|
)}
|
||||||
|
{indicatorPlacement === "inside" && indicator}
|
||||||
|
</ContentWrapper>
|
||||||
|
)}
|
||||||
|
{indicatorPlacement === "end" && indicator}
|
||||||
|
</RadioCard.ItemControl>
|
||||||
|
{addon && <RadioCard.ItemAddon>{addon}</RadioCard.ItemAddon>}
|
||||||
|
</RadioCard.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const RadioCardRoot = RadioCard.Root
|
||||||
|
export const RadioCardLabel = RadioCard.Label
|
||||||
|
export const RadioCardItemIndicator = RadioCard.ItemIndicator
|
||||||
24
src/components/ui/radio.tsx
Normal file
24
src/components/ui/radio.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface RadioProps extends ChakraRadioGroup.ItemProps {
|
||||||
|
rootRef?: React.Ref<HTMLDivElement>
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
|
||||||
|
function Radio(props, ref) {
|
||||||
|
const { children, inputProps, rootRef, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraRadioGroup.Item ref={rootRef} {...rest}>
|
||||||
|
<ChakraRadioGroup.ItemHiddenInput ref={ref} {...inputProps} />
|
||||||
|
<ChakraRadioGroup.ItemIndicator />
|
||||||
|
{children && (
|
||||||
|
<ChakraRadioGroup.ItemText>{children}</ChakraRadioGroup.ItemText>
|
||||||
|
)}
|
||||||
|
</ChakraRadioGroup.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const RadioGroup = ChakraRadioGroup.Root
|
||||||
27
src/components/ui/rating.tsx
Normal file
27
src/components/ui/rating.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { RatingGroup } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface RatingProps extends RatingGroup.RootProps {
|
||||||
|
icon?: React.ReactElement
|
||||||
|
count?: number
|
||||||
|
label?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
|
||||||
|
function Rating(props, ref) {
|
||||||
|
const { icon, count = 5, label, ...rest } = props
|
||||||
|
return (
|
||||||
|
<RatingGroup.Root ref={ref} count={count} {...rest}>
|
||||||
|
{label && <RatingGroup.Label>{label}</RatingGroup.Label>}
|
||||||
|
<RatingGroup.HiddenInput />
|
||||||
|
<RatingGroup.Control>
|
||||||
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
|
<RatingGroup.Item key={index} index={index + 1}>
|
||||||
|
<RatingGroup.ItemIndicator icon={icon} />
|
||||||
|
</RatingGroup.Item>
|
||||||
|
))}
|
||||||
|
</RatingGroup.Control>
|
||||||
|
</RatingGroup.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
47
src/components/ui/segmented-control.tsx
Normal file
47
src/components/ui/segmented-control.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { For, SegmentGroup } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
value: string
|
||||||
|
label: React.ReactNode
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SegmentedControlProps extends SegmentGroup.RootProps {
|
||||||
|
items: Array<string | Item>
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(items: Array<string | Item>): Item[] {
|
||||||
|
return items.map((item) => {
|
||||||
|
if (typeof item === "string") return { value: item, label: item }
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SegmentedControl = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
SegmentedControlProps
|
||||||
|
>(function SegmentedControl(props, ref) {
|
||||||
|
const { items, ...rest } = props
|
||||||
|
const data = React.useMemo(() => normalize(items), [items])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SegmentGroup.Root ref={ref} {...rest}>
|
||||||
|
<SegmentGroup.Indicator />
|
||||||
|
<For each={data}>
|
||||||
|
{(item) => (
|
||||||
|
<SegmentGroup.Item
|
||||||
|
key={item.value}
|
||||||
|
value={item.value}
|
||||||
|
disabled={item.disabled}
|
||||||
|
>
|
||||||
|
<SegmentGroup.ItemText>{item.label}</SegmentGroup.ItemText>
|
||||||
|
<SegmentGroup.ItemHiddenInput />
|
||||||
|
</SegmentGroup.Item>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</SegmentGroup.Root>
|
||||||
|
)
|
||||||
|
})
|
||||||
143
src/components/ui/select.tsx
Normal file
143
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { CollectionItem } from "@chakra-ui/react"
|
||||||
|
import { Select as ChakraSelect, Portal } from "@chakra-ui/react"
|
||||||
|
import { CloseButton } from "./close-button"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface SelectTriggerProps extends ChakraSelect.ControlProps {
|
||||||
|
clearable?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
SelectTriggerProps
|
||||||
|
>(function SelectTrigger(props, ref) {
|
||||||
|
const { children, clearable, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraSelect.Control {...rest}>
|
||||||
|
<ChakraSelect.Trigger ref={ref}>{children}</ChakraSelect.Trigger>
|
||||||
|
<ChakraSelect.IndicatorGroup>
|
||||||
|
{clearable && <SelectClearTrigger />}
|
||||||
|
<ChakraSelect.Indicator />
|
||||||
|
</ChakraSelect.IndicatorGroup>
|
||||||
|
</ChakraSelect.Control>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const SelectClearTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ChakraSelect.ClearTriggerProps
|
||||||
|
>(function SelectClearTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
|
||||||
|
<CloseButton
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
focusVisibleRing="inside"
|
||||||
|
focusRingWidth="2px"
|
||||||
|
pointerEvents="auto"
|
||||||
|
/>
|
||||||
|
</ChakraSelect.ClearTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
interface SelectContentProps extends ChakraSelect.ContentProps {
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
SelectContentProps
|
||||||
|
>(function SelectContent(props, ref) {
|
||||||
|
const { portalled = true, portalRef, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraSelect.Positioner>
|
||||||
|
<ChakraSelect.Content {...rest} ref={ref} />
|
||||||
|
</ChakraSelect.Positioner>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SelectItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraSelect.ItemProps
|
||||||
|
>(function SelectItem(props, ref) {
|
||||||
|
const { item, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
|
||||||
|
{children}
|
||||||
|
<ChakraSelect.ItemIndicator />
|
||||||
|
</ChakraSelect.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
interface SelectValueTextProps
|
||||||
|
extends Omit<ChakraSelect.ValueTextProps, "children"> {
|
||||||
|
children?(items: CollectionItem[]): React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectValueText = React.forwardRef<
|
||||||
|
HTMLSpanElement,
|
||||||
|
SelectValueTextProps
|
||||||
|
>(function SelectValueText(props, ref) {
|
||||||
|
const { children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraSelect.ValueText {...rest} ref={ref}>
|
||||||
|
<ChakraSelect.Context>
|
||||||
|
{(select) => {
|
||||||
|
const items = select.selectedItems
|
||||||
|
if (items.length === 0) return props.placeholder
|
||||||
|
if (children) return children(items)
|
||||||
|
if (items.length === 1)
|
||||||
|
return select.collection.stringifyItem(items[0])
|
||||||
|
return `${items.length} selected`
|
||||||
|
}}
|
||||||
|
</ChakraSelect.Context>
|
||||||
|
</ChakraSelect.ValueText>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SelectRoot = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraSelect.RootProps
|
||||||
|
>(function SelectRoot(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraSelect.Root
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
positioning={{ sameWidth: true, ...props.positioning }}
|
||||||
|
>
|
||||||
|
{props.asChild ? (
|
||||||
|
props.children
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChakraSelect.HiddenSelect />
|
||||||
|
{props.children}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ChakraSelect.Root>
|
||||||
|
)
|
||||||
|
}) as ChakraSelect.RootComponent
|
||||||
|
|
||||||
|
interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps {
|
||||||
|
label: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectItemGroup = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
SelectItemGroupProps
|
||||||
|
>(function SelectItemGroup(props, ref) {
|
||||||
|
const { children, label, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraSelect.ItemGroup {...rest} ref={ref}>
|
||||||
|
<ChakraSelect.ItemGroupLabel>{label}</ChakraSelect.ItemGroupLabel>
|
||||||
|
{children}
|
||||||
|
</ChakraSelect.ItemGroup>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SelectLabel = ChakraSelect.Label
|
||||||
|
export const SelectItemText = ChakraSelect.ItemText
|
||||||
47
src/components/ui/skeleton.tsx
Normal file
47
src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type {
|
||||||
|
SkeletonProps as ChakraSkeletonProps,
|
||||||
|
CircleProps,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import { Skeleton as ChakraSkeleton, Circle, Stack } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface SkeletonCircleProps extends ChakraSkeletonProps {
|
||||||
|
size?: CircleProps["size"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SkeletonCircle = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
SkeletonCircleProps
|
||||||
|
>(function SkeletonCircle(props, ref) {
|
||||||
|
const { size, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Circle size={size} asChild ref={ref}>
|
||||||
|
<ChakraSkeleton {...rest} />
|
||||||
|
</Circle>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface SkeletonTextProps extends ChakraSkeletonProps {
|
||||||
|
noOfLines?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
|
||||||
|
function SkeletonText(props, ref) {
|
||||||
|
const { noOfLines = 3, gap, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Stack gap={gap} width="full" ref={ref}>
|
||||||
|
{Array.from({ length: noOfLines }).map((_, index) => (
|
||||||
|
<ChakraSkeleton
|
||||||
|
height="4"
|
||||||
|
key={index}
|
||||||
|
{...props}
|
||||||
|
_last={{ maxW: "80%" }}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Skeleton = ChakraSkeleton
|
||||||
60
src/components/ui/slider.tsx
Normal file
60
src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Slider as ChakraSlider, HStack } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface SliderProps extends ChakraSlider.RootProps {
|
||||||
|
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||||
|
label?: React.ReactNode
|
||||||
|
showValue?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
||||||
|
function Slider(props, ref) {
|
||||||
|
const { marks: marksProp, label, showValue, ...rest } = props
|
||||||
|
const value = props.defaultValue ?? props.value
|
||||||
|
|
||||||
|
const marks = marksProp?.map((mark) => {
|
||||||
|
if (typeof mark === "number") return { value: mark, label: undefined }
|
||||||
|
return mark
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasMarkLabel = !!marks?.some((mark) => mark.label)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
|
||||||
|
{label && !showValue && (
|
||||||
|
<ChakraSlider.Label fontWeight="medium">{label}</ChakraSlider.Label>
|
||||||
|
)}
|
||||||
|
{label && showValue && (
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<ChakraSlider.Label fontWeight="medium">{label}</ChakraSlider.Label>
|
||||||
|
<ChakraSlider.ValueText />
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
<ChakraSlider.Control mb={hasMarkLabel ? "4" : undefined}>
|
||||||
|
<ChakraSlider.Track>
|
||||||
|
<ChakraSlider.Range />
|
||||||
|
</ChakraSlider.Track>
|
||||||
|
{value?.map((_, index) => (
|
||||||
|
<ChakraSlider.Thumb key={index} index={index}>
|
||||||
|
<ChakraSlider.HiddenInput />
|
||||||
|
</ChakraSlider.Thumb>
|
||||||
|
))}
|
||||||
|
</ChakraSlider.Control>
|
||||||
|
{marks?.length && (
|
||||||
|
<ChakraSlider.MarkerGroup>
|
||||||
|
{marks.map((mark, index) => {
|
||||||
|
const value = typeof mark === "number" ? mark : mark.value
|
||||||
|
const label = typeof mark === "number" ? undefined : mark.label
|
||||||
|
return (
|
||||||
|
<ChakraSlider.Marker key={index} value={value}>
|
||||||
|
<ChakraSlider.MarkerIndicator />
|
||||||
|
{label}
|
||||||
|
</ChakraSlider.Marker>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ChakraSlider.MarkerGroup>
|
||||||
|
)}
|
||||||
|
</ChakraSlider.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
68
src/components/ui/stat.tsx
Normal file
68
src/components/ui/stat.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
type BadgeProps,
|
||||||
|
Stat as ChakraStat,
|
||||||
|
FormatNumber,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import { InfoTip } from "./toggle-tip"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface StatLabelProps extends ChakraStat.LabelProps {
|
||||||
|
info?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatLabel = React.forwardRef<HTMLDivElement, StatLabelProps>(
|
||||||
|
function StatLabel(props, ref) {
|
||||||
|
const { info, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraStat.Label {...rest} ref={ref}>
|
||||||
|
{children}
|
||||||
|
{info && <InfoTip>{info}</InfoTip>}
|
||||||
|
</ChakraStat.Label>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface StatValueTextProps extends ChakraStat.ValueTextProps {
|
||||||
|
value?: number
|
||||||
|
formatOptions?: Intl.NumberFormatOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatValueText = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
StatValueTextProps
|
||||||
|
>(function StatValueText(props, ref) {
|
||||||
|
const { value, formatOptions, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraStat.ValueText {...rest} ref={ref}>
|
||||||
|
{children ||
|
||||||
|
(value != null && <FormatNumber value={value} {...formatOptions} />)}
|
||||||
|
</ChakraStat.ValueText>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const StatUpTrend = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||||
|
function StatUpTrend(props, ref) {
|
||||||
|
return (
|
||||||
|
<Badge colorPalette="green" gap="0" {...props} ref={ref}>
|
||||||
|
<ChakraStat.UpIndicator />
|
||||||
|
{props.children}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const StatDownTrend = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||||
|
function StatDownTrend(props, ref) {
|
||||||
|
return (
|
||||||
|
<Badge colorPalette="red" gap="0" {...props} ref={ref}>
|
||||||
|
<ChakraStat.DownIndicator />
|
||||||
|
{props.children}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const StatRoot = ChakraStat.Root
|
||||||
|
export const StatHelpText = ChakraStat.HelpText
|
||||||
|
export const StatValueUnit = ChakraStat.ValueUnit
|
||||||
29
src/components/ui/status.tsx
Normal file
29
src/components/ui/status.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ColorPalette } from "@chakra-ui/react"
|
||||||
|
import { Status as ChakraStatus } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
type StatusValue = "success" | "error" | "warning" | "info"
|
||||||
|
|
||||||
|
export interface StatusProps extends ChakraStatus.RootProps {
|
||||||
|
value?: StatusValue
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusMap: Record<StatusValue, ColorPalette> = {
|
||||||
|
success: "green",
|
||||||
|
error: "red",
|
||||||
|
warning: "orange",
|
||||||
|
info: "blue",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Status = React.forwardRef<HTMLDivElement, StatusProps>(
|
||||||
|
function Status(props, ref) {
|
||||||
|
const { children, value = "info", ...rest } = props
|
||||||
|
const colorPalette = rest.colorPalette ?? statusMap[value]
|
||||||
|
return (
|
||||||
|
<ChakraStatus.Root ref={ref} {...rest} colorPalette={colorPalette}>
|
||||||
|
<ChakraStatus.Indicator />
|
||||||
|
{children}
|
||||||
|
</ChakraStatus.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
49
src/components/ui/stepper-input.tsx
Normal file
49
src/components/ui/stepper-input.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { HStack, IconButton, NumberInput } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuMinus, LuPlus } from "react-icons/lu"
|
||||||
|
|
||||||
|
export interface StepperInputProps extends NumberInput.RootProps {
|
||||||
|
label?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepperInput = React.forwardRef<HTMLDivElement, StepperInputProps>(
|
||||||
|
function StepperInput(props, ref) {
|
||||||
|
const { label, ...rest } = props
|
||||||
|
return (
|
||||||
|
<NumberInput.Root {...rest} unstyled ref={ref}>
|
||||||
|
{label && <NumberInput.Label>{label}</NumberInput.Label>}
|
||||||
|
<HStack gap="2">
|
||||||
|
<DecrementTrigger />
|
||||||
|
<NumberInput.ValueText textAlign="center" fontSize="lg" minW="3ch" />
|
||||||
|
<IncrementTrigger />
|
||||||
|
</HStack>
|
||||||
|
</NumberInput.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const DecrementTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
NumberInput.DecrementTriggerProps
|
||||||
|
>(function DecrementTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<NumberInput.DecrementTrigger {...props} asChild ref={ref}>
|
||||||
|
<IconButton variant="outline" size="sm">
|
||||||
|
<LuMinus />
|
||||||
|
</IconButton>
|
||||||
|
</NumberInput.DecrementTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const IncrementTrigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
NumberInput.IncrementTriggerProps
|
||||||
|
>(function IncrementTrigger(props, ref) {
|
||||||
|
return (
|
||||||
|
<NumberInput.IncrementTrigger {...props} asChild ref={ref}>
|
||||||
|
<IconButton variant="outline" size="sm">
|
||||||
|
<LuPlus />
|
||||||
|
</IconButton>
|
||||||
|
</NumberInput.IncrementTrigger>
|
||||||
|
)
|
||||||
|
})
|
||||||
82
src/components/ui/steps.tsx
Normal file
82
src/components/ui/steps.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Box, Steps as ChakraSteps } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { LuCheck } from "react-icons/lu"
|
||||||
|
|
||||||
|
interface StepInfoProps {
|
||||||
|
title?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StepsItemProps
|
||||||
|
extends Omit<ChakraSteps.ItemProps, "title">,
|
||||||
|
StepInfoProps {
|
||||||
|
completedIcon?: React.ReactNode
|
||||||
|
icon?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepsItem = React.forwardRef<HTMLDivElement, StepsItemProps>(
|
||||||
|
function StepsItem(props, ref) {
|
||||||
|
const { title, description, completedIcon, icon, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ChakraSteps.Item {...rest} ref={ref}>
|
||||||
|
<ChakraSteps.Trigger>
|
||||||
|
<ChakraSteps.Indicator>
|
||||||
|
<ChakraSteps.Status
|
||||||
|
complete={completedIcon || <LuCheck />}
|
||||||
|
incomplete={icon || <ChakraSteps.Number />}
|
||||||
|
/>
|
||||||
|
</ChakraSteps.Indicator>
|
||||||
|
<StepInfo title={title} description={description} />
|
||||||
|
</ChakraSteps.Trigger>
|
||||||
|
<ChakraSteps.Separator />
|
||||||
|
</ChakraSteps.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const StepInfo = (props: StepInfoProps) => {
|
||||||
|
const { title, description } = props
|
||||||
|
|
||||||
|
if (title && description) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<ChakraSteps.Title>{title}</ChakraSteps.Title>
|
||||||
|
<ChakraSteps.Description>{description}</ChakraSteps.Description>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{title && <ChakraSteps.Title>{title}</ChakraSteps.Title>}
|
||||||
|
{description && (
|
||||||
|
<ChakraSteps.Description>{description}</ChakraSteps.Description>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StepsIndicatorProps {
|
||||||
|
completedIcon: React.ReactNode
|
||||||
|
icon?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepsIndicator = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
StepsIndicatorProps
|
||||||
|
>(function StepsIndicator(props, ref) {
|
||||||
|
const { icon = <ChakraSteps.Number />, completedIcon } = props
|
||||||
|
return (
|
||||||
|
<ChakraSteps.Indicator ref={ref}>
|
||||||
|
<ChakraSteps.Status complete={completedIcon} incomplete={icon} />
|
||||||
|
</ChakraSteps.Indicator>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const StepsList = ChakraSteps.List
|
||||||
|
export const StepsRoot = ChakraSteps.Root
|
||||||
|
export const StepsContent = ChakraSteps.Content
|
||||||
|
export const StepsCompletedContent = ChakraSteps.CompletedContent
|
||||||
|
|
||||||
|
export const StepsNextTrigger = ChakraSteps.NextTrigger
|
||||||
|
export const StepsPrevTrigger = ChakraSteps.PrevTrigger
|
||||||
39
src/components/ui/switch.tsx
Normal file
39
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Switch as ChakraSwitch } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface SwitchProps extends ChakraSwitch.RootProps {
|
||||||
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||||
|
rootRef?: React.Ref<HTMLLabelElement>
|
||||||
|
trackLabel?: { on: React.ReactNode; off: React.ReactNode }
|
||||||
|
thumbLabel?: { on: React.ReactNode; off: React.ReactNode }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
||||||
|
function Switch(props, ref) {
|
||||||
|
const { inputProps, children, rootRef, trackLabel, thumbLabel, ...rest } =
|
||||||
|
props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraSwitch.Root ref={rootRef} {...rest}>
|
||||||
|
<ChakraSwitch.HiddenInput ref={ref} {...inputProps} />
|
||||||
|
<ChakraSwitch.Control>
|
||||||
|
<ChakraSwitch.Thumb>
|
||||||
|
{thumbLabel && (
|
||||||
|
<ChakraSwitch.ThumbIndicator fallback={thumbLabel?.off}>
|
||||||
|
{thumbLabel?.on}
|
||||||
|
</ChakraSwitch.ThumbIndicator>
|
||||||
|
)}
|
||||||
|
</ChakraSwitch.Thumb>
|
||||||
|
{trackLabel && (
|
||||||
|
<ChakraSwitch.Indicator fallback={trackLabel.off}>
|
||||||
|
{trackLabel.on}
|
||||||
|
</ChakraSwitch.Indicator>
|
||||||
|
)}
|
||||||
|
</ChakraSwitch.Control>
|
||||||
|
{children != null && (
|
||||||
|
<ChakraSwitch.Label>{children}</ChakraSwitch.Label>
|
||||||
|
)}
|
||||||
|
</ChakraSwitch.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
39
src/components/ui/tag.tsx
Normal file
39
src/components/ui/tag.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Tag as ChakraTag } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface TagProps extends ChakraTag.RootProps {
|
||||||
|
startElement?: React.ReactNode
|
||||||
|
endElement?: React.ReactNode
|
||||||
|
onClose?: VoidFunction
|
||||||
|
closable?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
||||||
|
function Tag(props, ref) {
|
||||||
|
const {
|
||||||
|
startElement,
|
||||||
|
endElement,
|
||||||
|
onClose,
|
||||||
|
closable = !!onClose,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraTag.Root ref={ref} {...rest}>
|
||||||
|
{startElement && (
|
||||||
|
<ChakraTag.StartElement>{startElement}</ChakraTag.StartElement>
|
||||||
|
)}
|
||||||
|
<ChakraTag.Label>{children}</ChakraTag.Label>
|
||||||
|
{endElement && (
|
||||||
|
<ChakraTag.EndElement>{endElement}</ChakraTag.EndElement>
|
||||||
|
)}
|
||||||
|
{closable && (
|
||||||
|
<ChakraTag.EndElement>
|
||||||
|
<ChakraTag.CloseTrigger onClick={onClose} />
|
||||||
|
</ChakraTag.EndElement>
|
||||||
|
)}
|
||||||
|
</ChakraTag.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
21
src/components/ui/timeline.tsx
Normal file
21
src/components/ui/timeline.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Timeline as ChakraTimeline } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export const TimelineConnector = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraTimeline.IndicatorProps
|
||||||
|
>(function TimelineConnector(props, ref) {
|
||||||
|
return (
|
||||||
|
<ChakraTimeline.Connector ref={ref}>
|
||||||
|
<ChakraTimeline.Separator />
|
||||||
|
<ChakraTimeline.Indicator {...props} />
|
||||||
|
</ChakraTimeline.Connector>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const TimelineRoot = ChakraTimeline.Root
|
||||||
|
export const TimelineContent = ChakraTimeline.Content
|
||||||
|
export const TimelineItem = ChakraTimeline.Item
|
||||||
|
export const TimelineIndicator = ChakraTimeline.Indicator
|
||||||
|
export const TimelineTitle = ChakraTimeline.Title
|
||||||
|
export const TimelineDescription = ChakraTimeline.Description
|
||||||
43
src/components/ui/toaster.tsx
Normal file
43
src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Toaster as ChakraToaster,
|
||||||
|
Portal,
|
||||||
|
Spinner,
|
||||||
|
Stack,
|
||||||
|
Toast,
|
||||||
|
createToaster,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
|
||||||
|
export const toaster = createToaster({
|
||||||
|
placement: "bottom-end",
|
||||||
|
pauseOnPageIdle: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Toaster = () => {
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
|
||||||
|
{(toast) => (
|
||||||
|
<Toast.Root width={{ md: "sm" }}>
|
||||||
|
{toast.type === "loading" ? (
|
||||||
|
<Spinner size="sm" color="blue.solid" />
|
||||||
|
) : (
|
||||||
|
<Toast.Indicator />
|
||||||
|
)}
|
||||||
|
<Stack gap="1" flex="1" maxWidth="100%">
|
||||||
|
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||||
|
{toast.description && (
|
||||||
|
<Toast.Description>{toast.description}</Toast.Description>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
{toast.action && (
|
||||||
|
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
||||||
|
)}
|
||||||
|
{toast.meta?.closable && <Toast.CloseTrigger />}
|
||||||
|
</Toast.Root>
|
||||||
|
)}
|
||||||
|
</ChakraToaster>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
src/components/ui/toggle-tip.tsx
Normal file
70
src/components/ui/toggle-tip.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Popover as ChakraPopover, IconButton, Portal } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
import { HiOutlineInformationCircle } from "react-icons/hi"
|
||||||
|
|
||||||
|
export interface ToggleTipProps extends ChakraPopover.RootProps {
|
||||||
|
showArrow?: boolean
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
content?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggleTip = React.forwardRef<HTMLDivElement, ToggleTipProps>(
|
||||||
|
function ToggleTip(props, ref) {
|
||||||
|
const {
|
||||||
|
showArrow,
|
||||||
|
children,
|
||||||
|
portalled = true,
|
||||||
|
content,
|
||||||
|
portalRef,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraPopover.Root
|
||||||
|
{...rest}
|
||||||
|
positioning={{ ...rest.positioning, gutter: 4 }}
|
||||||
|
>
|
||||||
|
<ChakraPopover.Trigger asChild>{children}</ChakraPopover.Trigger>
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraPopover.Positioner>
|
||||||
|
<ChakraPopover.Content
|
||||||
|
width="auto"
|
||||||
|
px="2"
|
||||||
|
py="1"
|
||||||
|
textStyle="xs"
|
||||||
|
rounded="sm"
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{showArrow && (
|
||||||
|
<ChakraPopover.Arrow>
|
||||||
|
<ChakraPopover.ArrowTip />
|
||||||
|
</ChakraPopover.Arrow>
|
||||||
|
)}
|
||||||
|
{content}
|
||||||
|
</ChakraPopover.Content>
|
||||||
|
</ChakraPopover.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</ChakraPopover.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const InfoTip = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
Partial<ToggleTipProps>
|
||||||
|
>(function InfoTip(props, ref) {
|
||||||
|
const { children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ToggleTip content={children} {...rest} ref={ref}>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="info"
|
||||||
|
size="2xs"
|
||||||
|
colorPalette="gray"
|
||||||
|
>
|
||||||
|
<HiOutlineInformationCircle />
|
||||||
|
</IconButton>
|
||||||
|
</ToggleTip>
|
||||||
|
)
|
||||||
|
})
|
||||||
57
src/components/ui/toggle.tsx
Normal file
57
src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { ButtonProps } from "@chakra-ui/react"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Toggle as ChakraToggle,
|
||||||
|
useToggleContext,
|
||||||
|
} from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface ToggleProps extends ChakraToggle.RootProps {
|
||||||
|
variant?: keyof typeof variantMap
|
||||||
|
size?: ButtonProps["size"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantMap = {
|
||||||
|
solid: { on: "solid", off: "outline" },
|
||||||
|
surface: { on: "surface", off: "outline" },
|
||||||
|
subtle: { on: "subtle", off: "ghost" },
|
||||||
|
ghost: { on: "subtle", off: "ghost" },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
||||||
|
function Toggle(props, ref) {
|
||||||
|
const { variant = "subtle", size, children, ...rest } = props
|
||||||
|
const variantConfig = variantMap[variant]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraToggle.Root asChild {...rest}>
|
||||||
|
<ToggleBaseButton size={size} variant={variantConfig} ref={ref}>
|
||||||
|
{children}
|
||||||
|
</ToggleBaseButton>
|
||||||
|
</ChakraToggle.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface ToggleBaseButtonProps extends Omit<ButtonProps, "variant"> {
|
||||||
|
variant: Record<"on" | "off", ButtonProps["variant"]>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleBaseButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ToggleBaseButtonProps
|
||||||
|
>(function ToggleBaseButton(props, ref) {
|
||||||
|
const toggle = useToggleContext()
|
||||||
|
const { variant, ...rest } = props
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={toggle.pressed ? variant.on : variant.off}
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ToggleIndicator = ChakraToggle.Indicator
|
||||||
46
src/components/ui/tooltip.tsx
Normal file
46
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
export interface TooltipProps extends ChakraTooltip.RootProps {
|
||||||
|
showArrow?: boolean
|
||||||
|
portalled?: boolean
|
||||||
|
portalRef?: React.RefObject<HTMLElement>
|
||||||
|
content: React.ReactNode
|
||||||
|
contentProps?: ChakraTooltip.ContentProps
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||||
|
function Tooltip(props, ref) {
|
||||||
|
const {
|
||||||
|
showArrow,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
portalled,
|
||||||
|
content,
|
||||||
|
contentProps,
|
||||||
|
portalRef,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
if (disabled) return children
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraTooltip.Root {...rest}>
|
||||||
|
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||||
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
|
<ChakraTooltip.Positioner>
|
||||||
|
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||||
|
{showArrow && (
|
||||||
|
<ChakraTooltip.Arrow>
|
||||||
|
<ChakraTooltip.ArrowTip />
|
||||||
|
</ChakraTooltip.Arrow>
|
||||||
|
)}
|
||||||
|
{content}
|
||||||
|
</ChakraTooltip.Content>
|
||||||
|
</ChakraTooltip.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</ChakraTooltip.Root>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user