@suphark/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/components.json +21 -0
- package/package.json +108 -0
- package/scripts/audit-base-ui-props.mjs +179 -0
- package/scripts/audit-tailwind-typography.mjs +100 -0
- package/src/components/design-system/design-system-showcase.tsx +138 -0
- package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
- package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
- package/src/components/design-system/sections/showcase-forms.tsx +373 -0
- package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
- package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
- package/src/components/design-system/sections/showcase-overview.tsx +368 -0
- package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
- package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
- package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
- package/src/components/shared/button/add-new-button.tsx +19 -0
- package/src/components/shared/button/base/action-button.tsx +134 -0
- package/src/components/shared/button/base/link-button.tsx +130 -0
- package/src/components/shared/button/cancel-button.tsx +27 -0
- package/src/components/shared/button/confirm-button.tsx +26 -0
- package/src/components/shared/button/delete-button.tsx +36 -0
- package/src/components/shared/button/floating-save-button.tsx +89 -0
- package/src/components/shared/button/index.ts +13 -0
- package/src/components/shared/button/print-button.tsx +67 -0
- package/src/components/shared/button/refresh-button.tsx +63 -0
- package/src/components/shared/button/save-button.tsx +29 -0
- package/src/components/shared/button/submit-button.tsx +58 -0
- package/src/components/shared/client-redirect.tsx +11 -0
- package/src/components/shared/combobox.tsx +116 -0
- package/src/components/shared/data-list.tsx +65 -0
- package/src/components/shared/date/date-range-picker.tsx +106 -0
- package/src/components/shared/description-list.tsx +27 -0
- package/src/components/shared/empty-row.tsx +13 -0
- package/src/components/shared/empty-state.tsx +31 -0
- package/src/components/shared/env-snippet.tsx +62 -0
- package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
- package/src/components/shared/form/form-field-boolean.tsx +93 -0
- package/src/components/shared/form/form-field-color-select.tsx +86 -0
- package/src/components/shared/form/form-field-combobox.tsx +167 -0
- package/src/components/shared/form/form-field-date.tsx +55 -0
- package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
- package/src/components/shared/form/form-field-input.tsx +372 -0
- package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
- package/src/components/shared/form/form-field-radio-group.tsx +56 -0
- package/src/components/shared/form/form-field-select.tsx +312 -0
- package/src/components/shared/form/simple-select.tsx +152 -0
- package/src/components/shared/form-dialog.tsx +109 -0
- package/src/components/shared/form-message.tsx +30 -0
- package/src/components/shared/initials.tsx +33 -0
- package/src/components/shared/layout/error-state.tsx +160 -0
- package/src/components/shared/layout/page-shell.tsx +57 -0
- package/src/components/shared/link/transition-link.tsx +72 -0
- package/src/components/shared/multi-select-combobox.tsx +392 -0
- package/src/components/shared/name-id-fields.tsx +133 -0
- package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
- package/src/components/shared/notice.tsx +35 -0
- package/src/components/shared/number/formatted-number.tsx +133 -0
- package/src/components/shared/page-header.tsx +25 -0
- package/src/components/shared/page-skeleton.tsx +18 -0
- package/src/components/shared/row-actions.tsx +93 -0
- package/src/components/shared/row-menu.tsx +55 -0
- package/src/components/shared/secret-reveal.tsx +53 -0
- package/src/components/shared/section-card.tsx +44 -0
- package/src/components/shared/stat.tsx +26 -0
- package/src/components/shared/status-badge.tsx +47 -0
- package/src/components/shared/text/primary-secondary-text.tsx +186 -0
- package/src/components/shared/theme/index.ts +4 -0
- package/src/components/shared/theme/theme-menu-items.tsx +43 -0
- package/src/components/shared/theme/theme-provider.tsx +39 -0
- package/src/components/shared/theme/theme-switch.tsx +36 -0
- package/src/components/shared/theme/theme-toggle.tsx +76 -0
- package/src/components/shared/time/client-date-formatter.tsx +188 -0
- package/src/components/shared/ui-text.tsx +47 -0
- package/src/components/ui/__tests__/combobox.test.tsx +40 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
- package/src/components/ui/__tests__/slider.test.tsx +18 -0
- package/src/components/ui/__tests__/tabs.test.tsx +33 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +161 -0
- package/src/components/ui/alert.tsx +68 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +194 -0
- package/src/components/ui/avatar.tsx +92 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +102 -0
- package/src/components/ui/button-group.tsx +77 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +184 -0
- package/src/components/ui/card.tsx +87 -0
- package/src/components/ui/carousel.tsx +230 -0
- package/src/components/ui/chart.tsx +338 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +17 -0
- package/src/components/ui/combobox.tsx +272 -0
- package/src/components/ui/command.tsx +180 -0
- package/src/components/ui/context-menu.tsx +244 -0
- package/src/components/ui/date-picker.tsx +308 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +208 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +93 -0
- package/src/components/ui/field.tsx +223 -0
- package/src/components/ui/form.tsx +161 -0
- package/src/components/ui/hover-card.tsx +46 -0
- package/src/components/ui/input-group.tsx +145 -0
- package/src/components/ui/input-otp.tsx +85 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +187 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/menubar.tsx +266 -0
- package/src/components/ui/navigation-menu.tsx +160 -0
- package/src/components/ui/pagination.tsx +117 -0
- package/src/components/ui/popover.tsx +76 -0
- package/src/components/ui/progress.tsx +66 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/relative-date.tsx +40 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/scroll-area.tsx +67 -0
- package/src/components/ui/searchable-select.tsx +124 -0
- package/src/components/ui/select.tsx +189 -0
- package/src/components/ui/separator.tsx +21 -0
- package/src/components/ui/sheet.tsx +124 -0
- package/src/components/ui/sidebar.tsx +691 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +55 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +95 -0
- package/src/components/ui/tabs.tsx +79 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +54 -0
- package/src/hooks/use-copy-to-clipboard.ts +27 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/index.ts +156 -0
- package/src/lib/formatters.ts +44 -0
- package/src/lib/permission.ts +75 -0
- package/src/lib/status-colors.ts +52 -0
- package/src/lib/utils.ts +6 -0
- package/src/next.ts +15 -0
- package/src/styles.css +214 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FormControl,
|
|
3
|
+
FormField,
|
|
4
|
+
FormItem,
|
|
5
|
+
FormLabel,
|
|
6
|
+
FormMessage,
|
|
7
|
+
} from "@suphark/ui/components/ui/form";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "@suphark/ui/components/ui/select";
|
|
15
|
+
// --- START: CODE TO EDIT ---
|
|
16
|
+
// [EDIT] 👈 Import TAILWIND_COLORS เข้ามาโดยตรง
|
|
17
|
+
import { getStatusColorTheme, TAILWIND_COLORS } from "@suphark/ui/lib/status-colors";
|
|
18
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
19
|
+
import type { ComponentProps } from "react";
|
|
20
|
+
import type { Control, FieldValues, Path } from "react-hook-form";
|
|
21
|
+
|
|
22
|
+
// --- END: CODE TO EDIT ---
|
|
23
|
+
|
|
24
|
+
// [DELETE] 👈 ไม่จำเป็นต้องใช้ type นี้จากภายนอกแล้ว
|
|
25
|
+
// type SelectOption = {
|
|
26
|
+
// value: string;
|
|
27
|
+
// label: string;
|
|
28
|
+
// };
|
|
29
|
+
|
|
30
|
+
interface FormFieldColorSelectProps<T extends FieldValues> extends ComponentProps<typeof Select> {
|
|
31
|
+
control: Control<T>;
|
|
32
|
+
name: Path<T>;
|
|
33
|
+
label: string;
|
|
34
|
+
// [DELETE] 👈 เอา options prop ออก
|
|
35
|
+
// options: SelectOption[];
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function FormFieldColorSelect<T extends FieldValues>({
|
|
40
|
+
control,
|
|
41
|
+
name,
|
|
42
|
+
label,
|
|
43
|
+
placeholder,
|
|
44
|
+
...props
|
|
45
|
+
}: FormFieldColorSelectProps<T>) {
|
|
46
|
+
const colorOptions = TAILWIND_COLORS.map((color) => ({
|
|
47
|
+
value: color.name,
|
|
48
|
+
label: color.label,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<FormField
|
|
53
|
+
control={control}
|
|
54
|
+
name={name}
|
|
55
|
+
render={({ field }) => (
|
|
56
|
+
<FormItem>
|
|
57
|
+
<FormLabel>
|
|
58
|
+
{label} {props.required && <span className="text-red-500">*</span>}
|
|
59
|
+
</FormLabel>
|
|
60
|
+
<Select onValueChange={field.onChange} value={field.value} {...props}>
|
|
61
|
+
<FormControl>
|
|
62
|
+
<SelectTrigger>
|
|
63
|
+
<SelectValue placeholder={placeholder} />
|
|
64
|
+
</SelectTrigger>
|
|
65
|
+
</FormControl>
|
|
66
|
+
<SelectContent>
|
|
67
|
+
{/* ใช้ colorOptions ที่สร้างขึ้นภายใน */}
|
|
68
|
+
{colorOptions.map((option) => {
|
|
69
|
+
const theme = getStatusColorTheme(option.value);
|
|
70
|
+
return (
|
|
71
|
+
<SelectItem key={option.value} value={option.value}>
|
|
72
|
+
<div className="flex items-center gap-2">
|
|
73
|
+
<span className={cn("h-4 w-4 rounded-full", theme.background)} />
|
|
74
|
+
<span>{option.label}</span>
|
|
75
|
+
</div>
|
|
76
|
+
</SelectItem>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</SelectContent>
|
|
80
|
+
</Select>
|
|
81
|
+
<FormMessage />
|
|
82
|
+
</FormItem>
|
|
83
|
+
)}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Command,
|
|
6
|
+
CommandEmpty,
|
|
7
|
+
CommandGroup,
|
|
8
|
+
CommandInput,
|
|
9
|
+
CommandItem,
|
|
10
|
+
CommandList,
|
|
11
|
+
} from "@suphark/ui/components/ui/command";
|
|
12
|
+
import {
|
|
13
|
+
FormControl,
|
|
14
|
+
FormField,
|
|
15
|
+
FormItem,
|
|
16
|
+
FormLabel,
|
|
17
|
+
FormMessage,
|
|
18
|
+
} from "@suphark/ui/components/ui/form";
|
|
19
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
|
|
20
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
21
|
+
import { Check, ChevronsUpDown } from "lucide-react";
|
|
22
|
+
import * as React from "react";
|
|
23
|
+
import { useEffect, useState, useTransition } from "react";
|
|
24
|
+
import type { FieldValues, Path } from "react-hook-form";
|
|
25
|
+
|
|
26
|
+
interface FormFieldComboboxProps<T extends FieldValues> {
|
|
27
|
+
control: any; // Using any to bypass deep generic reconciliation errors with Control<T, any> in RHF 7
|
|
28
|
+
name: Path<T>;
|
|
29
|
+
label: string;
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
fetcher: () => Promise<{ label: string; value: string; subLabel?: string; [key: string]: any }[]>;
|
|
32
|
+
/**
|
|
33
|
+
* คีย์สำหรับแคชและตัดสินว่าต้องโหลดใหม่หรือไม่
|
|
34
|
+
* จำเป็นเมื่อส่ง fetcher เป็น arrow function แบบ inline (identity เปลี่ยนทุก render)
|
|
35
|
+
* ถ้าไม่ส่ง จะใช้ตัว fetcher เองเป็นคีย์ตามเดิม
|
|
36
|
+
*/
|
|
37
|
+
fetcherKey?: string;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
className?: string;
|
|
41
|
+
autoFocus?: boolean;
|
|
42
|
+
onSelect?: (option: { label: string; value: string; [key: string]: any }) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Singleton client-side cache to persist combobox options across mounts/renders
|
|
46
|
+
const comboboxCache = new Map<any, { label: string; value: string; subLabel?: string }[]>();
|
|
47
|
+
|
|
48
|
+
export function FormFieldCombobox<T extends FieldValues>({
|
|
49
|
+
control,
|
|
50
|
+
name,
|
|
51
|
+
label,
|
|
52
|
+
placeholder = "Select...",
|
|
53
|
+
fetcher,
|
|
54
|
+
fetcherKey,
|
|
55
|
+
disabled,
|
|
56
|
+
required,
|
|
57
|
+
className,
|
|
58
|
+
autoFocus,
|
|
59
|
+
onSelect,
|
|
60
|
+
}: FormFieldComboboxProps<T>) {
|
|
61
|
+
const cacheKey = fetcherKey ?? fetcher;
|
|
62
|
+
const [options, setOptions] = useState<{ label: string; value: string; subLabel?: string }[]>(
|
|
63
|
+
() => comboboxCache.get(cacheKey) || [],
|
|
64
|
+
);
|
|
65
|
+
const [isLoading, startTransition] = useTransition();
|
|
66
|
+
const [open, setOpen] = React.useState(false);
|
|
67
|
+
|
|
68
|
+
// เก็บ fetcher ล่าสุดไว้ใน ref เพื่อไม่ให้ arrow function แบบ inline
|
|
69
|
+
// (identity ใหม่ทุก render) ทำให้ยิงโหลดซ้ำทุกครั้งที่ฟอร์ม re-render
|
|
70
|
+
const fetcherRef = React.useRef(fetcher);
|
|
71
|
+
fetcherRef.current = fetcher;
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
// แสดงค่าจากแคชก่อนเพื่อลดการกระพริบ แล้วค่อยโหลดค่าล่าสุดทับ
|
|
75
|
+
const cached = comboboxCache.get(cacheKey);
|
|
76
|
+
if (cached) setOptions(cached);
|
|
77
|
+
|
|
78
|
+
startTransition(async () => {
|
|
79
|
+
try {
|
|
80
|
+
const data = await fetcherRef.current();
|
|
81
|
+
setOptions(data);
|
|
82
|
+
comboboxCache.set(cacheKey, data);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error("Failed to fetch combobox options:", error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}, [cacheKey]);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<FormField
|
|
91
|
+
control={control}
|
|
92
|
+
name={name}
|
|
93
|
+
render={({ field }) => (
|
|
94
|
+
<FormItem className={cn("flex flex-col gap-1.5", className)}>
|
|
95
|
+
<FormLabel>
|
|
96
|
+
{label} {required && <span className="text-red-500">*</span>}
|
|
97
|
+
</FormLabel>
|
|
98
|
+
<FormControl>
|
|
99
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
100
|
+
<PopoverTrigger
|
|
101
|
+
render={
|
|
102
|
+
<Button
|
|
103
|
+
variant="outline"
|
|
104
|
+
role="combobox"
|
|
105
|
+
aria-expanded={open}
|
|
106
|
+
className={cn(
|
|
107
|
+
"w-full justify-between font-normal",
|
|
108
|
+
!field.value && "text-muted-foreground",
|
|
109
|
+
)}
|
|
110
|
+
disabled={disabled}
|
|
111
|
+
autoFocus={autoFocus}
|
|
112
|
+
/>
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
<span className="flex-1 whitespace-normal text-left">
|
|
116
|
+
{field.value
|
|
117
|
+
? options.find((option) => option.value === field.value)?.label
|
|
118
|
+
: options.length === 0 && isLoading
|
|
119
|
+
? "Loading..."
|
|
120
|
+
: placeholder}
|
|
121
|
+
</span>
|
|
122
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
123
|
+
</PopoverTrigger>
|
|
124
|
+
<PopoverContent className="w-[--anchor-width] p-0">
|
|
125
|
+
<Command>
|
|
126
|
+
<CommandInput placeholder={`ค้นหา ${placeholder.toLowerCase()}...`} />
|
|
127
|
+
<CommandList>
|
|
128
|
+
<CommandEmpty>ไม่พบข้อมูล</CommandEmpty>
|
|
129
|
+
<CommandGroup>
|
|
130
|
+
{options.map((option) => (
|
|
131
|
+
<CommandItem
|
|
132
|
+
key={option.value}
|
|
133
|
+
value={`${option.label} ${option.subLabel || ""}`}
|
|
134
|
+
onSelect={() => {
|
|
135
|
+
field.onChange(option.value === field.value ? "" : option.value);
|
|
136
|
+
onSelect?.(option);
|
|
137
|
+
setOpen(false);
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<Check
|
|
141
|
+
className={cn(
|
|
142
|
+
"mr-2 h-4 w-4",
|
|
143
|
+
field.value === option.value ? "opacity-100" : "opacity-0",
|
|
144
|
+
)}
|
|
145
|
+
/>
|
|
146
|
+
<div className="flex flex-col">
|
|
147
|
+
<span className="font-medium text-sm">{option.label}</span>
|
|
148
|
+
{option.subLabel && (
|
|
149
|
+
<span className="text-muted-foreground text-xs leading-tight">
|
|
150
|
+
{option.subLabel}
|
|
151
|
+
</span>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</CommandItem>
|
|
155
|
+
))}
|
|
156
|
+
</CommandGroup>
|
|
157
|
+
</CommandList>
|
|
158
|
+
</Command>
|
|
159
|
+
</PopoverContent>
|
|
160
|
+
</Popover>
|
|
161
|
+
</FormControl>
|
|
162
|
+
<FormMessage />
|
|
163
|
+
</FormItem>
|
|
164
|
+
)}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DatePicker } from "@suphark/ui/components/ui/date-picker";
|
|
4
|
+
import {
|
|
5
|
+
FormControl,
|
|
6
|
+
FormField,
|
|
7
|
+
FormItem,
|
|
8
|
+
FormLabel,
|
|
9
|
+
FormMessage,
|
|
10
|
+
} from "@suphark/ui/components/ui/form";
|
|
11
|
+
import type { Control, FieldValues, Path } from "react-hook-form";
|
|
12
|
+
|
|
13
|
+
interface FormFieldDateProps<T extends FieldValues> {
|
|
14
|
+
control: Control<T>;
|
|
15
|
+
name: Path<T>;
|
|
16
|
+
label: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function FormFieldDate<T extends FieldValues>({
|
|
24
|
+
control,
|
|
25
|
+
name,
|
|
26
|
+
label,
|
|
27
|
+
placeholder = "เลือกวันที่...",
|
|
28
|
+
required,
|
|
29
|
+
disabled,
|
|
30
|
+
className,
|
|
31
|
+
}: FormFieldDateProps<T>) {
|
|
32
|
+
return (
|
|
33
|
+
<FormField
|
|
34
|
+
control={control}
|
|
35
|
+
name={name}
|
|
36
|
+
render={({ field }) => (
|
|
37
|
+
<FormItem className="flex flex-col">
|
|
38
|
+
<FormLabel>
|
|
39
|
+
{label} {required && <span className="text-red-500">*</span>}
|
|
40
|
+
</FormLabel>
|
|
41
|
+
<FormControl>
|
|
42
|
+
<DatePicker
|
|
43
|
+
value={field.value}
|
|
44
|
+
onChange={field.onChange}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
placeholder={placeholder}
|
|
47
|
+
className={className}
|
|
48
|
+
/>
|
|
49
|
+
</FormControl>
|
|
50
|
+
<FormMessage />
|
|
51
|
+
</FormItem>
|
|
52
|
+
)}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FormControl,
|
|
5
|
+
FormDescription,
|
|
6
|
+
FormField,
|
|
7
|
+
FormItem,
|
|
8
|
+
FormLabel,
|
|
9
|
+
FormMessage,
|
|
10
|
+
} from "@suphark/ui/components/ui/form";
|
|
11
|
+
import { Input } from "@suphark/ui/components/ui/input";
|
|
12
|
+
import type * as React from "react";
|
|
13
|
+
import type { Control, FieldValues, Path } from "react-hook-form"; // Removed unused imports
|
|
14
|
+
import {
|
|
15
|
+
type NumberFormatValues,
|
|
16
|
+
NumericFormat,
|
|
17
|
+
type NumericFormatProps,
|
|
18
|
+
} from "react-number-format";
|
|
19
|
+
|
|
20
|
+
// Get Input props type correctly
|
|
21
|
+
type ShadcnInputProps = React.ComponentPropsWithoutRef<typeof Input>;
|
|
22
|
+
|
|
23
|
+
// Define props based on NumericFormatProps and RHF needs
|
|
24
|
+
interface FormFieldFormattedNumberProps<TFieldValues extends FieldValues = FieldValues>
|
|
25
|
+
extends Omit<
|
|
26
|
+
NumericFormatProps<ShadcnInputProps>,
|
|
27
|
+
| "value"
|
|
28
|
+
| "defaultValue"
|
|
29
|
+
| "name"
|
|
30
|
+
| "form"
|
|
31
|
+
| "ref"
|
|
32
|
+
| "customInput"
|
|
33
|
+
| "getInputRef"
|
|
34
|
+
| "onValueChange"
|
|
35
|
+
| "onBlur"
|
|
36
|
+
| "type"
|
|
37
|
+
| "onChange"
|
|
38
|
+
| "valueIsNumericString"
|
|
39
|
+
> {
|
|
40
|
+
name: Path<TFieldValues>;
|
|
41
|
+
control: Control<TFieldValues>;
|
|
42
|
+
label?: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
required?: boolean;
|
|
45
|
+
inputClassName?: string;
|
|
46
|
+
formItemClassName?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function FormFieldFormattedNumber<TFieldValues extends FieldValues = FieldValues>({
|
|
50
|
+
name,
|
|
51
|
+
control,
|
|
52
|
+
label,
|
|
53
|
+
description,
|
|
54
|
+
required,
|
|
55
|
+
thousandSeparator = true,
|
|
56
|
+
decimalScale,
|
|
57
|
+
allowNegative = false,
|
|
58
|
+
prefix,
|
|
59
|
+
suffix,
|
|
60
|
+
inputClassName,
|
|
61
|
+
formItemClassName,
|
|
62
|
+
...numericFormatProps // Rest are valid NumericFormatProps passed down
|
|
63
|
+
}: FormFieldFormattedNumberProps<TFieldValues>) {
|
|
64
|
+
return (
|
|
65
|
+
<FormField
|
|
66
|
+
control={control}
|
|
67
|
+
name={name}
|
|
68
|
+
render={({ field: { onChange, onBlur, value, ref, ...fieldRest } }) => (
|
|
69
|
+
<FormItem className={formItemClassName}>
|
|
70
|
+
{label && (
|
|
71
|
+
<FormLabel>
|
|
72
|
+
{label}
|
|
73
|
+
{required && <span className="text-destructive">*</span>}
|
|
74
|
+
</FormLabel>
|
|
75
|
+
)}
|
|
76
|
+
<FormControl>
|
|
77
|
+
<NumericFormat
|
|
78
|
+
{...fieldRest}
|
|
79
|
+
getInputRef={ref}
|
|
80
|
+
value={value as string | number | null | undefined}
|
|
81
|
+
onValueChange={(values: NumberFormatValues) => {
|
|
82
|
+
onChange(values.floatValue ?? undefined);
|
|
83
|
+
}}
|
|
84
|
+
onBlur={onBlur}
|
|
85
|
+
thousandSeparator={thousandSeparator}
|
|
86
|
+
decimalScale={decimalScale}
|
|
87
|
+
allowNegative={allowNegative}
|
|
88
|
+
prefix={prefix}
|
|
89
|
+
suffix={suffix}
|
|
90
|
+
{...numericFormatProps}
|
|
91
|
+
customInput={Input}
|
|
92
|
+
className={inputClassName}
|
|
93
|
+
valueIsNumericString={false}
|
|
94
|
+
allowLeadingZeros={false}
|
|
95
|
+
/>
|
|
96
|
+
</FormControl>
|
|
97
|
+
{description && <FormDescription>{description}</FormDescription>}
|
|
98
|
+
<FormMessage />
|
|
99
|
+
</FormItem>
|
|
100
|
+
)}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|