main
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 2m42s

This commit is contained in:
2026-01-27 23:24:17 +03:00
commit dc7ed1c48c
165 changed files with 23798 additions and 0 deletions

BIN
src/components/ui/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,53 @@
'use client';
import { useEffect, useState } from 'react';
import { Icon, IconButton, Presence } from '@chakra-ui/react';
import { FiChevronUp } from 'react-icons/fi';
const BackToTop = () => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsVisible(window.pageYOffset > 300);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};
return (
<Presence
unmountOnExit
present={isVisible}
animationName={{ _open: 'fade-in', _closed: 'fade-out' }}
animationDuration='moderate'
>
<IconButton
variant={{ base: 'solid', _dark: 'subtle' }}
aria-label='Back to top'
position='fixed'
bottom='8'
right='8'
borderRadius='full'
size='lg'
shadow='lg'
zIndex='999'
onClick={scrollToTop}
>
<Icon>
<FiChevronUp />
</Icon>
</IconButton>
</Presence>
);
};
export default BackToTop;

View File

@@ -0,0 +1,33 @@
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>
);
});

View File

@@ -0,0 +1,14 @@
import type { ButtonProps } from '@chakra-ui/react';
import { IconButton as ChakraIconButton } from '@chakra-ui/react';
import * as React from 'react';
import { LuX } from 'react-icons/lu';
export type CloseButtonProps = ButtonProps;
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>
);
});

View File

@@ -0,0 +1,11 @@
'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');

View File

@@ -0,0 +1,44 @@
'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;

View File

@@ -0,0 +1,91 @@
'use client';
import { Combobox as ChakraCombobox, Portal } from '@chakra-ui/react';
import { CloseButton } from '@/components/ui/buttons/close-button';
import * as React from 'react';
interface ComboboxControlProps extends ChakraCombobox.ControlProps {
clearable?: boolean;
}
export const ComboboxControl = React.forwardRef<HTMLDivElement, ComboboxControlProps>(
function ComboboxControl(props, ref) {
const { children, clearable, ...rest } = props;
return (
<ChakraCombobox.Control {...rest} ref={ref}>
{children}
<ChakraCombobox.IndicatorGroup>
{clearable && <ComboboxClearTrigger />}
<ChakraCombobox.Trigger />
</ChakraCombobox.IndicatorGroup>
</ChakraCombobox.Control>
);
},
);
const ComboboxClearTrigger = React.forwardRef<HTMLButtonElement, ChakraCombobox.ClearTriggerProps>(
function ComboboxClearTrigger(props, ref) {
return (
<ChakraCombobox.ClearTrigger asChild {...props} ref={ref}>
<CloseButton size='xs' variant='plain' focusVisibleRing='inside' focusRingWidth='2px' pointerEvents='auto' />
</ChakraCombobox.ClearTrigger>
);
},
);
interface ComboboxContentProps extends ChakraCombobox.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
}
export const ComboboxContent = React.forwardRef<HTMLDivElement, ComboboxContentProps>(
function ComboboxContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props;
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraCombobox.Positioner>
<ChakraCombobox.Content {...rest} ref={ref} />
</ChakraCombobox.Positioner>
</Portal>
);
},
);
export const ComboboxItem = React.forwardRef<HTMLDivElement, ChakraCombobox.ItemProps>(
function ComboboxItem(props, ref) {
const { item, children, ...rest } = props;
return (
<ChakraCombobox.Item key={item.value} item={item} {...rest} ref={ref}>
{children}
<ChakraCombobox.ItemIndicator />
</ChakraCombobox.Item>
);
},
);
export const ComboboxRoot = React.forwardRef<HTMLDivElement, ChakraCombobox.RootProps>(
function ComboboxRoot(props, ref) {
return <ChakraCombobox.Root {...props} ref={ref} positioning={{ sameWidth: true, ...props.positioning }} />;
},
) as ChakraCombobox.RootComponent;
interface ComboboxItemGroupProps extends ChakraCombobox.ItemGroupProps {
label: React.ReactNode;
}
export const ComboboxItemGroup = React.forwardRef<HTMLDivElement, ComboboxItemGroupProps>(
function ComboboxItemGroup(props, ref) {
const { children, label, ...rest } = props;
return (
<ChakraCombobox.ItemGroup {...rest} ref={ref}>
<ChakraCombobox.ItemGroupLabel>{label}</ChakraCombobox.ItemGroupLabel>
{children}
</ChakraCombobox.ItemGroup>
);
},
);
export const ComboboxLabel = ChakraCombobox.Label;
export const ComboboxInput = ChakraCombobox.Input;
export const ComboboxEmpty = ChakraCombobox.Empty;
export const ComboboxItemText = ChakraCombobox.ItemText;

View File

@@ -0,0 +1,28 @@
'use client';
import { Listbox as ChakraListbox } from '@chakra-ui/react';
import * as React from 'react';
export const ListboxRoot = React.forwardRef<HTMLDivElement, ChakraListbox.RootProps>(function ListboxRoot(props, ref) {
return <ChakraListbox.Root {...props} ref={ref} />;
}) as ChakraListbox.RootComponent;
export const ListboxContent = React.forwardRef<HTMLDivElement, ChakraListbox.ContentProps>(
function ListboxContent(props, ref) {
return <ChakraListbox.Content {...props} ref={ref} />;
},
);
export const ListboxItem = React.forwardRef<HTMLDivElement, ChakraListbox.ItemProps>(function ListboxItem(props, ref) {
const { children, ...rest } = props;
return (
<ChakraListbox.Item {...rest} ref={ref}>
{children}
<ChakraListbox.ItemIndicator />
</ChakraListbox.Item>
);
});
export const ListboxLabel = ChakraListbox.Label;
export const ListboxItemText = ChakraListbox.ItemText;
export const ListboxEmpty = ChakraListbox.Empty;

View File

@@ -0,0 +1,118 @@
'use client';
import type { CollectionItem } from '@chakra-ui/react';
import { Select as ChakraSelect, Portal } from '@chakra-ui/react';
import { CloseButton } from '../buttons/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 | null>;
}
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;

View File

@@ -0,0 +1,60 @@
'use client';
import { TreeView as ChakraTreeView } from '@chakra-ui/react';
import * as React from 'react';
export const TreeViewRoot = React.forwardRef<HTMLDivElement, ChakraTreeView.RootProps>(
function TreeViewRoot(props, ref) {
return <ChakraTreeView.Root {...props} ref={ref} />;
},
);
interface TreeViewTreeProps extends ChakraTreeView.TreeProps {}
export const TreeViewTree = React.forwardRef<HTMLDivElement, TreeViewTreeProps>(function TreeViewTree(props, ref) {
const { ...rest } = props;
return <ChakraTreeView.Tree {...rest} ref={ref} />;
});
export const TreeViewBranch = React.forwardRef<HTMLDivElement, ChakraTreeView.BranchProps>(
function TreeViewBranch(props, ref) {
return <ChakraTreeView.Branch {...props} ref={ref} />;
},
);
export const TreeViewBranchControl = React.forwardRef<HTMLDivElement, ChakraTreeView.BranchControlProps>(
function TreeViewBranchControl(props, ref) {
return <ChakraTreeView.BranchControl {...props} ref={ref} />;
},
);
export const TreeViewItem = React.forwardRef<HTMLDivElement, ChakraTreeView.ItemProps>(
function TreeViewItem(props, ref) {
return <ChakraTreeView.Item {...props} ref={ref} />;
},
);
export const TreeViewLabel = ChakraTreeView.Label;
export const TreeViewBranchIndicator = ChakraTreeView.BranchIndicator;
export const TreeViewBranchText = ChakraTreeView.BranchText;
export const TreeViewBranchContent = ChakraTreeView.BranchContent;
export const TreeViewBranchIndentGuide = ChakraTreeView.BranchIndentGuide;
export const TreeViewItemText = ChakraTreeView.ItemText;
export const TreeViewNode = ChakraTreeView.Node;
export const TreeViewNodeProvider = ChakraTreeView.NodeProvider;
export const TreeView = {
Root: TreeViewRoot,
Label: TreeViewLabel,
Tree: TreeViewTree,
Branch: TreeViewBranch,
BranchControl: TreeViewBranchControl,
BranchIndicator: TreeViewBranchIndicator,
BranchText: TreeViewBranchText,
BranchContent: TreeViewBranchContent,
BranchIndentGuide: TreeViewBranchIndentGuide,
Item: TreeViewItem,
ItemText: TreeViewItemText,
Node: TreeViewNode,
NodeProvider: TreeViewNodeProvider,
};

View File

@@ -0,0 +1,108 @@
'use client';
import type { IconButtonProps, SpanProps } from '@chakra-ui/react';
import { ClientOnly, IconButton, Skeleton, Span } 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 type ColorMode = 'light' | 'dark';
export interface UseColorModeReturn {
colorMode: ColorMode;
setColorMode: (colorMode: ColorMode) => void;
toggleColorMode: () => void;
}
export function useColorMode(): UseColorModeReturn {
const { resolvedTheme, setTheme, forcedTheme } = useTheme();
const colorMode = forcedTheme || resolvedTheme;
const toggleColorMode = () => {
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
};
return {
colorMode: colorMode as ColorMode,
setColorMode: setTheme,
toggleColorMode,
};
}
export function useColorModeValue<T>(light: T, dark: T) {
const { colorMode } = useColorMode();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
if (!mounted) {
return light;
}
return colorMode === 'dark' ? dark : light;
}
export function ColorModeIcon() {
const { colorMode } = useColorMode();
return colorMode === 'dark' ? <LuMoon /> : <LuSun />;
}
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='9' />}>
<IconButton
onClick={toggleColorMode}
variant='ghost'
aria-label='Toggle color mode'
size='sm'
ref={ref}
{...props}
css={{
_icon: {
width: '5',
height: '5',
},
}}
>
<ColorModeIcon />
</IconButton>
</ClientOnly>
);
},
);
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(function LightMode(props, ref) {
return (
<Span
color='fg'
display='contents'
className='chakra-theme light'
colorPalette='gray'
colorScheme='light'
ref={ref}
{...props}
/>
);
});
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(function DarkMode(props, ref) {
return (
<Span
color='fg'
display='contents'
className='chakra-theme dark'
colorPalette='gray'
colorScheme='dark'
ref={ref}
{...props}
/>
);
});

View File

@@ -0,0 +1,26 @@
import { Avatar as ChakraAvatar, AvatarGroup as ChakraAvatarGroup } 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}>
<ChakraAvatar.Fallback name={name}>{icon || fallback}</ChakraAvatar.Fallback>
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
{children}
</ChakraAvatar.Root>
);
});
export const AvatarGroup = ChakraAvatarGroup;

View File

@@ -0,0 +1,79 @@
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;

View File

@@ -0,0 +1,26 @@
import { DataList as ChakraDataList } from '@chakra-ui/react';
import { InfoTip } from '@/components/ui/overlays/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>
);
});

View File

@@ -0,0 +1,20 @@
import { QrCode as ChakraQrCode } from '@chakra-ui/react';
import * as React from 'react';
export interface QrCodeProps extends Omit<ChakraQrCode.RootProps, 'fill' | 'overlay'> {
fill?: string;
overlay?: React.ReactNode;
}
export const QrCode = React.forwardRef<HTMLDivElement, QrCodeProps>(function QrCode(props, ref) {
const { children, fill, overlay, ...rest } = props;
return (
<ChakraQrCode.Root ref={ref} {...rest}>
<ChakraQrCode.Frame style={{ fill }}>
<ChakraQrCode.Pattern />
</ChakraQrCode.Frame>
{children}
{overlay && <ChakraQrCode.Overlay>{overlay}</ChakraQrCode.Overlay>}
</ChakraQrCode.Root>
);
});

View File

@@ -0,0 +1,53 @@
import { Badge, type BadgeProps, Stat as ChakraStat, FormatNumber } from '@chakra-ui/react';
import { InfoTip } from '@/components/ui/overlays/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;

View File

@@ -0,0 +1,26 @@
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>
);
});

View File

@@ -0,0 +1,25 @@
import { Timeline as ChakraTimeline } from '@chakra-ui/react';
import * as React from 'react';
interface TimelineConnectorProps extends ChakraTimeline.IndicatorProps {
icon?: React.ReactNode;
}
export const TimelineConnector = React.forwardRef<HTMLDivElement, TimelineConnectorProps>(function TimelineConnector(
{ icon, ...props },
ref,
) {
return (
<ChakraTimeline.Connector ref={ref}>
<ChakraTimeline.Separator />
<ChakraTimeline.Indicator {...props}>{icon}</ChakraTimeline.Indicator>
</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;

View File

@@ -0,0 +1,45 @@
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;

View File

@@ -0,0 +1,35 @@
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;

View File

@@ -0,0 +1,182 @@
'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 '@/components/ui/buttons/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} - ${Math.min(pageRange.end, count)} of ${count}`;
}, [format, page, totalPages, pageRange, count]);
return (
<Text fontWeight='medium' ref={ref} {...rest}>
{content}
</Text>
);
},
);

View File

@@ -0,0 +1,73 @@
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;
disableTrigger?: boolean;
}
export const StepsItem = React.forwardRef<HTMLDivElement, StepsItemProps>(function StepsItem(props, ref) {
const { title, description, completedIcon, icon, disableTrigger, ...rest } = props;
return (
<ChakraSteps.Item {...rest} ref={ref}>
<ChakraSteps.Trigger disabled={disableTrigger}>
<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;

View File

@@ -0,0 +1,27 @@
import { Alert as ChakraAlert } from '@chakra-ui/react';
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;
}
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(props, ref) {
const { title, children, icon, 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}
</ChakraAlert.Root>
);
});

View File

@@ -0,0 +1,28 @@
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>
);
});

View File

@@ -0,0 +1,32 @@
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;

View File

@@ -0,0 +1,30 @@
import { Progress as ChakraProgress } from '@chakra-ui/react';
import { InfoTip } from '../overlays/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;

View File

@@ -0,0 +1,35 @@
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;

View File

@@ -0,0 +1,27 @@
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>
);
});

View File

@@ -0,0 +1,28 @@
'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.closable && <Toast.CloseTrigger />}
</Toast.Root>
)}
</ChakraToaster>
</Portal>
);
};

View File

@@ -0,0 +1,49 @@
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;

View File

@@ -0,0 +1,19 @@
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.RefObject<HTMLLabelElement | null>;
}
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>
);
});

View File

@@ -0,0 +1,174 @@
import type { IconButtonProps, StackProps } from '@chakra-ui/react';
import { ColorPicker as ChakraColorPicker, For, IconButton, Portal, Span, Stack, Text, VStack } from '@chakra-ui/react';
import * as React from 'react';
import { LuCheck, LuPipette } from 'react-icons/lu';
export const ColorPickerTrigger = React.forwardRef<
HTMLButtonElement,
ChakraColorPicker.TriggerProps & { fitContent?: boolean }
>(function ColorPickerTrigger(props, ref) {
const { fitContent, ...rest } = props;
return (
<ChakraColorPicker.Trigger data-fit-content={fitContent || undefined} ref={ref} {...rest}>
{props.children || <ChakraColorPicker.ValueSwatch />}
</ChakraColorPicker.Trigger>
);
});
export const ColorPickerInput = React.forwardRef<
HTMLInputElement,
Omit<ChakraColorPicker.ChannelInputProps, 'channel'>
>(function ColorHexInput(props, ref) {
return <ChakraColorPicker.ChannelInput channel='hex' ref={ref} {...props} />;
});
interface ColorPickerContentProps extends ChakraColorPicker.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
}
export const ColorPickerContent = React.forwardRef<HTMLDivElement, ColorPickerContentProps>(
function ColorPickerContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props;
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraColorPicker.Positioner>
<ChakraColorPicker.Content ref={ref} {...rest} />
</ChakraColorPicker.Positioner>
</Portal>
);
},
);
export const ColorPickerInlineContent = React.forwardRef<HTMLDivElement, ChakraColorPicker.ContentProps>(
function ColorPickerInlineContent(props, ref) {
return <ChakraColorPicker.Content animation='none' shadow='none' padding='0' ref={ref} {...props} />;
},
);
export const ColorPickerSliders = React.forwardRef<HTMLDivElement, StackProps>(function ColorPickerSliders(props, ref) {
return (
<Stack gap='1' flex='1' px='1' ref={ref} {...props}>
<ColorPickerChannelSlider channel='hue' />
<ColorPickerChannelSlider channel='alpha' />
</Stack>
);
});
export const ColorPickerArea = React.forwardRef<HTMLDivElement, ChakraColorPicker.AreaProps>(
function ColorPickerArea(props, ref) {
return (
<ChakraColorPicker.Area ref={ref} {...props}>
<ChakraColorPicker.AreaBackground />
<ChakraColorPicker.AreaThumb />
</ChakraColorPicker.Area>
);
},
);
export const ColorPickerEyeDropper = React.forwardRef<HTMLButtonElement, IconButtonProps>(
function ColorPickerEyeDropper(props, ref) {
return (
<ChakraColorPicker.EyeDropperTrigger asChild>
<IconButton size='xs' variant='outline' ref={ref} {...props}>
<LuPipette />
</IconButton>
</ChakraColorPicker.EyeDropperTrigger>
);
},
);
export const ColorPickerChannelSlider = React.forwardRef<HTMLDivElement, ChakraColorPicker.ChannelSliderProps>(
function ColorPickerSlider(props, ref) {
return (
<ChakraColorPicker.ChannelSlider ref={ref} {...props}>
<ChakraColorPicker.TransparencyGrid size='0.6rem' />
<ChakraColorPicker.ChannelSliderTrack />
<ChakraColorPicker.ChannelSliderThumb />
</ChakraColorPicker.ChannelSlider>
);
},
);
export const ColorPickerSwatchTrigger = React.forwardRef<
HTMLButtonElement,
ChakraColorPicker.SwatchTriggerProps & {
swatchSize?: ChakraColorPicker.SwatchTriggerProps['boxSize'];
}
>(function ColorPickerSwatchTrigger(props, ref) {
const { swatchSize, children, ...rest } = props;
return (
<ChakraColorPicker.SwatchTrigger ref={ref} style={{ ['--color' as string]: props.value }} {...rest}>
{children || (
<ChakraColorPicker.Swatch boxSize={swatchSize} value={props.value}>
<ChakraColorPicker.SwatchIndicator>
<LuCheck />
</ChakraColorPicker.SwatchIndicator>
</ChakraColorPicker.Swatch>
)}
</ChakraColorPicker.SwatchTrigger>
);
});
export const ColorPickerRoot = React.forwardRef<HTMLDivElement, ChakraColorPicker.RootProps>(
function ColorPickerRoot(props, ref) {
return (
<ChakraColorPicker.Root ref={ref} {...props}>
{props.children}
<ChakraColorPicker.HiddenInput tabIndex={-1} />
</ChakraColorPicker.Root>
);
},
);
const formatMap = {
rgba: ['red', 'green', 'blue', 'alpha'],
hsla: ['hue', 'saturation', 'lightness', 'alpha'],
hsba: ['hue', 'saturation', 'brightness', 'alpha'],
hexa: ['hex', 'alpha'],
} as const;
export const ColorPickerChannelInputs = React.forwardRef<HTMLDivElement, ChakraColorPicker.ViewProps>(
function ColorPickerChannelInputs(props, ref) {
const channels = formatMap[props.format];
return (
<ChakraColorPicker.View flexDirection='row' ref={ref} {...props}>
{channels.map((channel) => (
<VStack gap='1' key={channel} flex='1'>
<ColorPickerChannelInput channel={channel} px='0' height='7' textStyle='xs' textAlign='center' />
<Text textStyle='xs' color='fg.muted' fontWeight='medium'>
{channel.charAt(0).toUpperCase()}
</Text>
</VStack>
))}
</ChakraColorPicker.View>
);
},
);
export const ColorPickerChannelSliders = React.forwardRef<HTMLDivElement, ChakraColorPicker.ViewProps>(
function ColorPickerChannelSliders(props, ref) {
const channels = formatMap[props.format];
return (
<ChakraColorPicker.View {...props} ref={ref}>
<For each={channels}>
{(channel) => (
<Stack gap='1' key={channel}>
<Span textStyle='xs' minW='5ch' textTransform='capitalize' fontWeight='medium'>
{channel}
</Span>
<ColorPickerChannelSlider channel={channel} />
</Stack>
)}
</For>
</ChakraColorPicker.View>
);
},
);
export const ColorPickerLabel = ChakraColorPicker.Label;
export const ColorPickerControl = ChakraColorPicker.Control;
export const ColorPickerValueText = ChakraColorPicker.ValueText;
export const ColorPickerValueSwatch = ChakraColorPicker.ValueSwatch;
export const ColorPickerChannelInput = ChakraColorPicker.ChannelInput;
export const ColorPickerSwatchGroup = ChakraColorPicker.SwatchGroup;

View File

@@ -0,0 +1,26 @@
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>
);
});

View File

@@ -0,0 +1,150 @@
'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;
export const FileUploadFileText = ChakraFileUpload.FileText;

View 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<InputElementProps>;
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;
const child = React.Children.only<React.ReactElement<InputElementProps>>(children);
return (
<Group ref={ref} {...rest}>
{startElement && (
<InputElement pointerEvents='none' {...startElementProps}>
{startElement}
</InputElement>
)}
{React.cloneElement(child, {
...(startElement && {
ps: `calc(var(--input-height) - ${startOffset})`,
}),
...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }),
...children.props,
})}
{endElement && (
<InputElement placement='end' {...endElementProps}>
{endElement}
</InputElement>
)}
</Group>
);
});

View File

@@ -0,0 +1,52 @@
'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 NativeSelectFieldProps extends Select.FieldProps {
items?: Array<string | NativeSelectItem>;
}
export const NativeSelectField = React.forwardRef<HTMLSelectElement, NativeSelectFieldProps>(
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>
);
},
);

View File

@@ -0,0 +1,21 @@
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 NumberInputScrubber = ChakraNumberInput.Scrubber;
export const NumberInputLabel = ChakraNumberInput.Label;

View File

@@ -0,0 +1,136 @@
'use client';
import type { ButtonProps, GroupProps, InputProps, StackProps } from '@chakra-ui/react';
import { Box, HStack, IconButton, Input, InputGroup, Stack, mergeRefs, useControllableState } from '@chakra-ui/react';
import { useTranslations } from 'next-intl';
import * as React from 'react';
import { LuEye, LuEyeOff } from 'react-icons/lu';
export interface PasswordVisibilityProps {
/**
* The default visibility state of the password input.
*/
defaultVisible?: boolean;
/**
* The controlled visibility state of the password input.
*/
visible?: boolean;
/**
* Callback invoked when the visibility state changes.
*/
onVisibleChange?: (visible: boolean) => void;
/**
* Custom icons for the visibility toggle button.
*/
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
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'
borderRadius='full'
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 t = useTranslations();
function getColorPalette(percent: number) {
switch (true) {
case percent < 33:
return { label: t('low'), colorPalette: 'red' };
case percent < 66:
return { label: t('medium'), colorPalette: 'orange' };
default:
return { label: t('high'), colorPalette: 'green' };
}
}
const percent = (value / max) * 100;
const { label, colorPalette } = getColorPalette(percent);
return (
<Stack align='flex-end' gap='1' ref={ref} {...rest}>
<HStack width='full' {...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>
);
},
);

View File

@@ -0,0 +1,25 @@
import { PinInput as ChakraPinInput, Group } from '@chakra-ui/react';
import * as React from 'react';
export interface PinInputProps extends ChakraPinInput.RootProps {
rootRef?: React.RefObject<HTMLDivElement | null>;
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>
);
});

View File

@@ -0,0 +1,51 @@
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;

View File

@@ -0,0 +1,20 @@
import { RadioGroup as ChakraRadioGroup } from '@chakra-ui/react';
import * as React from 'react';
export interface RadioProps extends ChakraRadioGroup.ItemProps {
rootRef?: React.RefObject<HTMLDivElement | null>;
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;

View File

@@ -0,0 +1,25 @@
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>
);
});

View File

@@ -0,0 +1,42 @@
'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>
);
},
);

View File

@@ -0,0 +1,78 @@
import { Slider as ChakraSlider, For, 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;
thumb?: React.ReactNode;
}
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(function Slider(props, ref) {
const { marks: marksProp, label, showValue, thumb, ...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>{label}</ChakraSlider.Label>}
{label && showValue && (
<HStack justify='space-between'>
<ChakraSlider.Label>{label}</ChakraSlider.Label>
<ChakraSlider.ValueText />
</HStack>
)}
<ChakraSlider.Control data-has-mark-label={hasMarkLabel || undefined}>
<ChakraSlider.Track>
<ChakraSlider.Range />
</ChakraSlider.Track>
<SliderThumbs value={value} thumb={thumb} />
<SliderMarks marks={marks} />
</ChakraSlider.Control>
</ChakraSlider.Root>
);
});
function SliderThumbs(props: { value?: number[]; thumb?: React.ReactNode }) {
const { value, thumb } = props;
return (
<For each={value}>
{(_, index) => (
<ChakraSlider.Thumb key={index} index={index}>
<ChakraSlider.HiddenInput />
{thumb}
</ChakraSlider.Thumb>
)}
</For>
);
}
interface SliderMarksProps {
marks?: Array<number | { value: number; label: React.ReactNode }>;
}
const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>(function SliderMarks(props, ref) {
const { marks } = props;
if (!marks?.length) return null;
return (
<ChakraSlider.MarkerGroup ref={ref}>
{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>
);
});

View File

@@ -0,0 +1,45 @@
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>
);
},
);

View File

@@ -0,0 +1,28 @@
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.RefObject<HTMLLabelElement | null>;
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>
);
});

View File

@@ -0,0 +1,67 @@
'use client';
import React, { useTransition } from 'react';
import { Locale, useLocale } from 'next-intl';
import {
SelectContent,
SelectItem,
SelectRoot,
SelectTrigger,
SelectValueText,
} from '@/components/ui/collections/select';
import { useParams } from 'next/navigation';
import { createListCollection } from '@chakra-ui/react';
import { usePathname, useRouter } from '@/i18n/navigation';
const LocaleSwitcher = () => {
const locale = useLocale();
const [isPending, startTransition] = useTransition();
const router = useRouter();
const pathname = usePathname();
const params = useParams();
const collections = createListCollection({
items: [
{ label: 'English', value: 'en' },
{ label: 'Türkçe', value: 'tr' },
],
});
function onSelectChange({ value }: { value: string[] }) {
const nextLocale = value.at(0) as Locale;
startTransition(() => {
router.replace(
// @ts-expect-error -- TypeScript will validate that only known `params`
// are used in combination with a given `pathname`. Since the two will
// always match for the current route, we can skip runtime checks.
{ pathname, params },
{ locale: nextLocale },
);
});
}
return (
<SelectRoot
disabled={isPending}
value={[locale]}
onValueChange={onSelectChange}
w={{ base: 'full', lg: '24' }}
size='sm'
variant='outline'
borderRadius='md'
collection={collections}
>
<SelectTrigger>
<SelectValueText placeholder='Select a language' />
</SelectTrigger>
<SelectContent zIndex='9999'>
{collections.items.map((collection) => (
<SelectItem key={collection.value} item={collection}>
{collection.label}
</SelectItem>
))}
</SelectContent>
</SelectRoot>
);
};
export default LocaleSwitcher;

View File

@@ -0,0 +1,38 @@
import { ActionBar, Portal } from '@chakra-ui/react';
import { CloseButton } from '@/components/ui/buttons/close-button';
import * as React from 'react';
interface ActionBarContentProps extends ActionBar.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
}
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;

View File

@@ -0,0 +1,46 @@
import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react';
import { CloseButton } from '@/components/ui/buttons/close-button';
import * as React from 'react';
interface DialogContentProps extends ChakraDialog.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
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;

View File

@@ -0,0 +1,42 @@
import { Drawer as ChakraDrawer, Portal } from '@chakra-ui/react';
import { CloseButton } from '@/components/ui/buttons/close-button';
import * as React from 'react';
interface DrawerContentProps extends ChakraDrawer.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
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;

View File

@@ -0,0 +1,34 @@
import { HoverCard, Portal } from '@chakra-ui/react';
import * as React from 'react';
interface HoverCardContentProps extends HoverCard.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
}
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;

View File

@@ -0,0 +1,99 @@
'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 | null>;
}
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 ps='8' ref={ref} {...props}>
<AbsoluteCenter axis='horizontal' insetStart='4' asChild>
<ChakraMenu.ItemIndicator>
<LuCheck />
</ChakraMenu.ItemIndicator>
</AbsoluteCenter>
{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' insetStart='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;

View File

@@ -0,0 +1,49 @@
import { Popover as ChakraPopover, Portal } from '@chakra-ui/react';
import { CloseButton } from '../buttons/close-button';
import * as React from 'react';
interface PopoverContentProps extends ChakraPopover.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
}
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;

View File

@@ -0,0 +1,48 @@
import { Popover as ChakraPopover, IconButton, type IconButtonProps, 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 | null>;
content?: React.ReactNode;
contentProps?: ChakraPopover.ContentProps;
}
export const ToggleTip = React.forwardRef<HTMLDivElement, ToggleTipProps>(function ToggleTip(props, ref) {
const { showArrow, children, portalled = true, content, contentProps, 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} {...contentProps}>
{showArrow && (
<ChakraPopover.Arrow>
<ChakraPopover.ArrowTip />
</ChakraPopover.Arrow>
)}
{content}
</ChakraPopover.Content>
</ChakraPopover.Positioner>
</Portal>
</ChakraPopover.Root>
);
});
export interface InfoTipProps extends Partial<ToggleTipProps> {
buttonProps?: IconButtonProps | undefined;
}
export const InfoTip = React.forwardRef<HTMLDivElement, InfoTipProps>(function InfoTip(props, ref) {
const { children, buttonProps, ...rest } = props;
return (
<ToggleTip content={children} {...rest} ref={ref}>
<IconButton variant='ghost' aria-label='info' size='2xs' colorPalette='gray' {...buttonProps}>
<HiOutlineInformationCircle />
</IconButton>
</ToggleTip>
);
});

View File

@@ -0,0 +1,35 @@
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 | null>;
content: React.ReactNode;
contentProps?: ChakraTooltip.ContentProps;
disabled?: boolean;
}
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(function Tooltip(props, ref) {
const { showArrow, children, disabled, portalled = true, 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>
);
});

View File

@@ -0,0 +1,20 @@
"use client";
import { ChakraProvider } from "@chakra-ui/react";
import { SessionProvider } from "next-auth/react";
import { ColorModeProvider, type ColorModeProviderProps } from "./color-mode";
import { system } from "../../theme/theme";
import { Toaster } from "./feedback/toaster";
import TopLoader from "./top-loader";
export function Provider(props: ColorModeProviderProps) {
return (
<SessionProvider>
<ChakraProvider value={system}>
<TopLoader />
<ColorModeProvider {...props} />
<Toaster />
</ChakraProvider>
</SessionProvider>
);
}

View File

@@ -0,0 +1,10 @@
'use client';
import NextTopLoader from 'nextjs-toploader';
import { useToken } from '@chakra-ui/react';
export default function TopLoader() {
const [color] = useToken('colors', ['primary.500']);
return <NextTopLoader color={color} showSpinner={false} />;
}

View File

@@ -0,0 +1,27 @@
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 ? <>&mdash;</> : null} <cite>{cite}</cite>
</ChakraBlockquote.Caption>
)}
</ChakraBlockquote.Root>
);
});
export const BlockquoteIcon = ChakraBlockquote.Icon;

View File

@@ -0,0 +1,275 @@
'use client';
import { chakra } from '@chakra-ui/react';
const TRAILING_PSEUDO_REGEX = /(::?[\w-]+(?:\([^)]*\))?)+$/;
const EXCLUDE_CLASSNAME = '.not-prose';
function inWhere<T extends string>(selector: T): T {
const rebuiltSelector = selector.startsWith('& ') ? selector.slice(2) : selector;
const match = selector.match(TRAILING_PSEUDO_REGEX);
const pseudo = match ? match[0] : '';
const base = match ? selector.slice(0, -match[0].length) : rebuiltSelector;
return `& :where(${base}):not(${EXCLUDE_CLASSNAME}, ${EXCLUDE_CLASSNAME} *)${pseudo}` as T;
}
export const Prose = chakra('div', {
base: {
color: 'fg.muted',
maxWidth: '65ch',
fontSize: 'sm',
lineHeight: '1.7em',
[inWhere('& p')]: {
marginTop: '1em',
marginBottom: '1em',
},
[inWhere('& blockquote')]: {
marginTop: '1.285em',
marginBottom: '1.285em',
paddingInline: '1.285em',
borderInlineStartWidth: '0.25em',
color: 'fg',
},
[inWhere('& a')]: {
color: 'fg',
textDecoration: 'underline',
textUnderlineOffset: '3px',
textDecorationThickness: '2px',
textDecorationColor: 'border.muted',
fontWeight: '500',
},
[inWhere('& strong')]: {
fontWeight: '600',
},
[inWhere('& a strong')]: {
color: 'inherit',
},
[inWhere('& h1')]: {
fontSize: '2.15em',
letterSpacing: '-0.02em',
marginTop: '0',
marginBottom: '0.8em',
lineHeight: '1.2em',
},
[inWhere('& h2')]: {
fontSize: '1.4em',
letterSpacing: '-0.02em',
marginTop: '1.6em',
marginBottom: '0.8em',
lineHeight: '1.4em',
},
[inWhere('& h3')]: {
fontSize: '1.285em',
letterSpacing: '-0.01em',
marginTop: '1.5em',
marginBottom: '0.4em',
lineHeight: '1.5em',
},
[inWhere('& h4')]: {
marginTop: '1.4em',
marginBottom: '0.5em',
letterSpacing: '-0.01em',
lineHeight: '1.5em',
},
[inWhere('& img')]: {
marginTop: '1.7em',
marginBottom: '1.7em',
borderRadius: 'lg',
boxShadow: 'inset',
},
[inWhere('& picture')]: {
marginTop: '1.7em',
marginBottom: '1.7em',
},
[inWhere('& picture > img')]: {
marginTop: '0',
marginBottom: '0',
},
[inWhere('& video')]: {
marginTop: '1.7em',
marginBottom: '1.7em',
},
[inWhere('& 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)',
},
[inWhere('& code')]: {
fontSize: '0.925em',
letterSpacing: '-0.01em',
borderRadius: 'md',
borderWidth: '1px',
padding: '0.25em',
},
[inWhere('& pre code')]: {
fontSize: 'inherit',
letterSpacing: 'inherit',
borderWidth: 'inherit',
padding: '0',
},
[inWhere('& h2 code')]: {
fontSize: '0.9em',
},
[inWhere('& h3 code')]: {
fontSize: '0.8em',
},
[inWhere('& 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',
},
[inWhere('& ol')]: {
marginTop: '1em',
marginBottom: '1em',
paddingInlineStart: '1.5em',
},
[inWhere('& ul')]: {
marginTop: '1em',
marginBottom: '1em',
paddingInlineStart: '1.5em',
},
[inWhere('& li')]: {
marginTop: '0.285em',
marginBottom: '0.285em',
},
[inWhere('& ol > li')]: {
paddingInlineStart: '0.4em',
listStyleType: 'decimal',
'&::marker': {
color: 'fg.muted',
},
},
[inWhere('& ul > li')]: {
paddingInlineStart: '0.4em',
listStyleType: 'disc',
'&::marker': {
color: 'fg.muted',
},
},
[inWhere('& > ul > li p')]: {
marginTop: '0.5em',
marginBottom: '0.5em',
},
[inWhere('& > ul > li > p:first-of-type')]: {
marginTop: '1em',
},
[inWhere('& > ul > li > p:last-of-type')]: {
marginBottom: '1em',
},
[inWhere('& > ol > li > p:first-of-type')]: {
marginTop: '1em',
},
[inWhere('& > ol > li > p:last-of-type')]: {
marginBottom: '1em',
},
[inWhere('& ul ul, ul ol, ol ul, ol ol')]: {
marginTop: '0.5em',
marginBottom: '0.5em',
},
[inWhere('& dl')]: {
marginTop: '1em',
marginBottom: '1em',
},
[inWhere('& dt')]: {
fontWeight: '600',
marginTop: '1em',
},
[inWhere('& dd')]: {
marginTop: '0.285em',
paddingInlineStart: '1.5em',
},
[inWhere('& hr')]: {
marginTop: '2.25em',
marginBottom: '2.25em',
},
[inWhere('& :is(h1,h2,h3,h4,h5,hr) + *')]: {
marginTop: '0',
},
[inWhere('& table')]: {
width: '100%',
tableLayout: 'auto',
textAlign: 'start',
lineHeight: '1.5em',
marginTop: '2em',
marginBottom: '2em',
},
[inWhere('& thead')]: {
borderBottomWidth: '1px',
color: 'fg',
},
[inWhere('& tbody tr')]: {
borderBottomWidth: '1px',
borderBottomColor: 'border',
},
[inWhere('& thead th')]: {
paddingInlineEnd: '1em',
paddingBottom: '0.65em',
paddingInlineStart: '1em',
fontWeight: 'medium',
textAlign: 'start',
},
[inWhere('& thead th:first-of-type')]: {
paddingInlineStart: '0',
},
[inWhere('& thead th:last-of-type')]: {
paddingInlineEnd: '0',
},
[inWhere('& tbody td, tfoot td')]: {
paddingTop: '0.65em',
paddingInlineEnd: '1em',
paddingBottom: '0.65em',
paddingInlineStart: '1em',
},
[inWhere('& tbody td:first-of-type, tfoot td:first-of-type')]: {
paddingInlineStart: '0',
},
[inWhere('& tbody td:last-of-type, tfoot td:last-of-type')]: {
paddingInlineEnd: '0',
},
[inWhere('& figure')]: {
marginTop: '1.625em',
marginBottom: '1.625em',
},
[inWhere('& figure > *')]: {
marginTop: '0',
marginBottom: '0',
},
[inWhere('& figcaption')]: {
fontSize: '0.85em',
lineHeight: '1.25em',
marginTop: '0.85em',
color: 'fg.muted',
},
[inWhere('& h1, h2, h3, h4')]: {
color: 'fg',
fontWeight: '600',
},
},
variants: {
size: {
md: {
fontSize: 'sm',
},
lg: {
fontSize: 'md',
},
},
},
defaultVariants: {
size: 'md',
},
});