@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,38 @@
1
+ "use client";
2
+
3
+ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
4
+ import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
5
+
6
+ import { cn } from "@suphark/ui/lib/utils";
7
+
8
+ function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
9
+ return (
10
+ <RadioGroupPrimitive
11
+ data-slot="radio-group"
12
+ className={cn("grid w-full gap-2", className)}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
19
+ return (
20
+ <RadioPrimitive.Root
21
+ data-slot="radio-group-item"
22
+ className={cn(
23
+ "group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-[:focus-visible]/field-label:ring-0 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:bg-input/30 dark:data-checked:bg-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
24
+ className,
25
+ )}
26
+ {...props}
27
+ >
28
+ <RadioPrimitive.Indicator
29
+ data-slot="radio-group-indicator"
30
+ className="flex size-4 items-center justify-center"
31
+ >
32
+ <span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
33
+ </RadioPrimitive.Indicator>
34
+ </RadioPrimitive.Root>
35
+ );
36
+ }
37
+
38
+ export { RadioGroup, RadioGroupItem };
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { formatRelativeThaiDate, formatThaiFullDate } from "@suphark/ui/lib/formatters";
4
+ import { Calendar } from "lucide-react";
5
+
6
+ interface RelativeDateProps {
7
+ date: Date | string | number | null | undefined;
8
+ showIcon?: boolean;
9
+ className?: string;
10
+ iconClassName?: string;
11
+ }
12
+
13
+ export function RelativeDate({
14
+ date,
15
+ showIcon = true,
16
+ className = "flex items-center gap-0.5 text-slate-600 font-medium",
17
+ iconClassName = "h-2.5 w-2.5 text-slate-400 shrink-0",
18
+ }: RelativeDateProps) {
19
+ if (!date) {
20
+ return (
21
+ <span
22
+ className="flex items-center gap-0.5 font-normal text-slate-400 text-xs italic"
23
+ title="ยังไม่ได้ระบุวันที่อ้างอิงราคา (Price Reference Date)"
24
+ >
25
+ {showIcon && <Calendar className={iconClassName} />}
26
+ ไม่ได้ระบุ
27
+ </span>
28
+ );
29
+ }
30
+
31
+ const relativeText = formatRelativeThaiDate(date);
32
+ const fullText = formatThaiFullDate(date);
33
+
34
+ return (
35
+ <span className={className} title={`วันที่อ้างอิงราคาจริง: ${fullText}`}>
36
+ {showIcon && <Calendar className={iconClassName} />}
37
+ {relativeText}
38
+ </span>
39
+ );
40
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import { cn } from "@suphark/ui/lib/utils";
4
+ import * as ResizablePrimitive from "react-resizable-panels";
5
+
6
+ function ResizablePanelGroup({ className, ...props }: ResizablePrimitive.GroupProps) {
7
+ return (
8
+ <ResizablePrimitive.Group
9
+ data-slot="resizable-panel-group"
10
+ className={cn("flex h-full w-full aria-[orientation=vertical]:flex-col", className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
17
+ return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
18
+ }
19
+
20
+ function ResizableHandle({
21
+ withHandle,
22
+ className,
23
+ ...props
24
+ }: ResizablePrimitive.SeparatorProps & {
25
+ withHandle?: boolean;
26
+ }) {
27
+ return (
28
+ <ResizablePrimitive.Separator
29
+ data-slot="resizable-handle"
30
+ className={cn(
31
+ "relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {withHandle && <div className="z-10 flex h-6 w-1 shrink-0 rounded-lg bg-border" />}
37
+ </ResizablePrimitive.Separator>
38
+ );
39
+ }
40
+
41
+ export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
4
+
5
+ import { cn } from "@suphark/ui/lib/utils";
6
+
7
+ function ScrollArea({ className, children, ...props }: ScrollAreaPrimitive.Root.Props) {
8
+ return (
9
+ <ScrollAreaPrimitive.Root
10
+ data-slot="scroll-area"
11
+ className={cn("relative", className)}
12
+ {...props}
13
+ >
14
+ <ScrollAreaPrimitive.Viewport
15
+ data-slot="scroll-area-viewport"
16
+ className="size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:ring-ring/50"
17
+ >
18
+ {children}
19
+ </ScrollAreaPrimitive.Viewport>
20
+ <ScrollBar />
21
+ <ScrollAreaPrimitive.Corner />
22
+ </ScrollAreaPrimitive.Root>
23
+ );
24
+ }
25
+
26
+ function ScrollBar({
27
+ className,
28
+ orientation = "vertical",
29
+ ...props
30
+ }: ScrollAreaPrimitive.Scrollbar.Props) {
31
+ return (
32
+ <ScrollAreaPrimitive.Scrollbar
33
+ data-slot="scroll-area-scrollbar"
34
+ data-orientation={orientation}
35
+ orientation={orientation}
36
+ className={cn(
37
+ // Base UI ใส่ทิศทางไว้ที่ `data-orientation` ไม่ใช่ `data-vertical`/`data-horizontal`
38
+ // ของเดิมเขียนตาม Radix ทำให้คลาสความกว้างไม่เคยถูกใช้ แถบเลื่อนจึงกว้างแค่ 2px
39
+ // (เหลือแค่ padding) และหัวจับกว้าง 0 — มองไม่เห็นทั้งแอป
40
+ "group/scrollbar flex touch-none select-none p-px transition-colors",
41
+ "data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2.5",
42
+ "data-[orientation=vertical]:border-l data-[orientation=vertical]:border-l-transparent",
43
+ "data-[orientation=horizontal]:h-2.5 data-[orientation=horizontal]:flex-col",
44
+ "data-[orientation=horizontal]:border-t data-[orientation=horizontal]:border-t-transparent",
45
+ className,
46
+ )}
47
+ {...props}
48
+ >
49
+ {/*
50
+ Base UI ติด `data-hovering` ให้แถบเลื่อนเมื่อเมาส์อยู่เหนือพื้นที่เลื่อน
51
+ และ `data-scrolling` ระหว่างเลื่อน — ใช้สองตัวนี้เข้มสีหัวจับ
52
+ ปกติจางไว้ไม่ให้รบกวนสายตา แต่พอจะใช้งานจริงต้องเห็นชัดและจับได้ถนัด
53
+ */}
54
+ <ScrollAreaPrimitive.Thumb
55
+ data-slot="scroll-area-thumb"
56
+ className={cn(
57
+ "relative flex-1 rounded-full bg-border transition-colors",
58
+ "group-data-[hovering]/scrollbar:bg-muted-foreground/60",
59
+ "group-data-[scrolling]/scrollbar:bg-muted-foreground/60",
60
+ "hover:bg-muted-foreground/80 active:bg-muted-foreground",
61
+ )}
62
+ />
63
+ </ScrollAreaPrimitive.Scrollbar>
64
+ );
65
+ }
66
+
67
+ export { ScrollArea, ScrollBar };
@@ -0,0 +1,124 @@
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 { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
13
+ import { cn } from "@suphark/ui/lib/utils";
14
+ import { Check, ChevronsUpDown } from "lucide-react";
15
+ import { useState } from "react";
16
+
17
+ export interface SearchableSelectOption {
18
+ value: string;
19
+ label: string;
20
+ [key: string]: any;
21
+ }
22
+
23
+ interface SearchableSelectProps {
24
+ value: string | null | undefined;
25
+ onChange: (val: string | null) => void;
26
+ options: SearchableSelectOption[];
27
+ placeholder?: string;
28
+ searchPlaceholder?: string;
29
+ emptyText?: string;
30
+ clearLabel?: string;
31
+ allowClear?: boolean;
32
+ disabled?: boolean;
33
+ className?: string;
34
+ popoverWidth?: string;
35
+ }
36
+
37
+ export function SearchableSelect({
38
+ value,
39
+ onChange,
40
+ options,
41
+ placeholder = "เลือกรายการ...",
42
+ searchPlaceholder = "ค้นหา...",
43
+ emptyText = "ไม่พบข้อมูล",
44
+ clearLabel = "-- ไม่ระบุ (General) --",
45
+ allowClear = true,
46
+ disabled = false,
47
+ className,
48
+ popoverWidth = "w-[240px]",
49
+ }: SearchableSelectProps) {
50
+ const [open, setOpen] = useState(false);
51
+
52
+ const selectedOption = options.find((opt) => opt.value === value);
53
+
54
+ return (
55
+ <Popover open={open} onOpenChange={setOpen}>
56
+ <PopoverTrigger
57
+ render={
58
+ <Button
59
+ variant="outline"
60
+ role="combobox"
61
+ aria-expanded={open}
62
+ disabled={disabled}
63
+ className={cn(
64
+ "h-8 w-full justify-between bg-background font-normal text-xs",
65
+ className,
66
+ )}
67
+ />
68
+ }
69
+ >
70
+ <span className="truncate">{selectedOption ? selectedOption.label : placeholder}</span>
71
+ <ChevronsUpDown className="ml-1.5 h-3 w-3 shrink-0 opacity-50" />
72
+ </PopoverTrigger>
73
+ <PopoverContent className={cn("p-0", popoverWidth)} align="start">
74
+ <Command>
75
+ <CommandInput placeholder={searchPlaceholder} className="h-8 text-xs" />
76
+ <CommandList>
77
+ <CommandEmpty className="py-2 text-center text-muted-foreground text-xs">
78
+ {emptyText}
79
+ </CommandEmpty>
80
+ <CommandGroup>
81
+ {allowClear && (
82
+ <CommandItem
83
+ value="none_clear_option"
84
+ onSelect={() => {
85
+ onChange(null);
86
+ setOpen(false);
87
+ }}
88
+ className="font-medium text-muted-foreground text-xs"
89
+ >
90
+ <Check
91
+ className={cn(
92
+ "mr-2 h-3.5 w-3.5",
93
+ !value || value === "null" || value === "none" ? "opacity-100" : "opacity-0",
94
+ )}
95
+ />
96
+ {clearLabel}
97
+ </CommandItem>
98
+ )}
99
+ {options.map((option) => (
100
+ <CommandItem
101
+ key={option.value}
102
+ value={option.label}
103
+ onSelect={() => {
104
+ onChange(option.value);
105
+ setOpen(false);
106
+ }}
107
+ className="text-xs"
108
+ >
109
+ <Check
110
+ className={cn(
111
+ "mr-2 h-3.5 w-3.5",
112
+ value === option.value ? "opacity-100" : "opacity-0",
113
+ )}
114
+ />
115
+ <span className="truncate">{option.label}</span>
116
+ </CommandItem>
117
+ ))}
118
+ </CommandGroup>
119
+ </CommandList>
120
+ </Command>
121
+ </PopoverContent>
122
+ </Popover>
123
+ );
124
+ }
@@ -0,0 +1,189 @@
1
+ "use client";
2
+
3
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
4
+ import { cn } from "@suphark/ui/lib/utils";
5
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
6
+ import type * as React from "react";
7
+
8
+ const Select = SelectPrimitive.Root;
9
+
10
+ function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
11
+ return (
12
+ <SelectPrimitive.Group
13
+ data-slot="select-group"
14
+ className={cn("scroll-my-1 p-1", className)}
15
+ {...props}
16
+ />
17
+ );
18
+ }
19
+
20
+ function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
21
+ return (
22
+ <SelectPrimitive.Value
23
+ data-slot="select-value"
24
+ className={cn("flex flex-1 text-left", className)}
25
+ {...props}
26
+ />
27
+ );
28
+ }
29
+
30
+ function SelectTrigger({
31
+ className,
32
+ size = "default",
33
+ children,
34
+ ...props
35
+ }: SelectPrimitive.Trigger.Props & {
36
+ size?: "sm" | "default";
37
+ }) {
38
+ return (
39
+ <SelectPrimitive.Trigger
40
+ data-slot="select-trigger"
41
+ data-size={size}
42
+ className={cn(
43
+ "flex w-fit select-none items-center justify-between gap-1.5 whitespace-nowrap rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-placeholder:text-muted-foreground *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
44
+ className,
45
+ )}
46
+ {...props}
47
+ >
48
+ {children}
49
+ <SelectPrimitive.Icon
50
+ render={<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />}
51
+ />
52
+ </SelectPrimitive.Trigger>
53
+ );
54
+ }
55
+
56
+ function SelectContent({
57
+ className,
58
+ children,
59
+ side = "bottom",
60
+ sideOffset = 4,
61
+ align = "center",
62
+ alignOffset = 0,
63
+ alignItemWithTrigger = true,
64
+ ...props
65
+ }: SelectPrimitive.Popup.Props &
66
+ Pick<
67
+ SelectPrimitive.Positioner.Props,
68
+ "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
69
+ >) {
70
+ return (
71
+ <SelectPrimitive.Portal>
72
+ <SelectPrimitive.Positioner
73
+ side={side}
74
+ sideOffset={sideOffset}
75
+ align={align}
76
+ alignOffset={alignOffset}
77
+ alignItemWithTrigger={alignItemWithTrigger}
78
+ className="isolate z-50"
79
+ >
80
+ <SelectPrimitive.Popup
81
+ data-slot="select-content"
82
+ data-align-trigger={alignItemWithTrigger}
83
+ className={cn(
84
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-y-auto overflow-x-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-closed:animate-out data-open:animate-in",
85
+ className,
86
+ )}
87
+ {...props}
88
+ >
89
+ <SelectScrollUpButton />
90
+ <SelectPrimitive.List>{children}</SelectPrimitive.List>
91
+ <SelectScrollDownButton />
92
+ </SelectPrimitive.Popup>
93
+ </SelectPrimitive.Positioner>
94
+ </SelectPrimitive.Portal>
95
+ );
96
+ }
97
+
98
+ function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
99
+ return (
100
+ <SelectPrimitive.GroupLabel
101
+ data-slot="select-label"
102
+ className={cn("px-1.5 py-1 text-muted-foreground text-xs", className)}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
109
+ return (
110
+ <SelectPrimitive.Item
111
+ data-slot="select-item"
112
+ className={cn(
113
+ "relative flex w-full cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
114
+ className,
115
+ )}
116
+ {...props}
117
+ >
118
+ <SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
119
+ {children}
120
+ </SelectPrimitive.ItemText>
121
+ <SelectPrimitive.ItemIndicator
122
+ render={
123
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
124
+ }
125
+ >
126
+ <CheckIcon className="pointer-events-none" />
127
+ </SelectPrimitive.ItemIndicator>
128
+ </SelectPrimitive.Item>
129
+ );
130
+ }
131
+
132
+ function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
133
+ return (
134
+ <SelectPrimitive.Separator
135
+ data-slot="select-separator"
136
+ className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
137
+ {...props}
138
+ />
139
+ );
140
+ }
141
+
142
+ function SelectScrollUpButton({
143
+ className,
144
+ ...props
145
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
146
+ return (
147
+ <SelectPrimitive.ScrollUpArrow
148
+ data-slot="select-scroll-up-button"
149
+ className={cn(
150
+ "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
151
+ className,
152
+ )}
153
+ {...props}
154
+ >
155
+ <ChevronUpIcon />
156
+ </SelectPrimitive.ScrollUpArrow>
157
+ );
158
+ }
159
+
160
+ function SelectScrollDownButton({
161
+ className,
162
+ ...props
163
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
164
+ return (
165
+ <SelectPrimitive.ScrollDownArrow
166
+ data-slot="select-scroll-down-button"
167
+ className={cn(
168
+ "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
169
+ className,
170
+ )}
171
+ {...props}
172
+ >
173
+ <ChevronDownIcon />
174
+ </SelectPrimitive.ScrollDownArrow>
175
+ );
176
+ }
177
+
178
+ export {
179
+ Select,
180
+ SelectContent,
181
+ SelectGroup,
182
+ SelectItem,
183
+ SelectLabel,
184
+ SelectScrollDownButton,
185
+ SelectScrollUpButton,
186
+ SelectSeparator,
187
+ SelectTrigger,
188
+ SelectValue,
189
+ };
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
4
+
5
+ import { cn } from "@suphark/ui/lib/utils";
6
+
7
+ function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
8
+ return (
9
+ <SeparatorPrimitive
10
+ data-slot="separator"
11
+ orientation={orientation}
12
+ className={cn(
13
+ "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Separator };
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
4
+ import { Button } from "@suphark/ui/components/ui/button";
5
+ import { cn } from "@suphark/ui/lib/utils";
6
+ import { XIcon } from "lucide-react";
7
+ import type * as React from "react";
8
+
9
+ function Sheet({ ...props }: SheetPrimitive.Root.Props) {
10
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />;
11
+ }
12
+
13
+ function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
14
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
15
+ }
16
+
17
+ function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
18
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
19
+ }
20
+
21
+ function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
22
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
23
+ }
24
+
25
+ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
26
+ return (
27
+ <SheetPrimitive.Backdrop
28
+ data-slot="sheet-overlay"
29
+ className={cn(
30
+ "fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
31
+ className,
32
+ )}
33
+ {...props}
34
+ />
35
+ );
36
+ }
37
+
38
+ function SheetContent({
39
+ className,
40
+ children,
41
+ side = "right",
42
+ showCloseButton = true,
43
+ ...props
44
+ }: SheetPrimitive.Popup.Props & {
45
+ side?: "top" | "right" | "bottom" | "left";
46
+ showCloseButton?: boolean;
47
+ }) {
48
+ return (
49
+ <SheetPortal>
50
+ <SheetOverlay />
51
+ <SheetPrimitive.Popup
52
+ data-slot="sheet-content"
53
+ data-side={side}
54
+ className={cn(
55
+ "fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-popover-foreground text-sm shadow-lg transition duration-200 ease-in-out data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=bottom]:inset-x-0 data-[side=top]:inset-x-0 data-[side=left]:inset-y-0 data-[side=right]:inset-y-0 data-[side=top]:top-0 data-[side=right]:right-0 data-[side=bottom]:bottom-0 data-[side=left]:left-0 data-[side=bottom]:h-auto data-[side=left]:h-full data-[side=right]:h-full data-[side=top]:h-auto data-[side=left]:w-3/4 data-[side=right]:w-3/4 data-[side=bottom]:border-t data-[side=left]:border-r data-[side=top]:border-b data-[side=right]:border-l data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
56
+ className,
57
+ )}
58
+ {...props}
59
+ >
60
+ {children}
61
+ {showCloseButton && (
62
+ <SheetPrimitive.Close
63
+ data-slot="sheet-close"
64
+ render={<Button variant="ghost" className="absolute top-3 right-3" size="icon-sm" />}
65
+ >
66
+ <XIcon />
67
+ <span className="sr-only">Close</span>
68
+ </SheetPrimitive.Close>
69
+ )}
70
+ </SheetPrimitive.Popup>
71
+ </SheetPortal>
72
+ );
73
+ }
74
+
75
+ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
76
+ return (
77
+ <div
78
+ data-slot="sheet-header"
79
+ className={cn("flex flex-col gap-0.5 p-4", className)}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
86
+ return (
87
+ <div
88
+ data-slot="sheet-footer"
89
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
96
+ return (
97
+ <SheetPrimitive.Title
98
+ data-slot="sheet-title"
99
+ className={cn("font-medium text-base text-foreground", className)}
100
+ {...props}
101
+ />
102
+ );
103
+ }
104
+
105
+ function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
106
+ return (
107
+ <SheetPrimitive.Description
108
+ data-slot="sheet-description"
109
+ className={cn("text-muted-foreground text-sm", className)}
110
+ {...props}
111
+ />
112
+ );
113
+ }
114
+
115
+ export {
116
+ Sheet,
117
+ SheetClose,
118
+ SheetContent,
119
+ SheetDescription,
120
+ SheetFooter,
121
+ SheetHeader,
122
+ SheetTitle,
123
+ SheetTrigger,
124
+ };