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

This commit is contained in:
SDI
2026-07-15 18:05:12 +09:00
commit 12e4f17b62
4633 changed files with 817125 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"ui": "src/components",
"utils": "@/lib/utils"
}
}
+11
View File
@@ -0,0 +1,11 @@
import baseConfig from '@ufb/eslint-config/base';
import reactConfig from '@ufb/eslint-config/react';
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: ['postcss.js'],
},
...baseConfig,
...reactConfig,
];
+59
View File
@@ -0,0 +1,59 @@
{
"name": "@ufb/react",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"clean": "git clean -xdf .cache .turbo dist node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@remixicon/react": "^4.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.577.0",
"react-day-picker": "8.10.1",
"react-hook-form": "^7.72.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@ufb/eslint-config": "workspace:^",
"@ufb/prettier-config": "workspace:^",
"@ufb/tailwindcss": "workspace:^",
"@ufb/tsconfig": "workspace:^",
"eslint": "catalog:",
"prettier": "catalog:",
"react": "^19.2.4",
"tailwindcss": "catalog:",
"typescript": "catalog:"
},
"prettier": "@ufb/prettier-config"
}
+1
View File
@@ -0,0 +1 @@
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } };
@@ -0,0 +1,179 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { ICON_SIZE } from '../constants';
import { cn } from '../lib/utils';
import type { Size } from '../types';
import { Icon } from './icon';
const DefaultValue = {
iconSize: 'small',
iconAlign: 'right',
border: false,
} as const;
const AccordionContext = React.createContext<{
iconSize: Size;
iconAlign: 'left' | 'right';
}>({
iconSize: DefaultValue.iconSize,
iconAlign: DefaultValue.iconAlign,
});
const Accordion = React.forwardRef<
React.ComponentRef<typeof AccordionPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root> & {
iconSize?: Size;
iconAlign?: 'left' | 'right';
border?: boolean;
}
>(
(
{
iconAlign = DefaultValue.iconAlign,
iconSize = DefaultValue.iconSize,
border = DefaultValue.border,
className,
...props
},
ref,
) => (
<AccordionContext.Provider value={{ iconAlign, iconSize }}>
<AccordionPrimitive.Root
ref={ref}
className={cn('accordion', border && 'accordion-border', className)}
{...props}
/>
</AccordionContext.Provider>
),
);
Accordion.displayName = 'Accordion';
const AccordionItem = React.forwardRef<
React.ComponentRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> & {
divider?: boolean;
}
>(({ divider = true, className, ...props }, ref) => {
return (
<AccordionPrimitive.Item
ref={ref}
className={cn(
'accordion-item',
divider && 'accordion-item-border',
className,
)}
{...props}
/>
);
});
AccordionItem.displayName = 'AccordionItem';
const AccordionTrigger = React.forwardRef<
React.ComponentRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => {
const { iconSize, iconAlign } = React.useContext(AccordionContext);
return (
<AccordionPrimitive.Header>
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'accordion-trigger',
iconAlign === 'left' && 'accordion-trigger-align-left',
className,
)}
{...props}
>
{iconAlign === 'left' && (
<Icon name="RiArrowDownSLine" size={ICON_SIZE[iconSize]} />
)}
<Slottable>{children}</Slottable>
{iconAlign === 'right' && (
<Icon name="RiArrowDownSLine" size={ICON_SIZE[iconSize]} />
)}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
});
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const accordionContentVariants = cva('accordion-content', {
variants: {
iconAlign: {
left: '',
right: '',
},
iconSize: {
small: '',
medium: '',
large: '',
},
},
compoundVariants: [
{
iconAlign: 'left',
iconSize: 'small',
className: 'accordion-content-inset-small',
},
{
iconAlign: 'left',
iconSize: 'medium',
className: 'accordion-content-inset-medium',
},
{
iconAlign: 'left',
iconSize: 'large',
className: 'accordion-content-inset-large',
},
],
defaultVariants: {
iconAlign: DefaultValue.iconAlign,
iconSize: DefaultValue.iconSize,
},
});
const AccordionContent = React.forwardRef<
React.ComponentRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => {
const { iconAlign, iconSize } = React.useContext(AccordionContext);
return (
<AccordionPrimitive.Content
ref={ref}
className="accordion-content-box"
{...props}
>
<div
className={cn(
accordionContentVariants({ iconAlign, iconSize, className }),
)}
>
{children}
</div>
</AccordionPrimitive.Content>
);
});
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+214
View File
@@ -0,0 +1,214 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { ALERT_DEFAULT_ICON } from '../constants';
import { cn } from '../lib/utils';
import { Button } from './button';
import type { IconNameType } from './icon';
import { Icon } from './icon';
import useTheme from './use-theme';
const alertVariants = cva('alert', {
variants: {
variant: {
default: 'alert-default',
warning: 'alert-warning',
success: 'alert-success',
error: 'alert-error',
informative: 'alert-informative',
},
radius: {
small: 'alert-radius-small',
medium: 'alert-radius-medium',
large: 'alert-radius-large',
},
},
defaultVariants: {
variant: 'default',
radius: undefined,
},
});
const AlertContext = React.createContext<VariantProps<typeof alertVariants>>({
variant: 'default',
radius: undefined,
});
interface AlertProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof alertVariants> {}
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
({ children, className, variant = 'default', radius, ...props }, ref) => {
const { themeRadius } = useTheme();
return (
<div
ref={ref}
role="alert"
className={cn(
alertVariants({ variant, radius: radius ?? themeRadius }),
className,
)}
{...props}
>
<AlertContext.Provider
value={{ variant, radius: radius ?? themeRadius }}
>
{children}
</AlertContext.Provider>
</div>
);
},
);
Alert.displayName = 'Alert';
type AlertContentProps = React.HTMLAttributes<HTMLHeadingElement>;
const AlertContent = React.forwardRef<HTMLParagraphElement, AlertContentProps>(
({ children, className, ...props }, ref) => (
<div ref={ref} className={cn('alert-content', className)} {...props}>
{children}
</div>
),
);
AlertContent.displayName = 'AlertContent';
type AlertTextContainerProps = React.HTMLAttributes<HTMLHeadingElement>;
const AlertTextContainer = React.forwardRef<
HTMLParagraphElement,
AlertTextContainerProps
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('alert-text-container', className)} {...props} />
));
AlertTextContainer.displayName = 'AlertTextContainer';
type AlertTitleProps = React.HTMLAttributes<HTMLHeadingElement>;
const AlertTitle = React.forwardRef<HTMLParagraphElement, AlertTitleProps>(
({ className, ...props }, ref) => (
<h5 ref={ref} className={cn('alert-title', className)} {...props} />
),
);
AlertTitle.displayName = 'AlertTitle';
type AlertDescriptionProps = React.HTMLAttributes<HTMLHeadingElement>;
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
AlertDescriptionProps
>(({ className, ...props }, ref) => (
<p className={cn('alert-description', className)} {...props} ref={ref} />
));
AlertDescription.displayName = 'AlertDescription';
interface AlertIconButtonProps
extends Omit<React.ComponentPropsWithoutRef<typeof Button>, 'icon'> {
icon?: IconNameType;
}
const AlertIconButton = React.forwardRef<
HTMLButtonElement,
AlertIconButtonProps
>(({ icon, variant, size, className, ...props }, ref) => {
const { radius } = React.useContext(AlertContext);
return (
<Button
ref={ref}
radius={radius ?? 'medium'}
size={size ?? 'medium'}
variant={variant ?? 'ghost'}
className={cn('alert-close', className)}
{...props}
>
<Icon name={icon ?? 'RiCloseFill'} />
</Button>
);
});
AlertIconButton.displayName = 'AlertIconButton';
const AlertButton = React.forwardRef<
HTMLButtonElement,
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant, size, className, ...props }, ref) => {
const { radius } = React.useContext(AlertContext);
return (
<Button
ref={ref}
variant={variant ?? 'outline'}
size={size ?? 'medium'}
radius={radius ?? 'medium'}
className={cn('alert-button', className)}
{...props}
/>
);
});
AlertButton.displayName = 'AlertButton';
const alertIconVariants = cva('alert-icon', {
variants: {
variant: {
default: 'alert-icon-default',
warning: 'alert-icon-warning',
success: 'alert-icon-success',
error: 'alert-icon-error',
informative: 'alert-icon-informative',
},
},
defaultVariants: {
variant: 'default',
},
});
interface AlertIconProps
extends Omit<React.ComponentPropsWithoutRef<typeof Icon>, 'name'> {
name?: IconNameType;
}
const AlertIcon = ({
className,
name = undefined,
size = 20,
...props
}: AlertIconProps) => {
const { variant } = React.useContext(AlertContext);
return (
<Icon
className={cn(alertIconVariants({ variant, className }))}
name={name ?? ALERT_DEFAULT_ICON[variant ?? 'default']}
size={size}
{...props}
/>
);
};
AlertIcon.displayName = 'AlertIcon';
export {
Alert,
AlertContent,
AlertTextContainer,
AlertDescription,
AlertTitle,
AlertIconButton,
AlertButton,
AlertIcon,
type AlertProps,
};
+170
View File
@@ -0,0 +1,170 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import type { Color, Radius } from '../lib/types';
import { cn } from '../lib/utils';
import useTheme from './use-theme';
type BadgeVariant = 'bold' | 'subtle' | 'outline';
const badgeVariants = cva('badge', {
variants: {
radius: {
large: 'badge-radius-large',
medium: 'badge-radius-medium',
small: 'badge-radius-small',
},
variant: {
bold: '',
subtle: '',
outline: '',
},
color: {
default: '',
blue: '',
orange: '',
red: '',
green: '',
},
},
compoundVariants: [
{
variant: 'bold',
color: 'default',
className: 'badge-bold-default',
},
{
variant: 'bold',
color: 'blue',
className: 'badge-bold-blue',
},
{
variant: 'bold',
color: 'orange',
className: 'badge-bold-orange',
},
{
variant: 'bold',
color: 'red',
className: 'badge-bold-red',
},
{
variant: 'bold',
color: 'green',
className: 'badge-bold-green',
},
{
variant: 'subtle',
color: 'default',
className: 'badge-subtle-default',
},
{
variant: 'subtle',
color: 'blue',
className: 'badge-subtle-blue',
},
{
variant: 'subtle',
color: 'orange',
className: 'badge-subtle-orange',
},
{
variant: 'subtle',
color: 'red',
className: 'badge-subtle-red',
},
{
variant: 'subtle',
color: 'green',
className: 'badge-subtle-green',
},
{
variant: 'outline',
color: 'default',
className: 'badge-outline-default',
},
{
variant: 'outline',
color: 'blue',
className: 'badge-outline-blue',
},
{
variant: 'outline',
color: 'orange',
className: 'badge-outline-orange',
},
{
variant: 'outline',
color: 'red',
className: 'badge-outline-red',
},
{
variant: 'outline',
color: 'green',
className: 'badge-outline-green',
},
],
defaultVariants: {
radius: undefined,
variant: 'bold',
color: 'default',
},
});
interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {
radius?: Radius;
variant?: BadgeVariant;
color?: Color;
asChild?: boolean;
}
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
(
{
radius,
variant = 'bold',
color = 'default',
className,
asChild,
...props
},
ref,
) => {
const { themeRadius } = useTheme();
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
className={cn(
badgeVariants({ radius: radius ?? themeRadius, variant, color }),
className,
)}
{...props}
/>
);
},
);
Badge.displayName = 'Badge';
export { Badge, type BadgeProps };
@@ -0,0 +1,156 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { Slot, Slottable } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import type { ButtonVariant, Radius, Size } from '../lib/types';
import { cn, composeRefs } from '../lib/utils';
import { Spinner } from './spinner';
import useTheme from './use-theme';
const defaultVariants: {
variant: ButtonVariant;
size?: Size;
radius?: Radius;
loading?: boolean;
} = {
variant: 'primary',
size: undefined,
radius: undefined,
loading: false,
};
const buttonVariants = cva('button', {
variants: {
variant: {
primary: 'button-primary',
secondary: 'button-secondary',
destructive: 'button-destructive',
ghost: 'button-ghost',
outline: 'button-outline',
},
size: {
small: 'button-small',
medium: 'button-medium',
large: 'button-large',
},
radius: {
small: 'button-radius-small',
medium: 'button-radius-medium',
large: 'button-radius-large',
},
loading: {
true: '!text-transparent [&>*:not(.button-loading)]:!invisible',
false: '',
},
},
defaultVariants,
});
const buttonLoadingVariants = cva('button-loading', {
variants: {
variant: {
primary: 'button-loading-primary',
secondary: 'button-loading-secondary',
destructive: 'button-loading-destructive',
ghost: 'button-loading-ghost',
outline: 'button-loading-outline',
},
},
defaultVariants,
});
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
variant?: ButtonVariant;
size?: Size;
radius?: Radius;
loading?: boolean;
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
type = 'button',
variant = 'primary',
size,
radius,
disabled = false,
loading = false,
asChild = false,
children,
...props
},
ref,
) => {
const Comp = asChild ? Slot : 'button';
const { themeSize, themeRadius } = useTheme();
const buttonRef = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (!buttonRef.current) return;
const childNodes = buttonRef.current.childNodes;
const isSvgOnly = Array.from(childNodes).every(
(node) =>
((node as HTMLElement).nodeType === Node.ELEMENT_NODE &&
(node as HTMLElement).tagName.toLowerCase() === 'svg') ||
((node as HTMLElement).nodeType === Node.TEXT_NODE &&
!((node as HTMLElement).textContent || '').trim()),
);
if (isSvgOnly) {
buttonRef.current.classList.add('svg-only');
}
}, []);
return (
<Comp
className={cn(
buttonVariants({
variant,
size: size ?? themeSize,
radius: radius ?? themeRadius,
loading,
className,
}),
)}
type={type}
disabled={disabled || loading}
ref={composeRefs(buttonRef, ref)}
{...props}
>
<Slottable>{children}</Slottable>
{loading && (
<span className={cn(buttonLoadingVariants({ variant }))}>
<Spinner size={size ?? themeSize} />
</span>
)}
</Comp>
);
},
);
Button.displayName = 'Button';
export { Button, type ButtonProps, buttonVariants };
@@ -0,0 +1,77 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type { DateRange, DayPickerDefaultProps } from 'react-day-picker';
import { DayPicker } from 'react-day-picker';
import { cn } from '../lib/utils';
import { Icon } from './icon';
type CalendarProps = React.ComponentProps<typeof DayPicker> & {
showToday?: boolean;
};
function Calendar({
className,
classNames,
showToday = true,
showOutsideDays = true,
mode = 'default',
...props
}: CalendarProps) {
return (
<DayPicker
mode={mode as DayPickerDefaultProps['mode']}
showOutsideDays={showOutsideDays}
className={cn('calendar', className)}
classNames={{
months: 'calendar-months',
month: 'calendar-month',
caption: 'calendar-caption',
caption_label: 'calendar-caption-label',
nav: 'calendar-nav',
nav_button: 'calendar-nav-button',
nav_button_previous: 'calendar-nav-button-previous',
nav_button_next: 'calendar-nav-button-next',
table: 'calendar-table',
head_row: 'calendar-head-row',
head_cell: 'calendar-head-cell',
row: 'calendar-row',
cell: 'calendar-cell',
day: 'calendar-day',
day_range_start: 'calendar-day-range-start',
day_range_end: 'calendar-day-range-end',
day_selected: 'calendar-day-selected',
day_today: showToday ? 'calendar-day-today' : '',
day_outside: 'calendar-day-outside',
day_disabled: 'calendar-day-disabled',
day_range_middle: 'calendar-day-range-middle',
day_hidden: 'calendar-day-hidden',
...classNames,
}}
components={{
IconLeft: () => <Icon name="RiArrowLeftSLine" size={16} />,
IconRight: () => <Icon name="RiArrowRightSLine" size={16} />,
}}
{...props}
/>
);
}
Calendar.displayName = 'Calendar';
export { Calendar, type DateRange, type CalendarProps };
@@ -0,0 +1,83 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { Slot, Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { CAPTION_DEFAULT_ICON, ICON_SIZE } from '../constants';
import type { CaptionType } from '../lib/types';
import { cn } from '../lib/utils';
import type { IconNameType } from './icon';
import { Icon } from './icon';
import useTheme from './use-theme';
const captionVariants = cva('caption', {
variants: {
variant: {
default: 'caption-default',
success: 'caption-success',
info: 'caption-info',
error: 'caption-error',
},
defaultVariants: { variant: 'default' },
},
});
interface CaptionProps extends React.ComponentPropsWithoutRef<'p'> {
variant?: CaptionType;
icon?: IconNameType;
asChild?: boolean;
}
const Caption = React.forwardRef<HTMLParagraphElement, CaptionProps>(
(props, ref) => {
const {
icon = undefined,
variant,
className,
children,
asChild,
...rest
} = props;
const Comp = asChild ? Slot : 'p';
const isError = variant === 'error';
const { themeSize } = useTheme();
return (
<Comp
ref={ref}
className={cn(
captionVariants({ variant: variant ?? 'default', className }),
)}
data-error={isError}
{...rest}
>
<Icon
name={
icon ??
CAPTION_DEFAULT_ICON[isError ? 'error' : (variant ?? 'default')]
}
size={ICON_SIZE[themeSize]}
className="caption-icon"
/>
<Slottable>{children}</Slottable>
</Comp>
);
},
);
Caption.displayName = 'Caption';
export { Caption };
@@ -0,0 +1,101 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { CHECK_ICON_SIZE } from '../constants';
import type { Size } from '../lib/types';
import { cn } from '../lib/utils';
import { Icon } from './icon';
import useTheme from './use-theme';
const defaultVariants: {
size?: Size;
} = {
size: undefined,
};
const checkboxVariants = cva('checkbox', {
variants: {
size: {
small: 'checkbox-small',
medium: 'checkbox-medium',
large: 'checkbox-large',
},
},
defaultVariants,
});
const checkVariants = cva('check', {
variants: {
size: {
small: 'check-small',
medium: 'check-medium',
large: 'check-large',
},
},
defaultVariants,
});
interface CheckboxProps
extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {
size?: Size;
}
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
CheckboxProps
>(({ checked, size, children, className, onCheckedChange, ...props }, ref) => {
const { themeSize } = useTheme();
const [currentChecked, setCurrentChecked] =
React.useState<CheckboxPrimitive.CheckedState>(false);
const handleCheckedChange = (checked: CheckboxPrimitive.CheckedState) => {
setCurrentChecked(checked);
onCheckedChange?.(checked);
};
return (
<CheckboxPrimitive.Root
ref={ref}
className={cn(checkboxVariants({ size: size ?? themeSize, className }))}
checked={checked ?? currentChecked}
onCheckedChange={handleCheckedChange}
{...props}
>
<span className={cn(checkVariants({ size }))}>
<CheckboxPrimitive.Indicator className={cn('checkbox-icon')}>
<Icon
name={
(checked ?? currentChecked) === 'indeterminate' ?
'RiSubtractLine'
: 'RiCheckLine'
}
size={CHECK_ICON_SIZE[size ?? themeSize]}
/>
</CheckboxPrimitive.Indicator>
</span>
<Slottable>{children}</Slottable>
</CheckboxPrimitive.Root>
);
});
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox, type CheckboxProps };
@@ -0,0 +1,311 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { Slottable } from '@radix-ui/react-slot';
import { Command as CommandPrimitive } from 'cmdk';
import type { TriggerType } from '../lib/types';
import { cn } from '../lib/utils';
import { Button } from './button';
import { Icon } from './icon';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
import { ScrollArea, ScrollBar } from './scroll-area';
const ComboboxContext = React.createContext<{
trigger: TriggerType;
isHover: boolean;
setTrigger: React.Dispatch<React.SetStateAction<TriggerType>>;
setIsHover: React.Dispatch<React.SetStateAction<boolean>>;
}>({
trigger: 'click',
setTrigger: () => 'click',
isHover: false,
setIsHover: () => false,
});
const Combobox = ({
open,
onOpenChange,
...props
}: React.ComponentPropsWithoutRef<typeof Popover>) => {
const [trigger, setTrigger] = React.useState<TriggerType>('click');
const [isHover, setIsHover] = React.useState(false);
return (
<ComboboxContext.Provider
value={{ trigger, setTrigger, isHover, setIsHover }}
>
<Popover
{...props}
open={trigger === 'hover' ? !!open || isHover : open}
onOpenChange={(open: boolean) => {
if (trigger === 'hover') {
setIsHover(open);
}
onOpenChange?.(open);
}}
/>
</ComboboxContext.Provider>
);
};
const ComboboxContent = React.forwardRef<
React.ComponentRef<typeof PopoverContent>,
React.ComponentPropsWithoutRef<typeof PopoverContent> & {
options?: React.ComponentPropsWithoutRef<typeof CommandPrimitive>;
}
>(
(
{ className, children, onMouseEnter, onMouseLeave, options, ...props },
ref,
) => {
const { trigger, setIsHover } = React.useContext(ComboboxContext);
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
if (trigger === 'hover') {
setIsHover(true);
}
onMouseEnter?.(e);
};
const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
if (trigger === 'hover') {
setIsHover(false);
}
onMouseLeave?.(e);
};
return (
<PopoverContent
ref={ref}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
className={cn('combobox-content', className)}
>
<CommandPrimitive {...options}>{children}</CommandPrimitive>
</PopoverContent>
);
},
);
ComboboxContent.displayName = CommandPrimitive.displayName;
interface ComboboxInputProps
extends React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> {
icon?: React.ReactNode;
}
const ComboboxInput = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Input>,
ComboboxInputProps
>(({ icon, className, ...props }, ref) => (
<div className="combobox-input-box" cmdk-input-wrapper="">
{icon ?? <Icon name="RiSearchLine" size={16} />}
<CommandPrimitive.Input
ref={ref}
className={cn('combobox-input', className)}
{...props}
/>
</div>
));
ComboboxInput.displayName = CommandPrimitive.Input.displayName;
const ComboboxList = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> & {
maxHeight?: string;
}
>(({ maxHeight, className, ...props }, ref) => (
<ScrollArea maxHeight={maxHeight}>
<CommandPrimitive.List
ref={ref}
className={cn('combobox-list', className)}
{...props}
/>
<ScrollBar />
</ScrollArea>
));
ComboboxList.displayName = CommandPrimitive.List.displayName;
const ComboboxEmpty = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="combobox-empty" {...props} />
));
ComboboxEmpty.displayName = CommandPrimitive.Empty.displayName;
const ComboboxGroup = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn('combobox-group', className)}
{...props}
/>
));
ComboboxGroup.displayName = CommandPrimitive.Group.displayName;
const ComboboxSeparator = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn('combobox-separator', className)}
{...props}
/>
));
ComboboxSeparator.displayName = CommandPrimitive.Separator.displayName;
const ComboboxItem = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ children, className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn('combobox-item', className)}
{...props}
>
{children}
</CommandPrimitive.Item>
));
ComboboxItem.displayName = CommandPrimitive.Item.displayName;
const ComboboxCaption = React.forwardRef<
React.ComponentRef<'span'>,
React.ComponentPropsWithoutRef<'span'>
>(({ className, ...props }, ref) => (
<span ref={ref} className={cn('combobox-caption', className)} {...props} />
));
ComboboxCaption.displayName = 'ComboboxCaption';
const ComboboxSelectItem = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> & {
checked?: boolean;
}
>(({ checked, children, className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn('combobox-item', className)}
{...props}
>
<Icon
name="RiCheckLine"
size={20}
color={checked ? 'currentColor' : 'transparent'}
className="combobox-check"
/>
<Slottable>{children}</Slottable>
</CommandPrimitive.Item>
));
ComboboxSelectItem.displayName = 'ComboboxSelectItem';
const ComboboxTrigger = React.forwardRef<
React.ComponentRef<typeof Button>,
React.ComponentPropsWithoutRef<typeof Button> & {
trigger?: TriggerType;
}
>(
(
{
variant = 'outline',
trigger,
className,
children,
onMouseEnter,
onMouseLeave,
...props
},
ref,
) => {
const { setTrigger, setIsHover } = React.useContext(ComboboxContext);
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
if (trigger === 'hover') {
setIsHover(true);
}
onMouseEnter?.(e);
};
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
if (trigger === 'hover') {
setIsHover(false);
}
onMouseLeave?.(e);
};
React.useEffect(() => {
if (trigger) {
setTrigger(trigger);
}
}, []);
if (props.asChild) {
return (
<PopoverTrigger
className={className}
ref={ref}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
{children}
</PopoverTrigger>
);
}
return (
<PopoverTrigger asChild>
<Button
ref={ref}
variant={variant}
className={cn('combobox-trigger', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
{children}
</Button>
</PopoverTrigger>
);
},
);
ComboboxTrigger.displayName = 'ComboboxTrigger';
export {
Combobox,
ComboboxContent,
ComboboxCaption,
ComboboxInput,
ComboboxList,
ComboboxEmpty,
ComboboxGroup,
ComboboxItem,
ComboboxSelectItem,
ComboboxSeparator,
ComboboxTrigger,
};
@@ -0,0 +1,247 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { ALERT_DEFAULT_ICON } from '../constants';
import { cn } from '../lib/utils';
import type { Radius } from '../types';
import { Button } from './button';
import { Icon } from './icon';
import { ScrollArea, ScrollBar } from './scroll-area';
import useTheme from './use-theme';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Trigger>,
DialogPrimitive.DialogTriggerProps &
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant = 'outline', children, ...props }, ref) => {
if (props.asChild) {
return (
<DialogPrimitive.Trigger ref={ref} {...props}>
{children}
</DialogPrimitive.Trigger>
);
}
return (
<DialogPrimitive.Trigger asChild>
<Button variant={variant} ref={ref} {...props}>
{children}
</Button>
</DialogPrimitive.Trigger>
);
});
DialogTrigger.displayName = DialogPrimitive.DialogTrigger.displayName;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant, ...props }, ref) => (
<DialogPrimitive.Close asChild>
<Button ref={ref} variant={variant ?? 'outline'} {...props} />
</DialogPrimitive.Close>
));
DialogClose.displayName = 'DialogClose';
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn('dialog', className)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const dialogContentVariants = cva('dialog-content', {
variants: {
radius: {
large: 'dialog-content-radius-large',
medium: 'dialog-content-radius-medium',
small: 'dialog-content-radius-small',
},
defaultVariants: {
radius: undefined,
},
},
});
interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
radius?: Radius;
}
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(({ radius, className, children, ...props }, ref) => {
const { themeRadius } = useTheme();
return (
<DialogPortal>
<DialogOverlay onClick={(e) => e.stopPropagation()} />
<DialogPrimitive.Content
ref={ref}
className={cn(
dialogContentVariants({ radius: radius ?? themeRadius, className }),
)}
{...props}
>
{children}
<DialogPrimitive.Close asChild>
<Button
size="medium"
variant="ghost"
className="dialog-close"
aria-label="Close"
>
<Icon name="RiCloseLine" />
</Button>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;
const dialogIconVariants = cva('dialog-icon', {
variants: {
variant: {
default: 'dialog-icon-default',
warning: 'dialog-icon-warning',
success: 'dialog-icon-success',
error: 'dialog-icon-error',
informative: 'dialog-icon-informative',
},
},
defaultVariants: {
variant: 'default',
},
});
const DialogIcon = (
props: React.ComponentPropsWithoutRef<typeof Icon> &
VariantProps<typeof dialogIconVariants>,
) => {
const { className, name, size = 32, variant, ...rest } = props;
return (
<Icon
{...rest}
name={name ?? ALERT_DEFAULT_ICON[variant ?? 'default']}
size={size}
className={cn(dialogIconVariants({ variant, className }))}
/>
);
};
DialogIcon.displayName = 'DialogIcon';
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('dialog-header', className)} {...props} />
);
DialogHeader.displayName = 'DialogHeader';
const dialogFooterVariants = cva('dialog-footer', {
variants: {
align: {
left: 'dialog-footer-left',
right: 'dialog-footer-right',
center: 'dialog-footer-center',
between: 'dialog-footer-between',
full: 'dialog-footer-full',
},
defaultVariants: {
align: 'right',
},
},
});
interface DialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {
asChild?: boolean;
}
const DialogBody = ({ asChild, className, ...props }: DialogBodyProps) => {
const Comp = asChild ? Slot : 'div';
return (
<ScrollArea>
<Comp className={cn('dialog-body', className)} {...props} />
<ScrollBar />
</ScrollArea>
);
};
DialogBody.displayName = 'DialogBody';
interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {
align?: 'left' | 'right' | 'center' | 'between' | 'full';
}
const DialogFooter = ({
align = 'right',
className,
...props
}: DialogFooterProps) => (
<div className={cn(dialogFooterVariants({ align, className }))} {...props} />
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('dialog-title', className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('dialog-description', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogBody,
DialogFooter,
DialogTitle,
DialogDescription,
DialogIcon,
};
@@ -0,0 +1,112 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
const dividerVariants = cva('divider', {
variants: {
variant: {
bold: 'divider-bold',
subtle: 'divider-subtle',
},
orientation: {
horizontal: 'divider-horizontal',
vertical: 'divider-vertical',
},
indent: {
0: '',
8: '',
16: '',
24: '',
},
},
compoundVariants: [
{
orientation: 'horizontal',
indent: 8,
className: 'divider-horizontal-indent-8',
},
{
orientation: 'horizontal',
indent: 16,
className: 'divider-horizontal-indent-16',
},
{
orientation: 'horizontal',
indent: 24,
className: 'divider-horizontal-indent-24',
},
{
orientation: 'vertical',
indent: 8,
className: 'divider-vertical-indent-8',
},
{
orientation: 'vertical',
indent: 16,
className: 'divider-vertical-indent-16',
},
{
orientation: 'vertical',
indent: 24,
className: 'divider-vertical-indent-24',
},
],
defaultVariants: {
variant: 'bold',
orientation: 'horizontal',
indent: 0,
},
});
interface DividerProps
extends React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> {
variant?: 'bold' | 'subtle';
indent?: 0 | 8 | 16 | 24;
}
const Divider = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
DividerProps
>(
(
{
variant = 'bold',
indent = 0,
orientation = 'horizontal',
className,
decorative = true,
...props
},
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
dividerVariants({ variant, orientation, indent, className }),
)}
{...props}
/>
),
);
Divider.displayName = SeparatorPrimitive.Root.displayName;
export { Divider };
@@ -0,0 +1,327 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type { DropdownMenuCheckboxItemProps } from '@radix-ui/react-dropdown-menu';
import * as DropdownPrimitive from '@radix-ui/react-dropdown-menu';
import { Slottable } from '@radix-ui/react-slot';
import type { TriggerType } from '../lib/types';
import { cn } from '../lib/utils';
import { Button } from './button';
import { Icon } from './icon';
import { ScrollArea, ScrollBar } from './scroll-area';
const DropdownContext = React.createContext<{
trigger: TriggerType;
isHover: boolean;
setTrigger: React.Dispatch<React.SetStateAction<TriggerType>>;
setIsHover: React.Dispatch<React.SetStateAction<boolean>>;
}>({
trigger: 'click',
setTrigger: () => 'click',
isHover: false,
setIsHover: () => false,
});
const Dropdown = ({
open = false,
// eslint-disable-next-line @typescript-eslint/unbound-method
onOpenChange,
...props
}: React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Root>) => {
const [trigger, setTrigger] = React.useState<TriggerType>('click');
const [isHover, setIsHover] = React.useState(false);
return (
<DropdownContext.Provider
value={{ trigger, setTrigger, isHover, setIsHover }}
>
<DropdownPrimitive.Root
{...props}
open={open || isHover}
onOpenChange={(open: boolean) => {
setIsHover(open);
onOpenChange?.(open);
}}
/>
</DropdownContext.Provider>
);
};
const DropdownTrigger = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Trigger>,
DropdownPrimitive.DropdownMenuTriggerProps &
React.ComponentPropsWithoutRef<typeof Button> & {
trigger?: TriggerType;
}
>(
(
{
variant = 'outline',
trigger,
onMouseEnter,
onMouseLeave,
className,
children,
...props
},
ref,
) => {
const { setTrigger, setIsHover } = React.useContext(DropdownContext);
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
if (trigger === 'hover') {
setIsHover(true);
}
onMouseEnter?.(e);
};
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
if (trigger === 'hover') {
setIsHover(false);
}
onMouseLeave?.(e);
};
React.useEffect(() => {
if (trigger) {
setTrigger(trigger);
}
}, []);
if (props.asChild) {
return (
<DropdownPrimitive.Trigger
className={cn('dropdown-trigger', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={ref}
{...props}
>
{children}
</DropdownPrimitive.Trigger>
);
}
return (
<DropdownPrimitive.Trigger asChild>
<Button
variant={variant}
className={cn('dropdown-trigger', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={ref}
{...props}
>
{children}
</Button>
</DropdownPrimitive.Trigger>
);
},
);
DropdownTrigger.displayName = DropdownPrimitive.DropdownMenuTrigger.displayName;
const DropdownGroup = DropdownPrimitive.Group;
const DropdownPortal = DropdownPrimitive.Portal;
const DropdownSub = DropdownPrimitive.Sub;
const DropdownRadioGroup = DropdownPrimitive.RadioGroup;
const DropdownSubTrigger = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.SubTrigger>
>(({ className, children, ...props }, ref) => {
return (
<DropdownPrimitive.SubTrigger
ref={ref}
className={cn('dropdown-sub-trigger', className)}
{...props}
>
{children}
</DropdownPrimitive.SubTrigger>
);
});
DropdownSubTrigger.displayName = DropdownPrimitive.SubTrigger.displayName;
const DropdownCaption = React.forwardRef<
React.ComponentRef<'span'>,
React.ComponentPropsWithoutRef<'span'>
>(({ className, ...props }, ref) => (
<span ref={ref} className={cn('dropdown-caption', className)} {...props} />
));
DropdownCaption.displayName = 'DropdownCaption';
const DropdownSubContent = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownPrimitive.SubContent
ref={ref}
className={cn('dropdown-sub-content', className)}
{...props}
/>
));
DropdownSubContent.displayName = DropdownPrimitive.SubContent.displayName;
const DropdownContent = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Content> & {
maxHeight?: string;
}
>(
(
{
children,
className,
sideOffset = 4,
maxHeight,
onMouseEnter,
onMouseLeave,
...props
},
ref,
) => {
const { trigger, setIsHover } = React.useContext(DropdownContext);
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
if (trigger === 'hover') {
setIsHover(true);
}
onMouseEnter?.(e);
};
const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
if (trigger === 'hover') {
setIsHover(false);
}
onMouseLeave?.(e);
};
return (
<DropdownPrimitive.Portal>
<DropdownPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn('dropdown-content', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
<ScrollArea maxHeight={maxHeight}>
{children}
<ScrollBar />
</ScrollArea>
</DropdownPrimitive.Content>
</DropdownPrimitive.Portal>
);
},
);
DropdownContent.displayName = DropdownPrimitive.Content.displayName;
const DropdownItem = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Item>
>(({ children, className, ...props }, ref) => (
<DropdownPrimitive.Item
ref={ref}
className={cn('dropdown-item', className)}
{...props}
>
{children}
</DropdownPrimitive.Item>
));
DropdownItem.displayName = DropdownPrimitive.Item.displayName;
const DropdownCheckboxItem = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownPrimitive.CheckboxItem
ref={ref}
className={cn('dropdown-checkbox', className)}
checked={checked}
{...props}
>
<DropdownPrimitive.ItemIndicator className="dropdown-checkbox-icon">
<Icon name="RiCheckLine" size={16} />
</DropdownPrimitive.ItemIndicator>
<Slottable>{children}</Slottable>
</DropdownPrimitive.CheckboxItem>
));
DropdownCheckboxItem.displayName = DropdownPrimitive.CheckboxItem.displayName;
const DropdownRadioItem = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownPrimitive.RadioItem
ref={ref}
className={cn('dropdown-radio', className)}
{...props}
>
<DropdownPrimitive.ItemIndicator className="dropdown-radio-icon">
<Icon name="RiCircleFill" size={8} className="fill-current" />
</DropdownPrimitive.ItemIndicator>
<Slottable>{children}</Slottable>
</DropdownPrimitive.RadioItem>
));
DropdownRadioItem.displayName = DropdownPrimitive.RadioItem.displayName;
const DropdownLabel = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Label>
>(({ className, ...props }, ref) => (
<DropdownPrimitive.Label
ref={ref}
className={cn('dropdown-label', className)}
{...props}
/>
));
DropdownLabel.displayName = DropdownPrimitive.Label.displayName;
const DropdownSeparator = React.forwardRef<
React.ComponentRef<typeof DropdownPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownPrimitive.Separator
ref={ref}
className={cn('dropdown-separator', className)}
{...props}
/>
));
DropdownSeparator.displayName = DropdownPrimitive.Separator.displayName;
export {
Dropdown,
DropdownTrigger,
DropdownContent,
DropdownCaption,
DropdownItem,
DropdownCheckboxItem,
DropdownRadioItem,
DropdownLabel,
DropdownSeparator,
DropdownGroup,
DropdownPortal,
DropdownSub,
DropdownSubContent,
DropdownSubTrigger,
DropdownRadioGroup,
type DropdownMenuCheckboxItemProps,
};
+169
View File
@@ -0,0 +1,169 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
import {
Controller,
FormProvider,
useFormContext,
useFormState,
} from 'react-hook-form';
import { Caption } from './caption';
import { Label } from './label';
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
function FormItem({ ...props }: React.ComponentProps<'div'>) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div data-slot="form-item" {...props} />
</FormItemContext.Provider>
);
}
function FormLabel({
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-error={!!error}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error ?
`${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
}
function FormCaption({ ...props }: React.ComponentProps<typeof Caption>) {
const { formDescriptionId } = useFormField();
return (
<Caption data-slot="form-description" id={formDescriptionId} {...props} />
);
}
function FormMessage({
children,
...props
}: React.ComponentProps<typeof Caption>) {
const { error, formMessageId } = useFormField();
const body = error ? String(error.message ?? '') : children;
if (!body) {
return null;
}
return (
<Caption
data-slot="form-message"
id={formMessageId}
variant="error"
{...props}
>
{body}
</Caption>
);
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormCaption,
FormMessage,
FormField,
};
@@ -0,0 +1,52 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import * as remixIcons from '@remixicon/react';
import { cn } from '../lib/utils';
const Icons = remixIcons;
const IconNames = Object.keys(Icons) as (keyof typeof Icons)[];
type IconNameType = keyof typeof Icons;
interface IconProps extends Omit<React.SVGProps<SVGSVGElement>, 'children'> {
name?: IconNameType;
color?: string;
size?: number | string;
}
const Icon: React.FC<IconProps> = ({
name,
color = 'currentColor',
size = 24,
className,
onClick,
...props
}) => {
if (!name) {
return <></>;
}
return React.createElement(Icons[name], {
color,
size,
className: cn('icon', onClick && 'icon-clickable', className),
onClick,
...props,
});
};
export { Icon, IconNames, type IconNameType, type IconProps };
@@ -0,0 +1,50 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export * from './alert';
export * from './accordion';
export * from './badge';
export * from './button';
export * from './checkbox';
export * from './calendar';
export * from './caption';
export * from './combobox';
export * from './divider';
export * from './dialog';
export * from './dropdown';
export * from './form';
export * from './icon';
export * from './input';
export * from './label';
export * from './menu';
export * from './pagination';
export * from './popover';
export * from './radio-card';
export * from './radio';
export * from './select';
export * from './spinner';
export * from './scroll-area';
export * from './multi-select';
export * from './switch';
export * from './sheet';
export * from './toast';
export * from './tag';
export * from './table';
export * from './toggle-group';
export * from './tooltip';
export * from './use-theme';
export * from './tabs';
export * from './textarea';
export * from './timepicker';
+257
View File
@@ -0,0 +1,257 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import type { ButtonHTMLAttributes, HTMLInputTypeAttribute } from 'react';
import React, { useRef } from 'react';
import { cva } from 'class-variance-authority';
import { ICON_SIZE } from '../constants';
import type { Radius, Size } from '../lib/types';
import { cn, composeRefs } from '../lib/utils';
import type { IconProps } from './icon';
import { Icon } from './icon';
import useTheme from './use-theme';
type TextInputType = (
| 'text'
| 'email'
| 'password'
| 'search'
| 'tel'
| 'number'
) &
HTMLInputTypeAttribute;
const InputField = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
return <div ref={ref} className={cn('input-field', className)} {...props} />;
});
InputField.displayName = 'InputField';
const defaultContext: TextInputProps = {
size: undefined,
};
const InputContext = React.createContext<TextInputProps>(defaultContext);
const inputVariants = cva('input', {
variants: {
size: {
large: 'input-large',
medium: 'input-medium',
small: 'input-small',
},
radius: {
large: 'input-radius-large',
medium: 'input-radius-medium',
small: 'input-radius-small',
},
defaultVariants: {
size: undefined,
radius: undefined,
},
},
});
interface InputBoxProps extends React.HTMLAttributes<HTMLDivElement> {
size?: Size;
}
const InputBox = React.forwardRef<HTMLInputElement, InputBoxProps>(
({ size, className, children, ...props }, ref) => {
const { themeSize } = useTheme();
return (
<InputContext.Provider value={{ size: size ?? themeSize }}>
<div ref={ref} className={cn('input-box', className)} {...props}>
{children}
</div>
</InputContext.Provider>
);
},
);
InputBox.displayName = 'InputBox';
interface TextInputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
size?: Size;
type?: TextInputType;
radius?: Radius;
}
const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
(props, ref) => {
const {
size,
type = 'text',
radius,
disabled = false,
onFocus,
onBlur,
className,
...rest
} = props;
const { themeRadius } = useTheme();
const { size: boxSize } = React.useContext(InputContext);
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.stopPropagation();
if (!e.currentTarget.contains(e.relatedTarget)) {
onFocus?.(e);
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
e.stopPropagation();
if (!e.currentTarget.contains(e.relatedTarget)) {
onBlur?.(e);
}
};
return (
<input
{...rest}
spellCheck="false"
type={type}
ref={composeRefs(inputRef, ref)}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
className={cn(
inputVariants({
size: size ?? boxSize,
radius: radius ?? themeRadius,
className,
}),
)}
/>
);
},
);
TextInput.displayName = 'TextInput';
const inputIconVariants = cva('input-icon', {
variants: {
size: {
large: 'input-icon-large',
medium: 'input-icon-medium',
small: 'input-icon-small',
},
defaultVariants: {
size: undefined,
},
},
});
const InputIcon = ({ className, ...props }: IconProps) => {
const { size } = React.useContext(InputContext);
const { themeSize } = useTheme();
return (
<Icon
{...props}
size={ICON_SIZE[size ?? themeSize]}
className={cn(inputIconVariants({ size: size ?? themeSize, className }))}
/>
);
};
InputIcon.displayName = 'InputIcon';
const inputButtonVariants = cva('input-button', {
variants: {
size: {
large: 'input-button-large',
medium: 'input-button-medium',
small: 'input-button-small',
},
defaultVariants: {
size: undefined,
},
},
});
const InputClearButton = React.forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, ...props }, ref) => {
const { size } = React.useContext(InputContext);
const { themeSize } = useTheme();
return (
<button
ref={ref}
type="button"
className={cn(
inputButtonVariants({ size: size ?? themeSize, className }),
'show-only-on-focus-and-has-value',
)}
aria-label="Reset input text"
{...props}
>
<Icon name="RiCloseCircleFill" size={ICON_SIZE[size ?? themeSize]} />
</button>
);
});
InputClearButton.displayName = 'InputClearButton';
const InputEyeButton = React.forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement> & {
onChangeVisibility?: (visible: boolean) => void;
}
>(({ className, onClick, onChangeVisibility, ...props }, ref) => {
const [visible, setVisible] = React.useState(false);
const { size } = React.useContext(InputContext);
const { themeSize } = useTheme();
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
setVisible((visible) => !visible);
onClick?.(e);
};
React.useEffect(() => {
onChangeVisibility?.(visible);
}, [visible]);
return (
<button
ref={ref}
type="button"
className={cn(
inputButtonVariants({ size: size ?? themeSize, className }),
)}
onClick={handleClick}
aria-label={visible ? 'hide password' : 'show password'}
{...props}
>
{visible ?
<Icon name="RiEyeCloseFill" size={ICON_SIZE[size ?? themeSize]} />
: <Icon name="RiEyeFill" size={ICON_SIZE[size ?? themeSize]} />}
</button>
);
});
InputEyeButton.displayName = 'InputEyeButton';
export {
InputField,
InputBox,
TextInput,
InputClearButton,
InputEyeButton,
InputIcon,
type TextInputProps,
};
@@ -0,0 +1,33 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '../lib/utils';
interface LabelProps extends React.ComponentProps<typeof LabelPrimitive.Root> {}
function Label({ className, ...props }: LabelProps) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn('label', className)}
{...props}
/>
);
}
export { Label };
+175
View File
@@ -0,0 +1,175 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import {
Dropdown,
DropdownContent,
DropdownGroup,
DropdownItem,
DropdownTrigger,
} from './dropdown';
import useTheme from './use-theme';
const DefaultValue = {
orientation: 'horizontal',
} as const;
const menuVariants = cva('menu', {
variants: {
orientation: {
horizontal: 'menu-horizontal',
vertical: 'menu-vertical',
},
defaultVariants: {
orientation: DefaultValue.orientation,
},
},
});
const MenuContext = React.createContext<{
orientation?: 'vertical' | 'horizontal';
size?: 'small' | 'medium' | 'large';
}>({
orientation: DefaultValue.orientation,
size: undefined,
});
const Menu = React.forwardRef<
React.ComponentRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & {
orientation?: 'vertical' | 'horizontal';
size?: 'small' | 'medium' | 'large';
}
>(
(
{
orientation = DefaultValue.orientation,
size,
children,
className,
...props
},
ref,
) => {
const { themeSize } = useTheme();
return (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn(menuVariants({ orientation, className }))}
{...props}
>
<MenuContext.Provider value={{ orientation, size: size ?? themeSize }}>
{children}
</MenuContext.Provider>
</ToggleGroupPrimitive.Root>
);
},
);
Menu.displayName = ToggleGroupPrimitive.Root.displayName;
const menuItemVariants = cva('menu-item', {
variants: {
size: {
small: 'menu-item-small',
medium: 'menu-item-medium',
large: 'menu-item-large',
},
defaultVariants: {
size: undefined,
},
},
});
const MenuItem = React.forwardRef<
React.ComponentRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item>
>(({ children, className, ...props }, ref) => {
const { size } = React.useContext(MenuContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(menuItemVariants({ size, className }))}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
MenuItem.displayName = ToggleGroupPrimitive.Item.displayName;
const MenuDropdown = Dropdown;
const MenuDropdownTrigger = React.forwardRef<
React.ComponentRef<typeof DropdownTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownTrigger>
>(({ className, children, ...props }, ref) => {
const { size } = React.useContext(MenuContext);
if (props.asChild) {
return (
<DropdownTrigger
className={cn(menuItemVariants({ size, className }))}
ref={ref}
{...props}
>
{children}
</DropdownTrigger>
);
}
return (
<DropdownTrigger
asChild
ref={ref}
className={cn(
'flex justify-between',
menuItemVariants({ size, className }),
)}
{...props}
>
<button type="button">{children}</button>
</DropdownTrigger>
);
});
MenuDropdownTrigger.displayName = 'MenuDropdownTrigger';
const MenuDropdownContent = DropdownContent;
const MenuDropdownGroup = DropdownGroup;
const MenuDropdownItem = React.forwardRef<
React.ComponentRef<typeof DropdownItem>,
React.ComponentPropsWithoutRef<typeof MenuItem>
>(({ className, ...props }, ref) => (
<DropdownItem ref={ref} className={cn('menu-dropdown-item', className)}>
<MenuItem {...props} />
</DropdownItem>
));
MenuDropdownItem.displayName = 'MenuDropdownItem';
export {
Menu,
MenuItem,
MenuDropdown,
MenuDropdownTrigger,
MenuDropdownContent,
MenuDropdownGroup,
MenuDropdownItem,
};
@@ -0,0 +1,377 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { Slot, Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { ICON_SIZE } from '../constants';
import { cn } from '../lib/utils';
import type { Radius, Size } from '../types';
import { Icon } from './icon';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
import { ScrollArea, ScrollBar } from './scroll-area';
import { Tag } from './tag';
import useTheme from './use-theme';
type MultiSelectItemType = { value: string; label: React.ReactNode };
type MultiSelectContextType = {
disabled: boolean;
size: Size;
radius: Radius;
selectedItems: MultiSelectItemType[];
setSelectedItems: (items: MultiSelectItemType[]) => void;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
toggleOption: (item: MultiSelectItemType) => void;
registeredItems: MultiSelectItemType[];
registerItem: (item: MultiSelectItemType) => void;
};
const MultiSelectContext = React.createContext<
MultiSelectContextType | undefined
>(undefined);
function useMultiSelectContext() {
const ctx = React.useContext(MultiSelectContext);
if (!ctx)
throw new Error('MultiSelect components must be used within <MultiSelect>');
return ctx;
}
interface MultiSelectProps {
value?: string[];
defaultValue?: string[];
onValueChange?: (value: string[]) => void;
children: React.ReactNode;
className?: string;
size?: Size;
radius?: Radius;
disabled?: boolean;
}
const selectVariants = cva('select', {
variants: {
size: {
small: 'select-small',
medium: 'select-medium',
large: 'select-large',
},
defaultVariants: { size: undefined },
},
});
const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
function MultiSelect(
{
value,
defaultValue = [],
onValueChange,
children,
className,
size,
radius,
disabled = false,
},
ref,
) {
const { themeSize, themeRadius } = useTheme();
const [registeredItems, setRegisteredItems] = React.useState<
MultiSelectItemType[]
>([]);
const [selectedValues, setSelectedValues] =
React.useState<string[]>(defaultValue);
const [isOpen, setIsOpen] = React.useState(false);
const values = value ?? selectedValues;
const setValues = (vals: string[]) => {
if (onValueChange) onValueChange(vals);
if (value === undefined) setSelectedValues(vals);
};
const registerItem = React.useCallback((item: MultiSelectItemType) => {
setRegisteredItems((prev) =>
prev.find((i) => i.value === item.value) ? prev : [...prev, item],
);
}, []);
const selectedItems = React.useMemo(
() =>
values
.map((val) => registeredItems.find((item) => item.value === val))
.filter(Boolean) as MultiSelectItemType[],
[values, registeredItems],
);
const toggleOption = (item: MultiSelectItemType) => {
setValues(
values.includes(item.value) ?
values.filter((v) => v !== item.value)
: [...values, item.value],
);
};
const hiddenRegisterItems = (
<div style={{ display: 'none' }}>
{React.Children.map(children, (child) => {
if (
React.isValidElement(child) &&
(child.type as React.ForwardRefExoticComponent<HTMLElement>)
.displayName === 'MultiSelectItem'
) {
return child;
}
if (
React.isValidElement(child) &&
(child.props as { children?: React.ReactNode }).children
) {
return React.Children.map(
(child.props as { children?: React.ReactNode }).children,
(c) => c,
);
}
return null;
})}
</div>
);
return (
<MultiSelectContext.Provider
value={{
disabled,
size: size ?? themeSize,
radius: radius ?? themeRadius,
selectedItems,
setSelectedItems: (items) => setValues(items.map((i) => i.value)),
isOpen,
setIsOpen,
toggleOption,
registeredItems,
registerItem,
}}
>
{hiddenRegisterItems}
<Popover open={isOpen} onOpenChange={setIsOpen}>
<div
ref={ref}
className={cn(
selectVariants({
size: size ?? themeSize,
className,
}),
)}
>
{children}
</div>
</Popover>
</MultiSelectContext.Provider>
);
},
);
interface MultiSelectTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
}
const selectTriggerVariants = cva('select-trigger', {
variants: {
size: {
small: 'select-trigger-small',
medium: 'select-trigger-medium',
large: 'select-trigger-large',
},
radius: {
small: 'select-trigger-radius-small',
medium: 'select-trigger-radius-medium',
large: 'select-trigger-radius-large',
},
},
defaultVariants: { size: undefined, radius: undefined },
});
const MultiSelectTrigger = React.forwardRef<
HTMLButtonElement,
MultiSelectTriggerProps
>(function MultiSelectTrigger({ asChild, ...props }, ref) {
const Comp = asChild ? Slot : MultiSelectTriggerButton;
return (
<PopoverTrigger asChild>
<Comp ref={ref} {...props} />
</PopoverTrigger>
);
});
interface MultiSelectTriggerButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
className?: string;
children?: React.ReactNode;
}
const MultiSelectTriggerButton = React.forwardRef<
HTMLButtonElement,
MultiSelectTriggerButtonProps
>(function MultiSelectTrigger({ className, children, ...props }, ref) {
const { size, radius, disabled } = useMultiSelectContext();
return (
<button
ref={ref}
type="button"
aria-haspopup="listbox"
className={cn(selectTriggerVariants({ size, radius, className }))}
data-placeholder
disabled={disabled}
{...props}
>
<Slottable>{children}</Slottable>
<Icon name="RiArrowDownSLine" size={ICON_SIZE[size]} />
</button>
);
});
interface MultiSelectContentProps extends React.HTMLAttributes<HTMLDivElement> {
maxHeight?: string;
}
const MultiSelectContent = React.forwardRef<
HTMLDivElement,
MultiSelectContentProps
>(function MultiSelectContent(
{ className, children, maxHeight = 'auto', ...props },
ref,
) {
return (
<PopoverContent
ref={ref}
className={cn('select-content', className)}
{...props}
>
<div className={cn('select-viewport')}>
<ScrollArea maxHeight={maxHeight}>
{children}
<ScrollBar />
</ScrollArea>
</div>
</PopoverContent>
);
});
const selectItemVariants = cva('select-item', {
variants: { check: { left: 'select-item-left', right: 'select-item-right' } },
defaultVariants: { check: 'left' },
});
const selectItemCheckVariants = cva('select-item-check', {
variants: {
check: { left: 'select-item-check-left', right: 'select-item-check-right' },
},
defaultVariants: { check: 'left' },
});
interface MultiSelectItemProps {
value: string;
children: React.ReactNode;
className?: string;
}
const MultiSelectItem = React.forwardRef<HTMLDivElement, MultiSelectItemProps>(
function MultiSelectItem({ value, children, className }, ref) {
const { selectedItems, toggleOption, registerItem } =
useMultiSelectContext();
const isSelected = selectedItems.some((item) => item.value === value);
// Register this item on mount
React.useEffect(() => {
registerItem({ value, label: children });
}, [value, children, registerItem]);
return (
<div
ref={ref}
role="option"
aria-selected={isSelected}
tabIndex={0}
className={cn(
selectItemVariants({
check: typeof children === 'string' ? 'left' : 'right',
className,
}),
)}
onClick={() => toggleOption({ value, label: children })}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ')
toggleOption({ value, label: children });
}}
>
{isSelected && (
<span
className={cn(
selectItemCheckVariants({
check: typeof children === 'string' ? 'left' : 'right',
}),
)}
>
<Icon name="RiCheckLine" size={16} />
</span>
)}
<Slottable>{children}</Slottable>
</div>
);
},
);
MultiSelectItem.displayName = 'MultiSelectItem';
const MultiSelectValue = React.forwardRef<
HTMLSpanElement,
{ placeholder?: React.ReactNode } & React.HTMLAttributes<HTMLSpanElement>
>(function MultiSelectValue({ placeholder, ...props }, ref) {
const { selectedItems, size } = useMultiSelectContext();
if (selectedItems.length === 0) {
return (
<span ref={ref} {...props}>
{placeholder}
</span>
);
}
return (
<span ref={ref} {...props}>
{selectedItems.map((item) => (
<Tag
key={item.value}
variant="outline"
size={size}
className="select-tag"
>
{item.label}
</Tag>
))}
</span>
);
});
export {
MultiSelect,
MultiSelectTrigger,
MultiSelectTriggerButton,
MultiSelectContent,
MultiSelectItem,
MultiSelectValue,
};
@@ -0,0 +1,165 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { cn } from '../lib/utils';
import type { ButtonProps } from './button';
import { Button } from './button';
import { Icon } from './icon';
import useTheme from './use-theme';
const PaginationContext = React.createContext<ButtonProps>({
size: undefined,
radius: undefined,
});
type PaginationProps = React.ComponentProps<'nav'> &
Pick<ButtonProps, 'size' | 'radius'>;
const Pagination = ({ size, radius, className, ...props }: PaginationProps) => {
const { themeSize, themeRadius } = useTheme();
return (
<PaginationContext.Provider
value={{ size: size ?? themeSize, radius: radius ?? themeRadius }}
>
<nav
role="navigation"
aria-label="pagination"
className={cn('pagination', className)}
{...props}
/>
</PaginationContext.Provider>
);
};
Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul ref={ref} className={cn('pagination-content', className)} {...props} />
));
PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('pagination-item', className)} {...props} />
));
PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = {
isActive?: boolean;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
const PaginationLink = ({
className,
isActive,
...props
}: PaginationLinkProps) => {
const { size, radius } = React.useContext(PaginationContext);
return (
<Button
variant={isActive ? 'outline' : 'ghost'}
size={size}
radius={radius}
className={cn('pagination-link', className)}
asChild
>
<a {...props} aria-current={isActive ? 'page' : undefined} />
</Button>
);
};
PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({
className,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const { size, radius } = React.useContext(PaginationContext);
return (
<Button
variant="ghost"
size={size}
radius={radius}
aria-label="Go to previous page"
className={cn('pagination-previous', className)}
asChild
>
<a {...props}>
<Icon name="RiArrowLeftSLine" />
Previous
</a>
</Button>
);
};
PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({
className,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const { size, radius } = React.useContext(PaginationContext);
return (
<Button
variant="ghost"
size={size}
radius={radius}
aria-label="Go to next page"
className={cn('pagination-next', className)}
asChild
>
<a {...props}>
Next
<Icon name="RiArrowRightSLine" />
</a>
</Button>
);
};
PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<'button'>) => {
const { size, radius } = React.useContext(PaginationContext);
return (
<Button
{...props}
variant="ghost"
aria-label="More Pages"
size={size}
radius={radius}
className={cn('pagination-ellipsis', className)}
>
<Icon name="RiMoreLine" />
</Button>
);
};
PaginationEllipsis.displayName = 'PaginationEllipsis';
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};
@@ -0,0 +1,52 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../lib/utils';
const Popover = ({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) => {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
};
const PopoverTrigger = ({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) => {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
};
const PopoverClose = PopoverPrimitive.Close;
const PopoverContent = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn('popover', className)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverClose };
@@ -0,0 +1,128 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import type { Radius } from '../types';
import useTheme from './use-theme';
const radioCardGroupVariants = cva('radio-card-group', {
variants: {
orientation: {
horizontal: 'radio-card-group-horizontal',
vertical: 'radio-card-group-vertical',
},
},
defaultVariants: {
orientation: 'horizontal',
},
});
const RadioGroupContext = React.createContext<RadioCardGroupProps>({
cardType: 'vertical',
radius: 'medium',
});
type RadioCardGroupProps = React.ComponentPropsWithoutRef<
typeof RadioGroupPrimitive.Root
> & {
cardType?: 'horizontal' | 'vertical';
radius?: Radius;
};
const RadioCardGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
RadioCardGroupProps
>(
(
{
orientation = 'horizontal',
radius,
cardType = 'vertical',
children,
className,
...props
},
ref,
) => {
const { themeRadius } = useTheme();
return (
<RadioGroupPrimitive.Root
className={cn(radioCardGroupVariants({ orientation, className }))}
{...props}
ref={ref}
>
<RadioGroupContext.Provider
value={{ cardType, radius: radius ?? themeRadius }}
>
{children}
</RadioGroupContext.Provider>
</RadioGroupPrimitive.Root>
);
},
);
RadioCardGroup.displayName = RadioGroupPrimitive.Root.displayName;
const radioCardVariants = cva('radio-card', {
variants: {
type: {
vertical: 'radio-card-vertical',
horizontal: 'radio-card-horizontal',
},
radius: {
small: 'radio-card-radius-small',
medium: 'radio-card-radius-medium',
large: 'radio-card-radius-large',
},
},
defaultVariants: {
type: 'vertical',
radius: undefined,
},
});
const RadioCard = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> &
VariantProps<typeof radioCardVariants> & {
icon?: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
}
>(({ icon, title, description, className, ...props }, ref) => {
const { radius, cardType: type } = React.useContext(RadioGroupContext);
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(radioCardVariants({ type, radius, className }))}
{...props}
>
{icon}
<span className="radio-card-text">
{title && <strong className="radio-card-title">{title}</strong>}
{description && (
<span className="radio-card-description">{description}</span>
)}
</span>
</RadioGroupPrimitive.Item>
);
});
RadioCard.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioCardGroup, RadioCard };
@@ -0,0 +1,72 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import { Icon } from './icon';
const radioGroupVariants = cva('radio-group', {
variants: {
orientation: {
horizontal: 'radio-group-horizontal',
vertical: 'radio-group-vertical',
},
},
defaultVariants: {
orientation: 'horizontal',
},
});
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ orientation, className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn(radioGroupVariants({ orientation, className }))}
{...props}
ref={ref}
/>
);
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ children, className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn('radio-item', className)}
{...props}
>
<span className="radio">
<RadioGroupPrimitive.Indicator className="radio-indicator">
<Icon name="RiCircleFill" size={6} />
</RadioGroupPrimitive.Indicator>
</span>
<Slottable>{children}</Slottable>
</RadioGroupPrimitive.Item>
);
});
RadioItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioItem };
@@ -0,0 +1,89 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
const scrollBarVariants = cva('scroll-bar', {
variants: {
orientation: {
vertical: 'scroll-bar-vertical',
horizontal: 'scroll-bar-horizontal',
},
},
defaultVariants: {
orientation: 'vertical',
},
});
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
maxWidth?: string;
maxHeight?: string;
}
>(
(
{
type = 'auto',
maxWidth,
maxHeight,
className,
children,
style,
...props
},
ref,
) => (
<ScrollAreaPrimitive.Root
ref={ref}
type={type}
className={cn('scroll-area', className)}
style={{ ...style, maxWidth }}
{...props}
>
<ScrollAreaPrimitive.Viewport
className="scroll-area-viewport"
style={{ maxHeight }}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
),
);
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(scrollBarVariants({ orientation, className }))}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="scroll-thumb" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
@@ -0,0 +1,226 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { Slottable } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { ICON_SIZE } from '../constants';
import type { Radius, Size } from '../lib/types';
import { cn } from '../lib/utils';
import { Icon } from './icon';
import { ScrollArea, ScrollBar } from './scroll-area';
import useTheme from './use-theme';
type SelectContextType = { size: Size; radius: Radius };
const SelectContext = React.createContext<SelectContextType | undefined>(
undefined,
);
function useSelectContext() {
const ctx = React.useContext(SelectContext);
if (!ctx) throw new Error('Select components must be used within <Select>');
return ctx;
}
interface SelectProps
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> {
className?: string;
size?: Size;
radius?: Radius;
}
const selectVariants = cva('select', {
variants: {
size: {
small: 'select-small',
medium: 'select-medium',
large: 'select-large',
},
defaultVariants: { size: undefined },
},
});
const Select = ({ size, radius, className, ...props }: SelectProps) => {
const { themeSize, themeRadius } = useTheme();
return (
<SelectContext.Provider
value={{ size: size ?? themeSize, radius: radius ?? themeRadius }}
>
<div
className={cn(selectVariants({ size: size ?? themeSize, className }))}
>
<SelectPrimitive.Root {...props}></SelectPrimitive.Root>
</div>
</SelectContext.Provider>
);
};
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const selectTriggerVariants = cva('select-trigger', {
variants: {
size: {
small: 'select-trigger-small',
medium: 'select-trigger-medium',
large: 'select-trigger-large',
},
radius: {
small: 'select-trigger-radius-small',
medium: 'select-trigger-radius-medium',
large: 'select-trigger-radius-large',
},
},
defaultVariants: { size: undefined, radius: undefined },
});
interface SelectTriggerProps
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {}
const SelectTrigger = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Trigger>,
SelectTriggerProps
>(({ className, children, ...props }, ref) => {
const { size, radius } = useSelectContext();
return (
<SelectPrimitive.Trigger
ref={ref}
className={cn(selectTriggerVariants({ size, radius, className }))}
{...props}
>
<Slottable>{children}</Slottable>
<SelectPrimitive.Icon asChild>
<Icon name="RiArrowDownSLine" size={ICON_SIZE[size]} />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
});
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
interface SelectContentProps
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> {
maxHeight?: string;
}
const SelectContent = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Content>,
SelectContentProps
>(
(
{ maxHeight = 'auto', className, children, position = 'popper', ...props },
ref,
) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn('select-content', className)}
position={position}
{...props}
>
<SelectPrimitive.Viewport className={cn('select-viewport')}>
<ScrollArea maxHeight={maxHeight}>
{children}
<ScrollBar />
</ScrollArea>
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
),
);
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectGroupLabel = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('select-group-label', className)}
{...props}
/>
));
SelectGroupLabel.displayName = 'SelectGroupLabel';
const selectItemVariants = cva('select-item', {
variants: { check: { left: 'select-item-left', right: 'select-item-right' } },
defaultVariants: { check: 'left' },
});
const selectItemCheckVariants = cva('select-item-check', {
variants: {
check: { left: 'select-item-check-left', right: 'select-item-check-right' },
},
defaultVariants: { check: 'left' },
});
type SelectItemProps = React.ComponentPropsWithoutRef<
typeof SelectPrimitive.Item
>;
const SelectItem = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Item>,
SelectItemProps
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
selectItemVariants({
check: typeof children === 'string' ? 'left' : 'right',
className,
}),
)}
{...props}
>
<SelectPrimitive.ItemIndicator
className={cn(
selectItemCheckVariants({
check: typeof children === 'string' ? 'left' : 'right',
}),
)}
>
<Icon name="RiCheckLine" size={16} />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('select-separator', className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectGroupLabel,
SelectItem,
SelectSeparator,
};
+217
View File
@@ -0,0 +1,217 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import { Button } from './button';
import type { IconNameType } from './icon';
import { Icon } from './icon';
import { ScrollArea, ScrollBar } from './scroll-area';
const Sheet = SheetPrimitive.Root;
const SheetTrigger = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Trigger>,
SheetPrimitive.DialogTriggerProps &
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant = 'outline', className, children, ...props }, ref) => {
if (props.asChild) {
return (
<SheetPrimitive.Trigger
className={cn('sheet-trigger', className)}
ref={ref}
{...props}
>
{children}
</SheetPrimitive.Trigger>
);
}
return (
<SheetPrimitive.Trigger asChild>
<Button
variant={variant}
className={cn('sheet-trigger', className)}
ref={ref}
{...props}
>
{children}
</Button>
</SheetPrimitive.Trigger>
);
});
SheetTrigger.displayName = SheetPrimitive.Trigger.displayName;
const SheetClose = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentPropsWithoutRef<typeof Button>
>(({ variant, ...props }, ref) => (
<SheetPrimitive.Close asChild>
<Button ref={ref} variant={variant ?? 'outline'} {...props} />
</SheetPrimitive.Close>
));
SheetClose.displayName = 'SheetClose';
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn('sheet-overlay', className)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva('sheet', {
variants: {
side: {
top: 'sheet-side-top',
bottom: 'sheet-side-bottom',
left: 'sheet-side-left',
right: 'sheet-side-right',
},
radius: {
small: 'sheet-radius-small',
medium: 'sheet-radius-medium',
large: 'sheet-radius-large',
},
},
defaultVariants: {
side: 'right',
radius: 'small',
},
});
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(
(
{ side = 'right', radius = 'small', className, children, ...props },
ref,
) => (
<SheetPortal>
<SheetOverlay>
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side, radius }), className)}
{...props}
>
{children}
<SheetPrimitive.Close asChild>
<Button
size="medium"
variant="ghost"
className="sheet-close"
aria-label="Close"
>
<Icon name="RiCloseLine" />
</Button>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetOverlay>
</SheetPortal>
),
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
interface SheetHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
icon?: IconNameType;
}
const SheetHeader = ({
icon,
children,
className,
...props
}: SheetHeaderProps) => (
<div className={cn('sheet-header', className)} {...props}>
{icon && <Icon name={icon} size={38} className="sheet-icon" />}
{children}
</div>
);
SheetHeader.displayName = 'SheetHeader';
interface SheetBodyProps extends React.HTMLAttributes<HTMLDivElement> {
asChild?: boolean;
}
const SheetBody = ({ asChild, className, ...props }: SheetBodyProps) => {
const Comp = asChild ? Slot : 'div';
return (
<ScrollArea>
<Comp className={cn(className)} {...props} />
<ScrollBar />
</ScrollArea>
);
};
SheetBody.displayName = 'SheetBody';
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('sheet-footer', className)} {...props} />
);
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('sheet-title', className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('sheet-description', className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetBody,
SheetFooter,
SheetTitle,
SheetDescription,
};
@@ -0,0 +1,55 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import type { Size } from '../types';
import useTheme from './use-theme';
export interface SpinnerProps extends React.HTMLAttributes<HTMLSpanElement> {
size?: Size;
}
const defaultVariants: {
size?: Size;
} = {
size: undefined,
};
const spinnerVariants = cva('spinner', {
variants: {
size: {
small: 'spinner-small',
medium: 'spinner-medium',
large: 'spinner-large',
},
},
defaultVariants,
});
const Spinner: React.FC<SpinnerProps> = (props) => {
const { size, className } = props;
const { themeSize } = useTheme();
return (
<span
className={cn(spinnerVariants({ size: size ?? themeSize }), className)}
aria-label="loading..."
></span>
);
};
export { Spinner };
@@ -0,0 +1,59 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cva } from 'class-variance-authority';
import type { Color } from '../lib/types';
import { cn } from '../lib/utils';
const switchVariants = cva('switch', {
variants: {
color: {
default: 'switch-default',
blue: 'switch-blue',
orange: 'switch-orange',
red: 'switch-red',
green: 'switch-green',
},
},
defaultVariants: {
color: 'default',
},
});
interface SwitchProps
extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> {
color?: Color;
}
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
SwitchProps
>(({ color, className, children, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(switchVariants({ color }), className)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb className={cn('switch-thumb')} />
{children}
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
+139
View File
@@ -0,0 +1,139 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
import type { IconNameType } from './icon';
import { Icon } from './icon';
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<table ref={ref} className={cn('table', className)} {...props} />
));
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('table-header', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('table-body', className)} {...props} />
));
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn('table-footer', className)} {...props} />
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr ref={ref} className={cn('table-row', className)} {...props} />
));
TableRow.displayName = 'TableRow';
const tableHeadVariants = cva('table-head', {
variants: {
textAlign: {
left: 'table-head-left',
center: 'table-head-center',
right: 'table-head-right',
},
},
defaultVariants: {
textAlign: 'left',
},
});
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement> &
VariantProps<typeof tableHeadVariants> & {
icon?: IconNameType;
}
>(({ icon, textAlign = 'left', children, className, ...props }, ref) => (
<th
ref={ref}
className={cn(tableHeadVariants({ textAlign, className }))}
{...props}
>
{icon && <Icon name={icon} size={16} />}
{children}
</th>
));
TableHead.displayName = 'TableHead';
const tableCellVariants = cva('table-cell', {
variants: {
textAlign: {
left: 'table-cell-left',
center: 'table-cell-center',
right: 'table-cell-right',
},
},
defaultVariants: {
textAlign: 'left',
},
});
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement> &
VariantProps<typeof tableCellVariants>
>(({ textAlign = 'left', className, ...props }, ref) => (
<td
ref={ref}
className={cn(tableCellVariants({ textAlign, className }))}
{...props}
/>
));
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('table-caption', className)} {...props} />
));
TableCaption.displayName = 'TableCaption';
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
@@ -0,0 +1,67 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '../lib/utils';
const Tabs = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Root ref={ref} className={cn('tabs', className)} {...props} />
));
Tabs.displayName = TabsPrimitive.Root.displayName;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn('tabs-list', className)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn('tabs-trigger', className)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn('tabs-content', className)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
+94
View File
@@ -0,0 +1,94 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import type { Radius, Size } from '../lib/types';
import { cn } from '../lib/utils';
import useTheme from './use-theme';
type TagVariant = 'primary' | 'secondary' | 'outline' | 'destructive';
const tagVariants = cva('tag', {
variants: {
variant: {
primary: 'tag-primary',
secondary: 'tag-secondary',
outline: 'tag-outline',
destructive: 'tag-destructive',
},
size: {
large: 'tag-large',
medium: 'tag-medium',
small: 'tag-small',
},
radius: {
large: 'tag-radius-large',
medium: 'tag-radius-medium',
small: 'tag-radius-small',
},
defaultVariants: {
variant: 'primary',
size: undefined,
radius: undefined,
},
},
});
interface TagProps extends React.HTMLAttributes<HTMLElement> {
variant?: TagVariant;
size?: Size;
radius?: Radius;
asChild?: boolean;
}
const Tag = React.forwardRef<HTMLElement, TagProps>((props, ref) => {
const {
variant = 'primary',
size,
radius,
className,
children,
asChild,
...rest
} = props;
const { themeSize, themeRadius } = useTheme();
const Comp = asChild ? Slot : 'span';
return (
<Comp
{...rest}
ref={ref}
className={cn(
tagVariants({
variant,
size: size ?? themeSize,
radius: radius ?? themeRadius,
className,
}),
)}
>
{children}
</Comp>
);
});
Tag.displayName = 'Tag';
export { Tag, type TagProps };
@@ -0,0 +1,30 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import { cn } from '../lib/utils';
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<'textarea'>
>(({ className, ...props }, ref) => {
return (
<textarea className={cn('textarea', className)} ref={ref} {...props} />
);
});
Textarea.displayName = 'Textarea';
export { Textarea };
@@ -0,0 +1,533 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import { cn } from '../lib/utils';
import { Icon } from './icon';
import type { TextInputProps } from './input';
import { InputBox, InputField, TextInput } from './input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from './select';
interface PeriodSelectorProps {
period: Period;
setPeriod?: (m: Period) => void;
date?: Date | null;
onDateChange?: (date: Date | undefined) => void;
onRightFocus?: () => void;
onLeftFocus?: () => void;
className?: string;
}
const TimePeriodSelect = React.forwardRef<
HTMLButtonElement,
PeriodSelectorProps
>(
(
{
period,
setPeriod,
date,
onDateChange,
onLeftFocus,
onRightFocus,
className,
},
ref,
) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === 'ArrowRight') onRightFocus?.();
if (e.key === 'ArrowLeft') onLeftFocus?.();
};
const handleValueChange = (value: Period) => {
setPeriod?.(value);
/**
* trigger an update whenever the user switches between AM and PM;
* otherwise user must manually change the hour each time
*/
if (date) {
const tempDate = new Date(date);
const hours = display12HourValue(date.getHours());
onDateChange?.(
setDateByType(
tempDate,
hours.toString(),
'12hours',
period === 'AM' ? 'PM' : 'AM',
),
);
}
};
return (
<Select
value={period}
onValueChange={(value: Period) => handleValueChange(value)}
>
<SelectTrigger
className={className}
ref={ref}
onKeyDown={handleKeyDown}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AM">AM</SelectItem>
<SelectItem value="PM">PM</SelectItem>
</SelectContent>
</Select>
);
},
);
TimePeriodSelect.displayName = 'TimePeriodSelect';
interface TimePickerInputProps extends React.PropsWithoutRef<TextInputProps> {
picker: TimePickerType;
date?: Date | null;
onDateChange?: (date: Date | undefined) => void;
period?: Period;
onRightFocus?: () => void;
onLeftFocus?: () => void;
}
const TimePickerInput = React.forwardRef<
HTMLInputElement,
TimePickerInputProps
>(
(
{
className,
type = 'tel',
value,
id,
name,
date = new Date(new Date().setHours(0, 0, 0, 0)),
onDateChange,
onChange,
onKeyDown,
picker,
period,
onLeftFocus,
onRightFocus,
...props
},
ref,
) => {
const [flag, setFlag] = React.useState<boolean>(false);
const [prevIntKey, setPrevIntKey] = React.useState<string>('0');
/**
* allow the user to enter the second digit within 2 seconds
* otherwise start again with entering first digit
*/
React.useEffect(() => {
if (flag) {
const timer = setTimeout(() => {
setFlag(false);
}, 2000);
return () => clearTimeout(timer);
}
}, [flag]);
const calculatedValue = React.useMemo(() => {
return getDateByType(date, picker);
}, [date, picker]);
const calculateNewValue = (key: string) => {
/*
* If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1.
* The second entered digit will break the condition and the value will be set to 10-12.
*/
if (picker === '12hours') {
if (flag && calculatedValue.slice(1, 2) === '1' && prevIntKey === '0')
return `0${key}`;
}
return !flag ? `0${key}` : calculatedValue.slice(1, 2) + key;
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Tab') return;
e.preventDefault();
if (e.key === 'ArrowRight') onRightFocus?.();
if (e.key === 'ArrowLeft') onLeftFocus?.();
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
const step = e.key === 'ArrowUp' ? 1 : -1;
const newValue = getArrowByType(calculatedValue, step, picker);
if (flag) setFlag(false);
const tempDate = date ? new Date(date) : new Date();
onDateChange?.(setDateByType(tempDate, newValue, picker, period));
}
if (e.key >= '0' && e.key <= '9') {
if (picker === '12hours') setPrevIntKey(e.key);
const newValue = calculateNewValue(e.key);
if (flag) onRightFocus?.();
setFlag((prev) => !prev);
const tempDate = date ? new Date(date) : new Date();
onDateChange?.(setDateByType(tempDate, newValue, picker, period));
}
};
return (
<InputField className={cn('w-24', className)}>
<InputBox>
<TextInput
ref={ref}
id={id ?? picker}
name={name ?? picker}
className="text-left font-mono tabular-nums caret-transparent [&::-webkit-inner-spin-button]:appearance-none"
value={value ?? calculatedValue}
onChange={(e) => {
e.preventDefault();
onChange?.(e);
}}
type={type}
inputMode="decimal"
onKeyDown={(e) => {
onKeyDown?.(e);
handleKeyDown(e);
}}
{...props}
/>
</InputBox>
</InputField>
);
},
);
TimePickerInput.displayName = 'TimePickerInput';
interface TimePickerProps {
date?: Date | null;
onChange?: (date: Date | undefined) => void;
hourCycle?: 12 | 24;
/**
* Determines the smallest unit that is displayed in the datetime picker.
* Default is 'second'.
* */
granularity?: Granularity;
}
interface TimePickerRef {
minuteRef: HTMLInputElement | null;
hourRef: HTMLInputElement | null;
secondRef: HTMLInputElement | null;
}
const TimePicker = React.forwardRef<TimePickerRef, TimePickerProps>(
({ date, onChange, hourCycle = 24, granularity = 'second' }, ref) => {
const minuteRef = React.useRef<HTMLInputElement>(null);
const hourRef = React.useRef<HTMLInputElement>(null);
const secondRef = React.useRef<HTMLInputElement>(null);
const periodRef = React.useRef<HTMLButtonElement>(null);
const [period, setPeriod] = React.useState<Period>(
date && date.getHours() >= 12 ? 'PM' : 'AM',
);
React.useImperativeHandle(
ref,
() => ({
minuteRef: minuteRef.current,
hourRef: hourRef.current,
secondRef: secondRef.current,
periodRef: periodRef.current,
}),
[minuteRef, hourRef, secondRef],
);
return (
<div className="flex items-center justify-center gap-2">
<label
htmlFor="datetime-picker-hour-input"
className="flex cursor-pointer items-center"
>
<Icon name="RiTimeLine" size={16} />
</label>
<TimePickerInput
picker={hourCycle === 24 ? 'hours' : '12hours'}
date={date}
id="datetime-picker-hour-input"
onDateChange={onChange}
ref={hourRef}
period={period}
onRightFocus={() => minuteRef.current?.focus()}
/>
{(granularity === 'minute' || granularity === 'second') && (
<>
:
<TimePickerInput
picker="minutes"
date={date}
onDateChange={onChange}
ref={minuteRef}
onLeftFocus={() => hourRef.current?.focus()}
onRightFocus={() => secondRef.current?.focus()}
/>
</>
)}
{granularity === 'second' && (
<>
:
<TimePickerInput
picker="seconds"
date={date}
onDateChange={onChange}
ref={secondRef}
onLeftFocus={() => minuteRef.current?.focus()}
onRightFocus={() => periodRef.current?.focus()}
className="w-24"
/>
</>
)}
{hourCycle === 12 && (
<TimePeriodSelect
period={period}
setPeriod={setPeriod}
date={date}
onDateChange={(date) => {
onChange?.(date);
if (date && date.getHours() >= 12) {
setPeriod('PM');
} else {
setPeriod('AM');
}
}}
ref={periodRef}
onLeftFocus={() => secondRef.current?.focus()}
/>
)}
</div>
);
},
);
TimePicker.displayName = 'TimePicker';
type Granularity = 'hour' | 'minute' | 'second';
// ---------- utils start ----------
/**
* regular expression to check for valid hour format (01-23)
*/
function isValidHour(value: string) {
return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value);
}
/**
* regular expression to check for valid 12 hour format (01-12)
*/
function isValid12Hour(value: string) {
return /^(0[1-9]|1[0-2])$/.test(value);
}
/**
* regular expression to check for valid minute format (00-59)
*/
function isValidMinuteOrSecond(value: string) {
return /^[0-5][0-9]$/.test(value);
}
type GetValidNumberConfig = { max: number; min?: number; loop?: boolean };
function getValidNumber(
value: string,
{ max, min = 0, loop = false }: GetValidNumberConfig,
) {
let numericValue = parseInt(value, 10);
if (!Number.isNaN(numericValue)) {
if (!loop) {
if (numericValue > max) numericValue = max;
if (numericValue < min) numericValue = min;
} else {
if (numericValue > max) numericValue = min;
if (numericValue < min) numericValue = max;
}
return numericValue.toString().padStart(2, '0');
}
return '00';
}
function getValidHour(value: string) {
if (isValidHour(value)) return value;
return getValidNumber(value, { max: 23 });
}
function getValid12Hour(value: string) {
if (isValid12Hour(value)) return value;
return getValidNumber(value, { min: 1, max: 12 });
}
function getValidMinuteOrSecond(value: string) {
if (isValidMinuteOrSecond(value)) return value;
return getValidNumber(value, { max: 59 });
}
type GetValidArrowNumberConfig = {
min: number;
max: number;
step: number;
};
function getValidArrowNumber(
value: string,
{ min, max, step }: GetValidArrowNumberConfig,
) {
let numericValue = parseInt(value, 10);
if (!Number.isNaN(numericValue)) {
numericValue += step;
return getValidNumber(String(numericValue), { min, max, loop: true });
}
return '00';
}
function getValidArrowHour(value: string, step: number) {
return getValidArrowNumber(value, { min: 0, max: 23, step });
}
function getValidArrow12Hour(value: string, step: number) {
return getValidArrowNumber(value, { min: 1, max: 12, step });
}
function getValidArrowMinuteOrSecond(value: string, step: number) {
return getValidArrowNumber(value, { min: 0, max: 59, step });
}
function setMinutes(date: Date, value: string) {
const minutes = getValidMinuteOrSecond(value);
date.setMinutes(parseInt(minutes, 10));
return date;
}
function setSeconds(date: Date, value: string) {
const seconds = getValidMinuteOrSecond(value);
date.setSeconds(parseInt(seconds, 10));
return date;
}
function setHours(date: Date, value: string) {
const hours = getValidHour(value);
date.setHours(parseInt(hours, 10));
return date;
}
function set12Hours(date: Date, value: string, period: Period) {
const hours = parseInt(getValid12Hour(value), 10);
const convertedHours = convert12HourTo24Hour(hours, period);
date.setHours(convertedHours);
return date;
}
type TimePickerType = 'minutes' | 'seconds' | 'hours' | '12hours';
export type Period = 'AM' | 'PM';
function setDateByType(
date: Date,
value: string,
type: TimePickerType,
period?: Period,
) {
switch (type) {
case 'minutes':
return setMinutes(date, value);
case 'seconds':
return setSeconds(date, value);
case 'hours':
return setHours(date, value);
case '12hours': {
if (!period) return date;
return set12Hours(date, value, period);
}
default:
return date;
}
}
function getDateByType(date: Date | null, type: TimePickerType) {
if (!date) return '00';
switch (type) {
case 'minutes':
return getValidMinuteOrSecond(String(date.getMinutes()));
case 'seconds':
return getValidMinuteOrSecond(String(date.getSeconds()));
case 'hours':
return getValidHour(String(date.getHours()));
case '12hours':
return getValid12Hour(String(display12HourValue(date.getHours())));
default:
return '00';
}
}
function getArrowByType(value: string, step: number, type: TimePickerType) {
switch (type) {
case 'minutes':
return getValidArrowMinuteOrSecond(value, step);
case 'seconds':
return getValidArrowMinuteOrSecond(value, step);
case 'hours':
return getValidArrowHour(value, step);
case '12hours':
return getValidArrow12Hour(value, step);
default:
return '00';
}
}
/**
* handles value change of 12-hour input
* 12:00 PM is 12:00
* 12:00 AM is 00:00
*/
function convert12HourTo24Hour(hour: number, period: Period) {
if (period === 'PM') {
if (hour <= 11) {
return hour + 12;
}
return hour;
}
if (hour === 12) return 0;
return hour;
}
/**
* time is stored in the 24-hour form,
* but needs to be displayed to the user
* in its 12-hour representation
*/
function display12HourValue(hours: number) {
if (hours === 0 || hours === 12) return '12';
if (hours >= 22) return `${hours - 12}`;
if (hours % 12 > 9) return `${hours}`;
return `0${hours % 12}`;
}
// ---------- utils end ----------
export { TimePickerInput, TimePeriodSelect };
export type { TimePickerInputProps, PeriodSelectorProps };
@@ -0,0 +1,98 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { Toaster as Sonner, toast } from 'sonner';
import { cn } from '../lib/utils';
import { Icon } from './icon';
import { Spinner } from './spinner';
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({
className,
toastOptions,
icons,
...props
}: ToasterProps) => {
return (
<Sonner
className={cn('toaster', className)}
toastOptions={{
unstyled: true,
classNames: {
toast: cn('toast', toastOptions?.classNames?.toast ?? ''),
title: cn('toast-title', toastOptions?.classNames?.title ?? ''),
description: cn(
'toast-description',
toastOptions?.classNames?.description ?? '',
),
loader: cn('toast-loader', toastOptions?.classNames?.loader ?? ''),
cancelButton: cn(
'toast-close button button-ghost button-medium button-radius-medium',
toastOptions?.classNames?.cancelButton ?? '',
),
actionButton: cn(
'toast-button button button-outline button-medium button-radius-medium',
toastOptions?.classNames?.actionButton ?? '',
),
success: cn('toast-success', toastOptions?.classNames?.success ?? ''),
error: cn('toast-error', toastOptions?.classNames?.error ?? ''),
info: cn('toast-info', toastOptions?.classNames?.info ?? ''),
warning: cn('toast-warning', toastOptions?.classNames?.warning ?? ''),
loading: '',
default: cn('toast-default', toastOptions?.classNames?.default ?? ''),
content: cn('toast-content', toastOptions?.classNames?.content ?? ''),
icon: cn('toast-icon', toastOptions?.classNames?.icon ?? ''),
},
}}
icons={{
...icons,
loading: icons?.loading ?? <Spinner size="small" />,
warning: icons?.warning ?? (
<Icon
name="RiErrorWarningFill"
size={20}
className="toast-icon-warning"
/>
),
success: icons?.success ?? (
<Icon
name="RiCheckboxCircleFill"
size={20}
className="toast-icon-success"
/>
),
error: icons?.success ?? (
<Icon
name="RiCloseCircleFill"
size={20}
className="toast-icon-error"
/>
),
info: icons?.info ?? (
<Icon
name="RiInformation2Fill"
size={20}
className="toast-icon-informative"
/>
),
}}
{...props}
/>
);
};
export { Toaster, toast };
@@ -0,0 +1,113 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
'use client';
import * as React from 'react';
import type * as TogglePrimitive from '@radix-ui/react-toggle';
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import type { Radius, Size } from '../lib/types';
import { cn } from '../lib/utils';
import useTheme from './use-theme';
const toggleVariants = cva('toggle-group-item', {
variants: {
size: {
small: 'toggle-group-item-small',
medium: 'toggle-group-item-medium',
large: 'toggle-group-item-large',
},
radius: {
small: 'toggle-group-item-radius-small',
medium: 'toggle-group-item-radius-medium',
large: 'toggle-group-item-radius-large',
},
},
defaultVariants: {
size: undefined,
radius: undefined,
},
});
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: undefined,
radius: undefined,
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, size, radius, children, ...props }, ref) => {
const { themeSize, themeRadius } = useTheme();
return (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn('toggle-group', className)}
{...props}
>
<ToggleGroupContext.Provider
value={{ size: size ?? themeSize, radius: radius ?? themeRadius }}
>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
});
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
interface ToggleGroupItemProps
extends React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root>,
Omit<VariantProps<typeof toggleVariants>, 'disabled'> {
size?: Size;
radius?: Radius;
value: string;
}
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
ToggleGroupItemProps
>(({ className, children, size, radius, disabled, value, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
size: size ?? context.size,
radius: radius ?? context.radius,
}),
className,
)}
value={value}
disabled={disabled}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };
@@ -0,0 +1,97 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import { Slottable } from '@radix-ui/react-slot';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
const tooltipVariants = cva('tooltip', {
variants: {
textAlign: {
left: 'tooltip-text-left',
center: 'tooltip-text-center',
right: 'tooltip-text-right',
},
defaultVariants: {
textAlign: 'center',
},
},
});
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
interface TooltipContentProps
extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> {
textAlign?: 'left' | 'center' | 'right';
}
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
TooltipContentProps
>(
(
{
title,
children,
className,
side,
sideOffset = 4,
textAlign = 'center',
...props
},
ref,
) => (
<TooltipPrimitive.Content
ref={ref}
side={side}
sideOffset={sideOffset}
className={cn(tooltipVariants({ textAlign, className }))}
{...props}
>
{title && <strong className={cn('tooltip-title')}>{title}</strong>}
<Slottable>{children}</Slottable>
{side !== undefined && (
<>
<TooltipPrimitive.TooltipArrow
width={10}
height={6}
className={cn('tooltip-arrow-border')}
/>
<TooltipPrimitive.TooltipArrow
width={8}
height={5}
className={cn('tooltip-arrow')}
/>
</>
)}
</TooltipPrimitive.Content>
),
);
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
type TooltipContentProps,
};
@@ -0,0 +1,38 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import type { Radius, Size } from '../types';
const DefaultValue: {
themeSize: Size;
themeRadius: Radius;
} = {
themeSize: 'small',
themeRadius: 'medium',
} as const;
const useTheme = () => {
if (typeof window !== 'undefined') {
const size = (document.body.getAttribute('data-size') ??
DefaultValue.themeSize) as Size;
const radius = (document.body.getAttribute('data-radius') ??
DefaultValue.themeRadius) as Radius;
return { themeSize: size, themeRadius: radius };
} else {
return DefaultValue;
}
};
export default useTheme;
+60
View File
@@ -0,0 +1,60 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import type { IconNameType } from './components';
import type { CaptionType } from './lib/types';
import type { Size } from './types';
export const ICON_SIZE: Record<Size, number> = {
large: 24,
medium: 20,
small: 16,
};
export const SMALL_ICON_SIZE: Record<Size, number> = {
large: 16,
medium: 16,
small: 12,
};
export const CHECK_ICON_SIZE: Record<Size, number> = {
large: 20,
medium: 16,
small: 14,
};
export const INPUT_CAPTION_ICON_SIZE: Record<Size, number> = {
large: 20,
medium: 16,
small: 16,
};
export const CAPTION_DEFAULT_ICON: Record<
CaptionType,
IconNameType | undefined
> = {
default: undefined,
error: 'RiErrorWarningFill',
info: 'RiInformationFill',
success: 'RiCheckboxCircleFill',
} as const;
export const ALERT_DEFAULT_ICON: Record<string, IconNameType | undefined> = {
default: undefined,
warning: 'RiErrorWarningFill',
informative: 'RiInformation2Fill',
success: 'RiCheckboxCircleFill',
error: 'RiCloseCircleFill',
};
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export * from './components';
export * from './types';
export * from './constants';
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export type Radius = 'small' | 'medium' | 'large';
export type Size = 'small' | 'medium' | 'large';
export type Color = 'default' | 'blue' | 'orange' | 'red' | 'green';
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'destructive'
| 'ghost'
| 'outline';
export type ButtonState = 'default' | 'loading' | 'disabled';
export type CaptionType = 'default' | 'success' | 'info' | 'error';
export type AlertVariantType =
| 'default'
| 'warning'
| 'success'
| 'error'
| 'informative';
export type TriggerType = 'click' | 'hover';
+84
View File
@@ -0,0 +1,84 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import type { ClassValue } from 'clsx';
import { clsx } from 'clsx';
import { extendTailwindMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
const customTwMerge = extendTailwindMerge({
extend: {
classGroups: {
'font-size': [
'text-title-h1',
'text-title-h2',
'text-title-h3',
'text-title-h4',
'text-title-h5',
'text-small-normal',
'text-small-strong',
'text-small-underline',
'text-small-delete',
'text-base-normal',
'text-base-strong',
'text-base-underline',
'text-base-delete',
'text-large-normal',
'text-large-strong',
'text-large-underline',
'text-large-delete',
'text-xlarge-normal',
'text-xlarge-strong',
'text-xlarge-underline',
'text-xlarge-delete',
],
},
},
});
return customTwMerge(clsx(inputs));
}
type PossibleRef<T> = React.Ref<T> | undefined;
/**
* Set a given ref to a given value
* This utility takes care of different types of refs: callback refs and RefObject(s)
*/
export function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === 'function') {
ref(value);
} else if (ref !== null && ref !== undefined) {
(ref as React.MutableRefObject<T>).current = value;
}
}
/**
* A utility to compose multiple refs together
* Accepts callback refs and RefObject(s)
*/
export function composeRefs<T>(...refs: PossibleRef<T>[]) {
return (node: T) => refs.forEach((ref) => setRef(ref, node));
}
/**
* A custom hook that composes multiple refs
* Accepts callback refs and RefObject(s)
*/
export function useComposedRefs<T>(...refs: PossibleRef<T>[]) {
// eslint-disable-next-line react-compiler/react-compiler
return React.useCallback(composeRefs(...refs), refs);
}
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export type Radius = 'small' | 'medium' | 'large';
export type Size = 'small' | 'medium' | 'large';
export type Color = 'default' | 'blue' | 'orange' | 'red' | 'green';
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'destructive'
| 'ghost'
| 'outline';
+7
View File
@@ -0,0 +1,7 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ['class'],
prefix: '',
content: ['./src/**/*.tsx'],
plugins: [require('@ufb/tailwindcss')],
};
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "@ufb/tsconfig/internal-package.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "ES2022"],
"jsx": "preserve",
"rootDir": "."
},
"include": ["src"],
"exclude": ["node_modules"]
}
+9
View File
@@ -0,0 +1,9 @@
import baseConfig from '@ufb/eslint-config/base';
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: ['dist/**'],
},
...baseConfig,
];
+28
View File
@@ -0,0 +1,28 @@
{
"name": "@ufb/shared",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"clean": "git clean -xdf .cache .turbo dist node_modules",
"dev": "tsup src/index.ts --format cjs,esm --dts",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"prettier": "@ufb/prettier-config",
"devDependencies": {
"@ufb/eslint-config": "workspace:*",
"@ufb/prettier-config": "workspace:*",
"@ufb/tsconfig": "workspace:*",
"eslint": "catalog:",
"prettier": "catalog:",
"react": "^19.2.4",
"tsup": "^8.5.1",
"typescript": "catalog:"
}
}
+127
View File
@@ -0,0 +1,127 @@
/**
* 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.
*/
const Tenant = {
TenantNotFound: 'TenantNotFound',
TenantAlreadyExists: 'TenantAlreadyExists',
};
const User = {
UserAlreadyExists: 'UserAlreadyExists',
UserNotFound: 'UserNotFound',
PasswordNotMatched: 'PasswordNotMatched',
EmailVerification: 'EmailVerification',
EmailNotVerified: 'EmailNotVerified',
PrivateServiceUserCreate: 'PrivateServiceUserCreate',
NotAllowDomain: 'NotAllowDomain',
InvalidCode: 'InvalidCode',
InvalidPassword: 'InvalidPassword',
};
const Auth = {
PasswordNotMatch: 'PasswordNotMatch',
BlockedUser: 'BlockedUser',
};
const Role = {
RoleNotFound: 'RoleNotFound',
OwnerIsImmutable: 'OwnerIsImmutable',
RoleAlreadyExists: 'RoleAlreadyExists',
};
const Mailing = {
NotVerifiedEmail: 'NotVerifiedEmail',
InvalidEmailCode: 'InvalidEmailCode',
};
const Common = {
InvalidDateFormat: 'InvalidDateFormat',
};
const Project = {
ProjectAlreadyExists: 'ProjectAlreadyExists',
ProjectNotFound: 'ProjectNotFound',
ProjectInvalidName: 'ProjectInvalidName',
};
const Channel = {
ChannelAlreadyExists: 'ChannelAlreadyExists',
ChannelNotFound: 'ChannelNotFound',
ChannelInvalidName: 'ChannelInvalidName',
};
const Issue = {
IssueNameDuplicated: 'IssueNameDuplicated',
IssueInvalidName: 'IssueInvalidName',
IssueNotFound: 'IssueNotFound',
};
const Category = {
CategoryNameDuplicated: 'CategoryNameDuplicated',
CategoryNameInvalid: 'CategoryNameInvalid',
CategoryNotFound: 'CategoryNotFound',
};
const Field = {
FieldNameDuplicated: 'FieldNameDuplicated',
FieldKeyDuplicated: 'FieldKeyDuplicated',
};
const Option = {
OptionNameDuplicated: 'OptionNameDuplicated',
OptionKeyDuplicated: 'OptionKeyDuplicated',
};
const Feedback = {
InvalidExpressionFormat: 'InvalidExpressionFormat',
InvalidFieldType: 'InvalidFieldType',
InvalidFieldRequest: 'InvalidFieldRequest',
};
const Member = {
MemberAlreadyExists: 'MemberAlreadyExists',
MemberNotFound: 'MemberNotFound',
MemberUpdateRoleNotMatchedProject: 'MemberUpdateRoleNotMatchedProject',
MemberInvalidUser: 'MemberInvalidUser',
};
const Opensearch = {
LargeWindow: 'LargeWindow',
};
const Webhook = {
WebhookAlreadyExists: 'WebhookAlreadyExists',
WebhookNotFound: 'WebhookNotFound',
};
export const ErrorCode = {
Tenant,
Role,
User,
Auth,
Mailing,
Common,
Feedback,
Project,
Channel,
Issue,
Category,
Field,
Option,
Member,
Opensearch,
Webhook,
};
export type ErrorCode = typeof ErrorCode;
+17
View File
@@ -0,0 +1,17 @@
/**
* 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 './error-code.enum';
export * from './timezone';
+54
View File
@@ -0,0 +1,54 @@
/**
* 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 TimezoneOffset =
| '-12:00'
| '-11:00'
| '-10:00'
| '-09:30'
| '-09:00'
| '-08:00'
| '-07:00'
| '-06:00'
| '-05:00'
| '-04:00'
| '-03:30'
| '-03:00'
| '-02:00'
| '-01:00'
| '+00:00'
| '+01:00'
| '+02:00'
| '+03:00'
| '+03:30'
| '+04:00'
| '+04:30'
| '+05:00'
| '+05:30'
| '+05:45'
| '+06:00'
| '+06:30'
| '+07:00'
| '+08:00'
| '+08:45'
| '+09:00'
| '+09:30'
| '+10:00'
| '+10:30'
| '+11:00'
| '+12:00'
| '+12:45'
| '+13:00'
| '+14:00';
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "@ufb/tsconfig/internal-package.json",
"include": ["src"],
"exclude": ["node_modules"]
}
@@ -0,0 +1,9 @@
import baseConfig from "@ufb/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+20
View File
@@ -0,0 +1,20 @@
/**
* 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 plugin from "tailwindcss/plugin";
declare const tailwindcss: ReturnType<typeof plugin>;
export default tailwindcss;
+22
View File
@@ -0,0 +1,22 @@
/**
* 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 plugin from 'tailwindcss/plugin';
export default plugin(({ addBase, addComponents }) => {
addBase(require('./dist/base.js'));
addComponents(require('./dist/components.js'));
addComponents(require('./dist/utilities.js'));
});
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@ufb/tailwindcss",
"version": "0.1.0",
"private": true,
"main": "index.js",
"types": "index.d.ts",
"typings": "index.d.ts",
"files": [
"dist/*.js",
"index.js",
"index.d.ts"
],
"scripts": {
"dev": "pnpm build",
"build": "node src/build",
"clean": "rm -rf .turbo node_modules dist .cache"
},
"devDependencies": {
"@ufb/eslint-config": "workspace:*",
"@ufb/prettier-config": "workspace:*",
"@ufb/tsconfig": "workspace:^",
"autoprefixer": "^10.4.27",
"eslint": "catalog:",
"glob": "^13.0.6",
"postcss": "^8.5.8",
"postcss-import": "^16.1.1",
"postcss-js": "^5.1.0",
"postcss-nesting": "^14.0.0",
"prettier": "catalog:",
"tailwindcss": "catalog:"
}
}
+356
View File
@@ -0,0 +1,356 @@
/**
* 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.
*/
@layer {
:root {
/* primitives */
--base-black: #000;
--base-white: #fff;
--base-transparent: transparent;
--neutral50: #fafafa;
--neutral100: #f5f5f5;
--neutral200: #efefef;
--neutral300: #dddcdc;
--neutral400: #a3a3a3;
--neutral500: #737373;
--neutral600: #525252;
--neutral700: #404040;
--neutral800: #262626;
--neutral900: #202020;
--neutral950: #1c1c1c;
--alpha-black50: rgb(0 0 0 / 8%);
--alpha-black100: rgb(0 0 0 / 12%);
--alpha-black200: rgb(0 0 0 / 16%);
--alpha-black300: rgb(0 0 0 / 20%);
--alpha-black400: rgb(0 0 0 / 24%);
--alpha-black500: rgb(0 0 0 / 32%);
--alpha-black600: rgb(0 0 0 / 46%);
--alpha-black700: rgb(0 0 0 / 62%);
--alpha-black800: rgb(0 0 0 / 71%);
--alpha-black900: rgb(0 0 0 / 81%);
--alpha-black950: rgb(0 0 0 / 90%);
--alpha-white50: rgb(255 255 255 / 8%);
--alpha-white100: rgb(255 255 255 / 12%);
--alpha-white200: rgb(255 255 255 / 16%);
--alpha-white300: rgb(255 255 255 / 20%);
--alpha-white400: rgb(255 255 255 / 24%);
--alpha-white500: rgb(255 255 255 / 32%);
--alpha-white600: rgb(255 255 255 / 46%);
--alpha-white700: rgb(255 255 255 / 62%);
--alpha-white800: rgb(255 255 255 / 71%);
--alpha-white900: rgb(255 255 255 / 81%);
--alpha-white950: rgb(255 255 255 / 90%);
--slate50: #f8fafc;
--slate100: #f1f5f9;
--slate200: #e2e8f0;
--slate300: #cbd5e1;
--slate400: #94a3b8;
--slate500: #64748b;
--slate600: #475569;
--slate700: #334155;
--slate800: #1e293b;
--slate900: #0f172a;
--slate950: #020617;
--gray50: #f9fafb;
--gray100: #f3f4f6;
--gray200: #e5e7eb;
--gray300: #d1d5db;
--gray400: #9ca3af;
--gray500: #6b7280;
--gray600: #4b5563;
--gray700: #374151;
--gray800: #1f2937;
--gray900: #111827;
--gray950: #030712;
--zinc50: #fafafa;
--zinc100: #f4f4f5;
--zinc200: #e4e4e7;
--zinc300: #d4d4d8;
--zinc400: #a1a1aa;
--zinc500: #71717a;
--zinc600: #52525b;
--zinc700: #3f3f46;
--zinc800: #27272a;
--zinc900: #18181b;
--zinc950: #09090b;
--stone50: #fafaf9;
--stone100: #f5f5f4;
--stone200: #e7e5e4;
--stone300: #d6d3d1;
--stone400: #a8a29e;
--stone500: #78716c;
--stone600: #57534e;
--stone700: #44403c;
--stone800: #292524;
--stone900: #1c1917;
--stone950: #0c0a09;
--red50: #fef2f2;
--red100: #fee2e2;
--red200: #fecaca;
--red300: #fca5a5;
--red400: #f87171;
--red500: #ef4444;
--red600: #dc2626;
--red700: #b91c1c;
--red800: #991b1b;
--red900: #7f1d1d;
--red950: #450a0a;
--orange50: #fff7ed;
--orange100: #ffedd5;
--orange200: #fed7aa;
--orange300: #fdba74;
--orange400: #fb923c;
--orange500: #f97316;
--orange600: #ea580c;
--orange700: #c2410c;
--orange800: #9a3412;
--orange900: #7c2d12;
--orange950: #431407;
--amber50: #fffbeb;
--amber100: #fef3c7;
--amber200: #fde68a;
--amber300: #fcd34d;
--amber400: #fbbf24;
--amber500: #f59e0b;
--amber600: #d97706;
--amber700: #b45309;
--amber800: #92400e;
--amber900: #78350f;
--amber950: #451a03;
--yellow50: #fefce8;
--yellow100: #fef9c3;
--yellow200: #fef08a;
--yellow300: #fde047;
--yellow400: #facc15;
--yellow500: #eab308;
--yellow600: #ca8a04;
--yellow700: #a16207;
--yellow800: #854d0e;
--yellow900: #713f12;
--yellow950: #422006;
--lime50: #f7fee7;
--lime100: #ecfccb;
--lime200: #d9f99d;
--lime300: #bef264;
--lime400: #a3e635;
--lime500: #84cc16;
--lime600: #65a30d;
--lime700: #4d7c0f;
--lime800: #3f6212;
--lime900: #365314;
--lime950: #1a2e05;
--green50: #f0fdf4;
--green100: #dcfce7;
--green200: #bbf7d0;
--green300: #86efac;
--green400: #4ade80;
--green500: #22c55e;
--green600: #16a34a;
--green700: #15803d;
--green800: #166534;
--green900: #14532d;
--green950: #052e16;
--emerald50: #ecfdf5;
--emerald100: #d1fae5;
--emerald200: #a7f3d0;
--emerald300: #6ee7b7;
--emerald400: #34d399;
--emerald500: #10b981;
--emerald600: #059669;
--emerald700: #047857;
--emerald800: #065f46;
--emerald900: #064e3b;
--emerald950: #022c22;
--teal50: #f0fdfa;
--teal100: #ccfbf1;
--teal200: #99f6e4;
--teal300: #5eead4;
--teal400: #2dd4bf;
--teal500: #14b8a6;
--teal600: #0d9488;
--teal700: #0f766e;
--teal800: #115e59;
--teal900: #134e4a;
--teal950: #042f2e;
--cyan50: #ecfeff;
--cyan100: #cffafe;
--cyan200: #a5f3fc;
--cyan300: #67e8f9;
--cyan400: #22d3ee;
--cyan500: #06b6d4;
--cyan600: #0891b2;
--cyan700: #0e7490;
--cyan800: #155e75;
--cyan900: #164e63;
--cyan950: #083344;
--sky50: #f0f9ff;
--sky100: #e0f2fe;
--sky200: #bae6fd;
--sky300: #7dd3fc;
--sky400: #38bdf8;
--sky500: #0ea5e9;
--sky600: #0284c7;
--sky700: #0369a1;
--sky800: #075985;
--sky900: #0c4a6e;
--sky950: #082f49;
--blue50: #eff6ff;
--blue100: #dbeafe;
--blue200: #bfdbfe;
--blue300: #93c5fd;
--blue400: #60a5fa;
--blue500: #3b82f6;
--blue600: #2563eb;
--blue700: #1d4ed8;
--blue800: #1e40af;
--blue900: #1e3a8a;
--blue950: #172554;
--indigo50: #eef2ff;
--indigo100: #e0e7ff;
--indigo200: #c7d2fe;
--indigo300: #a5b4fc;
--indigo400: #818cf8;
--indigo500: #6366f1;
--indigo600: #4f46e5;
--indigo700: #4338ca;
--indigo800: #3730a3;
--indigo900: #312e81;
--indigo950: #1e1b4b;
--violet50: #f5f3ff;
--violet100: #ede9fe;
--violet200: #ddd6fe;
--violet300: #c4b5fd;
--violet400: #a78bfa;
--violet500: #8b5cf6;
--violet600: #7c3aed;
--violet700: #6d28d9;
--violet800: #5b21b6;
--violet900: #4c1d95;
--violet950: #2e1065;
--purple50: #faf5ff;
--purple100: #f3e8ff;
--purple200: #e9d5ff;
--purple300: #d8b4fe;
--purple400: #c084fc;
--purple500: #a855f7;
--purple600: #9333ea;
--purple700: #7e22ce;
--purple800: #6b21a8;
--purple900: #581c87;
--purple950: #3b0764;
--fuchsia50: #fdf4ff;
--fuchsia100: #fae8ff;
--fuchsia200: #f5d0fe;
--fuchsia300: #f0abfc;
--fuchsia400: #e879f9;
--fuchsia500: #d946ef;
--fuchsia600: #c026d3;
--fuchsia700: #a21caf;
--fuchsia800: #86198f;
--fuchsia900: #701a75;
--fuchsia950: #4a044e;
--pink50: #fdf2f8;
--pink100: #fce7f3;
--pink200: #fbcfe8;
--pink300: #f9a8d4;
--pink400: #f472b6;
--pink500: #ec4899;
--pink600: #db2777;
--pink700: #be185d;
--pink800: #9d174d;
--pink900: #831843;
--pink950: #500724;
--rose50: #fff1f2;
--rose100: #ffe4e6;
--rose200: #fecdd3;
--rose300: #fda4af;
--rose400: #fb7185;
--rose500: #f43f5e;
--rose600: #e11d48;
--rose700: #be123c;
--rose800: #9f1239;
--rose900: #881337;
--rose950: #4c0519;
/* semantics */
--fg-neutral-primary: var(--base-black);
--fg-neutral-secondary: var(--neutral600);
--fg-neutral-tertiary: var(--neutral500);
--fg-neutral-inverse: var(--base-white);
--fg-neutral-static: var(--base-white);
--fg-tint-red: var(--red600);
--fg-tint-orange: var(--amber600);
--fg-tint-green: var(--green600);
--fg-tint-blue: var(--blue600);
--border-neutral-primary: var(--neutral950);
--border-neutral-secondary: var(--neutral500);
--border-neutral-tertiary: var(--neutral300);
--border-neutral-transparent: var(--base-transparent);
--border-tint-red: var(--red600);
--border-tint-orange: var(--amber600);
--border-tint-green: var(--green600);
--border-tint-blue: var(--blue600);
--bg-primary: var(--base-white);
--bg-secondary: var(--neutral100);
--bg-tertiary: var(--neutral200);
--bg-dim: var(--alpha-black600);
--bg-neutral-primary: var(--base-white);
--bg-neutral-secondary: var(--neutral300);
--bg-neutral-tertiary: var(--neutral200);
--bg-neutral-inverse: var(--neutral950);
--bg-neutral-hover: var(--alpha-black50);
--bg-neutral-transparent: var(--base-transparent);
--bg-tint-red-bold: var(--red600);
--bg-tint-red-subtle: var(--red100);
--bg-tint-red-hover: var(--red400);
--bg-tint-orange-bold: var(--amber600);
--bg-tint-orange-subtle: var(--amber100);
--bg-tint-orange-hover: var(--amber400);
--bg-tint-green-bold: var(--green600);
--bg-tint-green-subtle: var(--green100);
--bg-tint-green-hover: var(--green400);
--bg-tint-blue-bold: var(--blue600);
--bg-tint-blue-subtle: var(--blue100);
--bg-tint-blue-hover: var(--blue400);
}
.dark {
--fg-neutral-primary: var(--base-white);
--fg-neutral-secondary: var(--neutral300);
--fg-neutral-tertiary: var(--neutral400);
--fg-neutral-inverse: var(--base-black);
--fg-tint-red: var(--red500);
--fg-tint-orange: var(--amber500);
--fg-tint-green: var(--green500);
--fg-tint-blue: var(--blue500);
--border-neutral-primary: var(--neutral50);
--border-neutral-secondary: var(--neutral500);
--border-neutral-tertiary: var(--neutral600);
--border-tint-red: var(--red500);
--border-tint-orange: var(--amber500);
--border-tint-green: var(--green500);
--border-tint-blue: var(--blue500);
--bg-primary: var(--neutral950);
--bg-secondary: var(--neutral900);
--bg-tertiary: var(--neutral800);
--bg-dim: var(--alpha-black600);
--bg-neutral-primary: var(--neutral800);
--bg-neutral-secondary: var(--neutral700);
--bg-neutral-tertiary: var(--neutral600);
--bg-neutral-inverse: var(--neutral50);
--bg-neutral-hover: var(--alpha-white50);
}
}
@@ -0,0 +1,3 @@
module.exports = {
plugins: [require('postcss-import'), require('tailwindcss/nesting')],
};
@@ -0,0 +1,105 @@
/**
* 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.
*/
.text {
&-small {
&-normal {
font-weight: 400;
font-size: 0.75rem;
line-height: 1.125rem;
}
&-strong {
font-weight: 600;
font-size: 0.75rem;
line-height: 1.125rem;
}
}
&-base {
&-normal {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.375rem;
}
&-strong {
font-weight: 600;
font-size: 0.875rem;
line-height: 1.375rem;
}
}
&-large {
&-normal {
font-weight: 400;
font-size: 1rem;
line-height: 1.5rem;
}
&-strong {
font-weight: 600;
font-size: 1rem;
line-height: 1.5rem;
}
}
&-xlarge {
&-normal {
font-weight: 400;
font-size: 1.25rem;
line-height: 2rem;
}
&-strong {
font-weight: 600;
font-size: 1.25rem;
line-height: 2rem;
}
}
&-title {
&-h1 {
font-weight: 600;
font-size: 2.375rem;
line-height: 2.875rem;
}
&-h2 {
font-weight: 600;
font-size: 1.875rem;
line-height: 2.375rem;
}
&-h3 {
font-weight: 600;
font-size: 1.5rem;
line-height: 2rem;
}
&-h4 {
font-weight: 600;
font-size: 1.25rem;
line-height: 1.75rem;
}
&-h5 {
font-weight: 600;
font-size: 1rem;
line-height: 1.5rem;
}
}
}
@@ -0,0 +1,10 @@
module.exports = {
0: "0",
4: "calc(var(--rounded-base, 0px) + 0.25rem)",
6: "calc(var(--rounded-base, 0px) + 0.375rem)",
8: "calc(var(--rounded-base, 0px) + 0.5rem)",
12: "calc(var(--rounded-base, 0px) + 0.75rem)",
16: "calc(var(--rounded-base, 0px) + 1rem)",
24: "calc(var(--rounded-base, 0px) + 1.5rem)",
full: "62.4375rem",
};
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
sm: "0px 1px 2px 0px rgba(0, 0, 0, 0.05)",
default:
"0px 1px 3px 0px rgba(0, 0, 0, 0.10), 0px 1px 2px 0px rgba(0, 0, 0, 0.06)",
md: "0px 4px 6px -1px rgba(0, 0, 0, 0.10), 0px 2px 4px -1px rgba(0, 0, 0, 0.06)",
lg: "0px 10px 15px -3px rgba(0, 0, 0, 0.10), 0px 4px 6px -2px rgba(0, 0, 0, 0.05)",
xl: "0px 20px 25px -5px rgba(0, 0, 0, 0.10), 0px 10px 10px -5px rgba(0, 0, 0, 0.04)",
"2xl": "0px 25px 50px -12px rgba(0, 0, 0, 0.25)",
inner: "0px 2px 4px 0px rgba(0, 0, 0, 0.06) inset",
none: "none",
};
@@ -0,0 +1,75 @@
const fs = require("fs/promises");
const path = require("path");
const postcss = require("postcss");
const postcssJs = require("postcss-js");
const camelToKebab = (str) => {
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
};
const transformKeys = (obj) => {
if (typeof obj !== "object" || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(transformKeys);
}
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
return [
camelToKebab(key),
typeof value === "object" ? transformKeys(value) : value,
];
}),
);
};
const replaceApplyTrueWithEmptyObject = (obj) => {
const stack = [obj];
while (stack.length > 0) {
const currentObj = stack.pop();
for (const [key, value] of Object.entries(currentObj)) {
if (typeof value === "object" && value !== null) {
stack.push(value);
}
if (key.startsWith("@apply") && value === true) {
currentObj[key] = {};
}
}
}
};
async function convertCssToJs(type = "base") {
try {
const inputPath = path.resolve(process.cwd(), `dist/${type}.css`);
const outputPath = path.resolve(process.cwd(), `dist/${type}.js`);
// Read the CSS file
const cssContent = await fs.readFile(inputPath, "utf-8");
// Parse the CSS and convert to JS object
const root = postcss.parse(cssContent);
const jsContent = postcssJs.objectify(root);
const kebabCaseContent = transformKeys(jsContent);
replaceApplyTrueWithEmptyObject(kebabCaseContent);
// Convert JS object to string and format as a module
const jsOutput = `module.exports = ${JSON.stringify(kebabCaseContent, null, 2)};\n`;
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
await fs.mkdir(outputDir, { recursive: true });
// Write the JS file
await fs.writeFile(outputPath, jsOutput);
// console.log(`Successfully converted ${inputPath} to ${outputPath}`);
} catch (error) {
console.error(`Error generating JS from CSS: ${error.message}`);
throw error;
}
}
module.exports = convertCssToJs;
@@ -0,0 +1,58 @@
const fs = require("fs").promises;
const path = require("path");
const postcss = require("postcss");
const glob = require("glob");
/**
* PostCSS를 사용하여 CSS 파일들을 처리하는 함수
* 명령어 `postcss --config src/components src/components/*.css --base src --dir dist`와 동일한 기능을 수행합니다.
*/
async function generateCss(type) {
try {
// 설정 파일 로드
const configPath = path.resolve(process.cwd(), `src/${type}`);
const configFile = require(path.join(configPath, "postcss.config.js"));
// 플러그인 초기화
const plugins = configFile.plugins || [];
// PostCSS 프로세서 생성
const processor = postcss(plugins);
const cssFiles = glob.sync(`src/${type}/*.css`);
// 기본 경로
const baseDir = path.resolve(process.cwd(), "src");
const outputDir = path.resolve(process.cwd(), "dist");
// 디렉토리가 없으면 생성
fs.mkdir(outputDir, { recursive: true });
// 각 파일 처리
for (const file of cssFiles) {
const css = await fs.readFile(file, "utf8");
const relativePath = path.relative(baseDir, file);
const outputPath = path.join(outputDir, relativePath);
// 출력 디렉토리 확인
const outputFileDir = path.dirname(outputPath);
fs.mkdir(outputFileDir, { recursive: true });
// PostCSS 처리
const result = await processor.process(css, {
from: file,
to: outputPath,
map: { inline: false },
});
// 처리된 CSS 저장
await fs.writeFile(outputPath, result.css);
// console.log(`Generated: ${outputPath}`);
}
// console.log("CSS generation completed successfully.");
} catch (error) {
console.error("CSS generation failed:", error);
process.exit(1);
}
}
module.exports = generateCss;
@@ -0,0 +1,17 @@
const log = {
info: (message) =>
console.log(
`\x1b[36m[INFO]\x1b[0m ${new Date().toLocaleTimeString()} - ${message}`,
),
success: (message) =>
console.log(
`\x1b[32m[SUCCESS]\x1b[0m ${new Date().toLocaleTimeString()} - ${message}`,
),
error: (error) =>
console.error(
`\x1b[31m[ERROR]\x1b[0m ${new Date().toLocaleTimeString()} - ${error}`,
),
step: (step, total, message) =>
console.log(`\x1b[35m[${step}/${total}]\x1b[0m \x1b[33m${message}\x1b[0m`),
};
module.exports = log;
@@ -0,0 +1,45 @@
const fs = require("fs").promises;
const path = require("path");
const glob = require("glob");
async function mergeCss(type = "base") {
try {
const sourcePath = path.resolve(process.cwd(), `dist/${type}`);
const outputPath = path.resolve(process.cwd(), `dist/${type}.css`);
// 출력 디렉토리 확인 및 생성
const outputDir = path.dirname(outputPath);
await fs.mkdir(outputDir, { recursive: true });
// CSS 파일 검색
const cssFiles = glob.sync(path.join(sourcePath, "*.css"));
if (cssFiles.length === 0) {
console.log(`No CSS files found in ${sourcePath} with pattern *.css`);
return;
}
// 모든 CSS 내용을 담을 배열
const cssContents = [];
// 각 파일의 내용 읽기
for (const file of cssFiles) {
const content = await fs.readFile(file, "utf8");
cssContents.push(`/* ${path.basename(file)} */`);
cssContents.push(content);
}
// CSS 내용 병합
const mergedCss = cssContents.join("\n\n");
// 병합된 CSS 저장
await fs.writeFile(outputPath, mergedCss);
// console.log(`Merged CSS saved to: ${outputPath}`);
} catch (error) {
console.error("CSS merging failed:", error);
process.exit(1);
}
}
module.exports = mergeCss;
+52
View File
@@ -0,0 +1,52 @@
const generateCss = require("./build-functions/generate-css");
const mergeCss = require("./build-functions/merge-css");
const convertCssToJs = require("./build-functions/convert-css-to-js");
const log = require("./build-functions/log");
async function executeCommands() {
try {
const totalSteps = 9;
let currentStep = 0;
log.info("Starting CSS build process...");
// Base CSS processing
log.step(++currentStep, totalSteps, "Generating base CSS files");
await generateCss("base");
log.step(++currentStep, totalSteps, "Merging base CSS files");
await mergeCss("base");
log.step(++currentStep, totalSteps, "Converting base CSS to JS");
await convertCssToJs("base");
log.success("Base CSS processing completed");
// Utilities CSS processing
log.step(++currentStep, totalSteps, "Generating utilities CSS files");
await generateCss("utilities");
log.step(++currentStep, totalSteps, "Merging utilities CSS files");
await mergeCss("utilities");
log.step(++currentStep, totalSteps, "Converting utilities CSS to JS");
await convertCssToJs("utilities");
log.success("Utilities CSS processing completed");
// Components CSS processing
log.step(++currentStep, totalSteps, "Generating components CSS files");
await generateCss("components");
log.step(++currentStep, totalSteps, "Merging components CSS files");
await mergeCss("components");
log.step(++currentStep, totalSteps, "Converting components CSS to JS");
await convertCssToJs("components");
log.success("Components CSS processing completed");
log.success("All CSS files built successfully!");
} catch (error) {
log.error(`Error executing commands: ${error.message}`);
}
}
executeCommands();
@@ -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.
*/
.accordion {
--accordion-fg: var(--fg-neutral-primary);
--accordion-border: var(--border-neutral-tertiary);
--accordion-bg: var(--bg-primary);
@apply h-fit overflow-hidden bg-[var(--accordion-bg)] text-[var(--accordion-fg)];
}
.accordion-border {
@apply border border-[var(--accordion-border)] [&>div:last-of-type]:border-b-0;
}
.accordion-item {
@apply border-neutral-transparent;
}
.accordion-item-border {
@apply border-b border-[var(--accordion-border)];
}
.accordion-trigger {
@apply text-base-strong flex w-full items-center justify-between p-4 text-left transition-all [&>svg]:shrink-0 [&>svg]:transition-transform [&>svg]:duration-75 [&[data-state=open]>svg]:rotate-180;
}
.accordion-trigger-align-left {
@apply justify-start [&>svg:first-of-type]:mr-2;
}
.accordion-content-box {
@apply data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down text-base-normal overflow-hidden transition-all;
}
.accordion-content {
@apply p-4;
}
@@ -0,0 +1,69 @@
/**
* 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.
*/
.alert {
--alert-fg: var(--fg-neutral-primary);
--alert-description-fg: var(--fg-neutral-secondary);
--alert-fg-warning: var(--fg-tint-orange);
--alert-fg-success: var(--fg-tint-green);
--alert-fg-error: var(--fg-tint-red);
--alert-fg-informative: var(--fg-tint-blue);
--alert-border: var(--border-neutral-tertiary);
--alert-border-warning: var(--border-tint-orange);
--alert-border-success: var(--border-tint-green);
--alert-border-error: var(--border-tint-red);
--alert-border-informative: var(--border-tint-blue);
--alert-bg: var(--bg-neutral-primary);
@apply relative inline-flex min-w-[356px] items-center space-x-2 border bg-[var(--alert-bg)] px-5 py-4;
/* default */
@apply alert-radius-medium alert-default;
}
.alert-icon {
@apply relative mb-auto box-content w-5 flex-shrink-0 p-1;
/* default */
@apply alert-icon-default;
}
.alert-loader {
@apply flex scale-100;
}
.alert-content {
@apply flex flex-1 items-center space-x-2;
}
.alert-text-container {
@apply flex flex-1 flex-col items-start justify-center;
}
.alert-title {
@apply text-title-h5 flex flex-1 flex-col text-[var(--alert-fg)];
}
.alert-description {
@apply text-base-normal text-[var(--alert-description-fg)];
}
.alert-button {
@apply flex-shrink-0;
}
.alert-close {
@apply flex-shrink-0;
}
@@ -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.
*/
.badge {
--badge-bold-fg: var(--fg-neutral-inverse);
--badge-bold-fg-blue: var(--fg-neutral-inverse);
--badge-bold-fg-orange: var(--fg-neutral-inverse);
--badge-bold-fg-red: var(--fg-neutral-inverse);
--badge-bold-fg-green: var(--fg-neutral-inverse);
--badge-bold-border: var(--border-neutral-transparent);
--badge-bold-border-blue: var(--border-neutral-transparent);
--badge-bold-border-orange: var(--border-neutral-transparent);
--badge-bold-border-red: var(--border-neutral-transparent);
--badge-bold-border-green: var(--border-neutral-transparent);
--badge-bold-bg: var(--bg-neutral-inverse);
--badge-bold-bg-blue: var(--bg-tint-blue-bold);
--badge-bold-bg-orange: var(--bg-tint-orange-bold);
--badge-bold-bg-red: var(--bg-tint-red-bold);
--badge-bold-bg-green: var(--bg-tint-green-bold);
--badge-subtle-fg: var(--fg-neutral-primary);
--badge-subtle-fg-blue: var(--fg-tint-blue);
--badge-subtle-fg-orange: var(--fg-tint-orange);
--badge-subtle-fg-red: var(--fg-tint-red);
--badge-subtle-fg-green: var(--fg-tint-green);
--badge-subtle-border: var(--border-neutral-transparent);
--badge-subtle-border-blue: var(--border-neutral-transparent);
--badge-subtle-border-orange: var(--border-neutral-transparent);
--badge-subtle-border-red: var(--border-neutral-transparent);
--badge-subtle-border-green: var(--border-neutral-transparent);
--badge-subtle-bg: var(--bg-neutral-tertiary);
--badge-subtle-bg-blue: var(--bg-tint-blue-subtle);
--badge-subtle-bg-orange: var(--bg-tint-orange-subtle);
--badge-subtle-bg-red: var(--bg-tint-red-subtle);
--badge-subtle-bg-green: var(--bg-tint-green-subtle);
--badge-outline-fg: var(--fg-neutral-primary);
--badge-outline-fg-blue: var(--fg-tint-blue);
--badge-outline-fg-orange: var(--fg-tint-orange);
--badge-outline-fg-red: var(--fg-tint-red);
--badge-outline-fg-green: var(--fg-tint-green);
--badge-outline-border: var(--border-neutral-tertiary);
--badge-outline-border-blue: var(--border-tint-blue);
--badge-outline-border-orange: var(--border-tint-orange);
--badge-outline-border-red: var(--border-tint-red);
--badge-outline-border-green: var(--border-tint-green);
--badge-outline-bg: var(--bg-neutral-primary);
--badge-outline-bg-blue: var(--bg-neutral-primary);
--badge-outline-bg-orange: var(--bg-neutral-primary);
--badge-outline-bg-red: var(--bg-neutral-primary);
--badge-outline-bg-green: var(--bg-neutral-primary);
@apply text-small-strong inline-flex whitespace-nowrap px-2 py-0.5;
/* default */
@apply badge-bold-default badge-radius-medium;
}
@@ -0,0 +1,46 @@
/**
* 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.
*/
.button {
--button-primary-fg: var(--fg-neutral-inverse);
--button-secondary-fg: var(--fg-neutral-primary);
--button-destructive-fg: var(--fg-neutral-inverse);
--button-ghost-fg: var(--fg-neutral-primary);
--button-outline-fg: var(--tint, var(--fg-neutral-primary));
--button-primary-border: var(--border-neutral-transparent);
--button-secondary-border: var(--border-neutral-transparent);
--button-destructive-border: var(--border-neutral-transparent);
--button-ghost-border: var(--border-neutral-transparent);
--button-outline-border: var(--border-neutral-tertiary);
--button-primary-bg: var(--tint, var(--bg-neutral-inverse));
--button-secondary-bg: var(--bg-neutral-tertiary);
--button-destructive-bg: var(--bg-tint-red-bold);
--button-ghost-bg: var(--bg-neutral-transparent);
--button-ghost-bg-hover: var(--bg-neutral-secondary);
--button-outline-bg: var(--bg-neutral-transparent);
--button-outline-bg-hover: var(--tint-subtle, var(--bg-neutral-secondary));
@apply relative inline-flex h-fit cursor-pointer items-center justify-center transition-opacity disabled:cursor-not-allowed disabled:opacity-50 [&:not(:disabled)]:hover:opacity-80;
/* default */
@apply button-small button-radius-medium button-primary;
}
.button-loading {
@apply absolute-center pointer-events-none flex shadow-none;
/* default */
@apply button-loading-primary;
}
@@ -0,0 +1,107 @@
/**
* 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.
*/
.calendar {
--calendar-fg-default: var(--fg-neutral-primary);
--calendar-fg-pressed: var(--fg-neutral-inverse);
--calendar-week-fg: var(--fg-neutral-tertiary);
--calendar-border: var(--border-neutral-tertiary);
--calendar-bg: var(--bg-neutral-primary);
--calendar-bg-pressed: var(--tint, var(--bg-neutral-inverse));
--calendar-bg-hover: var(--bg-neutral-tertiary);
--calendar-bg-range: var(--bg-neutral-tertiary);
@apply rounded-8 relative box-content w-fit border border-[var(--calendar-border)] bg-[var(--calendar-bg)] p-2 shadow-sm;
}
.calendar-months {
@apply flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0;
}
.calendar-month {
@apply min-w-[15.75rem] space-y-4;
}
.calendar-caption {
@apply relative flex items-center justify-center pt-1;
}
.calendar-caption-label {
@apply text-base-strong text-[var(--calendar-fg-default)];
}
.calendar-nav {
@apply flex items-center;
}
.calendar-nav-button {
@apply flex p-2.5 hover:opacity-50;
}
.calendar-nav-button-previous {
@apply absolute left-0;
}
.calendar-nav-button-next {
@apply absolute right-0;
}
.calendar-table {
@apply !mt-4 w-full border-collapse;
}
.calendar-head-row {
@apply flex;
}
.calendar-head-cell {
@apply text-small-normal flex-1 px-2 py-0.5 font-normal text-[var(--calendar-week-fg)];
}
.calendar-row {
@apply mt-0.5 flex w-full;
}
.calendar-cell {
@apply [&:has([aria-selected].day-range-end)]:rounded-r-8 first:[&:has([aria-selected])]:rounded-l-8 last:[&:has([aria-selected])]:rounded-r-8 [&:has(.calendar-day-range-start)]:rounded-l-8 [&:has(.calendar-day-range-end)]:rounded-r-8 text-base-normal relative aspect-square flex-1 p-0 text-center focus-within:relative focus-within:z-20 [&:has(.calendar-day-range-end)]:bg-[var(--calendar-bg-hover)] [&:has(.calendar-day-range-start)]:bg-[var(--calendar-bg-hover)];
}
.calendar-day {
@apply rounded-8 text-base-normal relative flex h-full w-full items-center justify-center overflow-hidden p-0 transition-colors aria-selected:opacity-100 [&:not(.calendar-day-selected)]:hover:bg-[var(--calendar-bg-hover)];
}
.calendar-day-selected:not(.calendar-day-outside) {
@apply bg-[var(--calendar-bg-pressed)] text-[--calendar-fg-pressed];
}
.calendar-day-today {
@apply before:absolute before:left-1/2 before:top-0 before:flex before:h-1 before:w-1 before:-translate-x-1/2 before:rounded-full before:bg-[var(--calendar-bg-pressed)];
}
.calendar-day-outside {
@apply pointer-events-none !opacity-50;
}
.calendar-day-disabled {
@apply opacity-50;
}
.calendar-day-range-middle {
@apply rounded-0 aria-selected:bg-[var(--calendar-bg-hover)] aria-selected:text-[var(--calendar-fg-default)];
}
.calendar-day-hidden {
@apply invisible;
}
@@ -0,0 +1,29 @@
/**
* 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.
*/
.caption {
--caption-fg: var(--fg-neutral-tertiary);
--caption-fg-success: var(--fg-tint-green);
--caption-fg-error: var(--fg-tint-red);
@apply text-base-normal flex items-center gap-1;
/* default */
@apply caption-default;
}
.caption-icon {
@apply flex-shrink-0;
}
@@ -0,0 +1,41 @@
/**
* 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.
*/
.checkbox {
--checkbox-fg: var(--fg-neutral-primary);
--checkbox-fg-checked: var(--fg-neutral-primary);
--checkbox-fg-indeterminate: var(--fg-neutral-primary);
--checkbox-icon: var(--fg-neutral-inverse);
--checkbox-icon-checked: var(--fg-neutral-inverse);
--checkbox-border-default: var(--border-neutral-secondary);
--checkbox-bg: var(--bg-neutral-primary);
--checkbox-bg-checked: var(--tint, var(--bg-neutral-inverse));
@apply inline-flex cursor-pointer items-center text-[var(--checkbox-fg)] transition-opacity disabled:cursor-not-allowed disabled:opacity-50 [&:not(:disabled)]:hover:opacity-80;
/* default */
@apply checkbox-small;
}
.check {
@apply before:rounded-4 relative mr-2 flex items-center justify-center text-[var(--checkbox-icon)] before:absolute before:inset-0 before:flex before:border before:border-[var(--checkbox-border-default)];
/* default */
@apply check-small;
}
.checkbox-icon {
@apply rounded-4 absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[var(--checkbox-bg-checked)] text-[var(--checkbox-icon-checked)];
}
@@ -0,0 +1,76 @@
/**
* 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.
*/
.combobox-content {
--combobox-fg: var(--fg-neutral-primary);
--combobox-fg-empty: var(--fg-neutral-secondary);
--combobox-fg-placeholder: var(--fg-neutral-tertiary);
--combobox-shortcut-fg: var(--fg-neutral-tertiary);
--combobox-bg: var(--bg-neutral-primary);
--combobox-bg-hover: var(--bg-neutral-tertiary);
--combobox-border: var(--border-neutral-tertiary);
@apply flex h-full min-w-[--radix-popover-trigger-width] flex-col overflow-hidden p-1 text-[var(--combobox-fg)];
}
.combobox-empty {
@apply text-base-normal flex items-center justify-center p-6 text-center text-[var(--combobox-fg-empty)];
}
.combobox-item {
@apply rounded-6 text-base-normal relative flex cursor-pointer select-none items-center p-2 outline-none transition-colors hover:bg-[var(--combobox-bg-hover)] data-[selected=true]:bg-[var(--combobox-bg-hover)];
}
.combobox-item ~ .combobox-item {
@apply mt-1;
}
.combobox-check {
@apply mr-2;
}
.combobox-caption {
@apply text-small-normal ml-auto tracking-widest text-[var(--combobox-shortcut-fg)];
}
.combobox-separator {
@apply -mx-1 my-1 h-px bg-[var(--combobox-border)];
}
.combobox-group {
@apply [&_[cmdk-group-heading]]:text-base-strong overflow-hidden p-[1px] text-[var(--combobox-fg)] [&_[cmdk-group-heading]]:p-2;
}
.combobox-list {
@apply pt-1;
}
.combobox-input-box {
@apply relative flex items-center justify-center border-b border-b-[var(--combobox-border)] bg-[var(--combobox-bg)] [&>svg]:ml-2 [&>svg]:shrink-0 [&>svg]:opacity-50;
}
.combobox-input {
@apply text-base-normal min-w-0 flex-1 bg-[transparent] p-2 text-[var(--combobox-fg)] outline-none placeholder:text-[var(--combobox-fg-placeholder)];
}
.combobox-trigger {
--combobox-border-error: var(--border-tint-red);
@apply [&>svg:last-of-type]:!ml-auto;
}
.combobox-trigger[aria-invalid='true'] {
@apply !border-2 !border-[var(--combobox-border-error)];
}
@@ -0,0 +1,65 @@
/**
* 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.
*/
.dialog {
--dialog-bg: var(--bg-neutral-primary);
--dialog-border: var(--border-neutral-tertiary);
@apply data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-[var(--alpha-black800)];
}
.dialog-content {
--dialog-bg: var(--bg-neutral-primary);
--dialog-border: var(--border-neutral-tertiary);
@apply data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-sm translate-x-[-50%] translate-y-[-50%] gap-y-5 space-y-2 border border-[var(--dialog-border)] bg-[var(--dialog-bg)] p-5 duration-200;
/* default */
@apply dialog-content-radius-medium;
}
.dialog-header {
@apply flex flex-col gap-1 text-left;
}
.dialog-icon {
@apply p-1;
/* default */
@apply dialog-icon-default;
}
.dialog-title {
@apply text-title-h4 text-[var(--fg-neutral-primary)];
}
.dialog-description {
@apply text-base-normal text-[var(--fg-neutral-secondary)];
}
.dialog-body {
@apply space-y-3;
}
.dialog-footer {
@apply flex flex-wrap gap-2;
/* default */
@apply dialog-footer-right;
}
.dialog-close {
@apply !absolute right-2 top-2 !m-0;
}
@@ -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.
*/
.divider {
--divider-border-bold: var(--border-neutral-primary);
--divider-border-subtle: var(--border-neutral-tertiary);
@apply shrink-0;
/* default */
@apply [&:not(.divider-vertical)]:divider-horizontal divider-bold;
}
@@ -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.
*/
.dropdown-content {
--dropdown-fg: var(--fg-neutral-primary);
--dropdown-fg-pressed: var(--tint, var(--fg-neutral-primary));
--dropdown-fg-empty: var(--fg-neutral-secondary);
--dropdown-shortcut-fg: var(--fg-neutral-tertiary);
--dropdown-bg: var(--bg-neutral-primary);
--dropdown-bg-hover: var(--bg-neutral-tertiary);
--dropdown-bg-pressed: var(--bg-neutral-tertiary);
--dropdown-border: var(--border-neutral-tertiary);
@apply data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95;
@apply data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95;
@apply data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2;
@apply rounded-8 z-50 min-w-[--radix-dropdown-menu-trigger-width] overflow-hidden border p-1 shadow-md;
@apply border-[var(--dropdown-border)] bg-[var(--dropdown-bg)] text-[var(--dropdown-fg)];
}
.dropdown-sub-trigger {
@apply rounded-6 text-base-normal flex cursor-pointer select-none items-center p-2 outline-none focus:bg-[var(--dropdown-bg-hover)] data-[state=open]:bg-[var(--dropdown-bg-pressed)] data-[state=open]:text-[var(--dropdown-fg-pressed)];
}
.dropdown-sub-content {
--dropdown-fg: var(--fg-neutral-primary);
--dropdown-fg-pressed: var(--tint, var(--fg-neutral-primary));
--dropdown-fg-empty: var(--fg-neutral-secondary);
--dropdown-shortcut-fg: var(--fg-neutral-tertiary);
--dropdown-bg: var(--bg-neutral-primary);
--dropdown-bg-hover: var(--bg-neutral-tertiary);
--dropdown-bg-pressed: var(--bg-neutral-tertiary);
--dropdown-border: var(--border-neutral-tertiary);
@apply data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95;
@apply data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95;
@apply data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2;
@apply data-[side=bottom]:mt-1 data-[side=left]:mr-1 data-[side=right]:ml-1 data-[side=top]:mb-1;
@apply rounded-8 z-50 min-w-[--radix-dropdown-menu-trigger-width] overflow-hidden border p-1 shadow-lg;
@apply border-[var(--dropdown-border)] bg-[var(--dropdown-bg)] text-[var(--dropdown-fg)];
}
.dropdown-item {
@apply rounded-6 text-base-normal relative flex cursor-pointer select-none items-center p-2 outline-none transition-colors focus:bg-[var(--dropdown-bg-hover)] data-[disabled]:pointer-events-none data-[state=on]:bg-[var(--dropdown-bg-pressed)] data-[state=on]:text-[var(--dropdown-fg-pressed)] data-[disabled]:opacity-50;
@apply [&>svg]:h-4 [&>svg]:w-4;
}
.dropdown-checkbox {
@apply rounded-8 text-base-normal relative flex cursor-pointer select-none items-center py-2 pl-8 outline-none transition-colors focus:bg-[var(--dropdown-bg-hover)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
}
.dropdown-checkbox-icon {
@apply absolute left-2 flex h-4 w-4 items-center justify-center;
}
.dropdown-radio {
@apply rounded-8 text-base-normal relative flex cursor-pointer select-none items-center py-2 pl-8 outline-none transition-colors focus:bg-[var(--dropdown-bg-hover)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
}
.dropdown-radio-icon {
@apply absolute left-3.5 flex h-2 w-2 items-center justify-center;
}
.dropdown-item ~ .dropdown-item {
@apply mt-1;
}
.dropdown-label {
@apply text-base-strong space-x-2 p-2;
}
.dropdown-caption {
@apply text-small-normal ml-auto tracking-widest text-[var(--dropdown-shortcut-fg)];
}
.dropdown-separator {
@apply -mx-1 my-1 h-px bg-[var(--dropdown-border)];
}
.dropdown-empty {
@apply text-base-normal flex items-center justify-center p-6 text-[var(--dropdown-fg-empty)];
}
@@ -0,0 +1,22 @@
/**
* 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.
*/
.icon {
@apply pointer-events-none inline-flex text-inherit;
}
.icon-clickable {
@apply pointer-events-auto cursor-pointer;
}
@@ -0,0 +1,74 @@
/**
* 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.
*/
.input-field {
--input-fg: var(--fg-neutral-tertiary);
--input-fg-typing: var(--fg-neutral-primary);
--input-fg-filled: var(--fg-neutral-primary);
--input-border: var(--border-neutral-tertiary);
--input-border-typing: var(--tint, var(--border-neutral-primary));
--input-border-filled: var(--border-neutral-tertiary);
--input-border-error: var(--border-tint-red);
--input-bg: var(--bg-neutral-primary);
--input-bg-typing: var(--bg-neutral-tertiary);
--input-bg-filled: var(--bg-neutral-primary);
@apply flex flex-col gap-1.5;
}
.input-box {
@apply relative;
@apply [&:has(.input-button)>.input-large]:!pr-12 [&:has(.input-button)>.input-medium]:!pr-10 [&:has(.input-button)>.input-small]:!pr-8;
@apply [&:has(.input-icon)>.input-large]:!pl-12 [&:has(.input-icon)>.input-medium]:!pl-10 [&:has(.input-icon)>.input-small]:!pl-8;
}
.input {
@apply text-[var(--input-fg-filled)] placeholder:text-[var(--input-fg)] placeholder-shown:text-[var(--input-fg)] focus-visible:text-[var(--input-fg-typing)];
@apply border border-[var(--input-border-filled)] placeholder-shown:border placeholder-shown:border-[var(--input-border)] focus-visible:border-2 focus-visible:border-[var(--input-border-typing)];
@apply bg-[var(--input-bg-filled)] placeholder-shown:bg-[var(--input-bg)] focus-visible:bg-[var(--input-bg-typing)];
@apply w-full outline-none;
@apply disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-search-cancel-button]:hidden;
/* default */
@apply input-small input-radius-medium;
}
.input[aria-invalid='true'] {
@apply !border-2 !border-[var(--input-border-error)] !bg-[var(--input-bg-error)];
}
.input-button {
@apply absolute-y-center flex flex-shrink-0 items-center justify-center disabled:cursor-not-allowed disabled:opacity-50;
/* default */
@apply input-button-small;
}
.input-button.show-only-on-focus-and-has-value {
@apply hidden;
}
.input-box:focus-within
.input:not(:placeholder-shown)
~ .input-button.show-only-on-focus-and-has-value {
@apply flex;
}
.input-icon {
@apply absolute-y-center flex;
/* default */
@apply input-icon-small;
}
@@ -0,0 +1,21 @@
/**
* 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.
*/
.label {
--label-fg: var(--fg-neutral-primary);
--label-fg-error: var(--fg-tint-red);
@apply text-base-normal font-normal text-[var(--label-fg)] data-[error=true]:text-[var(--label-fg-error)];
}
@@ -0,0 +1,42 @@
/**
* 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.
*/
.menu {
--menu-fg: var(--fg-neutral-tertiary);
--menu-fg-hover: var(--fg-neutral-tertiary);
--menu-fg-pressed: var(--fg-neutral-primary);
--menu-bg-hover: var(--bg-neutral-tertiary);
--menu-bg-pressed: var(--bg-neutral-tertiary);
@apply rounded-8 inline-flex px-2 py-3;
/* default */
@apply [&:not(.menu-vertical)]:menu-horizontal;
}
.menu-item {
@apply rounded-8 relative flex w-full cursor-pointer select-none items-center gap-2 text-left text-[var(--menu-fg)] outline-none hover:bg-[var(--menu-bg-hover)] hover:text-[var(--menu-fg-hover)] hover:opacity-100 data-[disabled]:pointer-events-none data-[state=on]:bg-[var(--menu-bg-pressed)] data-[state=open]:!bg-[var(--menu-bg-pressed)] data-[state=on]:text-[var(--menu-fg-pressed)] data-[state=open]:text-[var(--menu-fg-pressed)] data-[disabled]:opacity-50 [&>svg]:flex-shrink-0;
/* default */
@apply menu-item-small;
}
.menu-dropdown-item {
@apply !p-0;
}
.menu-dropdown-item ~ .menu-dropdown-item {
@apply mt-1;
}
@@ -0,0 +1,39 @@
/**
* 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.
*/
/* todo remove */
.navbar {
@apply border-b-neutral-tertiary bg-neutral-primary flex h-14 items-center border-b px-6 py-2.5;
}
.navbar-menu {
@apply !p-0;
}
.navbar-logo {
@apply flex p-1.5;
}
.navbar-divider {
@apply mx-1;
}
.navbar-dropdown {
@apply ml-auto;
}
.navbar-dropdown ~ .navbar-button {
@apply ml-3;
}
@@ -0,0 +1,26 @@
/**
* 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.
*/
.pagination {
@apply mx-auto flex w-full justify-center;
}
.pagination-content {
@apply flex flex-row items-center space-x-1;
}
.pagination-item {
@apply flex-shrink-0;
}
@@ -0,0 +1,21 @@
/**
* 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.
*/
.popover {
@apply data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95;
@apply data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95;
@apply data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2;
@apply rounded-8 z-50 min-w-32 overflow-hidden border border-[var(--border-neutral-tertiary)] bg-[var(--bg-neutral-primary)] shadow-md;
}
@@ -0,0 +1,7 @@
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss/nesting'),
require('tailwindcss')('./src/components/tailwind.config.js'),
],
};
@@ -0,0 +1,47 @@
/**
* 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.
*/
.radio-card-group {
--radio-card-text-title: var(--fg-neutral-primary);
--radio-card-text-description: var(--fg-neutral-tertiary);
--radio-card-border-default: var(--border-neutral-tertiary);
--radio-card-border-selected: var(--tint, var(--border-neutral-primary));
--radio-card-fg-default: var(--bg-neutral-primary);
--radio-card-fg-hover: var(--bg-neutral-tertiary);
@apply inline-flex;
/* default */
@apply [&:not(.radio-card-group-vertical)]:radio-card-group-horizontal;
}
.radio-card {
@apply relative inline-flex border border-[var(--radio-card-border-default)] bg-[var(--radio-card-fg-default)] transition-colors disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-2 data-[state=checked]:border-[var(--radio-card-border-selected)] [&:not(:disabled)]:hover:bg-[var(--radio-card-fg-hover)];
/* default */
@apply [&:not(.radio-card-horizontal)]:radio-card-vertical radio-card-radius-medium;
}
.radio-card-text {
@apply block text-inherit;
}
.radio-card-title {
@apply text-base-strong block text-[var(--radio-card-text-title)];
}
.radio-card-description {
@apply text-base-normal block text-[var(--radio-card-text-description)];
}
@@ -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.
*/
.radio-group {
--radio-fg: var(--fg-neutral-primary);
--radio-border: var(--border-neutral-tertiary);
--radio-bg: var(--bg-neutral-primary);
--radio-bg-select: var(--tint, var(--bg-neutral-inverse));
@apply flex;
/* default */
@apply [&:not(.radio-group-vertical)]:radio-group-horizontal;
}
.radio-item {
@apply text-base-normal flex items-center py-2 text-[var(--radio-fg)] disabled:cursor-not-allowed disabled:opacity-50;
}
.radio {
@apply relative mr-2 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-[var(--radio-bg)] before:absolute before:inset-0 before:flex before:rounded-full before:border before:border-[var(--radio-border)];
}
.radio-indicator {
@apply absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-[var(--radio-bg-select)] text-[var(--radio-bg)] focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
}
@@ -0,0 +1,36 @@
/**
* 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.
*/
.scroll-area {
--scroll-area-bg: var(--bg-neutral-transparent);
--scroll-area-thumb-bg: var(--bg-neutral-tertiary);
@apply relative overflow-hidden bg-[var(--scroll-area-bg)];
}
.scroll-area-viewport {
@apply h-full w-full rounded-[inherit] has-[.scroll-bar-horizontal]:pb-2 has-[.scroll-bar-vertical]:pr-2;
}
.scroll-bar {
@apply flex touch-none select-none transition-colors;
/* default */
@apply [&:not(.scroll-bar-horizontal)]:scroll-bar-vertical;
}
.scroll-thumb {
@apply relative flex-1 rounded-full bg-[var(--scroll-area-thumb-bg)];
}
@@ -0,0 +1,86 @@
/**
* 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.
*/
.select {
/* default */
@apply select-small;
}
.select-trigger {
--select-fg: var(--fg-neutral-tertiary);
--select-fg-pressed: var(--fg-neutral-primary);
--select-fg-filled: var(--fg-neutral-primary);
--select-border: var(--border-neutral-tertiary);
--select-border-pressed: var(--tint, var(--border-neutral-primary));
--select-border-error: var(--border-tint-red);
--select-bg: var(--bg-neutral-primary);
--select-bg-pressed: var(--bg-neutral-tertiary);
@apply relative flex w-full items-center justify-start bg-[var(--select-bg)] text-left text-[var(--select-fg-filled)] transition-transform before:absolute before:inset-0 before:flex before:border before:border-[var(--select-border)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-[var(--select-fg)] data-[state=open]:text-[var(--select-fg-pressed)] data-[state=open]:before:border-2 data-[state=open]:before:border-[var(--select-border-pressed)] [&>span]:flex [&>span]:flex-1 [&>span]:items-center [&>span]:overflow-hidden [&>svg:last-of-type]:data-[state=open]:rotate-180;
/* default */
@apply select-trigger-medium select-trigger-radius-medium;
}
.select-trigger[aria-invalid='true'] {
@apply before:!border-2 before:!border-[var(--select-border-error)];
}
.select-content {
--select-fg: var(--fg-neutral-tertiary);
--select-fg-pressed: var(--fg-neutral-primary);
--select-fg-filled: var(--fg-neutral-primary);
--select-border: var(--border-neutral-tertiary);
--select-border-pressed: var(--tint, var(--border-neutral-primary));
--select-border-error: var(--border-tint-red);
--select-bg: var(--bg-neutral-primary);
--select-bg-pressed: var(--bg-neutral-tertiary);
@apply data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95;
@apply data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95;
@apply data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=bottom]:mt-1 data-[side=top]:mb-1;
@apply rounded-8 relative z-50 !min-w-[var(--radix-select-trigger-width,_var(--radix-popper-anchor-width))] overflow-hidden border border-[var(--select-border)] bg-[var(--select-bg)] p-1 text-[var(--select-fg-filled)] shadow-md;
}
.select-viewport {
@apply p-px;
}
.select-group-label {
@apply text-base-strong p-2;
}
.select-item {
@apply rounded-6 text-base-normal relative flex cursor-pointer select-none items-center py-2 outline-none transition-colors hover:bg-[var(--select-bg-pressed)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
/* default */
@apply [&:not(.select-item-right)]:select-item-left;
}
.select-item-check {
@apply absolute flex h-4 w-4 items-center justify-center;
/* default */
@apply [&:not(.select-item-check-right)]:select-item-check-left;
}
.select-separator {
@apply -mx-1 my-1 h-px bg-[var(--select-border)];
}
.select-tag {
@apply mr-2.5 !gap-0;
}
@@ -0,0 +1,51 @@
/**
* 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.
*/
.sheet-overlay {
@apply data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-dim fixed inset-0 z-50;
}
.sheet {
--sheet-border: var(--border-neutral-tertiary);
@apply data-[state=open]:animate-in data-[state=closed]:animate-out bg-neutral-primary text-neutral-primary fixed z-50 flex flex-col space-y-5 border-[var(--sheet-border)] p-5 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500;
/* default */
@apply sheet-radius-small [&:not([class*=sheet-side])]:sheet-side-right;
}
.sheet-close {
@apply !absolute right-2 top-2 !m-0;
}
.sheet-header {
@apply flex flex-shrink-0 flex-col gap-1 text-left;
}
.sheet-footer {
@apply flex flex-shrink-0 justify-end gap-2;
}
.sheet-icon {
@apply flex p-1;
}
.sheet-title {
@apply text-xlarge-strong;
}
.sheet-description {
@apply text-neutral-secondary text-base-normal;
}
@@ -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.
*/
.spinner {
@apply inline-flex animate-[spin_1s_cubic-bezier(0.4,0,0.2,1)_infinite];
}
.spinner-small {
@apply w-4 h-4;
}
.spinner-medium {
@apply w-5 h-5;
}
.spinner-large {
@apply w-6 h-6;
}
.spinner {
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath opacity='0.25' fill-rule='evenodd' clip-rule='evenodd' d='M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM12 24C18.6274 24 24 18.6274 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24Z' fill='black'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.47715 2 2 6.47715 2 12C2 12.5523 1.55228 13 1 13C0.447715 13 0 12.5523 0 12C0 5.37258 5.37258 0 12 0C18.6274 0 24 5.37258 24 12C24 12.5523 23.5523 13 23 13C22.4477 13 22 12.5523 22 12C22 6.47715 17.5228 2 12 2Z' fill='black'/%3E%3C/svg%3E%0A");
mask-position: center;
mask-repeat: no-repeat;
mask-size: 100%;
background-color: currentcolor;
}
@@ -0,0 +1,34 @@
/**
* 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.
*/
.switch {
--switch-fg: var(--fg-neutral-primary);
--switch-handle-bg: var(--bg-neutral-primary);
--switch-bg: var(--bg-neutral-inverse);
--switch-bg-blue: var(--bg-tint-blue-bold);
--switch-bg-orange: var(--bg-tint-orange-bold);
--switch-bg-red: var(--bg-tint-red-bold);
--switch-bg-green: var(--bg-tint-green-bold);
--switch-bg-off: var(--bg-neutral-secondary);
@apply text-base-normal relative inline-flex h-6 cursor-pointer items-center gap-2 text-[var(--switch-fg)] before:box-content before:flex before:h-5 before:w-10 before:rounded-full before:p-0.5 before:transition-colors disabled:cursor-not-allowed disabled:opacity-50 data-[state=unchecked]:before:bg-[var(--switch-bg-off)];
/* default */
@apply switch-default;
}
.switch-thumb {
@apply absolute left-0.5 top-0.5 flex h-5 w-5 translate-x-0 rounded-full bg-[var(--switch-handle-bg)] transition-transform data-[state=checked]:translate-x-full;
}
@@ -0,0 +1,58 @@
/**
* 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.
*/
.table {
--table-header-fg: var(--fg-neutral-tertiary);
--table-cell-fg: var(--fg-neutral-primary);
--table-border: var(--border-neutral-tertiary);
--table-bg: var(--bg-neutral-primary);
--table-bg-hover: var(--bg-neutral-tertiary);
@apply w-full caption-bottom border-[var(--table-border)];
}
.table-header {
@apply [&_th]:border-b [&_th]:border-b-[var(--table-border)];
}
.table-body {
@apply [&_td]:border-b [&_td]:border-b-[var(--table-border)] [&_tr:last-child_td]:border-0;
}
.table-footer {
@apply border-t border-t-[var(--table-border)] bg-[var(--table-bg-hover)] [&>tr]:last:border-b-0;
}
.table-row {
@apply border-b border-b-[var(--table-border)] transition-colors hover:bg-[var(--table-bg-hover)] data-[state=selected]:bg-[var(--table-bg-hover)];
}
.table-head {
@apply text-base-normal h-12 px-4 align-middle font-normal text-[var(--table-header-fg)] [&:has([role=checkbox])]:pr-0 [&>.badge]:mr-1 [&>svg]:mr-1 [&>svg]:text-[var(--table-header-fg)];
/* default */
@apply table-head-left;
}
.table-cell {
@apply text-base-normal px-4 py-3 align-middle text-[var(--table-cell-fg)] [&:has([role=checkbox])]:pr-0 [&>.badge]:mr-1 [&>svg]:text-[var(--table-cell-fg)];
/* default */
@apply table-cell-left;
}
.table-caption {
@apply text-small-normal mt-1 text-[--table-header-fg];
}
@@ -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.
*/
.tabs {
--tabs-fg-unselected: var(--fg-neutral-tertiary);
--tabs-fg-selected: var(--fg-neutral-primary);
--tabs-bg-unselected: var(--bg-neutral-tertiary);
--tabs-bg-selected: var(--bg-neutral-primary);
}
.tabs-list {
@apply rounded-8 h-9.5 inline-flex items-center justify-center bg-[var(--tabs-bg-unselected)] p-1 text-[var(--tabs-fg-selected)];
}
.tabs-trigger {
@apply rounded-6 data-[state=active]:shadow-default text-base-normal inline-flex items-center justify-center whitespace-nowrap px-3 py-1 font-medium text-[var(--tabs-fg-unselected)] transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-[var(--tabs-bg-selected)] data-[state=active]:text-[var(--tabs-fg-selected)] [&>svg]:h-4 [&>svg]:w-4;
}
.tabs-content {
@apply mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
}
@@ -0,0 +1,32 @@
/**
* 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.
*/
.tag {
--tag-primary-fg: var(--fg-neutral-inverse);
--tag-secondary-fg: var(--fg-neutral-primary);
--tag-destructive-fg: var(--fg-neutral-inverse);
--tag-outline-fg: var(--tint, var(--fg-neutral-primary));
--tag-outline-border: var(--border-neutral-tertiary);
--tag-primary-bg: var(--tint, var(--bg-neutral-inverse));
--tag-secondary-bg: var(--bg-neutral-tertiary);
--tag-destructive-bg: var(--bg-tint-red-bold);
--tag-outline-bg: var(--bg-neutral-transparent);
--tag-outline-bg-hover: var(--tint-subtle, var(--bg-neutral-secondary));
@apply inline-flex items-center whitespace-nowrap hover:opacity-80 [&>svg]:flex-shrink-0;
/* default */
@apply tag-small tag-radius-medium tag-primary;
}
@@ -0,0 +1,213 @@
const plugin = require("tailwindcss/plugin");
function filterDefault(values) {
return Object.fromEntries(
Object.entries(values).filter(([key]) => key !== "DEFAULT"),
);
}
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: [{ raw: "" }],
theme: require("../theme"),
plugins: [
plugin(
({ addUtilities, matchUtilities, theme }) => {
addUtilities({
"@keyframes enter": theme("keyframes.enter"),
"@keyframes exit": theme("keyframes.exit"),
".animate-in": {
animationName: "enter",
animationDuration: theme("animationDuration.DEFAULT"),
"--tw-enter-opacity": "initial",
"--tw-enter-scale": "initial",
"--tw-enter-rotate": "initial",
"--tw-enter-translate-x": "initial",
"--tw-enter-translate-y": "initial",
},
".animate-out": {
animationName: "exit",
animationDuration: theme("animationDuration.DEFAULT"),
"--tw-exit-opacity": "initial",
"--tw-exit-scale": "initial",
"--tw-exit-rotate": "initial",
"--tw-exit-translate-x": "initial",
"--tw-exit-translate-y": "initial",
},
});
matchUtilities(
{
"fade-in": (value) => ({ "--tw-enter-opacity": value }),
"fade-out": (value) => ({ "--tw-exit-opacity": value }),
},
{ values: theme("animationOpacity") },
);
matchUtilities(
{
"zoom-in": (value) => ({ "--tw-enter-scale": value }),
"zoom-out": (value) => ({ "--tw-exit-scale": value }),
},
{ values: theme("animationScale") },
);
matchUtilities(
{
"spin-in": (value) => ({ "--tw-enter-rotate": value }),
"spin-out": (value) => ({ "--tw-exit-rotate": value }),
},
{ values: theme("animationRotate") },
);
matchUtilities(
{
"slide-in-from-top": (value) => ({
"--tw-enter-translate-y": `-${value}`,
}),
"slide-in-from-bottom": (value) => ({
"--tw-enter-translate-y": value,
}),
"slide-in-from-left": (value) => ({
"--tw-enter-translate-x": `-${value}`,
}),
"slide-in-from-right": (value) => ({
"--tw-enter-translate-x": value,
}),
"slide-out-to-top": (value) => ({
"--tw-exit-translate-y": `-${value}`,
}),
"slide-out-to-bottom": (value) => ({
"--tw-exit-translate-y": value,
}),
"slide-out-to-left": (value) => ({
"--tw-exit-translate-x": `-${value}`,
}),
"slide-out-to-right": (value) => ({
"--tw-exit-translate-x": value,
}),
},
{ values: theme("animationTranslate") },
);
matchUtilities(
{ duration: (value) => ({ animationDuration: value }) },
{ values: filterDefault(theme("animationDuration")) },
);
matchUtilities(
{ delay: (value) => ({ animationDelay: value }) },
{ values: theme("animationDelay") },
);
matchUtilities(
{ ease: (value) => ({ animationTimingFunction: value }) },
{ values: filterDefault(theme("animationTimingFunction")) },
);
addUtilities({
".running": { animationPlayState: "running" },
".paused": { animationPlayState: "paused" },
});
matchUtilities(
{ "fill-mode": (value) => ({ animationFillMode: value }) },
{ values: theme("animationFillMode") },
);
matchUtilities(
{ direction: (value) => ({ animationDirection: value }) },
{ values: theme("animationDirection") },
);
matchUtilities(
{ repeat: (value) => ({ animationIterationCount: value }) },
{ values: theme("animationRepeat") },
);
},
{
theme: {
extend: {
animationDelay: ({ theme }) => ({
...theme("transitionDelay"),
}),
animationDuration: ({ theme }) => ({
0: "0ms",
...theme("transitionDuration"),
}),
animationTimingFunction: ({ theme }) => ({
...theme("transitionTimingFunction"),
}),
animationFillMode: {
none: "none",
forwards: "forwards",
backwards: "backwards",
both: "both",
},
animationDirection: {
normal: "normal",
reverse: "reverse",
alternate: "alternate",
"alternate-reverse": "alternate-reverse",
},
animationOpacity: ({ theme }) => ({
DEFAULT: 0,
...theme("opacity"),
}),
animationTranslate: ({ theme }) => ({
DEFAULT: "100%",
...theme("translate"),
}),
animationScale: ({ theme }) => ({
DEFAULT: 0,
...theme("scale"),
}),
animationRotate: ({ theme }) => ({
DEFAULT: "30deg",
...theme("rotate"),
}),
animationRepeat: {
0: "0",
1: "1",
infinite: "infinite",
},
animation: {
in: "enter 0.15s ease-in-out",
out: "exit 0.15s ease-in-out",
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
keyframes: {
enter: {
from: {
opacity: "var(--tw-enter-opacity, 1)",
transform:
"translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))",
},
},
exit: {
to: {
opacity: "var(--tw-exit-opacity, 1)",
transform:
"translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))",
},
},
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
},
},
},
),
plugin(({ addBase, addUtilities }) => {
addBase(require("../../dist/base"));
addUtilities(require("../../dist/utilities"));
}),
],
};
@@ -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.
*/
.textarea {
--textarea-fg: var(--fg-neutral-tertiary);
--textarea-fg-typing: var(--fg-neutral-primary);
--textarea-fg-filled: var(--fg-neutral-primary);
--textarea-border: var(--border-neutral-tertiary);
--textarea-border-typing: var(--tint, var(--border-neutral-primary));
--textarea-border-filled: var(--border-neutral-tertiary);
--textarea-bg: var(--bg-neutral-primary);
--textarea-bg-typing: var(--bg-neutral-primary);
--textarea-bg-filled: var(--bg-neutral-primary);
@apply text-[var(--textarea-fg-filled)] placeholder:text-[var(--textarea-fg)] placeholder-shown:text-[var(--textarea-fg)] focus-visible:text-[var(--textarea-fg-typing)];
@apply border-[var(--textarea-border-filled)] placeholder-shown:border-[var(--textarea-border)] focus-visible:border-[var(--textarea-border-typing)];
@apply bg-[var(--textarea-bg-filled)] placeholder-shown:bg-[var(--textarea-bg)] focus-visible:bg-[var(--textarea-bg-typing)];
@apply rounded-8 text-base-normal flex min-h-20 w-full border px-3 py-2 outline-none;
@apply disabled:cursor-not-allowed disabled:opacity-50;
}
@@ -0,0 +1,68 @@
/**
* 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.
*/
.toaster {
}
.toast {
--toast-fg: var(--fg-neutral-primary);
--toast-fg-secondary: var(--fg-neutral-secondary);
--toast-fg-warning: var(--fg-tint-orange);
--toast-fg-success: var(--fg-tint-green);
--toast-fg-error: var(--fg-tint-red);
--toast-fg-informative: var(--fg-tint-blue);
--toast-border: var(--border-neutral-tertiary);
--toast-border-warning: var(--border-tint-orange);
--toast-border-success: var(--border-tint-green);
--toast-border-error: var(--border-tint-red);
--toast-border-informative: var(--border-tint-blue);
--toast-bg: var(--bg-neutral-primary);
@apply !absolute !left-0 inline-flex min-w-[356px] items-center space-x-2 border bg-[var(--toast-bg)] px-5 py-4;
/* default */
@apply toast-radius-medium toast-default;
}
.toast-content {
@apply flex flex-1 flex-col items-start justify-center;
}
.toast-title {
@apply text-title-h5 flex flex-1 flex-col text-[var(--toast-fg)];
}
.toast-description {
@apply text-base-normal text-[var(--toast-fg-secondary)];
}
.toast-button {
@apply flex-shrink-0;
}
.toast-close {
@apply order-1 flex-shrink-0 !px-0;
}
.toast-icon {
@apply relative mb-auto box-content flex h-5 w-5 flex-shrink-0 p-1;
/* default */
@apply toast-icon-default;
}
.toast-loader {
@apply flex scale-100;
}
@@ -0,0 +1,47 @@
/**
* 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.
*/
.toggle-group {
--toggle-group-fg: var(--fg-neutral-primary);
--toggle-group-fg-hover: var(--fg-neutral-primary);
--toggle-group-fg-pressed: var(--tint, var(--fg-neutral-primary));
--toggle-group-border: var(--border-neutral-tertiary);
--toggle-group-border-hover: var(--border-neutral-tertiary);
--toggle-group-border-pressed: var(--border-neutral-tertiary);
--toggle-group-bg: var(--bg-neutral-primary);
--toggle-group-bg-hover: var(--bg-neutral-tertiary);
--toggle-group-bg-pressed: var(--bg-neutral-tertiary);
@apply relative inline-flex items-center justify-center;
}
.toggle-group-item {
@apply inline-flex cursor-pointer items-center justify-center gap-1 border border-[var(--toggle-group-border)] bg-[var(--toggle-group-bg)] text-[var(--toggle-group-fg)] transition-colors disabled:cursor-not-allowed disabled:opacity-50 data-[state=on]:border-[var(--toggle-group-border-pressed)] data-[state=on]:bg-[var(--toggle-group-bg-pressed)] data-[state=on]:text-[var(--toggle-group-fg-pressed)] [&:not(:disabled)]:hover:border-[var(--toggle-group-border-hover)] [&:not(:disabled)]:hover:bg-[var(--toggle-group-bg-hover)] [&:not(:disabled)]:hover:text-[var(--toggle-group-fg-hover)] data-[state=on]:[&:not(:disabled)]:hover:border-[var(--toggle-group-border-hover)] data-[state=on]:[&:not(:disabled)]:hover:bg-[var(--toggle-group-bg-hover)] data-[state=on]:[&:not(:disabled)]:hover:text-[var(--toggle-group-fg-hover)];
/* default */
@apply toggle-group-item-small toggle-group-item-radius-medium;
~ .toggle-group-item {
@apply -ml-px;
}
}
.toggle-group-icon {
@apply pointer-events-none;
}
.toggle-group-input {
@apply absolute m-0 block h-0 w-0 overflow-hidden p-0;
}

Some files were not shown because too many files have changed in this diff Show More