dev #46
|
|
@ -0,0 +1,257 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData, WebType } from "@/types/screen";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
// import { Checkbox } from "@/components/ui/checkbox";
|
||||
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Database, Type, Hash, List, AlignLeft, CheckSquare, Radio, Calendar, Code, Building, File } from "lucide-react";
|
||||
|
||||
interface RealtimePreviewProps {
|
||||
component: ComponentData;
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
onDragStart?: (e: React.DragEvent) => void;
|
||||
onDragEnd?: () => void;
|
||||
}
|
||||
|
||||
// 웹 타입에 따른 위젯 렌더링
|
||||
const renderWidget = (component: ComponentData) => {
|
||||
const { widgetType, label, placeholder, required, readonly, columnName } = component;
|
||||
|
||||
// 디버깅: 실제 widgetType 값 확인
|
||||
console.log('RealtimePreview - widgetType:', widgetType, 'columnName:', columnName);
|
||||
|
||||
const commonProps = {
|
||||
placeholder: placeholder || `입력하세요...`,
|
||||
disabled: readonly,
|
||||
required: required,
|
||||
className: "w-full",
|
||||
};
|
||||
|
||||
switch (widgetType) {
|
||||
case "text":
|
||||
case "email":
|
||||
case "tel":
|
||||
return <Input type={widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text"} {...commonProps} />;
|
||||
|
||||
case "number":
|
||||
case "decimal":
|
||||
return <Input type="number" step={widgetType === "decimal" ? "0.01" : "1"} {...commonProps} />;
|
||||
|
||||
case "date":
|
||||
case "datetime":
|
||||
return (
|
||||
<Input
|
||||
type={widgetType === "datetime" ? "datetime-local" : "date"}
|
||||
{...commonProps}
|
||||
/>
|
||||
);
|
||||
|
||||
case "select":
|
||||
case "dropdown":
|
||||
return (
|
||||
<select
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">{placeholder || "선택하세요..."}</option>
|
||||
<option value="option1">옵션 1</option>
|
||||
<option value="option2">옵션 2</option>
|
||||
<option value="option3">옵션 3</option>
|
||||
</select>
|
||||
);
|
||||
|
||||
case "textarea":
|
||||
case "text_area":
|
||||
return <Textarea {...commonProps} rows={3} />;
|
||||
|
||||
case "boolean":
|
||||
case "checkbox":
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`checkbox-${component.id}`}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor={`checkbox-${component.id}`} className="text-sm">
|
||||
{label || columnName}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "radio":
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`radio1-${component.id}`}
|
||||
name={`radio-${component.id}`}
|
||||
disabled={readonly}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor={`radio1-${component.id}`} className="text-sm">
|
||||
옵션 1
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`radio2-${component.id}`}
|
||||
name={`radio-${component.id}`}
|
||||
disabled={readonly}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor={`radio2-${component.id}`} className="text-sm">
|
||||
옵션 2
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "code":
|
||||
return (
|
||||
<Textarea
|
||||
{...commonProps}
|
||||
rows={4}
|
||||
className="w-full font-mono text-sm"
|
||||
placeholder="코드를 입력하세요..."
|
||||
/>
|
||||
);
|
||||
|
||||
case "entity":
|
||||
return (
|
||||
<select
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">엔티티를 선택하세요...</option>
|
||||
<option value="user">사용자</option>
|
||||
<option value="product">제품</option>
|
||||
<option value="order">주문</option>
|
||||
</select>
|
||||
);
|
||||
|
||||
case "file":
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return <Input type="text" {...commonProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
// 위젯 타입 아이콘
|
||||
const getWidgetIcon = (widgetType: WebType | undefined) => {
|
||||
switch (widgetType) {
|
||||
case "text":
|
||||
case "email":
|
||||
case "tel":
|
||||
return <Type className="h-4 w-4 text-blue-600" />;
|
||||
case "number":
|
||||
case "decimal":
|
||||
return <Hash className="h-4 w-4 text-green-600" />;
|
||||
case "date":
|
||||
case "datetime":
|
||||
return <Calendar className="h-4 w-4 text-purple-600" />;
|
||||
case "select":
|
||||
case "dropdown":
|
||||
return <List className="h-4 w-4 text-orange-600" />;
|
||||
case "textarea":
|
||||
case "text_area":
|
||||
return <AlignLeft className="h-4 w-4 text-indigo-600" />;
|
||||
case "boolean":
|
||||
case "checkbox":
|
||||
return <CheckSquare className="h-4 w-4 text-blue-600" />;
|
||||
case "radio":
|
||||
return <Radio className="h-4 w-4 text-blue-600" />;
|
||||
case "code":
|
||||
return <Code className="h-4 w-4 text-gray-600" />;
|
||||
case "entity":
|
||||
return <Building className="h-4 w-4 text-cyan-600" />;
|
||||
case "file":
|
||||
return <File className="h-4 w-4 text-yellow-600" />;
|
||||
default:
|
||||
return <Type className="h-4 w-4 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||
component,
|
||||
isSelected = false,
|
||||
onClick,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
}) => {
|
||||
const { type, label, tableName, columnName, widgetType, size, style } = component;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute rounded border-2 transition-all ${
|
||||
isSelected ? "border-blue-500 bg-blue-50 shadow-lg" : "border-gray-300 bg-white hover:border-gray-400"
|
||||
}`}
|
||||
style={{
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${size.width * 80 - 16}px`,
|
||||
height: `${size.height}px`,
|
||||
...style,
|
||||
}}
|
||||
onClick={onClick}
|
||||
draggable
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
{type === "container" && (
|
||||
<div className="flex h-full flex-col items-center justify-center p-4">
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<Database className="h-8 w-8 text-blue-600" />
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-medium">{label}</div>
|
||||
<div className="text-xs text-gray-500">{tableName}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === "widget" && (
|
||||
<div className="flex h-full flex-col p-3">
|
||||
{/* 위젯 헤더 */}
|
||||
<div className="mb-2 flex items-center space-x-2">
|
||||
{getWidgetIcon(widgetType)}
|
||||
<div className="flex-1">
|
||||
<Label className="text-sm font-medium">
|
||||
{label || columnName}
|
||||
{component.required && <span className="ml-1 text-red-500">*</span>}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 위젯 본체 */}
|
||||
<div className="flex-1">{renderWidget(component)}</div>
|
||||
|
||||
{/* 위젯 정보 */}
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
{columnName} ({widgetType})
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RealtimePreview;
|
||||
|
|
@ -25,9 +25,12 @@ import {
|
|||
Table,
|
||||
Settings,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Code,
|
||||
Building,
|
||||
File,
|
||||
List,
|
||||
AlignLeft,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
ScreenDefinition,
|
||||
|
|
@ -53,6 +56,7 @@ import StyleEditor from "./StyleEditor";
|
|||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { RealtimePreview } from "./RealtimePreview";
|
||||
|
||||
interface ScreenDesignerProps {
|
||||
selectedScreen: ScreenDefinition | null;
|
||||
|
|
@ -226,6 +230,30 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
dataType: "BOOLEAN",
|
||||
isNullable: "NO",
|
||||
},
|
||||
{
|
||||
tableName: "user_info",
|
||||
columnName: "profile_code",
|
||||
columnLabel: "프로필 코드",
|
||||
webType: "code",
|
||||
dataType: "TEXT",
|
||||
isNullable: "YES",
|
||||
},
|
||||
{
|
||||
tableName: "user_info",
|
||||
columnName: "department",
|
||||
columnLabel: "부서",
|
||||
webType: "entity",
|
||||
dataType: "VARCHAR",
|
||||
isNullable: "YES",
|
||||
},
|
||||
{
|
||||
tableName: "user_info",
|
||||
columnName: "profile_image",
|
||||
columnLabel: "프로필 이미지",
|
||||
webType: "file",
|
||||
dataType: "VARCHAR",
|
||||
isNullable: "YES",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -353,29 +381,44 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
|
||||
// 웹타입에 따른 위젯 타입 매핑
|
||||
const getWidgetTypeFromWebType = useCallback((webType: string): string => {
|
||||
console.log("getWidgetTypeFromWebType - input webType:", webType);
|
||||
switch (webType) {
|
||||
case "text":
|
||||
case "email":
|
||||
case "tel":
|
||||
return "text";
|
||||
case "email":
|
||||
return "email";
|
||||
case "tel":
|
||||
return "tel";
|
||||
case "number":
|
||||
case "decimal":
|
||||
return "number";
|
||||
case "decimal":
|
||||
return "decimal";
|
||||
case "date":
|
||||
case "datetime":
|
||||
return "date";
|
||||
case "datetime":
|
||||
return "datetime";
|
||||
case "select":
|
||||
case "dropdown":
|
||||
return "select";
|
||||
case "dropdown":
|
||||
return "dropdown";
|
||||
case "textarea":
|
||||
case "text_area":
|
||||
return "textarea";
|
||||
case "text_area":
|
||||
return "text_area";
|
||||
case "checkbox":
|
||||
case "boolean":
|
||||
return "checkbox";
|
||||
case "boolean":
|
||||
return "boolean";
|
||||
case "radio":
|
||||
return "radio";
|
||||
case "code":
|
||||
return "code";
|
||||
case "entity":
|
||||
return "entity";
|
||||
case "file":
|
||||
return "file";
|
||||
default:
|
||||
console.log("getWidgetTypeFromWebType - default case, returning text for:", webType);
|
||||
return "text";
|
||||
}
|
||||
}, []);
|
||||
|
|
@ -682,31 +725,41 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
key={column.columnName}
|
||||
className="flex cursor-pointer items-center space-x-2 p-2 pl-6 hover:bg-gray-100"
|
||||
draggable
|
||||
onDragStart={(e) =>
|
||||
onDragStart={(e) => {
|
||||
console.log("Drag start - column:", column.columnName, "webType:", column.webType);
|
||||
const widgetType = getWidgetTypeFromWebType(column.webType || "text");
|
||||
console.log("Drag start - widgetType:", widgetType);
|
||||
startDrag(
|
||||
{
|
||||
type: "widget",
|
||||
tableName: table.tableName,
|
||||
columnName: column.columnName,
|
||||
widgetType: getWidgetTypeFromWebType(column.webType || "text"),
|
||||
widgetType: widgetType as WebType,
|
||||
label: column.columnLabel || column.columnName,
|
||||
size: { width: 6, height: 60 },
|
||||
},
|
||||
e,
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{column.webType === "text" && <Type className="h-3 w-3 text-blue-600" />}
|
||||
{column.webType === "email" && <Type className="h-3 w-3 text-blue-600" />}
|
||||
{column.webType === "tel" && <Type className="h-3 w-3 text-blue-600" />}
|
||||
{column.webType === "number" && <Hash className="h-3 w-3 text-green-600" />}
|
||||
{column.webType === "decimal" && <Hash className="h-3 w-3 text-green-600" />}
|
||||
{column.webType === "date" && <Calendar className="h-3 w-3 text-purple-600" />}
|
||||
{column.webType === "datetime" && <Calendar className="h-3 w-3 text-purple-600" />}
|
||||
{column.webType === "select" && <List className="h-3 w-3 text-orange-600" />}
|
||||
{column.webType === "dropdown" && <List className="h-3 w-3 text-orange-600" />}
|
||||
{column.webType === "textarea" && <AlignLeft className="h-3 w-3 text-indigo-600" />}
|
||||
{column.webType === "text_area" && <AlignLeft className="h-3 w-3 text-indigo-600" />}
|
||||
{column.webType === "checkbox" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
||||
{column.webType === "boolean" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
||||
{column.webType === "radio" && <Radio className="h-3 w-3 text-blue-600" />}
|
||||
{!["text", "number", "date", "select", "textarea", "checkbox", "radio"].includes(
|
||||
column.webType,
|
||||
) && <Type className="h-3 w-3 text-blue-600" />}
|
||||
{column.webType === "code" && <Code className="h-3 w-3 text-gray-600" />}
|
||||
{column.webType === "entity" && <Building className="h-3 w-3 text-cyan-600" />}
|
||||
{column.webType === "file" && <File className="h-3 w-3 text-yellow-600" />}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{column.columnLabel || column.columnName}</div>
|
||||
|
|
@ -777,56 +830,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 컴포넌트들 */}
|
||||
{/* 컴포넌트들 - 실시간 미리보기 */}
|
||||
{layout.components.map((component) => (
|
||||
<div
|
||||
<RealtimePreview
|
||||
key={component.id}
|
||||
className={`absolute cursor-move rounded border-2 ${
|
||||
selectedComponent?.id === component.id
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-300 bg-white hover:border-gray-400"
|
||||
}`}
|
||||
style={{
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${component.size.width * 80 - 16}px`,
|
||||
height: `${component.size.height}px`,
|
||||
}}
|
||||
component={component}
|
||||
isSelected={selectedComponent?.id === component.id}
|
||||
onClick={() => handleComponentClick(component)}
|
||||
draggable
|
||||
onDragStart={(e) => startComponentDrag(component, e)}
|
||||
onDragEnd={endDrag}
|
||||
>
|
||||
<div className="flex h-full items-center justify-center p-2">
|
||||
{component.type === "container" && (
|
||||
<div className="flex flex-col items-center space-y-1">
|
||||
<Database className="h-6 w-6 text-blue-600" />
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-medium">{component.label}</div>
|
||||
<div className="text-xs text-gray-500">{component.tableName}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{component.type === "widget" && (
|
||||
<div className="flex flex-col items-center space-y-1">
|
||||
{component.widgetType === "text" && <Type className="h-6 w-6 text-blue-600" />}
|
||||
{component.widgetType === "number" && <Hash className="h-6 w-6 text-green-600" />}
|
||||
{component.widgetType === "date" && <Calendar className="h-6 w-6 text-purple-600" />}
|
||||
{component.widgetType === "select" && <List className="h-6 w-6 text-orange-600" />}
|
||||
{component.widgetType === "textarea" && <AlignLeft className="h-6 w-6 text-indigo-600" />}
|
||||
{component.widgetType === "checkbox" && <CheckSquare className="h-6 w-6 text-blue-600" />}
|
||||
{component.widgetType === "radio" && <Radio className="h-6 w-6 text-blue-600" />}
|
||||
{!["text", "number", "date", "select", "textarea", "checkbox", "radio"].includes(
|
||||
component.widgetType || "text",
|
||||
) && <Type className="h-6 w-6 text-blue-600" />}
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-medium">{component.label}</div>
|
||||
<div className="text-xs text-gray-500">{component.columnName}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
month: "space-y-4",
|
||||
caption: "flex justify-center pt-1 relative items-center",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "space-x-1 flex items-center",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex",
|
||||
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"),
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
||||
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Calendar.displayName = "Calendar";
|
||||
|
||||
export { Calendar };
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<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(
|
||||
"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 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-primary text-primary ring-offset-background focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -16,10 +16,13 @@
|
|||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
|
|
@ -32,6 +35,7 @@
|
|||
"lucide-react": "^0.525.0",
|
||||
"next": "15.4.4",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.9.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.61.1",
|
||||
"sonner": "^2.0.7",
|
||||
|
|
|
|||
Loading…
Reference in New Issue