first commit
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: ['class'],
|
||||
prefix: '',
|
||||
content: ['./src/**/*.tsx'],
|
||||
plugins: [require('@ufb/tailwindcss')],
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@ufb/tsconfig/internal-package.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"jsx": "preserve",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user