@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.
Files changed (147) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/components.json +21 -0
  5. package/package.json +108 -0
  6. package/scripts/audit-base-ui-props.mjs +179 -0
  7. package/scripts/audit-tailwind-typography.mjs +100 -0
  8. package/src/components/design-system/design-system-showcase.tsx +138 -0
  9. package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
  10. package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
  11. package/src/components/design-system/sections/showcase-forms.tsx +373 -0
  12. package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
  13. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
  14. package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
  15. package/src/components/design-system/sections/showcase-overview.tsx +368 -0
  16. package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
  17. package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
  18. package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
  19. package/src/components/shared/button/add-new-button.tsx +19 -0
  20. package/src/components/shared/button/base/action-button.tsx +134 -0
  21. package/src/components/shared/button/base/link-button.tsx +130 -0
  22. package/src/components/shared/button/cancel-button.tsx +27 -0
  23. package/src/components/shared/button/confirm-button.tsx +26 -0
  24. package/src/components/shared/button/delete-button.tsx +36 -0
  25. package/src/components/shared/button/floating-save-button.tsx +89 -0
  26. package/src/components/shared/button/index.ts +13 -0
  27. package/src/components/shared/button/print-button.tsx +67 -0
  28. package/src/components/shared/button/refresh-button.tsx +63 -0
  29. package/src/components/shared/button/save-button.tsx +29 -0
  30. package/src/components/shared/button/submit-button.tsx +58 -0
  31. package/src/components/shared/client-redirect.tsx +11 -0
  32. package/src/components/shared/combobox.tsx +116 -0
  33. package/src/components/shared/data-list.tsx +65 -0
  34. package/src/components/shared/date/date-range-picker.tsx +106 -0
  35. package/src/components/shared/description-list.tsx +27 -0
  36. package/src/components/shared/empty-row.tsx +13 -0
  37. package/src/components/shared/empty-state.tsx +31 -0
  38. package/src/components/shared/env-snippet.tsx +62 -0
  39. package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
  40. package/src/components/shared/form/form-field-boolean.tsx +93 -0
  41. package/src/components/shared/form/form-field-color-select.tsx +86 -0
  42. package/src/components/shared/form/form-field-combobox.tsx +167 -0
  43. package/src/components/shared/form/form-field-date.tsx +55 -0
  44. package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
  45. package/src/components/shared/form/form-field-input.tsx +372 -0
  46. package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
  47. package/src/components/shared/form/form-field-radio-group.tsx +56 -0
  48. package/src/components/shared/form/form-field-select.tsx +312 -0
  49. package/src/components/shared/form/simple-select.tsx +152 -0
  50. package/src/components/shared/form-dialog.tsx +109 -0
  51. package/src/components/shared/form-message.tsx +30 -0
  52. package/src/components/shared/initials.tsx +33 -0
  53. package/src/components/shared/layout/error-state.tsx +160 -0
  54. package/src/components/shared/layout/page-shell.tsx +57 -0
  55. package/src/components/shared/link/transition-link.tsx +72 -0
  56. package/src/components/shared/multi-select-combobox.tsx +392 -0
  57. package/src/components/shared/name-id-fields.tsx +133 -0
  58. package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
  59. package/src/components/shared/notice.tsx +35 -0
  60. package/src/components/shared/number/formatted-number.tsx +133 -0
  61. package/src/components/shared/page-header.tsx +25 -0
  62. package/src/components/shared/page-skeleton.tsx +18 -0
  63. package/src/components/shared/row-actions.tsx +93 -0
  64. package/src/components/shared/row-menu.tsx +55 -0
  65. package/src/components/shared/secret-reveal.tsx +53 -0
  66. package/src/components/shared/section-card.tsx +44 -0
  67. package/src/components/shared/stat.tsx +26 -0
  68. package/src/components/shared/status-badge.tsx +47 -0
  69. package/src/components/shared/text/primary-secondary-text.tsx +186 -0
  70. package/src/components/shared/theme/index.ts +4 -0
  71. package/src/components/shared/theme/theme-menu-items.tsx +43 -0
  72. package/src/components/shared/theme/theme-provider.tsx +39 -0
  73. package/src/components/shared/theme/theme-switch.tsx +36 -0
  74. package/src/components/shared/theme/theme-toggle.tsx +76 -0
  75. package/src/components/shared/time/client-date-formatter.tsx +188 -0
  76. package/src/components/shared/ui-text.tsx +47 -0
  77. package/src/components/ui/__tests__/combobox.test.tsx +40 -0
  78. package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
  79. package/src/components/ui/__tests__/slider.test.tsx +18 -0
  80. package/src/components/ui/__tests__/tabs.test.tsx +33 -0
  81. package/src/components/ui/accordion.tsx +69 -0
  82. package/src/components/ui/alert-dialog.tsx +161 -0
  83. package/src/components/ui/alert.tsx +68 -0
  84. package/src/components/ui/aspect-ratio.tsx +22 -0
  85. package/src/components/ui/attachment.tsx +194 -0
  86. package/src/components/ui/avatar.tsx +92 -0
  87. package/src/components/ui/badge.tsx +48 -0
  88. package/src/components/ui/breadcrumb.tsx +102 -0
  89. package/src/components/ui/button-group.tsx +77 -0
  90. package/src/components/ui/button.tsx +57 -0
  91. package/src/components/ui/calendar.tsx +184 -0
  92. package/src/components/ui/card.tsx +87 -0
  93. package/src/components/ui/carousel.tsx +230 -0
  94. package/src/components/ui/chart.tsx +338 -0
  95. package/src/components/ui/checkbox.tsx +27 -0
  96. package/src/components/ui/collapsible.tsx +17 -0
  97. package/src/components/ui/combobox.tsx +272 -0
  98. package/src/components/ui/command.tsx +180 -0
  99. package/src/components/ui/context-menu.tsx +244 -0
  100. package/src/components/ui/date-picker.tsx +308 -0
  101. package/src/components/ui/dialog.tsx +137 -0
  102. package/src/components/ui/drawer.tsx +208 -0
  103. package/src/components/ui/dropdown-menu.tsx +257 -0
  104. package/src/components/ui/empty.tsx +93 -0
  105. package/src/components/ui/field.tsx +223 -0
  106. package/src/components/ui/form.tsx +161 -0
  107. package/src/components/ui/hover-card.tsx +46 -0
  108. package/src/components/ui/input-group.tsx +145 -0
  109. package/src/components/ui/input-otp.tsx +85 -0
  110. package/src/components/ui/input.tsx +19 -0
  111. package/src/components/ui/item.tsx +187 -0
  112. package/src/components/ui/kbd.tsx +26 -0
  113. package/src/components/ui/label.tsx +19 -0
  114. package/src/components/ui/menubar.tsx +266 -0
  115. package/src/components/ui/navigation-menu.tsx +160 -0
  116. package/src/components/ui/pagination.tsx +117 -0
  117. package/src/components/ui/popover.tsx +76 -0
  118. package/src/components/ui/progress.tsx +66 -0
  119. package/src/components/ui/radio-group.tsx +38 -0
  120. package/src/components/ui/relative-date.tsx +40 -0
  121. package/src/components/ui/resizable.tsx +41 -0
  122. package/src/components/ui/scroll-area.tsx +67 -0
  123. package/src/components/ui/searchable-select.tsx +124 -0
  124. package/src/components/ui/select.tsx +189 -0
  125. package/src/components/ui/separator.tsx +21 -0
  126. package/src/components/ui/sheet.tsx +124 -0
  127. package/src/components/ui/sidebar.tsx +691 -0
  128. package/src/components/ui/skeleton.tsx +13 -0
  129. package/src/components/ui/slider.tsx +55 -0
  130. package/src/components/ui/sonner.tsx +45 -0
  131. package/src/components/ui/spinner.tsx +16 -0
  132. package/src/components/ui/switch.tsx +32 -0
  133. package/src/components/ui/table.tsx +95 -0
  134. package/src/components/ui/tabs.tsx +79 -0
  135. package/src/components/ui/textarea.tsx +17 -0
  136. package/src/components/ui/toggle-group.tsx +86 -0
  137. package/src/components/ui/toggle.tsx +44 -0
  138. package/src/components/ui/tooltip.tsx +54 -0
  139. package/src/hooks/use-copy-to-clipboard.ts +27 -0
  140. package/src/hooks/use-mobile.ts +21 -0
  141. package/src/index.ts +156 -0
  142. package/src/lib/formatters.ts +44 -0
  143. package/src/lib/permission.ts +75 -0
  144. package/src/lib/status-colors.ts +52 -0
  145. package/src/lib/utils.ts +6 -0
  146. package/src/next.ts +15 -0
  147. 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
+ }