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:
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