Merge branch 'master' into master_api_test개발
# Conflicts: # src/app/[locale]/(protected)/sales/pricing-management/page.tsx
This commit is contained in:
@@ -491,8 +491,6 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
|
||||
getItemId={(item: BillRecord) => item.id}
|
||||
renderTableRow={renderTableRow}
|
||||
renderMobileCard={renderMobileCard}
|
||||
addButtonLabel="어음 등록"
|
||||
onAddClick={() => router.push('/ko/accounting/bills/new')}
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
|
||||
@@ -92,6 +92,7 @@ const generateMockData = (): PurchaseRecord[] => {
|
||||
sourceDocument: i % 3 === 0 ? {
|
||||
type: i % 2 === 0 ? 'proposal' : 'expense_report',
|
||||
documentNo: `DOC-2025-${String(i + 1).padStart(4, '0')}`,
|
||||
title: `${i % 2 === 0 ? '품의' : '지출'} 건 - ${vendors[i % vendors.length]}`,
|
||||
expectedCost: supplyAmount,
|
||||
} : undefined,
|
||||
withdrawalAccount: {
|
||||
|
||||
@@ -168,7 +168,7 @@ export function DocumentCreate() {
|
||||
})),
|
||||
cardInfo: expenseReportData.cardId || '-',
|
||||
totalAmount: expenseReportData.totalAmount,
|
||||
attachments: expenseReportData.attachments,
|
||||
attachments: expenseReportData.attachments.map(f => f.name),
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
@@ -182,7 +182,7 @@ export function DocumentCreate() {
|
||||
description: proposalData.description || '-',
|
||||
reason: proposalData.reason || '-',
|
||||
estimatedCost: proposalData.estimatedCost,
|
||||
attachments: proposalData.attachments,
|
||||
attachments: proposalData.attachments.map(f => f.name),
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
|
||||
@@ -57,6 +57,10 @@ const initialFormData: EmployeeFormData = {
|
||||
confirmPassword: '',
|
||||
role: 'user',
|
||||
accountStatus: 'active',
|
||||
clockInLocation: '',
|
||||
clockOutLocation: '',
|
||||
resignationDate: '',
|
||||
resignationReason: '',
|
||||
};
|
||||
|
||||
export function EmployeeDialog({
|
||||
@@ -103,6 +107,10 @@ export function EmployeeDialog({
|
||||
confirmPassword: '',
|
||||
role: employee.userInfo?.role || 'user',
|
||||
accountStatus: employee.userInfo?.accountStatus || 'active',
|
||||
clockInLocation: employee.clockInLocation || '',
|
||||
clockOutLocation: employee.clockOutLocation || '',
|
||||
resignationDate: employee.resignationDate || '',
|
||||
resignationReason: employee.resignationReason || '',
|
||||
});
|
||||
} else if (open && mode === 'create') {
|
||||
setFormData(initialFormData);
|
||||
|
||||
@@ -125,6 +125,8 @@ export interface Employee {
|
||||
clockOutLocation?: string; // 퇴근 위치
|
||||
resignationDate?: string; // 퇴사일
|
||||
resignationReason?: string; // 퇴직사유
|
||||
concurrentPosition?: string; // 겸직 직위
|
||||
concurrentReason?: string; // 겸직 사유
|
||||
|
||||
// 사용자 정보 (시스템 계정)
|
||||
userInfo?: UserInfo;
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { VacationUsageRecord, VacationAdjustment, VacationType } from './types';
|
||||
import type { VacationUsageRecord, VacationAdjustment, AdjustableVacationType } from './types';
|
||||
import { VACATION_TYPE_LABELS } from './types';
|
||||
|
||||
interface VacationAdjustDialogProps {
|
||||
@@ -62,7 +62,7 @@ export function VacationAdjustDialog({
|
||||
}, [open]);
|
||||
|
||||
// 조정값 증가
|
||||
const handleIncrease = (type: VacationType) => {
|
||||
const handleIncrease = (type: AdjustableVacationType) => {
|
||||
setAdjustments(prev => ({
|
||||
...prev,
|
||||
[type]: prev[type] + 1,
|
||||
@@ -70,7 +70,7 @@ export function VacationAdjustDialog({
|
||||
};
|
||||
|
||||
// 조정값 감소
|
||||
const handleDecrease = (type: VacationType) => {
|
||||
const handleDecrease = (type: AdjustableVacationType) => {
|
||||
setAdjustments(prev => ({
|
||||
...prev,
|
||||
[type]: prev[type] - 1,
|
||||
@@ -78,7 +78,7 @@ export function VacationAdjustDialog({
|
||||
};
|
||||
|
||||
// 조정값 직접 입력
|
||||
const handleInputChange = (type: VacationType, value: string) => {
|
||||
const handleInputChange = (type: AdjustableVacationType, value: string) => {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (!isNaN(numValue)) {
|
||||
setAdjustments(prev => ({
|
||||
@@ -97,7 +97,7 @@ export function VacationAdjustDialog({
|
||||
const handleSave = () => {
|
||||
const adjustmentList: VacationAdjustment[] = [];
|
||||
|
||||
(Object.keys(adjustments) as VacationType[]).forEach((type) => {
|
||||
(Object.keys(adjustments) as AdjustableVacationType[]).forEach((type) => {
|
||||
if (adjustments[type] !== 0) {
|
||||
adjustmentList.push({
|
||||
vacationType: type,
|
||||
@@ -116,7 +116,7 @@ export function VacationAdjustDialog({
|
||||
|
||||
if (!record) return null;
|
||||
|
||||
const vacationTypes: VacationType[] = ['annual', 'monthly', 'reward', 'other'];
|
||||
const vacationTypes: AdjustableVacationType[] = ['annual', 'monthly', 'reward', 'other'];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
|
||||
@@ -9,6 +9,9 @@ export type MainTabType = 'usage' | 'grant' | 'request';
|
||||
// 휴가 유형
|
||||
export type VacationType = 'annual' | 'monthly' | 'reward' | 'condolence' | 'other';
|
||||
|
||||
// 조정 가능한 휴가 유형 (VacationAdjustDialog에서 사용)
|
||||
export type AdjustableVacationType = 'annual' | 'monthly' | 'reward' | 'other';
|
||||
|
||||
// 필터 옵션
|
||||
export type FilterOption = 'all' | 'hasVacation' | 'noVacation';
|
||||
|
||||
|
||||
@@ -55,7 +55,24 @@ export function DropdownField({
|
||||
unitOptions,
|
||||
}: DynamicFieldRendererProps) {
|
||||
const fieldKey = field.field_key || `field_${field.id}`;
|
||||
const stringValue = value !== null && value !== undefined ? String(value) : '';
|
||||
|
||||
// is_active 필드인지 확인
|
||||
const isActiveField = fieldKey === 'is_active' || fieldKey.endsWith('_is_active');
|
||||
|
||||
// 옵션을 먼저 정규화 (is_active 값 변환에 필요)
|
||||
const rawOptions = normalizeOptions(field.options);
|
||||
|
||||
// is_active 필드일 때 boolean 값을 옵션에 맞게 변환
|
||||
let stringValue = '';
|
||||
if (value !== null && value !== undefined) {
|
||||
if (isActiveField && rawOptions.length >= 2) {
|
||||
// boolean/숫자 값을 첫번째(활성) 또는 두번째(비활성) 옵션 값으로 매핑
|
||||
const isActive = value === true || value === 'true' || value === 1 || value === '1' || value === '활성';
|
||||
stringValue = isActive ? rawOptions[0].value : rawOptions[1].value;
|
||||
} else {
|
||||
stringValue = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
// field_key 또는 field_name이 '단위'/'unit' 관련이면 unitOptions 사용
|
||||
const isUnitField =
|
||||
@@ -73,8 +90,8 @@ export function DropdownField({
|
||||
value: u.value,
|
||||
}));
|
||||
} else {
|
||||
// field.options를 정규화
|
||||
options = normalizeOptions(field.options);
|
||||
// rawOptions는 이미 위에서 정규화됨
|
||||
options = rawOptions;
|
||||
}
|
||||
|
||||
// 옵션이 없으면 드롭다운을 disabled로 표시
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { DynamicFormData, ItemType, StructuredFieldConfig } from '../types';
|
||||
import { ItemFieldResponse } from '@/types/item';
|
||||
import { DynamicFormData, ItemType, StructuredFieldConfig, ItemFieldResponse } from '../types';
|
||||
|
||||
/**
|
||||
* 부품 유형 탐지 결과
|
||||
@@ -27,7 +26,7 @@ export interface UseFieldDetectionParams {
|
||||
/** 폼 구조 정보 */
|
||||
structure: StructuredFieldConfig | null;
|
||||
/** 현재 선택된 품목 유형 (FG, PT, SM, RM, CS) */
|
||||
selectedItemType: ItemType;
|
||||
selectedItemType: ItemType | '';
|
||||
/** 현재 폼 데이터 */
|
||||
formData: DynamicFormData;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { DynamicFormData, ItemType, StructuredFieldConfig } from '../types';
|
||||
import { BendingFieldKeys, CategoryKeyWithId } from './useItemCodeGeneration';
|
||||
import { BendingDetail } from '@/types/item';
|
||||
import type { DynamicFormData, DynamicFieldValue, ItemType, StructuredFieldConfig } from '../types';
|
||||
import type { BendingFieldKeys, CategoryKeyWithId } from './useItemCodeGeneration';
|
||||
import type { BendingDetail } from '@/types/item';
|
||||
|
||||
/**
|
||||
* usePartTypeHandling 훅 입력 파라미터
|
||||
@@ -20,7 +20,7 @@ export interface UsePartTypeHandlingParams {
|
||||
/** 품목명 필드 키 */
|
||||
itemNameKey: string;
|
||||
/** 필드 값 설정 함수 */
|
||||
setFieldValue: (key: string, value: unknown) => void;
|
||||
setFieldValue: (key: string, value: DynamicFieldValue) => void;
|
||||
/** 현재 폼 데이터 */
|
||||
formData: DynamicFormData;
|
||||
/** 절곡부품 필드 키 정보 */
|
||||
|
||||
@@ -12,6 +12,10 @@ import type {
|
||||
PageStructureResponse,
|
||||
} from '@/types/item-master-api';
|
||||
|
||||
// Re-export types for hooks
|
||||
export type { ItemFieldResponse } from '@/types/item-master-api';
|
||||
export type { ItemType } from '@/types/item';
|
||||
|
||||
// ============================================
|
||||
// 조건부 표시 타입
|
||||
// ============================================
|
||||
@@ -244,4 +248,14 @@ export function convertToFormStructure(
|
||||
orderNo: f.order_no,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 타입 별칭 (하위 호환성)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* StructuredFieldConfig는 DynamicFormStructure의 별칭
|
||||
* (hooks에서 사용하는 이름)
|
||||
*/
|
||||
export type StructuredFieldConfig = DynamicFormStructure;
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Search, Plus, Edit, Trash2, Package, Loader2 } from 'lucide-react';
|
||||
import { TableLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { useItemList } from '@/hooks/useItemList';
|
||||
import {
|
||||
IntegratedListTemplateV2,
|
||||
@@ -129,12 +130,7 @@ export default function ItemListClient() {
|
||||
|
||||
// 로딩 상태
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<span className="ml-3 text-muted-foreground">품목 목록 로딩 중...</span>
|
||||
</div>
|
||||
);
|
||||
return <TableLoadingSpinner text="품목 목록을 불러오는 중..." />;
|
||||
}
|
||||
|
||||
// 유형 변경 핸들러
|
||||
|
||||
@@ -47,11 +47,17 @@ export interface ListMobileCardProps {
|
||||
/** 카드 클릭 핸들러 */
|
||||
onCardClick?: () => void;
|
||||
|
||||
/** 카드 클릭 핸들러 (onCardClick 별칭) */
|
||||
onClick?: () => void;
|
||||
|
||||
/** 체크박스 표시 여부 */
|
||||
showCheckbox?: boolean;
|
||||
|
||||
/** 헤더 영역 뱃지들 (번호, 코드 등) */
|
||||
headerBadges?: ReactNode;
|
||||
|
||||
/** 카드 제목 (주요 정보) */
|
||||
title: string;
|
||||
title: string | ReactNode;
|
||||
|
||||
/** 상태 뱃지 (우측 상단) */
|
||||
statusBadge?: ReactNode;
|
||||
@@ -81,11 +87,13 @@ export interface InfoFieldProps {
|
||||
label: string;
|
||||
value: string | number | ReactNode;
|
||||
valueClassName?: string;
|
||||
/** 추가 className */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function InfoField({ label, value, valueClassName = "" }: InfoFieldProps) {
|
||||
export function InfoField({ label, value, valueClassName = "", className = "" }: InfoFieldProps) {
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<div className={`space-y-0.5 ${className}`}>
|
||||
<p className="text-xs text-muted-foreground">{label}</p>
|
||||
<div className={`text-sm font-medium ${valueClassName}`}>{value}</div>
|
||||
</div>
|
||||
@@ -97,6 +105,8 @@ export function ListMobileCard({
|
||||
isSelected,
|
||||
onToggleSelection,
|
||||
onCardClick,
|
||||
onClick,
|
||||
showCheckbox = true,
|
||||
headerBadges,
|
||||
title,
|
||||
statusBadge,
|
||||
@@ -106,6 +116,7 @@ export function ListMobileCard({
|
||||
topContent,
|
||||
bottomContent
|
||||
}: ListMobileCardProps) {
|
||||
const handleCardClick = onClick || onCardClick;
|
||||
return (
|
||||
<div
|
||||
className={`border rounded-lg p-5 space-y-4 bg-white dark:bg-card transition-all cursor-pointer ${
|
||||
@@ -113,7 +124,7 @@ export function ListMobileCard({
|
||||
? 'border-blue-500 bg-blue-50/50'
|
||||
: 'border-gray-200 hover:border-primary/50'
|
||||
} ${className}`}
|
||||
onClick={onCardClick}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
{/* 상단 추가 콘텐츠 */}
|
||||
{topContent}
|
||||
@@ -121,12 +132,14 @@ export function ListMobileCard({
|
||||
{/* 헤더: 체크박스 + 뱃지 + 제목 / 우측에 상태 뱃지 */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={onToggleSelection}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mt-0.5 h-5 w-5"
|
||||
/>
|
||||
{showCheckbox && (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={onToggleSelection}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mt-0.5 h-5 w-5"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* 헤더 뱃지들 (번호, 코드 등) */}
|
||||
{headerBadges && (
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import { ReactNode } from "react";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
interface PageHeaderProps {
|
||||
export interface PageHeaderProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: ReactNode;
|
||||
icon?: LucideIcon;
|
||||
versionBadge?: ReactNode;
|
||||
/** 뒤로가기 핸들러 */
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, description, actions, icon: Icon, versionBadge }: PageHeaderProps) {
|
||||
|
||||
@@ -94,7 +94,7 @@ async function getApiHeaders(): Promise<HeadersInit> {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,114 @@
|
||||
// 로딩 스피너 컴포넌트
|
||||
// API 호출 중 로딩 상태 표시용
|
||||
// 대시보드 스타일로 통일 (border-4 border-solid border-primary border-r-transparent)
|
||||
/**
|
||||
* 로딩 스피너 컴포넌트 (표준화됨)
|
||||
*
|
||||
* 사용 가이드:
|
||||
* - LoadingSpinner: 인라인/버튼 내부/작은 영역용
|
||||
* - ContentLoadingSpinner: 컨텐츠 영역 로딩용 (상세/수정 페이지)
|
||||
* - PageLoadingSpinner: 페이지 전환용 (loading.tsx, 전체 페이지)
|
||||
*
|
||||
* 스타일: border-4 border-solid border-primary border-r-transparent
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
// ============================================
|
||||
// 1. 기본 스피너 (인라인/버튼 내부용)
|
||||
// ============================================
|
||||
interface LoadingSpinnerProps {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
xs: 'h-3 w-3 border-2',
|
||||
sm: 'h-4 w-4 border-2',
|
||||
md: 'h-8 w-8 border-3',
|
||||
lg: 'h-12 w-12 border-4'
|
||||
};
|
||||
|
||||
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
||||
size = 'md',
|
||||
className = '',
|
||||
text
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
sm: 'h-4 w-4 border-2',
|
||||
md: 'h-8 w-8 border-4',
|
||||
lg: 'h-12 w-12 border-4'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col items-center justify-center gap-2 ${className}`}>
|
||||
<div className={`animate-spin rounded-full border-solid border-primary border-r-transparent ${sizeClasses[size]}`} />
|
||||
<div
|
||||
className={`animate-spin rounded-full border-solid border-primary border-r-transparent ${sizeClasses[size]}`}
|
||||
/>
|
||||
{text && <p className="text-sm text-muted-foreground">{text}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 페이지 레벨 로딩 스피너 (전체 화면 중앙 배치)
|
||||
// ============================================
|
||||
// 2. 컨텐츠 영역 스피너 (상세/수정 페이지용)
|
||||
// ============================================
|
||||
interface ContentLoadingSpinnerProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export const ContentLoadingSpinner: React.FC<ContentLoadingSpinnerProps> = ({
|
||||
text = '불러오는 중...'
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="inline-block h-10 w-10 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent" />
|
||||
<p className="text-sm text-muted-foreground">{text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 3. 페이지 레벨 스피너 (페이지 전환용)
|
||||
// ============================================
|
||||
interface PageLoadingSpinnerProps {
|
||||
text?: string;
|
||||
minHeight?: string;
|
||||
}
|
||||
|
||||
export const PageLoadingSpinner: React.FC<PageLoadingSpinnerProps> = ({
|
||||
text = '불러오는 중...',
|
||||
minHeight = 'min-h-[60vh]'
|
||||
text = '페이지를 불러오는 중...'
|
||||
}) => {
|
||||
return (
|
||||
<div className={`flex items-center justify-center ${minHeight}`}>
|
||||
<div className="flex items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"></div>
|
||||
<div className="inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent" />
|
||||
<p className="text-muted-foreground font-medium">{text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 4. 테이블/리스트 오버레이 스피너
|
||||
// ============================================
|
||||
interface TableLoadingSpinnerProps {
|
||||
text?: string;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
export const TableLoadingSpinner: React.FC<TableLoadingSpinnerProps> = ({
|
||||
text = '데이터를 불러오는 중...',
|
||||
rows = 5
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent" />
|
||||
<p className="text-sm text-muted-foreground">{text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 5. 버튼 내부 스피너 (저장 중 등)
|
||||
// ============================================
|
||||
export const ButtonSpinner: React.FC = () => {
|
||||
return (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent" />
|
||||
);
|
||||
};
|
||||
@@ -8,8 +8,11 @@ const Textarea = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex min-h-[80px] w-full rounded-md border bg-input-background px-3 py-2 text-base transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
Reference in New Issue
Block a user