first commit
CI / typecheck (push) Successful in 1m8s
CI / format (push) Failing after 1m6s
CI / lint (push) Failing after 49s
CI / test (push) Failing after 1m8s

This commit is contained in:
SDI
2026-07-15 18:05:12 +09:00
commit 12e4f17b62
4633 changed files with 817125 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"ui": "src/components",
"utils": "@/lib/utils"
}
}
+11
View File
@@ -0,0 +1,11 @@
import baseConfig from '@ufb/eslint-config/base';
import reactConfig from '@ufb/eslint-config/react';
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: ['postcss.js'],
},
...baseConfig,
...reactConfig,
];
+59
View File
@@ -0,0 +1,59 @@
{
"name": "@ufb/react",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"clean": "git clean -xdf .cache .turbo dist node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@remixicon/react": "^4.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.577.0",
"react-day-picker": "8.10.1",
"react-hook-form": "^7.72.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@ufb/eslint-config": "workspace:^",
"@ufb/prettier-config": "workspace:^",
"@ufb/tailwindcss": "workspace:^",
"@ufb/tsconfig": "workspace:^",
"eslint": "catalog:",
"prettier": "catalog:",
"react": "^19.2.4",
"tailwindcss": "catalog:",
"typescript": "catalog:"
},
"prettier": "@ufb/prettier-config"
}
+1
View File
@@ -0,0 +1 @@
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } };
@@ -0,0 +1,179 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { ICON_SIZE } from '../constants';
import { cn } from '../lib/utils';
import type { Size } from '../types';
import { Icon } from './icon';
const DefaultValue = {
iconSize: 'small',
iconAlign: 'right',
border: false,
} as const;
const AccordionContext = React.createContext<{
iconSize: Size;
iconAlign: 'left' | 'right';
}>({
iconSize: DefaultValue.iconSize,
iconAlign: DefaultValue.iconAlign,
});
const Accordion = React.forwardRef<
React.ComponentRef<typeof AccordionPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root> & {
iconSize?: Size;
iconAlign?: 'left' | 'right';
border?: boolean;
}
>(
(
{
iconAlign = DefaultValue.iconAlign,
iconSize = DefaultValue.iconSize,
border = DefaultValue.border,
className,
...props
},
ref,
) => (
<AccordionContext.Provider value={{ iconAlign, iconSize }}>
<AccordionPrimitive.Root
ref={ref}
className={cn('accordion', border && 'accordion-border', className)}
{...props}
/>
</AccordionContext.Provider>
),
);
Accordion.displayName = 'Accordion';
const AccordionItem = React.forwardRef<
React.ComponentRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> & {
divider?: boolean;
}
>(({ divider = true, className, ...props }, ref) => {
return (
<AccordionPrimitive.Item
ref={ref}
className={cn(
'accordion-item',
divider && 'accordion-item-border',
className,
)}
{...props}
/>
);
});
AccordionItem.displayName = 'AccordionItem';
const AccordionTrigger = React.forwardRef<
React.ComponentRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => {
const { iconSize, iconAlign } = React.useContext(AccordionContext);
return (
<AccordionPrimitive.Header>
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'accordion-trigger',
iconAlign === 'left' && 'accordion-trigger-align-left',
className,
)}
{...props}
>
{iconAlign === 'left' && (
<Icon name="RiArrowDownSLine" size={ICON_SIZE[iconSize]} />
)}
<Slottable>{children}</Slottable>
{iconAlign === 'right' && (
<Icon name="RiArrowDownSLine" size={ICON_SIZE[iconSize]} />
)}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
});
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const accordionContentVariants = cva('accordion-content', {
variants: {
iconAlign: {
left: '',
right: '',
},
iconSize: {
small: '',
medium: '',
large: '',
},
},
compoundVariants: [
{
iconAlign: 'left',
iconSize: 'small',
className: 'accordion-content-inset-small',
},
{
iconAlign: 'left',
iconSize: 'medium',
className: 'accordion-content-inset-medium',
},
{
iconAlign: 'left',
iconSize: 'large',
className: 'accordion-content-inset-large',
},
],
defaultVariants: {
iconAlign: DefaultValue.iconAlign,
iconSize: DefaultValue.iconSize,
},
});
const AccordionContent = React.forwardRef<
React.ComponentRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => {
const { iconAlign, iconSize } = React.useContext(AccordionContext);
return (
<AccordionPrimitive.Content
ref={ref}
className="accordion-content-box"
{...props}
>
<div
className={cn(
accordionContentVariants({ iconAlign, iconSize, className }),
)}
>
{children}
</div>
</AccordionPrimitive.Content>
);
});
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+214
View File
@@ -0,0 +1,214 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { ALERT_DEFAULT_ICON } from '../constants';
import { cn } from '../lib/utils';
import { Button } from './button';
import type { IconNameType } from './icon';
import { Icon } from './icon';
import useTheme from './use-theme';
const alertVariants = cva('alert', {
variants: {
variant: {
default: 'alert-default',
warning: 'alert-warning',
success: 'alert-success',
error: 'alert-error',
informative: 'alert-informative',
},
radius: {
small: 'alert-radius-small',
medium: 'alert-radius-medium',
large: 'alert-radius-large',
},
},
defaultVariants: {
variant: 'default',
radius: undefined,
},
});
const AlertContext = React.createContext<VariantProps<typeof alertVariants>>({
variant: 'default',
radius: undefined,
});
interface AlertProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof alertVariants> {}
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
({ children, className, variant = 'default', radius, ...props }, ref) => {
const { themeRadius } = useTheme();
return (
<div
ref={ref}
role="alert"
className={cn(
alertVariants({ variant, radius: radius ?? themeRadius }),
className,
)}
{...props}
>
<AlertContext.Provider
value={{ variant, radius: radius ?? themeRadius }}
>
{children}
</AlertContext.Provider>
</div>
);
},
);
Alert.displayName = 'Alert';
type AlertContentProps = React.HTMLAttributes<HTMLHeadingElement>;
const AlertContent = React.forwardRef<HTMLParagraphElement, AlertContentProps>(
({ children, className, ...props }, ref) => (
<div ref={ref} className={cn('alert-content', className)} {...props}>
{children}
</div>
),
);
AlertContent.displayName = 'AlertContent';
type AlertTextContainerProps = React.HTMLAttributes<HTMLHeadingElement>;
const AlertTextContainer = React.forwardRef<
HTMLParagraphElement,
AlertTextContainerProps
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('alert-text-container', className)} {...props} />
));
AlertTextContainer.displayName = 'AlertTextContainer';
type AlertTitleProps = React.HTMLAttributes<HTMLHeadingElement>;
const AlertTitle = React.forwardRef<HTMLParagraphElement, AlertTitleProps>(
({ className, ...props }, ref) => (
<h5 ref={ref} className={cn('alert-title', className)} {...props} />
),
);
AlertTitle.displayName = 'AlertTitle';
type AlertDescriptionProps = React.HTMLAttributes<HTMLHeadingElement>;
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
AlertDescriptionProps
>(({ className, ...props }, ref) => (
<p className={cn('alert-description', className)} {...props} ref={ref} />
));
AlertDescription.displayName = 'AlertDescription';
interface AlertIconButtonProps
extends Omit<React.ComponentPropsWithoutRef<typeof Button>, 'icon'> {
icon?: IconNameType;
}
const AlertIconButton = React.forwardRef<
HTMLButtonElement,
AlertIconButtonProps
>(({ icon, variant, size, className, ...props }, ref) => {
const { radius } = React.useContext(AlertContext);
return (
<Button
ref={ref}
radius={radius ?? 'medium'}
size={size ?? 'medium'}
variant={variant ?? 'ghost'}
className={cn('alert-close', className)}
{...props}
>
<Icon name={icon ?? 'RiCloseFill'} />
</Button>
);
});
AlertIconButton.displayName = 'AlertIconButton';
const AlertButton = React.forwardRef<
HTMLButtonElement,
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant, size, className, ...props }, ref) => {
const { radius } = React.useContext(AlertContext);
return (
<Button
ref={ref}
variant={variant ?? 'outline'}
size={size ?? 'medium'}
radius={radius ?? 'medium'}
className={cn('alert-button', className)}
{...props}
/>
);
});
AlertButton.displayName = 'AlertButton';
const alertIconVariants = cva('alert-icon', {
variants: {
variant: {
default: 'alert-icon-default',
warning: 'alert-icon-warning',
success: 'alert-icon-success',
error: 'alert-icon-error',
informative: 'alert-icon-informative',
},
},
defaultVariants: {
variant: 'default',
},
});
interface AlertIconProps
extends Omit<React.ComponentPropsWithoutRef<typeof Icon>, 'name'> {
name?: IconNameType;
}
const AlertIcon = ({
className,
name = undefined,
size = 20,
...props
}: AlertIconProps) => {
const { variant } = React.useContext(AlertContext);
return (
<Icon
className={cn(alertIconVariants({ variant, className }))}
name={name ?? ALERT_DEFAULT_ICON[variant ?? 'default']}
size={size}
{...props}
/>
);
};
AlertIcon.displayName = 'AlertIcon';
export {
Alert,
AlertContent,
AlertTextContainer,
AlertDescription,
AlertTitle,
AlertIconButton,
AlertButton,
AlertIcon,
type AlertProps,
};
+170
View File
@@ -0,0 +1,170 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import type { Color, Radius } from '../lib/types';
import { cn } from '../lib/utils';
import useTheme from './use-theme';
type BadgeVariant = 'bold' | 'subtle' | 'outline';
const badgeVariants = cva('badge', {
variants: {
radius: {
large: 'badge-radius-large',
medium: 'badge-radius-medium',
small: 'badge-radius-small',
},
variant: {
bold: '',
subtle: '',
outline: '',
},
color: {
default: '',
blue: '',
orange: '',
red: '',
green: '',
},
},
compoundVariants: [
{
variant: 'bold',
color: 'default',
className: 'badge-bold-default',
},
{
variant: 'bold',
color: 'blue',
className: 'badge-bold-blue',
},
{
variant: 'bold',
color: 'orange',
className: 'badge-bold-orange',
},
{
variant: 'bold',
color: 'red',
className: 'badge-bold-red',
},
{
variant: 'bold',
color: 'green',
className: 'badge-bold-green',
},
{
variant: 'subtle',
color: 'default',
className: 'badge-subtle-default',
},
{
variant: 'subtle',
color: 'blue',
className: 'badge-subtle-blue',
},
{
variant: 'subtle',
color: 'orange',
className: 'badge-subtle-orange',
},
{
variant: 'subtle',
color: 'red',
className: 'badge-subtle-red',
},
{
variant: 'subtle',
color: 'green',
className: 'badge-subtle-green',
},
{
variant: 'outline',
color: 'default',
className: 'badge-outline-default',
},
{
variant: 'outline',
color: 'blue',
className: 'badge-outline-blue',
},
{
variant: 'outline',
color: 'orange',
className: 'badge-outline-orange',
},
{
variant: 'outline',
color: 'red',
className: 'badge-outline-red',
},
{
variant: 'outline',
color: 'green',
className: 'badge-outline-green',
},
],
defaultVariants: {
radius: undefined,
variant: 'bold',
color: 'default',
},
});
interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {
radius?: Radius;
variant?: BadgeVariant;
color?: Color;
asChild?: boolean;
}
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
(
{
radius,
variant = 'bold',
color = 'default',
className,
asChild,
...props
},
ref,
) => {
const { themeRadius } = useTheme();
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
className={cn(
badgeVariants({ radius: radius ?? themeRadius, variant, color }),
className,
)}
{...props}
/>
);
},
);
Badge.displayName = 'Badge';
export { Badge, type BadgeProps };
@@ -0,0 +1,156 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { Slot, Slottable } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import type { ButtonVariant, Radius, Size } from '../lib/types';
import { cn, composeRefs } from '../lib/utils';
import { Spinner } from './spinner';
import useTheme from './use-theme';
const defaultVariants: {
variant: ButtonVariant;
size?: Size;
radius?: Radius;
loading?: boolean;
} = {
variant: 'primary',
size: undefined,
radius: undefined,
loading: false,
};
const buttonVariants = cva('button', {
variants: {
variant: {
primary: 'button-primary',
secondary: 'button-secondary',
destructive: 'button-destructive',
ghost: 'button-ghost',
outline: 'button-outline',
},
size: {
small: 'button-small',
medium: 'button-medium',
large: 'button-large',
},
radius: {
small: 'button-radius-small',
medium: 'button-radius-medium',
large: 'button-radius-large',
},
loading: {
true: '!text-transparent [&>*:not(.button-loading)]:!invisible',
false: '',
},
},
defaultVariants,
});
const buttonLoadingVariants = cva('button-loading', {
variants: {
variant: {
primary: 'button-loading-primary',
secondary: 'button-loading-secondary',
destructive: 'button-loading-destructive',
ghost: 'button-loading-ghost',
outline: 'button-loading-outline',
},
},
defaultVariants,
});
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
variant?: ButtonVariant;
size?: Size;
radius?: Radius;
loading?: boolean;
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
type = 'button',
variant = 'primary',
size,
radius,
disabled = false,
loading = false,
asChild = false,
children,
...props
},
ref,
) => {
const Comp = asChild ? Slot : 'button';
const { themeSize, themeRadius } = useTheme();
const buttonRef = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (!buttonRef.current) return;
const childNodes = buttonRef.current.childNodes;
const isSvgOnly = Array.from(childNodes).every(
(node) =>
((node as HTMLElement).nodeType === Node.ELEMENT_NODE &&
(node as HTMLElement).tagName.toLowerCase() === 'svg') ||
((node as HTMLElement).nodeType === Node.TEXT_NODE &&
!((node as HTMLElement).textContent || '').trim()),
);
if (isSvgOnly) {
buttonRef.current.classList.add('svg-only');
}
}, []);
return (
<Comp
className={cn(
buttonVariants({
variant,
size: size ?? themeSize,
radius: radius ?? themeRadius,
loading,
className,
}),
)}
type={type}
disabled={disabled || loading}
ref={composeRefs(buttonRef, ref)}
{...props}
>
<Slottable>{children}</Slottable>
{loading && (
<span className={cn(buttonLoadingVariants({ variant }))}>
<Spinner size={size ?? themeSize} />
</span>
)}
</Comp>
);
},
);
Button.displayName = 'Button';
export { Button, type ButtonProps, buttonVariants };
@@ -0,0 +1,77 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type { DateRange, DayPickerDefaultProps } from 'react-day-picker';
import { DayPicker } from 'react-day-picker';
import { cn } from '../lib/utils';
import { Icon } from './icon';
type CalendarProps = React.ComponentProps<typeof DayPicker> & {
showToday?: boolean;
};
function Calendar({
className,
classNames,
showToday = true,
showOutsideDays = true,
mode = 'default',
...props
}: CalendarProps) {
return (
<DayPicker
mode={mode as DayPickerDefaultProps['mode']}
showOutsideDays={showOutsideDays}
className={cn('calendar', className)}
classNames={{
months: 'calendar-months',
month: 'calendar-month',
caption: 'calendar-caption',
caption_label: 'calendar-caption-label',
nav: 'calendar-nav',
nav_button: 'calendar-nav-button',
nav_button_previous: 'calendar-nav-button-previous',
nav_button_next: 'calendar-nav-button-next',
table: 'calendar-table',
head_row: 'calendar-head-row',
head_cell: 'calendar-head-cell',
row: 'calendar-row',
cell: 'calendar-cell',
day: 'calendar-day',
day_range_start: 'calendar-day-range-start',
day_range_end: 'calendar-day-range-end',
day_selected: 'calendar-day-selected',
day_today: showToday ? 'calendar-day-today' : '',
day_outside: 'calendar-day-outside',
day_disabled: 'calendar-day-disabled',
day_range_middle: 'calendar-day-range-middle',
day_hidden: 'calendar-day-hidden',
...classNames,
}}
components={{
IconLeft: () => <Icon name="RiArrowLeftSLine" size={16} />,
IconRight: () => <Icon name="RiArrowRightSLine" size={16} />,
}}
{...props}
/>
);
}
Calendar.displayName = 'Calendar';
export { Calendar, type DateRange, type CalendarProps };
@@ -0,0 +1,83 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { Slot, Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { CAPTION_DEFAULT_ICON, ICON_SIZE } from '../constants';
import type { CaptionType } from '../lib/types';
import { cn } from '../lib/utils';
import type { IconNameType } from './icon';
import { Icon } from './icon';
import useTheme from './use-theme';
const captionVariants = cva('caption', {
variants: {
variant: {
default: 'caption-default',
success: 'caption-success',
info: 'caption-info',
error: 'caption-error',
},
defaultVariants: { variant: 'default' },
},
});
interface CaptionProps extends React.ComponentPropsWithoutRef<'p'> {
variant?: CaptionType;
icon?: IconNameType;
asChild?: boolean;
}
const Caption = React.forwardRef<HTMLParagraphElement, CaptionProps>(
(props, ref) => {
const {
icon = undefined,
variant,
className,
children,
asChild,
...rest
} = props;
const Comp = asChild ? Slot : 'p';
const isError = variant === 'error';
const { themeSize } = useTheme();
return (
<Comp
ref={ref}
className={cn(
captionVariants({ variant: variant ?? 'default', className }),
)}
data-error={isError}
{...rest}
>
<Icon
name={
icon ??
CAPTION_DEFAULT_ICON[isError ? 'error' : (variant ?? 'default')]
}
size={ICON_SIZE[themeSize]}
className="caption-icon"
/>
<Slottable>{children}</Slottable>
</Comp>
);
},
);
Caption.displayName = 'Caption';
export { Caption };
@@ -0,0 +1,101 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { CHECK_ICON_SIZE } from '../constants';
import type { Size } from '../lib/types';
import { cn } from '../lib/utils';
import { Icon } from './icon';
import useTheme from './use-theme';
const defaultVariants: {
size?: Size;
} = {
size: undefined,
};
const checkboxVariants = cva('checkbox', {
variants: {
size: {
small: 'checkbox-small',
medium: 'checkbox-medium',
large: 'checkbox-large',
},
},
defaultVariants,
});
const checkVariants = cva('check', {
variants: {
size: {
small: 'check-small',
medium: 'check-medium',
large: 'check-large',
},
},
defaultVariants,
});
interface CheckboxProps
extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {
size?: Size;
}
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
CheckboxProps
>(({ checked, size, children, className, onCheckedChange, ...props }, ref) => {
const { themeSize } = useTheme();
const [currentChecked, setCurrentChecked] =
React.useState<CheckboxPrimitive.CheckedState>(false);
const handleCheckedChange = (checked: CheckboxPrimitive.CheckedState) => {
setCurrentChecked(checked);
onCheckedChange?.(checked);
};
return (
<CheckboxPrimitive.Root
ref={ref}
className={cn(checkboxVariants({ size: size ?? themeSize, className }))}
checked={checked ?? currentChecked}
onCheckedChange={handleCheckedChange}
{...props}
>
<span className={cn(checkVariants({ size }))}>
<CheckboxPrimitive.Indicator className={cn('checkbox-icon')}>
<Icon
name={
(checked ?? currentChecked) === 'indeterminate' ?
'RiSubtractLine'
: 'RiCheckLine'
}
size={CHECK_ICON_SIZE[size ?? themeSize]}
/>
</CheckboxPrimitive.Indicator>
</span>
<Slottable>{children}</Slottable>
</CheckboxPrimitive.Root>
);
});
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox, type CheckboxProps };
@@ -0,0 +1,311 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { Slottable } from '@radix-ui/react-slot';
import { Command as CommandPrimitive } from 'cmdk';
import type { TriggerType } from '../lib/types';
import { cn } from '../lib/utils';
import { Button } from './button';
import { Icon } from './icon';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
import { ScrollArea, ScrollBar } from './scroll-area';
const ComboboxContext = React.createContext<{
trigger: TriggerType;
isHover: boolean;
setTrigger: React.Dispatch<React.SetStateAction<TriggerType>>;
setIsHover: React.Dispatch<React.SetStateAction<boolean>>;
}>({
trigger: 'click',
setTrigger: () => 'click',
isHover: false,
setIsHover: () => false,
});
const Combobox = ({
open,
onOpenChange,
...props
}: React.ComponentPropsWithoutRef<typeof Popover>) => {
const [trigger, setTrigger] = React.useState<TriggerType>('click');
const [isHover, setIsHover] = React.useState(false);
return (
<ComboboxContext.Provider
value={{ trigger, setTrigger, isHover, setIsHover }}
>
<Popover
{...props}
open={trigger === 'hover' ? !!open || isHover : open}
onOpenChange={(open: boolean) => {
if (trigger === 'hover') {
setIsHover(open);
}
onOpenChange?.(open);
}}
/>
</ComboboxContext.Provider>
);
};
const ComboboxContent = React.forwardRef<
React.ComponentRef<typeof PopoverContent>,
React.ComponentPropsWithoutRef<typeof PopoverContent> & {
options?: React.ComponentPropsWithoutRef<typeof CommandPrimitive>;
}
>(
(
{ className, children, onMouseEnter, onMouseLeave, options, ...props },
ref,
) => {
const { trigger, setIsHover } = React.useContext(ComboboxContext);
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
if (trigger === 'hover') {
setIsHover(true);
}
onMouseEnter?.(e);
};
const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
if (trigger === 'hover') {
setIsHover(false);
}
onMouseLeave?.(e);
};
return (
<PopoverContent
ref={ref}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
className={cn('combobox-content', className)}
>
<CommandPrimitive {...options}>{children}</CommandPrimitive>
</PopoverContent>
);
},
);
ComboboxContent.displayName = CommandPrimitive.displayName;
interface ComboboxInputProps
extends React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> {
icon?: React.ReactNode;
}
const ComboboxInput = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Input>,
ComboboxInputProps
>(({ icon, className, ...props }, ref) => (
<div className="combobox-input-box" cmdk-input-wrapper="">
{icon ?? <Icon name="RiSearchLine" size={16} />}
<CommandPrimitive.Input
ref={ref}
className={cn('combobox-input', className)}
{...props}
/>
</div>
));
ComboboxInput.displayName = CommandPrimitive.Input.displayName;
const ComboboxList = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> & {
maxHeight?: string;
}
>(({ maxHeight, className, ...props }, ref) => (
<ScrollArea maxHeight={maxHeight}>
<CommandPrimitive.List
ref={ref}
className={cn('combobox-list', className)}
{...props}
/>
<ScrollBar />
</ScrollArea>
));
ComboboxList.displayName = CommandPrimitive.List.displayName;
const ComboboxEmpty = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="combobox-empty" {...props} />
));
ComboboxEmpty.displayName = CommandPrimitive.Empty.displayName;
const ComboboxGroup = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn('combobox-group', className)}
{...props}
/>
));
ComboboxGroup.displayName = CommandPrimitive.Group.displayName;
const ComboboxSeparator = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn('combobox-separator', className)}
{...props}
/>
));
ComboboxSeparator.displayName = CommandPrimitive.Separator.displayName;
const ComboboxItem = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ children, className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn('combobox-item', className)}
{...props}
>
{children}
</CommandPrimitive.Item>
));
ComboboxItem.displayName = CommandPrimitive.Item.displayName;
const ComboboxCaption = React.forwardRef<
React.ComponentRef<'span'>,
React.ComponentPropsWithoutRef<'span'>
>(({ className, ...props }, ref) => (
<span ref={ref} className={cn('combobox-caption', className)} {...props} />
));
ComboboxCaption.displayName = 'ComboboxCaption';
const ComboboxSelectItem = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> & {
checked?: boolean;
}
>(({ checked, children, className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn('combobox-item', className)}
{...props}
>
<Icon
name="RiCheckLine"
size={20}
color={checked ? 'currentColor' : 'transparent'}
className="combobox-check"
/>
<Slottable>{children}</Slottable>
</CommandPrimitive.Item>
));
ComboboxSelectItem.displayName = 'ComboboxSelectItem';
const ComboboxTrigger = React.forwardRef<
React.ComponentRef<typeof Button>,
React.ComponentPropsWithoutRef<typeof Button> & {
trigger?: TriggerType;
}
>(
(
{
variant = 'outline',
trigger,
className,
children,
onMouseEnter,
onMouseLeave,
...props
},
ref,
) => {
const { setTrigger, setIsHover } = React.useContext(ComboboxContext);
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
if (trigger === 'hover') {
setIsHover(true);
}
onMouseEnter?.(e);
};
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
if (trigger === 'hover') {
setIsHover(false);
}
onMouseLeave?.(e);
};
React.useEffect(() => {
if (trigger) {
setTrigger(trigger);
}
}, []);
if (props.asChild) {
return (
<PopoverTrigger
className={className}
ref={ref}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
{children}
</PopoverTrigger>
);
}
return (
<PopoverTrigger asChild>
<Button
ref={ref}
variant={variant}
className={cn('combobox-trigger', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
{children}
</Button>
</PopoverTrigger>
);
},
);
ComboboxTrigger.displayName = 'ComboboxTrigger';
export {
Combobox,
ComboboxContent,
ComboboxCaption,
ComboboxInput,
ComboboxList,
ComboboxEmpty,
ComboboxGroup,
ComboboxItem,
ComboboxSelectItem,
ComboboxSeparator,
ComboboxTrigger,
};
@@ -0,0 +1,247 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { ALERT_DEFAULT_ICON } from '../constants';
import { cn } from '../lib/utils';
import type { Radius } from '../types';
import { Button } from './button';
import { Icon } from './icon';
import { ScrollArea, ScrollBar } from './scroll-area';
import useTheme from './use-theme';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Trigger>,
DialogPrimitive.DialogTriggerProps &
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant = 'outline', children, ...props }, ref) => {
if (props.asChild) {
return (
<DialogPrimitive.Trigger ref={ref} {...props}>
{children}
</DialogPrimitive.Trigger>
);
}
return (
<DialogPrimitive.Trigger asChild>
<Button variant={variant} ref={ref} {...props}>
{children}
</Button>
</DialogPrimitive.Trigger>
);
});
DialogTrigger.displayName = DialogPrimitive.DialogTrigger.displayName;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant, ...props }, ref) => (
<DialogPrimitive.Close asChild>
<Button ref={ref} variant={variant ?? 'outline'} {...props} />
</DialogPrimitive.Close>
));
DialogClose.displayName = 'DialogClose';
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn('dialog', className)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const dialogContentVariants = cva('dialog-content', {
variants: {
radius: {
large: 'dialog-content-radius-large',
medium: 'dialog-content-radius-medium',
small: 'dialog-content-radius-small',
},
defaultVariants: {
radius: undefined,
},
},
});
interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
radius?: Radius;
}
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(({ radius, className, children, ...props }, ref) => {
const { themeRadius } = useTheme();
return (
<DialogPortal>
<DialogOverlay onClick={(e) => e.stopPropagation()} />
<DialogPrimitive.Content
ref={ref}
className={cn(
dialogContentVariants({ radius: radius ?? themeRadius, className }),
)}
{...props}
>
{children}
<DialogPrimitive.Close asChild>
<Button
size="medium"
variant="ghost"
className="dialog-close"
aria-label="Close"
>
<Icon name="RiCloseLine" />
</Button>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;
const dialogIconVariants = cva('dialog-icon', {
variants: {
variant: {
default: 'dialog-icon-default',
warning: 'dialog-icon-warning',
success: 'dialog-icon-success',
error: 'dialog-icon-error',
informative: 'dialog-icon-informative',
},
},
defaultVariants: {
variant: 'default',
},
});
const DialogIcon = (
props: React.ComponentPropsWithoutRef<typeof Icon> &
VariantProps<typeof dialogIconVariants>,
) => {
const { className, name, size = 32, variant, ...rest } = props;
return (
<Icon
{...rest}
name={name ?? ALERT_DEFAULT_ICON[variant ?? 'default']}
size={size}
className={cn(dialogIconVariants({ variant, className }))}
/>
);
};
DialogIcon.displayName = 'DialogIcon';
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('dialog-header', className)} {...props} />
);
DialogHeader.displayName = 'DialogHeader';
const dialogFooterVariants = cva('dialog-footer', {
variants: {
align: {
left: 'dialog-footer-left',
right: 'dialog-footer-right',
center: 'dialog-footer-center',
between: 'dialog-footer-between',
full: 'dialog-footer-full',
},
defaultVariants: {
align: 'right',
},
},
});
interface DialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {
asChild?: boolean;
}
const DialogBody = ({ asChild, className, ...props }: DialogBodyProps) => {
const Comp = asChild ? Slot : 'div';
return (
<ScrollArea>
<Comp className={cn('dialog-body', className)} {...props} />
<ScrollBar />
</ScrollArea>
);
};
DialogBody.displayName = 'DialogBody';
interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {
align?: 'left' | 'right' | 'center' | 'between' | 'full';
}
const DialogFooter = ({
align = 'right',
className,
...props
}: DialogFooterProps) => (
<div className={cn(dialogFooterVariants({ align, className }))} {...props} />
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('dialog-title', className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('dialog-description', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogBody,
DialogFooter,
DialogTitle,
DialogDescription,
DialogIcon,
};
@@ -0,0 +1,112 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
const dividerVariants = cva('divider', {
variants: {
variant: {
bold: 'divider-bold',
subtle: 'divider-subtle',
},
orientation: {
horizontal: 'divider-horizontal',
vertical: 'divider-vertical',
},
indent: {
0: '',
8: '',
16: '',
24: '',
},
},
compoundVariants: [
{
orientation: 'horizontal',
indent: 8,
className: 'divider-horizontal-indent-8',
},
{
orientation: 'horizontal',
indent: 16,
className: 'divider-horizontal-indent-16',
},
{
orientation: 'horizontal',
indent: 24,
className: 'divider-horizontal-indent-24',
},
{
orientation: 'vertical',
indent: 8,
className: 'divider-vertical-indent-8',
},
{
orientation: 'vertical',
indent: 16,
className: 'divider-vertical-indent-16',
},
{
orientation: 'vertical',
indent: 24,
className: 'divider-vertical-indent-24',
},
],
defaultVariants: {
variant: 'bold',
orientation: 'horizontal',
indent: 0,
},
});
interface DividerProps
extends React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> {
variant?: 'bold' | 'subtle';
indent?: 0 | 8 | 16 | 24;
}
const Divider = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
DividerProps
>(
(
{
variant = 'bold',
indent = 0,
orientation = 'horizontal',
className,
decorative = true,
...props
},
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
dividerVariants({ variant, orientation, indent, className }),
)}
{...props}
/>
),
);
Divider.displayName = SeparatorPrimitive.Root.displayName;
export { Divider };
@@ -0,0 +1,327 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type { DropdownMenuCheckboxItemProps } from '@radix-ui/react-dropdown-menu';
import * as DropdownPrimitive from '@radix-ui/react-dropdown-menu';
import { Slottable } from '@radix-ui/react-slot';
import type { TriggerType } from '../lib/types';
import { cn } from '../lib/utils';
import { Button } from './button';
import { Icon } from './icon';
import { ScrollArea, ScrollBar } from './scroll-area';
const DropdownContext = React.createContext<{
trigger: TriggerType;
isHover: boolean;
setTrigger: React.Dispatch<React.SetStateAction<TriggerType>>;
setIsHover: React.Dispatch<React.SetStateAction<boolean>>;
}>({
trigger: 'click',
setTrigger: () => 'click',
isHover: false,
setIsHover: () => false,
});
const Dropdown = ({
open = false,
// eslint-disable-next-line @typescript-eslint/unbound-method
onOpenChange,
...props
}: React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Root>) => {
const [trigger, setTrigger] = React.useState<TriggerType>('click');
const [isHover, setIsHover] = React.useState(false);
return (
<DropdownContext.Provider
value={{ trigger, setTrigger, isHover, setIsHover }}
>
<DropdownPrimitive.Root
{...props}
open={open || isHover}
onOpenChange={(open: boolean) => {
setIsHover(open);
onOpenChange?.(open);
}}
/>
</DropdownContext.Provider>
);
};
const DropdownTrigger = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Trigger>,
DropdownPrimitive.DropdownMenuTriggerProps &
React.ComponentPropsWithoutRef<typeof Button> & {
trigger?: TriggerType;
}
>(
(
{
variant = 'outline',
trigger,
onMouseEnter,
onMouseLeave,
className,
children,
...props
},
ref,
) => {
const { setTrigger, setIsHover } = React.useContext(DropdownContext);
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
if (trigger === 'hover') {
setIsHover(true);
}
onMouseEnter?.(e);
};
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
if (trigger === 'hover') {
setIsHover(false);
}
onMouseLeave?.(e);
};
React.useEffect(() => {
if (trigger) {
setTrigger(trigger);
}
}, []);
if (props.asChild) {
return (
<DropdownPrimitive.Trigger
className={cn('dropdown-trigger', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={ref}
{...props}
>
{children}
</DropdownPrimitive.Trigger>
);
}
return (
<DropdownPrimitive.Trigger asChild>
<Button
variant={variant}
className={cn('dropdown-trigger', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={ref}
{...props}
>
{children}
</Button>
</DropdownPrimitive.Trigger>
);
},
);
DropdownTrigger.displayName = DropdownPrimitive.DropdownMenuTrigger.displayName;
const DropdownGroup = DropdownPrimitive.Group;
const DropdownPortal = DropdownPrimitive.Portal;
const DropdownSub = DropdownPrimitive.Sub;
const DropdownRadioGroup = DropdownPrimitive.RadioGroup;
const DropdownSubTrigger = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.SubTrigger>
>(({ className, children, ...props }, ref) => {
return (
<DropdownPrimitive.SubTrigger
ref={ref}
className={cn('dropdown-sub-trigger', className)}
{...props}
>
{children}
</DropdownPrimitive.SubTrigger>
);
});
DropdownSubTrigger.displayName = DropdownPrimitive.SubTrigger.displayName;
const DropdownCaption = React.forwardRef<
React.ComponentRef<'span'>,
React.ComponentPropsWithoutRef<'span'>
>(({ className, ...props }, ref) => (
<span ref={ref} className={cn('dropdown-caption', className)} {...props} />
));
DropdownCaption.displayName = 'DropdownCaption';
const DropdownSubContent = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownPrimitive.SubContent
ref={ref}
className={cn('dropdown-sub-content', className)}
{...props}
/>
));
DropdownSubContent.displayName = DropdownPrimitive.SubContent.displayName;
const DropdownContent = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Content> & {
maxHeight?: string;
}
>(
(
{
children,
className,
sideOffset = 4,
maxHeight,
onMouseEnter,
onMouseLeave,
...props
},
ref,
) => {
const { trigger, setIsHover } = React.useContext(DropdownContext);
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
if (trigger === 'hover') {
setIsHover(true);
}
onMouseEnter?.(e);
};
const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
if (trigger === 'hover') {
setIsHover(false);
}
onMouseLeave?.(e);
};
return (
<DropdownPrimitive.Portal>
<DropdownPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn('dropdown-content', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
<ScrollArea maxHeight={maxHeight}>
{children}
<ScrollBar />
</ScrollArea>
</DropdownPrimitive.Content>
</DropdownPrimitive.Portal>
);
},
);
DropdownContent.displayName = DropdownPrimitive.Content.displayName;
const DropdownItem = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Item>
>(({ children, className, ...props }, ref) => (
<DropdownPrimitive.Item
ref={ref}
className={cn('dropdown-item', className)}
{...props}
>
{children}
</DropdownPrimitive.Item>
));
DropdownItem.displayName = DropdownPrimitive.Item.displayName;
const DropdownCheckboxItem = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownPrimitive.CheckboxItem
ref={ref}
className={cn('dropdown-checkbox', className)}
checked={checked}
{...props}
>
<DropdownPrimitive.ItemIndicator className="dropdown-checkbox-icon">
<Icon name="RiCheckLine" size={16} />
</DropdownPrimitive.ItemIndicator>
<Slottable>{children}</Slottable>
</DropdownPrimitive.CheckboxItem>
));
DropdownCheckboxItem.displayName = DropdownPrimitive.CheckboxItem.displayName;
const DropdownRadioItem = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownPrimitive.RadioItem
ref={ref}
className={cn('dropdown-radio', className)}
{...props}
>
<DropdownPrimitive.ItemIndicator className="dropdown-radio-icon">
<Icon name="RiCircleFill" size={8} className="fill-current" />
</DropdownPrimitive.ItemIndicator>
<Slottable>{children}</Slottable>
</DropdownPrimitive.RadioItem>
));
DropdownRadioItem.displayName = DropdownPrimitive.RadioItem.displayName;
const DropdownLabel = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Label>
>(({ className, ...props }, ref) => (
<DropdownPrimitive.Label
ref={ref}
className={cn('dropdown-label', className)}
{...props}
/>
));
DropdownLabel.displayName = DropdownPrimitive.Label.displayName;
const DropdownSeparator = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownPrimitive.Separator
ref={ref}
className={cn('dropdown-separator', className)}
{...props}
/>
));
DropdownSeparator.displayName = DropdownPrimitive.Separator.displayName;
export {
Dropdown,
DropdownTrigger,
DropdownContent,
DropdownCaption,
DropdownItem,
DropdownCheckboxItem,
DropdownRadioItem,
DropdownLabel,
DropdownSeparator,
DropdownGroup,
DropdownPortal,
DropdownSub,
DropdownSubContent,
DropdownSubTrigger,
DropdownRadioGroup,
type DropdownMenuCheckboxItemProps,
};
+169
View File
@@ -0,0 +1,169 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
import {
Controller,
FormProvider,
useFormContext,
useFormState,
} from 'react-hook-form';
import { Caption } from './caption';
import { Label } from './label';
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
function FormItem({ ...props }: React.ComponentProps<'div'>) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div data-slot="form-item" {...props} />
</FormItemContext.Provider>
);
}
function FormLabel({
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-error={!!error}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error ?
`${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
}
function FormCaption({ ...props }: React.ComponentProps<typeof Caption>) {
const { formDescriptionId } = useFormField();
return (
<Caption data-slot="form-description" id={formDescriptionId} {...props} />
);
}
function FormMessage({
children,
...props
}: React.ComponentProps<typeof Caption>) {
const { error, formMessageId } = useFormField();
const body = error ? String(error.message ?? '') : children;
if (!body) {
return null;
}
return (
<Caption
data-slot="form-message"
id={formMessageId}
variant="error"
{...props}
>
{body}
</Caption>
);
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormCaption,
FormMessage,
FormField,
};
@@ -0,0 +1,52 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import * as remixIcons from '@remixicon/react';
import { cn } from '../lib/utils';
const Icons = remixIcons;
const IconNames = Object.keys(Icons) as (keyof typeof Icons)[];
type IconNameType = keyof typeof Icons;
interface IconProps extends Omit<React.SVGProps<SVGSVGElement>, 'children'> {
name?: IconNameType;
color?: string;
size?: number | string;
}
const Icon: React.FC<IconProps> = ({
name,
color = 'currentColor',
size = 24,
className,
onClick,
...props
}) => {
if (!name) {
return <></>;
}
return React.createElement(Icons[name], {
color,
size,
className: cn('icon', onClick && 'icon-clickable', className),
onClick,
...props,
});
};
export { Icon, IconNames, type IconNameType, type IconProps };
@@ -0,0 +1,50 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export * from './alert';
export * from './accordion';
export * from './badge';
export * from './button';
export * from './checkbox';
export * from './calendar';
export * from './caption';
export * from './combobox';
export * from './divider';
export * from './dialog';
export * from './dropdown';
export * from './form';
export * from './icon';
export * from './input';
export * from './label';
export * from './menu';
export * from './pagination';
export * from './popover';
export * from './radio-card';
export * from './radio';
export * from './select';
export * from './spinner';
export * from './scroll-area';
export * from './multi-select';
export * from './switch';
export * from './sheet';
export * from './toast';
export * from './tag';
export * from './table';
export * from './toggle-group';
export * from './tooltip';
export * from './use-theme';
export * from './tabs';
export * from './textarea';
export * from './timepicker';
+257
View File
@@ -0,0 +1,257 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import type { ButtonHTMLAttributes, HTMLInputTypeAttribute } from 'react';
import React, { useRef } from 'react';
import { cva } from 'class-variance-authority';
import { ICON_SIZE } from '../constants';
import type { Radius, Size } from '../lib/types';
import { cn, composeRefs } from '../lib/utils';
import type { IconProps } from './icon';
import { Icon } from './icon';
import useTheme from './use-theme';
type TextInputType = (
| 'text'
| 'email'
| 'password'
| 'search'
| 'tel'
| 'number'
) &
HTMLInputTypeAttribute;
const InputField = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
return <div ref={ref} className={cn('input-field', className)} {...props} />;
});
InputField.displayName = 'InputField';
const defaultContext: TextInputProps = {
size: undefined,
};
const InputContext = React.createContext<TextInputProps>(defaultContext);
const inputVariants = cva('input', {
variants: {
size: {
large: 'input-large',
medium: 'input-medium',
small: 'input-small',
},
radius: {
large: 'input-radius-large',
medium: 'input-radius-medium',
small: 'input-radius-small',
},
defaultVariants: {
size: undefined,
radius: undefined,
},
},
});
interface InputBoxProps extends React.HTMLAttributes<HTMLDivElement> {
size?: Size;
}
const InputBox = React.forwardRef<HTMLInputElement, InputBoxProps>(
({ size, className, children, ...props }, ref) => {
const { themeSize } = useTheme();
return (
<InputContext.Provider value={{ size: size ?? themeSize }}>
<div ref={ref} className={cn('input-box', className)} {...props}>
{children}
</div>
</InputContext.Provider>
);
},
);
InputBox.displayName = 'InputBox';
interface TextInputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
size?: Size;
type?: TextInputType;
radius?: Radius;
}
const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
(props, ref) => {
const {
size,
type = 'text',
radius,
disabled = false,
onFocus,
onBlur,
className,
...rest
} = props;
const { themeRadius } = useTheme();
const { size: boxSize } = React.useContext(InputContext);
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.stopPropagation();
if (!e.currentTarget.contains(e.relatedTarget)) {
onFocus?.(e);
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
e.stopPropagation();
if (!e.currentTarget.contains(e.relatedTarget)) {
onBlur?.(e);
}
};
return (
<input
{...rest}
spellCheck="false"
type={type}
ref={composeRefs(inputRef, ref)}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
className={cn(
inputVariants({
size: size ?? boxSize,
radius: radius ?? themeRadius,
className,
}),
)}
/>
);
},
);
TextInput.displayName = 'TextInput';
const inputIconVariants = cva('input-icon', {
variants: {
size: {
large: 'input-icon-large',
medium: 'input-icon-medium',
small: 'input-icon-small',
},
defaultVariants: {
size: undefined,
},
},
});
const InputIcon = ({ className, ...props }: IconProps) => {
const { size } = React.useContext(InputContext);
const { themeSize } = useTheme();
return (
<Icon
{...props}
size={ICON_SIZE[size ?? themeSize]}
className={cn(inputIconVariants({ size: size ?? themeSize, className }))}
/>
);
};
InputIcon.displayName = 'InputIcon';
const inputButtonVariants = cva('input-button', {
variants: {
size: {
large: 'input-button-large',
medium: 'input-button-medium',
small: 'input-button-small',
},
defaultVariants: {
size: undefined,
},
},
});
const InputClearButton = React.forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, ...props }, ref) => {
const { size } = React.useContext(InputContext);
const { themeSize } = useTheme();
return (
<button
ref={ref}
type="button"
className={cn(
inputButtonVariants({ size: size ?? themeSize, className }),
'show-only-on-focus-and-has-value',
)}
aria-label="Reset input text"
{...props}
>
<Icon name="RiCloseCircleFill" size={ICON_SIZE[size ?? themeSize]} />
</button>
);
});
InputClearButton.displayName = 'InputClearButton';
const InputEyeButton = React.forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement> & {
onChangeVisibility?: (visible: boolean) => void;
}
>(({ className, onClick, onChangeVisibility, ...props }, ref) => {
const [visible, setVisible] = React.useState(false);
const { size } = React.useContext(InputContext);
const { themeSize } = useTheme();
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
setVisible((visible) => !visible);
onClick?.(e);
};
React.useEffect(() => {
onChangeVisibility?.(visible);
}, [visible]);
return (
<button
ref={ref}
type="button"
className={cn(
inputButtonVariants({ size: size ?? themeSize, className }),
)}
onClick={handleClick}
aria-label={visible ? 'hide password' : 'show password'}
{...props}
>
{visible ?
<Icon name="RiEyeCloseFill" size={ICON_SIZE[size ?? themeSize]} />
: <Icon name="RiEyeFill" size={ICON_SIZE[size ?? themeSize]} />}
</button>
);
});
InputEyeButton.displayName = 'InputEyeButton';
export {
InputField,
InputBox,
TextInput,
InputClearButton,
InputEyeButton,
InputIcon,
type TextInputProps,
};
@@ -0,0 +1,33 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '../lib/utils';
interface LabelProps extends React.ComponentProps<typeof LabelPrimitive.Root> {}
function Label({ className, ...props }: LabelProps) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn('label', className)}
{...props}
/>
);
}
export { Label };
+175
View File
@@ -0,0 +1,175 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import {
Dropdown,
DropdownContent,
DropdownGroup,
DropdownItem,
DropdownTrigger,
} from './dropdown';
import useTheme from './use-theme';
const DefaultValue = {
orientation: 'horizontal',
} as const;
const menuVariants = cva('menu', {
variants: {
orientation: {
horizontal: 'menu-horizontal',
vertical: 'menu-vertical',
},
defaultVariants: {
orientation: DefaultValue.orientation,
},
},
});
const MenuContext = React.createContext<{
orientation?: 'vertical' | 'horizontal';
size?: 'small' | 'medium' | 'large';
}>({
orientation: DefaultValue.orientation,
size: undefined,
});
const Menu = React.forwardRef<
React.ComponentRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & {
orientation?: 'vertical' | 'horizontal';
size?: 'small' | 'medium' | 'large';
}
>(
(
{
orientation = DefaultValue.orientation,
size,
children,
className,
...props
},
ref,
) => {
const { themeSize } = useTheme();
return (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn(menuVariants({ orientation, className }))}
{...props}
>
<MenuContext.Provider value={{ orientation, size: size ?? themeSize }}>
{children}
</MenuContext.Provider>
</ToggleGroupPrimitive.Root>
);
},
);
Menu.displayName = ToggleGroupPrimitive.Root.displayName;
const menuItemVariants = cva('menu-item', {
variants: {
size: {
small: 'menu-item-small',
medium: 'menu-item-medium',
large: 'menu-item-large',
},
defaultVariants: {
size: undefined,
},
},
});
const MenuItem = React.forwardRef<
React.ComponentRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item>
>(({ children, className, ...props }, ref) => {
const { size } = React.useContext(MenuContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(menuItemVariants({ size, className }))}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
MenuItem.displayName = ToggleGroupPrimitive.Item.displayName;
const MenuDropdown = Dropdown;
const MenuDropdownTrigger = React.forwardRef<
React.ComponentRef<typeof DropdownTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownTrigger>
>(({ className, children, ...props }, ref) => {
const { size } = React.useContext(MenuContext);
if (props.asChild) {
return (
<DropdownTrigger
className={cn(menuItemVariants({ size, className }))}
ref={ref}
{...props}
>
{children}
</DropdownTrigger>
);
}
return (
<DropdownTrigger
asChild
ref={ref}
className={cn(
'flex justify-between',
menuItemVariants({ size, className }),
)}
{...props}
>
<button type="button">{children}</button>
</DropdownTrigger>
);
});
MenuDropdownTrigger.displayName = 'MenuDropdownTrigger';
const MenuDropdownContent = DropdownContent;
const MenuDropdownGroup = DropdownGroup;
const MenuDropdownItem = React.forwardRef<
React.ComponentRef<typeof DropdownItem>,
React.ComponentPropsWithoutRef<typeof MenuItem>
>(({ className, ...props }, ref) => (
<DropdownItem ref={ref} className={cn('menu-dropdown-item', className)}>
<MenuItem {...props} />
</DropdownItem>
));
MenuDropdownItem.displayName = 'MenuDropdownItem';
export {
Menu,
MenuItem,
MenuDropdown,
MenuDropdownTrigger,
MenuDropdownContent,
MenuDropdownGroup,
MenuDropdownItem,
};
@@ -0,0 +1,377 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { Slot, Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { ICON_SIZE } from '../constants';
import { cn } from '../lib/utils';
import type { Radius, Size } from '../types';
import { Icon } from './icon';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
import { ScrollArea, ScrollBar } from './scroll-area';
import { Tag } from './tag';
import useTheme from './use-theme';
type MultiSelectItemType = { value: string; label: React.ReactNode };
type MultiSelectContextType = {
disabled: boolean;
size: Size;
radius: Radius;
selectedItems: MultiSelectItemType[];
setSelectedItems: (items: MultiSelectItemType[]) => void;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
toggleOption: (item: MultiSelectItemType) => void;
registeredItems: MultiSelectItemType[];
registerItem: (item: MultiSelectItemType) => void;
};
const MultiSelectContext = React.createContext<
MultiSelectContextType | undefined
>(undefined);
function useMultiSelectContext() {
const ctx = React.useContext(MultiSelectContext);
if (!ctx)
throw new Error('MultiSelect components must be used within <MultiSelect>');
return ctx;
}
interface MultiSelectProps {
value?: string[];
defaultValue?: string[];
onValueChange?: (value: string[]) => void;
children: React.ReactNode;
className?: string;
size?: Size;
radius?: Radius;
disabled?: boolean;
}
const selectVariants = cva('select', {
variants: {
size: {
small: 'select-small',
medium: 'select-medium',
large: 'select-large',
},
defaultVariants: { size: undefined },
},
});
const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
function MultiSelect(
{
value,
defaultValue = [],
onValueChange,
children,
className,
size,
radius,
disabled = false,
},
ref,
) {
const { themeSize, themeRadius } = useTheme();
const [registeredItems, setRegisteredItems] = React.useState<
MultiSelectItemType[]
>([]);
const [selectedValues, setSelectedValues] =
React.useState<string[]>(defaultValue);
const [isOpen, setIsOpen] = React.useState(false);
const values = value ?? selectedValues;
const setValues = (vals: string[]) => {
if (onValueChange) onValueChange(vals);
if (value === undefined) setSelectedValues(vals);
};
const registerItem = React.useCallback((item: MultiSelectItemType) => {
setRegisteredItems((prev) =>
prev.find((i) => i.value === item.value) ? prev : [...prev, item],
);
}, []);
const selectedItems = React.useMemo(
() =>
values
.map((val) => registeredItems.find((item) => item.value === val))
.filter(Boolean) as MultiSelectItemType[],
[values, registeredItems],
);
const toggleOption = (item: MultiSelectItemType) => {
setValues(
values.includes(item.value) ?
values.filter((v) => v !== item.value)
: [...values, item.value],
);
};
const hiddenRegisterItems = (
<div style={{ display: 'none' }}>
{React.Children.map(children, (child) => {
if (
React.isValidElement(child) &&
(child.type as React.ForwardRefExoticComponent<HTMLElement>)
.displayName === 'MultiSelectItem'
) {
return child;
}
if (
React.isValidElement(child) &&
(child.props as { children?: React.ReactNode }).children
) {
return React.Children.map(
(child.props as { children?: React.ReactNode }).children,
(c) => c,
);
}
return null;
})}
</div>
);
return (
<MultiSelectContext.Provider
value={{
disabled,
size: size ?? themeSize,
radius: radius ?? themeRadius,
selectedItems,
setSelectedItems: (items) => setValues(items.map((i) => i.value)),
isOpen,
setIsOpen,
toggleOption,
registeredItems,
registerItem,
}}
>
{hiddenRegisterItems}
<Popover open={isOpen} onOpenChange={setIsOpen}>
<div
ref={ref}
className={cn(
selectVariants({
size: size ?? themeSize,
className,
}),
)}
>
{children}
</div>
</Popover>
</MultiSelectContext.Provider>
);
},
);
interface MultiSelectTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
}
const selectTriggerVariants = cva('select-trigger', {
variants: {
size: {
small: 'select-trigger-small',
medium: 'select-trigger-medium',
large: 'select-trigger-large',
},
radius: {
small: 'select-trigger-radius-small',
medium: 'select-trigger-radius-medium',
large: 'select-trigger-radius-large',
},
},
defaultVariants: { size: undefined, radius: undefined },
});
const MultiSelectTrigger = React.forwardRef<
HTMLButtonElement,
MultiSelectTriggerProps
>(function MultiSelectTrigger({ asChild, ...props }, ref) {
const Comp = asChild ? Slot : MultiSelectTriggerButton;
return (
<PopoverTrigger asChild>
<Comp ref={ref} {...props} />
</PopoverTrigger>
);
});
interface MultiSelectTriggerButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
className?: string;
children?: React.ReactNode;
}
const MultiSelectTriggerButton = React.forwardRef<
HTMLButtonElement,
MultiSelectTriggerButtonProps
>(function MultiSelectTrigger({ className, children, ...props }, ref) {
const { size, radius, disabled } = useMultiSelectContext();
return (
<button
ref={ref}
type="button"
aria-haspopup="listbox"
className={cn(selectTriggerVariants({ size, radius, className }))}
data-placeholder
disabled={disabled}
{...props}
>
<Slottable>{children}</Slottable>
<Icon name="RiArrowDownSLine" size={ICON_SIZE[size]} />
</button>
);
});
interface MultiSelectContentProps extends React.HTMLAttributes<HTMLDivElement> {
maxHeight?: string;
}
const MultiSelectContent = React.forwardRef<
HTMLDivElement,
MultiSelectContentProps
>(function MultiSelectContent(
{ className, children, maxHeight = 'auto', ...props },
ref,
) {
return (
<PopoverContent
ref={ref}
className={cn('select-content', className)}
{...props}
>
<div className={cn('select-viewport')}>
<ScrollArea maxHeight={maxHeight}>
{children}
<ScrollBar />
</ScrollArea>
</div>
</PopoverContent>
);
});
const selectItemVariants = cva('select-item', {
variants: { check: { left: 'select-item-left', right: 'select-item-right' } },
defaultVariants: { check: 'left' },
});
const selectItemCheckVariants = cva('select-item-check', {
variants: {
check: { left: 'select-item-check-left', right: 'select-item-check-right' },
},
defaultVariants: { check: 'left' },
});
interface MultiSelectItemProps {
value: string;
children: React.ReactNode;
className?: string;
}
const MultiSelectItem = React.forwardRef<HTMLDivElement, MultiSelectItemProps>(
function MultiSelectItem({ value, children, className }, ref) {
const { selectedItems, toggleOption, registerItem } =
useMultiSelectContext();
const isSelected = selectedItems.some((item) => item.value === value);
// Register this item on mount
React.useEffect(() => {
registerItem({ value, label: children });
}, [value, children, registerItem]);
return (
<div
ref={ref}
role="option"
aria-selected={isSelected}
tabIndex={0}
className={cn(
selectItemVariants({
check: typeof children === 'string' ? 'left' : 'right',
className,
}),
)}
onClick={() => toggleOption({ value, label: children })}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ')
toggleOption({ value, label: children });
}}
>
{isSelected && (
<span
className={cn(
selectItemCheckVariants({
check: typeof children === 'string' ? 'left' : 'right',
}),
)}
>
<Icon name="RiCheckLine" size={16} />
</span>
)}
<Slottable>{children}</Slottable>
</div>
);
},
);
MultiSelectItem.displayName = 'MultiSelectItem';
const MultiSelectValue = React.forwardRef<
HTMLSpanElement,
{ placeholder?: React.ReactNode } & React.HTMLAttributes<HTMLSpanElement>
>(function MultiSelectValue({ placeholder, ...props }, ref) {
const { selectedItems, size } = useMultiSelectContext();
if (selectedItems.length === 0) {
return (
<span ref={ref} {...props}>
{placeholder}
</span>
);
}
return (
<span ref={ref} {...props}>
{selectedItems.map((item) => (
<Tag
key={item.value}
variant="outline"
size={size}
className="select-tag"
>
{item.label}
</Tag>
))}
</span>
);
});
export {
MultiSelect,
MultiSelectTrigger,
MultiSelectTriggerButton,
MultiSelectContent,
MultiSelectItem,
MultiSelectValue,
};
@@ -0,0 +1,165 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { cn } from '../lib/utils';
import type { ButtonProps } from './button';
import { Button } from './button';
import { Icon } from './icon';
import useTheme from './use-theme';
const PaginationContext = React.createContext<ButtonProps>({
size: undefined,
radius: undefined,
});
type PaginationProps = React.ComponentProps<'nav'> &
Pick<ButtonProps, 'size' | 'radius'>;
const Pagination = ({ size, radius, className, ...props }: PaginationProps) => {
const { themeSize, themeRadius } = useTheme();
return (
<PaginationContext.Provider
value={{ size: size ?? themeSize, radius: radius ?? themeRadius }}
>
<nav
role="navigation"
aria-label="pagination"
className={cn('pagination', className)}
{...props}
/>
</PaginationContext.Provider>
);
};
Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul ref={ref} className={cn('pagination-content', className)} {...props} />
));
PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('pagination-item', className)} {...props} />
));
PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = {
isActive?: boolean;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
const PaginationLink = ({
className,
isActive,
...props
}: PaginationLinkProps) => {
const { size, radius } = React.useContext(PaginationContext);
return (
<Button
variant={isActive ? 'outline' : 'ghost'}
size={size}
radius={radius}
className={cn('pagination-link', className)}
asChild
>
<a {...props} aria-current={isActive ? 'page' : undefined} />
</Button>
);
};
PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({
className,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const { size, radius } = React.useContext(PaginationContext);
return (
<Button
variant="ghost"
size={size}
radius={radius}
aria-label="Go to previous page"
className={cn('pagination-previous', className)}
asChild
>
<a {...props}>
<Icon name="RiArrowLeftSLine" />
Previous
</a>
</Button>
);
};
PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({
className,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const { size, radius } = React.useContext(PaginationContext);
return (
<Button
variant="ghost"
size={size}
radius={radius}
aria-label="Go to next page"
className={cn('pagination-next', className)}
asChild
>
<a {...props}>
Next
<Icon name="RiArrowRightSLine" />
</a>
</Button>
);
};
PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<'button'>) => {
const { size, radius } = React.useContext(PaginationContext);
return (
<Button
{...props}
variant="ghost"
aria-label="More Pages"
size={size}
radius={radius}
className={cn('pagination-ellipsis', className)}
>
<Icon name="RiMoreLine" />
</Button>
);
};
PaginationEllipsis.displayName = 'PaginationEllipsis';
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};
@@ -0,0 +1,52 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../lib/utils';
const Popover = ({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) => {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
};
const PopoverTrigger = ({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) => {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
};
const PopoverClose = PopoverPrimitive.Close;
const PopoverContent = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn('popover', className)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverClose };
@@ -0,0 +1,128 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import type { Radius } from '../types';
import useTheme from './use-theme';
const radioCardGroupVariants = cva('radio-card-group', {
variants: {
orientation: {
horizontal: 'radio-card-group-horizontal',
vertical: 'radio-card-group-vertical',
},
},
defaultVariants: {
orientation: 'horizontal',
},
});
const RadioGroupContext = React.createContext<RadioCardGroupProps>({
cardType: 'vertical',
radius: 'medium',
});
type RadioCardGroupProps = React.ComponentPropsWithoutRef<
typeof RadioGroupPrimitive.Root
> & {
cardType?: 'horizontal' | 'vertical';
radius?: Radius;
};
const RadioCardGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
RadioCardGroupProps
>(
(
{
orientation = 'horizontal',
radius,
cardType = 'vertical',
children,
className,
...props
},
ref,
) => {
const { themeRadius } = useTheme();
return (
<RadioGroupPrimitive.Root
className={cn(radioCardGroupVariants({ orientation, className }))}
{...props}
ref={ref}
>
<RadioGroupContext.Provider
value={{ cardType, radius: radius ?? themeRadius }}
>
{children}
</RadioGroupContext.Provider>
</RadioGroupPrimitive.Root>
);
},
);
RadioCardGroup.displayName = RadioGroupPrimitive.Root.displayName;
const radioCardVariants = cva('radio-card', {
variants: {
type: {
vertical: 'radio-card-vertical',
horizontal: 'radio-card-horizontal',
},
radius: {
small: 'radio-card-radius-small',
medium: 'radio-card-radius-medium',
large: 'radio-card-radius-large',
},
},
defaultVariants: {
type: 'vertical',
radius: undefined,
},
});
const RadioCard = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> &
VariantProps<typeof radioCardVariants> & {
icon?: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
}
>(({ icon, title, description, className, ...props }, ref) => {
const { radius, cardType: type } = React.useContext(RadioGroupContext);
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(radioCardVariants({ type, radius, className }))}
{...props}
>
{icon}
<span className="radio-card-text">
{title && <strong className="radio-card-title">{title}</strong>}
{description && (
<span className="radio-card-description">{description}</span>
)}
</span>
</RadioGroupPrimitive.Item>
);
});
RadioCard.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioCardGroup, RadioCard };
@@ -0,0 +1,72 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import { Icon } from './icon';
const radioGroupVariants = cva('radio-group', {
variants: {
orientation: {
horizontal: 'radio-group-horizontal',
vertical: 'radio-group-vertical',
},
},
defaultVariants: {
orientation: 'horizontal',
},
});
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ orientation, className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn(radioGroupVariants({ orientation, className }))}
{...props}
ref={ref}
/>
);
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ children, className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn('radio-item', className)}
{...props}
>
<span className="radio">
<RadioGroupPrimitive.Indicator className="radio-indicator">
<Icon name="RiCircleFill" size={6} />
</RadioGroupPrimitive.Indicator>
</span>
<Slottable>{children}</Slottable>
</RadioGroupPrimitive.Item>
);
});
RadioItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioItem };
@@ -0,0 +1,89 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
const scrollBarVariants = cva('scroll-bar', {
variants: {
orientation: {
vertical: 'scroll-bar-vertical',
horizontal: 'scroll-bar-horizontal',
},
},
defaultVariants: {
orientation: 'vertical',
},
});
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
maxWidth?: string;
maxHeight?: string;
}
>(
(
{
type = 'auto',
maxWidth,
maxHeight,
className,
children,
style,
...props
},
ref,
) => (
<ScrollAreaPrimitive.Root
ref={ref}
type={type}
className={cn('scroll-area', className)}
style={{ ...style, maxWidth }}
{...props}
>
<ScrollAreaPrimitive.Viewport
className="scroll-area-viewport"
style={{ maxHeight }}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
),
);
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(scrollBarVariants({ orientation, className }))}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="scroll-thumb" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
@@ -0,0 +1,226 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { ICON_SIZE } from '../constants';
import type { Radius, Size } from '../lib/types';
import { cn } from '../lib/utils';
import { Icon } from './icon';
import { ScrollArea, ScrollBar } from './scroll-area';
import useTheme from './use-theme';
type SelectContextType = { size: Size; radius: Radius };
const SelectContext = React.createContext<SelectContextType | undefined>(
undefined,
);
function useSelectContext() {
const ctx = React.useContext(SelectContext);
if (!ctx) throw new Error('Select components must be used within <Select>');
return ctx;
}
interface SelectProps
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> {
className?: string;
size?: Size;
radius?: Radius;
}
const selectVariants = cva('select', {
variants: {
size: {
small: 'select-small',
medium: 'select-medium',
large: 'select-large',
},
defaultVariants: { size: undefined },
},
});
const Select = ({ size, radius, className, ...props }: SelectProps) => {
const { themeSize, themeRadius } = useTheme();
return (
<SelectContext.Provider
value={{ size: size ?? themeSize, radius: radius ?? themeRadius }}
>
<div
className={cn(selectVariants({ size: size ?? themeSize, className }))}
>
<SelectPrimitive.Root {...props}></SelectPrimitive.Root>
</div>
</SelectContext.Provider>
);
};
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const selectTriggerVariants = cva('select-trigger', {
variants: {
size: {
small: 'select-trigger-small',
medium: 'select-trigger-medium',
large: 'select-trigger-large',
},
radius: {
small: 'select-trigger-radius-small',
medium: 'select-trigger-radius-medium',
large: 'select-trigger-radius-large',
},
},
defaultVariants: { size: undefined, radius: undefined },
});
interface SelectTriggerProps
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {}
const SelectTrigger = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Trigger>,
SelectTriggerProps
>(({ className, children, ...props }, ref) => {
const { size, radius } = useSelectContext();
return (
<SelectPrimitive.Trigger
ref={ref}
className={cn(selectTriggerVariants({ size, radius, className }))}
{...props}
>
<Slottable>{children}</Slottable>
<SelectPrimitive.Icon asChild>
<Icon name="RiArrowDownSLine" size={ICON_SIZE[size]} />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
});
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
interface SelectContentProps
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> {
maxHeight?: string;
}
const SelectContent = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Content>,
SelectContentProps
>(
(
{ maxHeight = 'auto', className, children, position = 'popper', ...props },
ref,
) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn('select-content', className)}
position={position}
{...props}
>
<SelectPrimitive.Viewport className={cn('select-viewport')}>
<ScrollArea maxHeight={maxHeight}>
{children}
<ScrollBar />
</ScrollArea>
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
),
);
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectGroupLabel = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('select-group-label', className)}
{...props}
/>
));
SelectGroupLabel.displayName = 'SelectGroupLabel';
const selectItemVariants = cva('select-item', {
variants: { check: { left: 'select-item-left', right: 'select-item-right' } },
defaultVariants: { check: 'left' },
});
const selectItemCheckVariants = cva('select-item-check', {
variants: {
check: { left: 'select-item-check-left', right: 'select-item-check-right' },
},
defaultVariants: { check: 'left' },
});
type SelectItemProps = React.ComponentPropsWithoutRef<
typeof SelectPrimitive.Item
>;
const SelectItem = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Item>,
SelectItemProps
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
selectItemVariants({
check: typeof children === 'string' ? 'left' : 'right',
className,
}),
)}
{...props}
>
<SelectPrimitive.ItemIndicator
className={cn(
selectItemCheckVariants({
check: typeof children === 'string' ? 'left' : 'right',
}),
)}
>
<Icon name="RiCheckLine" size={16} />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('select-separator', className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectGroupLabel,
SelectItem,
SelectSeparator,
};
+217
View File
@@ -0,0 +1,217 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import { Button } from './button';
import type { IconNameType } from './icon';
import { Icon } from './icon';
import { ScrollArea, ScrollBar } from './scroll-area';
const Sheet = SheetPrimitive.Root;
const SheetTrigger = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Trigger>,
SheetPrimitive.DialogTriggerProps &
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant = 'outline', className, children, ...props }, ref) => {
if (props.asChild) {
return (
<SheetPrimitive.Trigger
className={cn('sheet-trigger', className)}
ref={ref}
{...props}
>
{children}
</SheetPrimitive.Trigger>
);
}
return (
<SheetPrimitive.Trigger asChild>
<Button
variant={variant}
className={cn('sheet-trigger', className)}
ref={ref}
{...props}
>
{children}
</Button>
</SheetPrimitive.Trigger>
);
});
SheetTrigger.displayName = SheetPrimitive.Trigger.displayName;
const SheetClose = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant, ...props }, ref) => (
<SheetPrimitive.Close asChild>
<Button ref={ref} variant={variant ?? 'outline'} {...props} />
</SheetPrimitive.Close>
));
SheetClose.displayName = 'SheetClose';
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn('sheet-overlay', className)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva('sheet', {
variants: {
side: {
top: 'sheet-side-top',
bottom: 'sheet-side-bottom',
left: 'sheet-side-left',
right: 'sheet-side-right',
},
radius: {
small: 'sheet-radius-small',
medium: 'sheet-radius-medium',
large: 'sheet-radius-large',
},
},
defaultVariants: {
side: 'right',
radius: 'small',
},
});
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(
(
{ side = 'right', radius = 'small', className, children, ...props },
ref,
) => (
<SheetPortal>
<SheetOverlay>
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side, radius }), className)}
{...props}
>
{children}
<SheetPrimitive.Close asChild>
<Button
size="medium"
variant="ghost"
className="sheet-close"
aria-label="Close"
>
<Icon name="RiCloseLine" />
</Button>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetOverlay>
</SheetPortal>
),
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
interface SheetHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
icon?: IconNameType;
}
const SheetHeader = ({
icon,
children,
className,
...props
}: SheetHeaderProps) => (
<div className={cn('sheet-header', className)} {...props}>
{icon && <Icon name={icon} size={38} className="sheet-icon" />}
{children}
</div>
);
SheetHeader.displayName = 'SheetHeader';
interface SheetBodyProps extends React.HTMLAttributes<HTMLDivElement> {
asChild?: boolean;
}
const SheetBody = ({ asChild, className, ...props }: SheetBodyProps) => {
const Comp = asChild ? Slot : 'div';
return (
<ScrollArea>
<Comp className={cn(className)} {...props} />
<ScrollBar />
</ScrollArea>
);
};
SheetBody.displayName = 'SheetBody';
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('sheet-footer', className)} {...props} />
);
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('sheet-title', className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('sheet-description', className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetBody,
SheetFooter,
SheetTitle,
SheetDescription,
};
@@ -0,0 +1,55 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import type { Size } from '../types';
import useTheme from './use-theme';
export interface SpinnerProps extends React.HTMLAttributes<HTMLSpanElement> {
size?: Size;
}
const defaultVariants: {
size?: Size;
} = {
size: undefined,
};
const spinnerVariants = cva('spinner', {
variants: {
size: {
small: 'spinner-small',
medium: 'spinner-medium',
large: 'spinner-large',
},
},
defaultVariants,
});
const Spinner: React.FC<SpinnerProps> = (props) => {
const { size, className } = props;
const { themeSize } = useTheme();
return (
<span
className={cn(spinnerVariants({ size: size ?? themeSize }), className)}
aria-label="loading..."
></span>
);
};
export { Spinner };
@@ -0,0 +1,59 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cva } from 'class-variance-authority';
import type { Color } from '../lib/types';
import { cn } from '../lib/utils';
const switchVariants = cva('switch', {
variants: {
color: {
default: 'switch-default',
blue: 'switch-blue',
orange: 'switch-orange',
red: 'switch-red',
green: 'switch-green',
},
},
defaultVariants: {
color: 'default',
},
});
interface SwitchProps
extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> {
color?: Color;
}
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
SwitchProps
>(({ color, className, children, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(switchVariants({ color }), className)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb className={cn('switch-thumb')} />
{children}
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
+139
View File
@@ -0,0 +1,139 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import type { IconNameType } from './icon';
import { Icon } from './icon';
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<table ref={ref} className={cn('table', className)} {...props} />
));
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('table-header', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('table-body', className)} {...props} />
));
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn('table-footer', className)} {...props} />
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr ref={ref} className={cn('table-row', className)} {...props} />
));
TableRow.displayName = 'TableRow';
const tableHeadVariants = cva('table-head', {
variants: {
textAlign: {
left: 'table-head-left',
center: 'table-head-center',
right: 'table-head-right',
},
},
defaultVariants: {
textAlign: 'left',
},
});
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement> &
VariantProps<typeof tableHeadVariants> & {
icon?: IconNameType;
}
>(({ icon, textAlign = 'left', children, className, ...props }, ref) => (
<th
ref={ref}
className={cn(tableHeadVariants({ textAlign, className }))}
{...props}
>
{icon && <Icon name={icon} size={16} />}
{children}
</th>
));
TableHead.displayName = 'TableHead';
const tableCellVariants = cva('table-cell', {
variants: {
textAlign: {
left: 'table-cell-left',
center: 'table-cell-center',
right: 'table-cell-right',
},
},
defaultVariants: {
textAlign: 'left',
},
});
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement> &
VariantProps<typeof tableCellVariants>
>(({ textAlign = 'left', className, ...props }, ref) => (
<td
ref={ref}
className={cn(tableCellVariants({ textAlign, className }))}
{...props}
/>
));
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('table-caption', className)} {...props} />
));
TableCaption.displayName = 'TableCaption';
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
@@ -0,0 +1,67 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '../lib/utils';
const Tabs = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Root ref={ref} className={cn('tabs', className)} {...props} />
));
Tabs.displayName = TabsPrimitive.Root.displayName;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn('tabs-list', className)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn('tabs-trigger', className)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn('tabs-content', className)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
+94
View File
@@ -0,0 +1,94 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import type { Radius, Size } from '../lib/types';
import { cn } from '../lib/utils';
import useTheme from './use-theme';
type TagVariant = 'primary' | 'secondary' | 'outline' | 'destructive';
const tagVariants = cva('tag', {
variants: {
variant: {
primary: 'tag-primary',
secondary: 'tag-secondary',
outline: 'tag-outline',
destructive: 'tag-destructive',
},
size: {
large: 'tag-large',
medium: 'tag-medium',
small: 'tag-small',
},
radius: {
large: 'tag-radius-large',
medium: 'tag-radius-medium',
small: 'tag-radius-small',
},
defaultVariants: {
variant: 'primary',
size: undefined,
radius: undefined,
},
},
});
interface TagProps extends React.HTMLAttributes<HTMLElement> {
variant?: TagVariant;
size?: Size;
radius?: Radius;
asChild?: boolean;
}
const Tag = React.forwardRef<HTMLElement, TagProps>((props, ref) => {
const {
variant = 'primary',
size,
radius,
className,
children,
asChild,
...rest
} = props;
const { themeSize, themeRadius } = useTheme();
const Comp = asChild ? Slot : 'span';
return (
<Comp
{...rest}
ref={ref}
className={cn(
tagVariants({
variant,
size: size ?? themeSize,
radius: radius ?? themeRadius,
className,
}),
)}
>
{children}
</Comp>
);
});
Tag.displayName = 'Tag';
export { Tag, type TagProps };
@@ -0,0 +1,30 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import { cn } from '../lib/utils';
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<'textarea'>
>(({ className, ...props }, ref) => {
return (
<textarea className={cn('textarea', className)} ref={ref} {...props} />
);
});
Textarea.displayName = 'Textarea';
export { Textarea };
@@ -0,0 +1,533 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { cn } from '../lib/utils';
import { Icon } from './icon';
import type { TextInputProps } from './input';
import { InputBox, InputField, TextInput } from './input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from './select';
interface PeriodSelectorProps {
period: Period;
setPeriod?: (m: Period) => void;
date?: Date | null;
onDateChange?: (date: Date | undefined) => void;
onRightFocus?: () => void;
onLeftFocus?: () => void;
className?: string;
}
const TimePeriodSelect = React.forwardRef<
HTMLButtonElement,
PeriodSelectorProps
>(
(
{
period,
setPeriod,
date,
onDateChange,
onLeftFocus,
onRightFocus,
className,
},
ref,
) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === 'ArrowRight') onRightFocus?.();
if (e.key === 'ArrowLeft') onLeftFocus?.();
};
const handleValueChange = (value: Period) => {
setPeriod?.(value);
/**
* trigger an update whenever the user switches between AM and PM;
* otherwise user must manually change the hour each time
*/
if (date) {
const tempDate = new Date(date);
const hours = display12HourValue(date.getHours());
onDateChange?.(
setDateByType(
tempDate,
hours.toString(),
'12hours',
period === 'AM' ? 'PM' : 'AM',
),
);
}
};
return (
<Select
value={period}
onValueChange={(value: Period) => handleValueChange(value)}
>
<SelectTrigger
className={className}
ref={ref}
onKeyDown={handleKeyDown}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AM">AM</SelectItem>
<SelectItem value="PM">PM</SelectItem>
</SelectContent>
</Select>
);
},
);
TimePeriodSelect.displayName = 'TimePeriodSelect';
interface TimePickerInputProps extends React.PropsWithoutRef<TextInputProps> {
picker: TimePickerType;
date?: Date | null;
onDateChange?: (date: Date | undefined) => void;
period?: Period;
onRightFocus?: () => void;
onLeftFocus?: () => void;
}
const TimePickerInput = React.forwardRef<
HTMLInputElement,
TimePickerInputProps
>(
(
{
className,
type = 'tel',
value,
id,
name,
date = new Date(new Date().setHours(0, 0, 0, 0)),
onDateChange,
onChange,
onKeyDown,
picker,
period,
onLeftFocus,
onRightFocus,
...props
},
ref,
) => {
const [flag, setFlag] = React.useState<boolean>(false);
const [prevIntKey, setPrevIntKey] = React.useState<string>('0');
/**
* allow the user to enter the second digit within 2 seconds
* otherwise start again with entering first digit
*/
React.useEffect(() => {
if (flag) {
const timer = setTimeout(() => {
setFlag(false);
}, 2000);
return () => clearTimeout(timer);
}
}, [flag]);
const calculatedValue = React.useMemo(() => {
return getDateByType(date, picker);
}, [date, picker]);
const calculateNewValue = (key: string) => {
/*
* If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1.
* The second entered digit will break the condition and the value will be set to 10-12.
*/
if (picker === '12hours') {
if (flag && calculatedValue.slice(1, 2) === '1' && prevIntKey === '0')
return `0${key}`;
}
return !flag ? `0${key}` : calculatedValue.slice(1, 2) + key;
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Tab') return;
e.preventDefault();
if (e.key === 'ArrowRight') onRightFocus?.();
if (e.key === 'ArrowLeft') onLeftFocus?.();
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
const step = e.key === 'ArrowUp' ? 1 : -1;
const newValue = getArrowByType(calculatedValue, step, picker);
if (flag) setFlag(false);
const tempDate = date ? new Date(date) : new Date();
onDateChange?.(setDateByType(tempDate, newValue, picker, period));
}
if (e.key >= '0' && e.key <= '9') {
if (picker === '12hours') setPrevIntKey(e.key);
const newValue = calculateNewValue(e.key);
if (flag) onRightFocus?.();
setFlag((prev) => !prev);
const tempDate = date ? new Date(date) : new Date();
onDateChange?.(setDateByType(tempDate, newValue, picker, period));
}
};
return (
<InputField className={cn('w-24', className)}>
<InputBox>
<TextInput
ref={ref}
id={id ?? picker}
name={name ?? picker}
className="text-left font-mono tabular-nums caret-transparent [&::-webkit-inner-spin-button]:appearance-none"
value={value ?? calculatedValue}
onChange={(e) => {
e.preventDefault();
onChange?.(e);
}}
type={type}
inputMode="decimal"
onKeyDown={(e) => {
onKeyDown?.(e);
handleKeyDown(e);
}}
{...props}
/>
</InputBox>
</InputField>
);
},
);
TimePickerInput.displayName = 'TimePickerInput';
interface TimePickerProps {
date?: Date | null;
onChange?: (date: Date | undefined) => void;
hourCycle?: 12 | 24;
/**
* Determines the smallest unit that is displayed in the datetime picker.
* Default is 'second'.
* */
granularity?: Granularity;
}
interface TimePickerRef {
minuteRef: HTMLInputElement | null;
hourRef: HTMLInputElement | null;
secondRef: HTMLInputElement | null;
}
const TimePicker = React.forwardRef<TimePickerRef, TimePickerProps>(
({ date, onChange, hourCycle = 24, granularity = 'second' }, ref) => {
const minuteRef = React.useRef<HTMLInputElement>(null);
const hourRef = React.useRef<HTMLInputElement>(null);
const secondRef = React.useRef<HTMLInputElement>(null);
const periodRef = React.useRef<HTMLButtonElement>(null);
const [period, setPeriod] = React.useState<Period>(
date && date.getHours() >= 12 ? 'PM' : 'AM',
);
React.useImperativeHandle(
ref,
() => ({
minuteRef: minuteRef.current,
hourRef: hourRef.current,
secondRef: secondRef.current,
periodRef: periodRef.current,
}),
[minuteRef, hourRef, secondRef],
);
return (
<div className="flex items-center justify-center gap-2">
<label
htmlFor="datetime-picker-hour-input"
className="flex cursor-pointer items-center"
>
<Icon name="RiTimeLine" size={16} />
</label>
<TimePickerInput
picker={hourCycle === 24 ? 'hours' : '12hours'}
date={date}
id="datetime-picker-hour-input"
onDateChange={onChange}
ref={hourRef}
period={period}
onRightFocus={() => minuteRef.current?.focus()}
/>
{(granularity === 'minute' || granularity === 'second') && (
<>
:
<TimePickerInput
picker="minutes"
date={date}
onDateChange={onChange}
ref={minuteRef}
onLeftFocus={() => hourRef.current?.focus()}
onRightFocus={() => secondRef.current?.focus()}
/>
</>
)}
{granularity === 'second' && (
<>
:
<TimePickerInput
picker="seconds"
date={date}
onDateChange={onChange}
ref={secondRef}
onLeftFocus={() => minuteRef.current?.focus()}
onRightFocus={() => periodRef.current?.focus()}
className="w-24"
/>
</>
)}
{hourCycle === 12 && (
<TimePeriodSelect
period={period}
setPeriod={setPeriod}
date={date}
onDateChange={(date) => {
onChange?.(date);
if (date && date.getHours() >= 12) {
setPeriod('PM');
} else {
setPeriod('AM');
}
}}
ref={periodRef}
onLeftFocus={() => secondRef.current?.focus()}
/>
)}
</div>
);
},
);
TimePicker.displayName = 'TimePicker';
type Granularity = 'hour' | 'minute' | 'second';
// ---------- utils start ----------
/**
* regular expression to check for valid hour format (01-23)
*/
function isValidHour(value: string) {
return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value);
}
/**
* regular expression to check for valid 12 hour format (01-12)
*/
function isValid12Hour(value: string) {
return /^(0[1-9]|1[0-2])$/.test(value);
}
/**
* regular expression to check for valid minute format (00-59)
*/
function isValidMinuteOrSecond(value: string) {
return /^[0-5][0-9]$/.test(value);
}
type GetValidNumberConfig = { max: number; min?: number; loop?: boolean };
function getValidNumber(
value: string,
{ max, min = 0, loop = false }: GetValidNumberConfig,
) {
let numericValue = parseInt(value, 10);
if (!Number.isNaN(numericValue)) {
if (!loop) {
if (numericValue > max) numericValue = max;
if (numericValue < min) numericValue = min;
} else {
if (numericValue > max) numericValue = min;
if (numericValue < min) numericValue = max;
}
return numericValue.toString().padStart(2, '0');
}
return '00';
}
function getValidHour(value: string) {
if (isValidHour(value)) return value;
return getValidNumber(value, { max: 23 });
}
function getValid12Hour(value: string) {
if (isValid12Hour(value)) return value;
return getValidNumber(value, { min: 1, max: 12 });
}
function getValidMinuteOrSecond(value: string) {
if (isValidMinuteOrSecond(value)) return value;
return getValidNumber(value, { max: 59 });
}
type GetValidArrowNumberConfig = {
min: number;
max: number;
step: number;
};
function getValidArrowNumber(
value: string,
{ min, max, step }: GetValidArrowNumberConfig,
) {
let numericValue = parseInt(value, 10);
if (!Number.isNaN(numericValue)) {
numericValue += step;
return getValidNumber(String(numericValue), { min, max, loop: true });
}
return '00';
}
function getValidArrowHour(value: string, step: number) {
return getValidArrowNumber(value, { min: 0, max: 23, step });
}
function getValidArrow12Hour(value: string, step: number) {
return getValidArrowNumber(value, { min: 1, max: 12, step });
}
function getValidArrowMinuteOrSecond(value: string, step: number) {
return getValidArrowNumber(value, { min: 0, max: 59, step });
}
function setMinutes(date: Date, value: string) {
const minutes = getValidMinuteOrSecond(value);
date.setMinutes(parseInt(minutes, 10));
return date;
}
function setSeconds(date: Date, value: string) {
const seconds = getValidMinuteOrSecond(value);
date.setSeconds(parseInt(seconds, 10));
return date;
}
function setHours(date: Date, value: string) {
const hours = getValidHour(value);
date.setHours(parseInt(hours, 10));
return date;
}
function set12Hours(date: Date, value: string, period: Period) {
const hours = parseInt(getValid12Hour(value), 10);
const convertedHours = convert12HourTo24Hour(hours, period);
date.setHours(convertedHours);
return date;
}
type TimePickerType = 'minutes' | 'seconds' | 'hours' | '12hours';
export type Period = 'AM' | 'PM';
function setDateByType(
date: Date,
value: string,
type: TimePickerType,
period?: Period,
) {
switch (type) {
case 'minutes':
return setMinutes(date, value);
case 'seconds':
return setSeconds(date, value);
case 'hours':
return setHours(date, value);
case '12hours': {
if (!period) return date;
return set12Hours(date, value, period);
}
default:
return date;
}
}
function getDateByType(date: Date | null, type: TimePickerType) {
if (!date) return '00';
switch (type) {
case 'minutes':
return getValidMinuteOrSecond(String(date.getMinutes()));
case 'seconds':
return getValidMinuteOrSecond(String(date.getSeconds()));
case 'hours':
return getValidHour(String(date.getHours()));
case '12hours':
return getValid12Hour(String(display12HourValue(date.getHours())));
default:
return '00';
}
}
function getArrowByType(value: string, step: number, type: TimePickerType) {
switch (type) {
case 'minutes':
return getValidArrowMinuteOrSecond(value, step);
case 'seconds':
return getValidArrowMinuteOrSecond(value, step);
case 'hours':
return getValidArrowHour(value, step);
case '12hours':
return getValidArrow12Hour(value, step);
default:
return '00';
}
}
/**
* handles value change of 12-hour input
* 12:00 PM is 12:00
* 12:00 AM is 00:00
*/
function convert12HourTo24Hour(hour: number, period: Period) {
if (period === 'PM') {
if (hour <= 11) {
return hour + 12;
}
return hour;
}
if (hour === 12) return 0;
return hour;
}
/**
* time is stored in the 24-hour form,
* but needs to be displayed to the user
* in its 12-hour representation
*/
function display12HourValue(hours: number) {
if (hours === 0 || hours === 12) return '12';
if (hours >= 22) return `${hours - 12}`;
if (hours % 12 > 9) return `${hours}`;
return `0${hours % 12}`;
}
// ---------- utils end ----------
export { TimePickerInput, TimePeriodSelect };
export type { TimePickerInputProps, PeriodSelectorProps };
@@ -0,0 +1,98 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { Toaster as Sonner, toast } from 'sonner';
import { cn } from '../lib/utils';
import { Icon } from './icon';
import { Spinner } from './spinner';
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({
className,
toastOptions,
icons,
...props
}: ToasterProps) => {
return (
<Sonner
className={cn('toaster', className)}
toastOptions={{
unstyled: true,
classNames: {
toast: cn('toast', toastOptions?.classNames?.toast ?? ''),
title: cn('toast-title', toastOptions?.classNames?.title ?? ''),
description: cn(
'toast-description',
toastOptions?.classNames?.description ?? '',
),
loader: cn('toast-loader', toastOptions?.classNames?.loader ?? ''),
cancelButton: cn(
'toast-close button button-ghost button-medium button-radius-medium',
toastOptions?.classNames?.cancelButton ?? '',
),
actionButton: cn(
'toast-button button button-outline button-medium button-radius-medium',
toastOptions?.classNames?.actionButton ?? '',
),
success: cn('toast-success', toastOptions?.classNames?.success ?? ''),
error: cn('toast-error', toastOptions?.classNames?.error ?? ''),
info: cn('toast-info', toastOptions?.classNames?.info ?? ''),
warning: cn('toast-warning', toastOptions?.classNames?.warning ?? ''),
loading: '',
default: cn('toast-default', toastOptions?.classNames?.default ?? ''),
content: cn('toast-content', toastOptions?.classNames?.content ?? ''),
icon: cn('toast-icon', toastOptions?.classNames?.icon ?? ''),
},
}}
icons={{
...icons,
loading: icons?.loading ?? <Spinner size="small" />,
warning: icons?.warning ?? (
<Icon
name="RiErrorWarningFill"
size={20}
className="toast-icon-warning"
/>
),
success: icons?.success ?? (
<Icon
name="RiCheckboxCircleFill"
size={20}
className="toast-icon-success"
/>
),
error: icons?.success ?? (
<Icon
name="RiCloseCircleFill"
size={20}
className="toast-icon-error"
/>
),
info: icons?.info ?? (
<Icon
name="RiInformation2Fill"
size={20}
className="toast-icon-informative"
/>
),
}}
{...props}
/>
);
};
export { Toaster, toast };
@@ -0,0 +1,113 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type * as TogglePrimitive from '@radix-ui/react-toggle';
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import type { Radius, Size } from '../lib/types';
import { cn } from '../lib/utils';
import useTheme from './use-theme';
const toggleVariants = cva('toggle-group-item', {
variants: {
size: {
small: 'toggle-group-item-small',
medium: 'toggle-group-item-medium',
large: 'toggle-group-item-large',
},
radius: {
small: 'toggle-group-item-radius-small',
medium: 'toggle-group-item-radius-medium',
large: 'toggle-group-item-radius-large',
},
},
defaultVariants: {
size: undefined,
radius: undefined,
},
});
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: undefined,
radius: undefined,
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, size, radius, children, ...props }, ref) => {
const { themeSize, themeRadius } = useTheme();
return (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn('toggle-group', className)}
{...props}
>
<ToggleGroupContext.Provider
value={{ size: size ?? themeSize, radius: radius ?? themeRadius }}
>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
});
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
interface ToggleGroupItemProps
extends React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root>,
Omit<VariantProps<typeof toggleVariants>, 'disabled'> {
size?: Size;
radius?: Radius;
value: string;
}
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
ToggleGroupItemProps
>(({ className, children, size, radius, disabled, value, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
size: size ?? context.size,
radius: radius ?? context.radius,
}),
className,
)}
value={value}
disabled={disabled}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };
@@ -0,0 +1,97 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import { Slottable } from '@radix-ui/react-slot';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
const tooltipVariants = cva('tooltip', {
variants: {
textAlign: {
left: 'tooltip-text-left',
center: 'tooltip-text-center',
right: 'tooltip-text-right',
},
defaultVariants: {
textAlign: 'center',
},
},
});
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
interface TooltipContentProps
extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> {
textAlign?: 'left' | 'center' | 'right';
}
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
TooltipContentProps
>(
(
{
title,
children,
className,
side,
sideOffset = 4,
textAlign = 'center',
...props
},
ref,
) => (
<TooltipPrimitive.Content
ref={ref}
side={side}
sideOffset={sideOffset}
className={cn(tooltipVariants({ textAlign, className }))}
{...props}
>
{title && <strong className={cn('tooltip-title')}>{title}</strong>}
<Slottable>{children}</Slottable>
{side !== undefined && (
<>
<TooltipPrimitive.TooltipArrow
width={10}
height={6}
className={cn('tooltip-arrow-border')}
/>
<TooltipPrimitive.TooltipArrow
width={8}
height={5}
className={cn('tooltip-arrow')}
/>
</>
)}
</TooltipPrimitive.Content>
),
);
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
type TooltipContentProps,
};
@@ -0,0 +1,38 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import type { Radius, Size } from '../types';
const DefaultValue: {
themeSize: Size;
themeRadius: Radius;
} = {
themeSize: 'small',
themeRadius: 'medium',
} as const;
const useTheme = () => {
if (typeof window !== 'undefined') {
const size = (document.body.getAttribute('data-size') ??
DefaultValue.themeSize) as Size;
const radius = (document.body.getAttribute('data-radius') ??
DefaultValue.themeRadius) as Radius;
return { themeSize: size, themeRadius: radius };
} else {
return DefaultValue;
}
};
export default useTheme;
+60
View File
@@ -0,0 +1,60 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import type { IconNameType } from './components';
import type { CaptionType } from './lib/types';
import type { Size } from './types';
export const ICON_SIZE: Record<Size, number> = {
large: 24,
medium: 20,
small: 16,
};
export const SMALL_ICON_SIZE: Record<Size, number> = {
large: 16,
medium: 16,
small: 12,
};
export const CHECK_ICON_SIZE: Record<Size, number> = {
large: 20,
medium: 16,
small: 14,
};
export const INPUT_CAPTION_ICON_SIZE: Record<Size, number> = {
large: 20,
medium: 16,
small: 16,
};
export const CAPTION_DEFAULT_ICON: Record<
CaptionType,
IconNameType | undefined
> = {
default: undefined,
error: 'RiErrorWarningFill',
info: 'RiInformationFill',
success: 'RiCheckboxCircleFill',
} as const;
export const ALERT_DEFAULT_ICON: Record<string, IconNameType | undefined> = {
default: undefined,
warning: 'RiErrorWarningFill',
informative: 'RiInformation2Fill',
success: 'RiCheckboxCircleFill',
error: 'RiCloseCircleFill',
};
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export * from './components';
export * from './types';
export * from './constants';
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export type Radius = 'small' | 'medium' | 'large';
export type Size = 'small' | 'medium' | 'large';
export type Color = 'default' | 'blue' | 'orange' | 'red' | 'green';
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'destructive'
| 'ghost'
| 'outline';
export type ButtonState = 'default' | 'loading' | 'disabled';
export type CaptionType = 'default' | 'success' | 'info' | 'error';
export type AlertVariantType =
| 'default'
| 'warning'
| 'success'
| 'error'
| 'informative';
export type TriggerType = 'click' | 'hover';
+84
View File
@@ -0,0 +1,84 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import type { ClassValue } from 'clsx';
import { clsx } from 'clsx';
import { extendTailwindMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
const customTwMerge = extendTailwindMerge({
extend: {
classGroups: {
'font-size': [
'text-title-h1',
'text-title-h2',
'text-title-h3',
'text-title-h4',
'text-title-h5',
'text-small-normal',
'text-small-strong',
'text-small-underline',
'text-small-delete',
'text-base-normal',
'text-base-strong',
'text-base-underline',
'text-base-delete',
'text-large-normal',
'text-large-strong',
'text-large-underline',
'text-large-delete',
'text-xlarge-normal',
'text-xlarge-strong',
'text-xlarge-underline',
'text-xlarge-delete',
],
},
},
});
return customTwMerge(clsx(inputs));
}
type PossibleRef<T> = React.Ref<T> | undefined;
/**
* Set a given ref to a given value
* This utility takes care of different types of refs: callback refs and RefObject(s)
*/
export function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === 'function') {
ref(value);
} else if (ref !== null && ref !== undefined) {
(ref as React.MutableRefObject<T>).current = value;
}
}
/**
* A utility to compose multiple refs together
* Accepts callback refs and RefObject(s)
*/
export function composeRefs<T>(...refs: PossibleRef<T>[]) {
return (node: T) => refs.forEach((ref) => setRef(ref, node));
}
/**
* A custom hook that composes multiple refs
* Accepts callback refs and RefObject(s)
*/
export function useComposedRefs<T>(...refs: PossibleRef<T>[]) {
// eslint-disable-next-line react-compiler/react-compiler
return React.useCallback(composeRefs(...refs), refs);
}
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export type Radius = 'small' | 'medium' | 'large';
export type Size = 'small' | 'medium' | 'large';
export type Color = 'default' | 'blue' | 'orange' | 'red' | 'green';
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'destructive'
| 'ghost'
| 'outline';
+7
View File
@@ -0,0 +1,7 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ['class'],
prefix: '',
content: ['./src/**/*.tsx'],
plugins: [require('@ufb/tailwindcss')],
};
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "@ufb/tsconfig/internal-package.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "ES2022"],
"jsx": "preserve",
"rootDir": "."
},
"include": ["src"],
"exclude": ["node_modules"]
}