feat: [공통] UI 컴포넌트 개선 (TabChip, FormField, Select 등)
- TabChip, FormField, MobileCard, Select 컴포넌트 개선 - IntegratedListTemplateV2, UniversalListPage 타입 보강 - LoginPage UI 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -42,25 +42,19 @@ export function TabChip({
|
|||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`
|
className={`
|
||||||
flex items-center gap-2 px-4 py-2.5 rounded-full border transition-all
|
inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg border text-sm whitespace-nowrap transition-all
|
||||||
${
|
${
|
||||||
isActiveState
|
isActiveState
|
||||||
? "border-primary bg-primary text-white shadow-sm"
|
? "border-primary bg-primary text-white shadow-sm font-medium"
|
||||||
: "border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50"
|
: "border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 text-gray-600"
|
||||||
}
|
}
|
||||||
${className}
|
${className}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<span
|
{label}
|
||||||
className={`text-sm ${
|
|
||||||
isActiveState ? "text-white font-medium" : "text-gray-600 font-normal"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
{count !== undefined && (
|
{count !== undefined && (
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-semibold ${
|
className={`font-semibold ${
|
||||||
isActiveState ? "text-white" : "text-gray-900"
|
isActiveState ? "text-white" : "text-gray-900"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export interface FormFieldProps {
|
|||||||
showButtons?: boolean;
|
showButtons?: boolean;
|
||||||
/** 텍스트 입력 최대 길이 */
|
/** 텍스트 입력 최대 길이 */
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
|
/** SelectContent에 전달할 className (예: max-h-60) */
|
||||||
|
selectContentClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormField({
|
export function FormField({
|
||||||
@@ -108,6 +110,7 @@ export function FormField({
|
|||||||
suffix,
|
suffix,
|
||||||
showButtons,
|
showButtons,
|
||||||
maxLength,
|
maxLength,
|
||||||
|
selectContentClassName,
|
||||||
}: FormFieldProps) {
|
}: FormFieldProps) {
|
||||||
|
|
||||||
const renderInput = () => {
|
const renderInput = () => {
|
||||||
@@ -122,7 +125,7 @@ export function FormField({
|
|||||||
<SelectTrigger className={`${error ? 'border-red-500' : ''} ${inputClassName}`}>
|
<SelectTrigger className={`${error ? 'border-red-500' : ''} ${inputClassName}`}>
|
||||||
<SelectValue placeholder={selectPlaceholder} />
|
<SelectValue placeholder={selectPlaceholder} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent className={selectContentClassName}>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export const InfoField = memo(function InfoField({
|
|||||||
className = '',
|
className = '',
|
||||||
}: InfoFieldProps) {
|
}: InfoFieldProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn('space-y-0.5', className)}>
|
<div className={cn('space-y-0.5 min-w-0', className)}>
|
||||||
<p className="text-xs text-muted-foreground">{label}</p>
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||||||
<div className={cn('text-sm font-medium', valueClassName)}>{value}</div>
|
<div className={cn('text-sm font-medium break-words', valueClassName)}>{value}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -194,7 +194,7 @@ export function MobileCard({
|
|||||||
<div
|
<div
|
||||||
key={`${detail.label}-${index}`}
|
key={`${detail.label}-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1',
|
'flex items-center gap-1 min-w-0',
|
||||||
detail.colSpan === 2 && 'col-span-2'
|
detail.colSpan === 2 && 'col-span-2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -264,7 +264,7 @@ export function MobileCard({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'border rounded-lg p-5 space-y-4 bg-white dark:bg-card transition-all',
|
'border rounded-lg p-5 space-y-4 bg-white dark:bg-card transition-all overflow-hidden',
|
||||||
handleClick && 'cursor-pointer',
|
handleClick && 'cursor-pointer',
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-blue-500 bg-blue-50/50'
|
? 'border-blue-500 bg-blue-50/50'
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { formatNumber } from '@/lib/utils/amount';
|
|||||||
export interface TabOption {
|
export interface TabOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
mobileLabel?: string;
|
||||||
count: number;
|
count: number;
|
||||||
color?: string; // 모바일 탭 색상
|
color?: string; // 모바일 탭 색상
|
||||||
}
|
}
|
||||||
@@ -805,7 +806,7 @@ export function IntegratedListTemplateV2<T = any>({
|
|||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<TabChip
|
<TabChip
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
label={tab.label}
|
label={tab.mobileLabel || tab.label}
|
||||||
count={tab.count}
|
count={tab.count}
|
||||||
active={activeTab === tab.value}
|
active={activeTab === tab.value}
|
||||||
onClick={() => onTabChange?.(tab.value)}
|
onClick={() => onTabChange?.(tab.value)}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type { FilterFieldConfig, FilterValues };
|
|||||||
export interface TabOption {
|
export interface TabOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
mobileLabel?: string;
|
||||||
count: number;
|
count: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function SelectTrigger({
|
|||||||
function SelectContent({
|
function SelectContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
position = "item-aligned",
|
position = "popper",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
@@ -66,7 +66,7 @@ function SelectContent({
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground 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-[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 relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
"bg-popover text-popover-foreground 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-[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 relative z-50 max-h-60 min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
className,
|
className,
|
||||||
|
|||||||
Reference in New Issue
Block a user