@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,188 @@
1
+ "use client";
2
+
3
+ import { formatDistanceToNow, type Locale } from "date-fns";
4
+ import { enUS, th } from "date-fns/locale"; // Import locales you need
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+
7
+ /**
8
+ * Props for the ClientDateFormatter component.
9
+ */
10
+ interface ClientDateFormatterProps {
11
+ /**
12
+ * The date value to format. Can be a Date object, ISO string, or timestamp number.
13
+ */
14
+ date: Date | string | number | null | undefined;
15
+ /**
16
+ * Determines the format type: 'standard' for regular date/time, 'relative' for distance from now.
17
+ * Defaults to 'standard'.
18
+ */
19
+ formatType?: "standard" | "relative";
20
+ /**
21
+ * Optional Intl.DateTimeFormat options (used only if formatType is 'standard').
22
+ * Defaults to { day: 'numeric', month: 'short', year: 'numeric' }.
23
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
24
+ */
25
+ options?: Intl.DateTimeFormatOptions;
26
+ /**
27
+ * The locale to use for formatting. Affects both 'standard' and 'relative' formats.
28
+ * Defaults to 'th-TH' (maps to 'th' for date-fns).
29
+ */
30
+ locale?: string;
31
+ /**
32
+ * The time zone to use (used only if formatType is 'standard').
33
+ * Defaults to 'Asia/Bangkok' (GMT+7).
34
+ */
35
+ timeZone?: string;
36
+ /**
37
+ * Placeholder text to display while the component is mounting or if the date is invalid.
38
+ * Defaults to "-".
39
+ */
40
+ placeholder?: string;
41
+ /**
42
+ * Optional CSS class name(s) to apply to the span element.
43
+ */
44
+ className?: string;
45
+ /**
46
+ * If formatType is 'relative', automatically update the time every interval (in milliseconds).
47
+ * Set to 0 or undefined to disable automatic updates. Defaults to 60000 (1 minute).
48
+ */
49
+ updateInterval?: number;
50
+ }
51
+
52
+ // Helper to map locale string to date-fns locale object
53
+ const getLocaleObject = (localeString: string): Locale => {
54
+ switch (localeString.toLowerCase().split("-")[0]) {
55
+ case "th":
56
+ return th;
57
+ default:
58
+ return enUS;
59
+ }
60
+ };
61
+
62
+ export function ClientDateFormatter({
63
+ date,
64
+ formatType = "standard",
65
+ options: userOptions,
66
+ locale = "th-TH",
67
+ timeZone = "Asia/Bangkok",
68
+ placeholder = "-",
69
+ className,
70
+ updateInterval = 60000,
71
+ }: ClientDateFormatterProps) {
72
+ const [isMounted, setIsMounted] = useState(false);
73
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
74
+
75
+ // [EDIT] 👇 Wrap formatDate logic in useCallback
76
+ const formatDateCallback = useCallback(() => {
77
+ if (date == null) {
78
+ return placeholder;
79
+ }
80
+
81
+ try {
82
+ const dateObj = new Date(date);
83
+ if (Number.isNaN(dateObj.getTime())) {
84
+ console.warn("ClientDateFormatter received an invalid date:", date);
85
+ return placeholder;
86
+ }
87
+
88
+ const localeObj = getLocaleObject(locale);
89
+
90
+ if (formatType === "relative") {
91
+ return formatDistanceToNow(dateObj, {
92
+ addSuffix: true,
93
+ locale: localeObj,
94
+ });
95
+ } else {
96
+ const defaultOptions: Intl.DateTimeFormatOptions = {
97
+ day: "numeric",
98
+ month: "short",
99
+ year: "numeric",
100
+ timeZone: timeZone,
101
+ };
102
+ const finalOptions = { ...defaultOptions, ...userOptions };
103
+ const formatter = new Intl.DateTimeFormat(locale, finalOptions);
104
+ return formatter.format(dateObj);
105
+ }
106
+ } catch (error) {
107
+ console.error("Error formatting date in ClientDateFormatter:", error);
108
+ return placeholder;
109
+ }
110
+ // Add dependencies that formatDate relies on
111
+ }, [date, formatType, locale, placeholder, timeZone, userOptions]);
112
+
113
+ const [, setTick] = useState(0);
114
+
115
+ useEffect(() => {
116
+ queueMicrotask(() => {
117
+ setIsMounted(true);
118
+ });
119
+ }, []);
120
+
121
+ useEffect(() => {
122
+ if (!isMounted) return;
123
+
124
+ if (formatType === "relative" && updateInterval && updateInterval > 0) {
125
+ intervalRef.current = setInterval(() => {
126
+ setTick((t) => t + 1);
127
+ }, updateInterval);
128
+ }
129
+
130
+ return () => {
131
+ if (intervalRef.current) {
132
+ clearInterval(intervalRef.current);
133
+ intervalRef.current = null;
134
+ }
135
+ };
136
+ }, [isMounted, updateInterval, formatType]);
137
+
138
+ if (!isMounted) {
139
+ return <span className={className}>{placeholder}</span>;
140
+ }
141
+
142
+ // Use tick as a dependency so it re-renders (state change triggers it)
143
+ return <span className={className}>{formatDateCallback()}</span>;
144
+ }
145
+
146
+ /*
147
+
148
+ ตัวอย่าง
149
+
150
+ import { ClientDateFormatter } from '@/components/shared/client-date-formatter';
151
+
152
+ // ...
153
+
154
+ // แสดงวันที่แบบไทย (Default)
155
+ <ClientDateFormatter date={someDate} />
156
+
157
+ // แสดงแบบ Relative Time (เช่น "ประมาณ 2 ชั่วโมงที่แล้ว") อัปเดตทุกนาที (default)
158
+ <ClientDateFormatter date={auditLog.createdAt} formatType="relative" />
159
+
160
+ // แสดงแบบ Relative Time ภาษาอังกฤษ อัปเดตทุก 30 วินาที
161
+ <ClientDateFormatter
162
+ date={auditLog.createdAt}
163
+ formatType="relative"
164
+ locale="en-US"
165
+ updateInterval={30000}
166
+ />
167
+
168
+ // แสดงแบบ Relative Time แต่ไม่อัปเดตอัตโนมัติ
169
+ <ClientDateFormatter
170
+ date={auditLog.createdAt}
171
+ formatType="relative"
172
+ updateInterval={0} // Disable interval
173
+ />
174
+
175
+ // แสดงวันที่เวลาแบบ Standard ภาษาอังกฤษ โซนเวลา New York
176
+ <ClientDateFormatter
177
+ date={someDate}
178
+ formatType="standard" // Explicitly standard
179
+ locale="en-US"
180
+ timeZone="America/New_York"
181
+ options={{ hour: '2-digit', minute: '2-digit' }}
182
+ />
183
+
184
+
185
+
186
+
187
+
188
+ */
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { createContext, type ReactNode, useContext } from "react";
4
+
5
+ /** ข้อความคงที่ที่ component ของ @pi/ui ใช้เอง — แอปส่งชุดภาษาอื่นเข้ามาผ่าน UiTextProvider */
6
+ export interface UiText {
7
+ cancel: string;
8
+ done: string;
9
+ save: string;
10
+ actions: string;
11
+ copy: string;
12
+ copied: string;
13
+ showOnce: string;
14
+ success: string;
15
+ }
16
+
17
+ export const UI_TEXT_TH: UiText = {
18
+ cancel: "ยกเลิก",
19
+ done: "เสร็จแล้ว",
20
+ save: "บันทึก",
21
+ actions: "การกระทำ",
22
+ copy: "คัดลอก",
23
+ copied: "คัดลอกแล้ว",
24
+ showOnce: "แสดงครั้งเดียว — คัดลอกเก็บไว้ทันที",
25
+ success: "สำเร็จ",
26
+ };
27
+
28
+ export const UI_TEXT_EN: UiText = {
29
+ cancel: "Cancel",
30
+ done: "Done",
31
+ save: "Save",
32
+ actions: "Actions",
33
+ copy: "Copy",
34
+ copied: "Copied",
35
+ showOnce: "Shown once — copy it now",
36
+ success: "Done",
37
+ };
38
+
39
+ const UiTextContext = createContext<UiText>(UI_TEXT_TH);
40
+
41
+ export function UiTextProvider({ text, children }: { text: UiText; children: ReactNode }) {
42
+ return <UiTextContext.Provider value={text}>{children}</UiTextContext.Provider>;
43
+ }
44
+
45
+ export function useUiText(): UiText {
46
+ return useContext(UiTextContext);
47
+ }
@@ -0,0 +1,40 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxList } from "../combobox";
4
+
5
+ describe("ComboboxContent", () => {
6
+ it("renders selectable items inside a supplied modal container", () => {
7
+ const onValueChange = vi.fn();
8
+
9
+ render(<ModalCombobox onValueChange={onValueChange} />);
10
+
11
+ const option = screen.getByText("ครัว");
12
+
13
+ fireEvent.click(option);
14
+
15
+ expect(onValueChange).toHaveBeenCalledWith("ครัว", expect.anything());
16
+ });
17
+ });
18
+
19
+ function ModalCombobox({
20
+ onValueChange,
21
+ }: {
22
+ onValueChange: (value: string | null, eventDetails: unknown) => void;
23
+ }) {
24
+ return (
25
+ <div data-testid="modal-container">
26
+ <Combobox items={["ครัว"]} open onValueChange={onValueChange}>
27
+ <ComboboxInput />
28
+ <ComboboxContent>
29
+ <ComboboxList>
30
+ {(item) => (
31
+ <ComboboxItem key={item} value={item}>
32
+ {item}
33
+ </ComboboxItem>
34
+ )}
35
+ </ComboboxList>
36
+ </ComboboxContent>
37
+ </Combobox>
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,53 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { DatePicker } from "../date-picker";
4
+
5
+ function renderInsideForm(onChange = vi.fn(), onSubmit = vi.fn()) {
6
+ const view = render(
7
+ <form
8
+ onSubmit={(event) => {
9
+ event.preventDefault();
10
+ onSubmit();
11
+ }}
12
+ >
13
+ <DatePicker
14
+ value={new Date(2025, 9, 10)}
15
+ onChange={onChange}
16
+ showPresets={false}
17
+ allowClear={false}
18
+ />
19
+ <button type="submit">Save parent form</button>
20
+ </form>,
21
+ );
22
+
23
+ fireEvent.click(screen.getByRole("button", { name: /10 ต.ค. 2568/ }));
24
+ const smartInput = screen.getByPlaceholderText("เช่น 1/7, 1/7/25, 1-7-2568...");
25
+ fireEvent.change(smartInput, { target: { value: "16/10/2025" } });
26
+
27
+ return { ...view, smartInput };
28
+ }
29
+
30
+ describe("DatePicker smart input", () => {
31
+ it("confirms a typed date without submitting its parent form when clicking ตกลง", () => {
32
+ const onChange = vi.fn();
33
+ const onSubmit = vi.fn();
34
+ const { container } = renderInsideForm(onChange, onSubmit);
35
+
36
+ fireEvent.click(screen.getByRole("button", { name: "ตกลง" }));
37
+
38
+ expect(container.querySelectorAll("form")).toHaveLength(1);
39
+ expect(onSubmit).not.toHaveBeenCalled();
40
+ expect(onChange).toHaveBeenCalledWith(new Date(2025, 9, 16));
41
+ });
42
+
43
+ it("confirms a typed date without submitting its parent form when pressing Enter", () => {
44
+ const onChange = vi.fn();
45
+ const onSubmit = vi.fn();
46
+ const { smartInput } = renderInsideForm(onChange, onSubmit);
47
+
48
+ fireEvent.keyDown(smartInput, { key: "Enter", code: "Enter" });
49
+
50
+ expect(onSubmit).not.toHaveBeenCalled();
51
+ expect(onChange).toHaveBeenCalledWith(new Date(2025, 9, 16));
52
+ });
53
+ });
@@ -0,0 +1,18 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { Slider } from "../slider";
4
+
5
+ describe("Slider", () => {
6
+ it("renders the standard accessible slider contract", async () => {
7
+ const { container } = render(<Slider aria-label="ระดับ" defaultValue={[25]} />);
8
+
9
+ expect(screen.getByRole("group", { name: "ระดับ" })).toBeInTheDocument();
10
+ expect(container.querySelector('[data-slot="slider"]')).toHaveAttribute(
11
+ "data-orientation",
12
+ "horizontal",
13
+ );
14
+ await waitFor(() => {
15
+ expect(container.querySelector('input[type="range"]')).toHaveAttribute("aria-valuenow", "25");
16
+ });
17
+ });
18
+ });
@@ -0,0 +1,33 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../tabs";
4
+
5
+ describe("Tabs", () => {
6
+ it("uses the orientation data attributes supplied by Base UI", () => {
7
+ const { container } = render(<ExampleTabs />);
8
+
9
+ expect(container.firstElementChild).toHaveAttribute("data-horizontal");
10
+ expect(container.firstElementChild).toHaveAttribute("data-orientation", "horizontal");
11
+ });
12
+
13
+ it("changes the active panel when a trigger is selected", () => {
14
+ render(<ExampleTabs />);
15
+
16
+ fireEvent.click(screen.getByRole("tab", { name: "รายละเอียด" }));
17
+
18
+ expect(screen.getByRole("tabpanel")).toHaveTextContent("เนื้อหารายละเอียด");
19
+ });
20
+ });
21
+
22
+ function ExampleTabs() {
23
+ return (
24
+ <Tabs defaultValue="overview">
25
+ <TabsList>
26
+ <TabsTrigger value="overview">ภาพรวม</TabsTrigger>
27
+ <TabsTrigger value="details">รายละเอียด</TabsTrigger>
28
+ </TabsList>
29
+ <TabsContent value="overview">เนื้อหาภาพรวม</TabsContent>
30
+ <TabsContent value="details">เนื้อหารายละเอียด</TabsContent>
31
+ </Tabs>
32
+ );
33
+ }
@@ -0,0 +1,69 @@
1
+ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
2
+ import { cn } from "@suphark/ui/lib/utils";
3
+ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
4
+
5
+ function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
6
+ return (
7
+ <AccordionPrimitive.Root
8
+ data-slot="accordion"
9
+ className={cn("flex w-full flex-col", className)}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+
15
+ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
16
+ return (
17
+ <AccordionPrimitive.Item
18
+ data-slot="accordion-item"
19
+ className={cn("not-last:border-b", className)}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) {
26
+ return (
27
+ <AccordionPrimitive.Header className="flex">
28
+ <AccordionPrimitive.Trigger
29
+ data-slot="accordion-trigger"
30
+ className={cn(
31
+ "group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left font-medium text-sm outline-none transition-all hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronDownIcon
38
+ data-slot="accordion-trigger-icon"
39
+ className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
40
+ />
41
+ <ChevronUpIcon
42
+ data-slot="accordion-trigger-icon"
43
+ className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
44
+ />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ );
48
+ }
49
+
50
+ function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) {
51
+ return (
52
+ <AccordionPrimitive.Panel
53
+ data-slot="accordion-content"
54
+ className="overflow-hidden text-sm data-closed:animate-accordion-up data-open:animate-accordion-down"
55
+ {...props}
56
+ >
57
+ <div
58
+ className={cn(
59
+ "h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
60
+ className,
61
+ )}
62
+ >
63
+ {children}
64
+ </div>
65
+ </AccordionPrimitive.Panel>
66
+ );
67
+ }
68
+
69
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+ import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
4
+ import { Button } from "@suphark/ui/components/ui/button";
5
+ import { cn } from "@suphark/ui/lib/utils";
6
+ import type * as React from "react";
7
+
8
+ function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
9
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
10
+ }
11
+
12
+ function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
13
+ return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
14
+ }
15
+
16
+ function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
17
+ return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
18
+ }
19
+
20
+ function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
21
+ return (
22
+ <AlertDialogPrimitive.Backdrop
23
+ data-slot="alert-dialog-overlay"
24
+ className={cn(
25
+ "data-open:fade-in-0 data-closed:fade-out-0 fixed inset-0 isolate z-50 bg-black/10 duration-100 data-closed:animate-out data-open:animate-in supports-backdrop-filter:backdrop-blur-xs",
26
+ className,
27
+ )}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ function AlertDialogContent({
34
+ className,
35
+ size = "default",
36
+ ...props
37
+ }: AlertDialogPrimitive.Popup.Props & {
38
+ size?: "default" | "sm";
39
+ }) {
40
+ return (
41
+ <AlertDialogPortal>
42
+ <AlertDialogOverlay />
43
+ <AlertDialogPrimitive.Popup
44
+ data-slot="alert-dialog-content"
45
+ data-size={size}
46
+ className={cn(
47
+ "group/alert-dialog-content data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground outline-none ring-1 ring-foreground/10 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-closed:animate-out data-open:animate-in data-[size=default]:sm:max-w-sm",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ </AlertDialogPortal>
53
+ );
54
+ }
55
+
56
+ function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
57
+ return (
58
+ <div
59
+ data-slot="alert-dialog-header"
60
+ className={cn(
61
+ "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
62
+ className,
63
+ )}
64
+ {...props}
65
+ />
66
+ );
67
+ }
68
+
69
+ function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div
72
+ data-slot="alert-dialog-footer"
73
+ className={cn(
74
+ "-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
75
+ className,
76
+ )}
77
+ {...props}
78
+ />
79
+ );
80
+ }
81
+
82
+ function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="alert-dialog-media"
86
+ className={cn(
87
+ "mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function AlertDialogTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
99
+ return (
100
+ <AlertDialogPrimitive.Title
101
+ data-slot="alert-dialog-title"
102
+ className={cn(
103
+ "font-medium text-base sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
104
+ className,
105
+ )}
106
+ {...props}
107
+ />
108
+ );
109
+ }
110
+
111
+ function AlertDialogDescription({
112
+ className,
113
+ ...props
114
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
115
+ return (
116
+ <AlertDialogPrimitive.Description
117
+ data-slot="alert-dialog-description"
118
+ className={cn(
119
+ "text-balance text-muted-foreground text-sm md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
120
+ className,
121
+ )}
122
+ {...props}
123
+ />
124
+ );
125
+ }
126
+
127
+ function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof Button>) {
128
+ return <Button data-slot="alert-dialog-action" className={cn(className)} {...props} />;
129
+ }
130
+
131
+ function AlertDialogCancel({
132
+ className,
133
+ variant = "outline",
134
+ size = "default",
135
+ ...props
136
+ }: AlertDialogPrimitive.Close.Props &
137
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
138
+ return (
139
+ <AlertDialogPrimitive.Close
140
+ data-slot="alert-dialog-cancel"
141
+ className={cn(className)}
142
+ render={<Button variant={variant} size={size} />}
143
+ {...props}
144
+ />
145
+ );
146
+ }
147
+
148
+ export {
149
+ AlertDialog,
150
+ AlertDialogAction,
151
+ AlertDialogCancel,
152
+ AlertDialogContent,
153
+ AlertDialogDescription,
154
+ AlertDialogFooter,
155
+ AlertDialogHeader,
156
+ AlertDialogMedia,
157
+ AlertDialogOverlay,
158
+ AlertDialogPortal,
159
+ AlertDialogTitle,
160
+ AlertDialogTrigger,
161
+ };
@@ -0,0 +1,68 @@
1
+ import { cn } from "@suphark/ui/lib/utils";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import type * as React from "react";
4
+
5
+ const alertVariants = cva(
6
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 has-data-[slot=alert-action]:pr-18 *:[svg:not([class*='size-'])]:size-4 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-card text-card-foreground",
11
+ destructive:
12
+ "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ variant: "default",
17
+ },
18
+ },
19
+ );
20
+
21
+ function Alert({
22
+ className,
23
+ variant,
24
+ ...props
25
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
26
+ return (
27
+ <div
28
+ data-slot="alert"
29
+ role="alert"
30
+ className={cn(alertVariants({ variant }), className)}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
37
+ return (
38
+ <div
39
+ data-slot="alert-title"
40
+ className={cn(
41
+ "font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
50
+ return (
51
+ <div
52
+ data-slot="alert-description"
53
+ className={cn(
54
+ "text-balance text-muted-foreground text-sm md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
55
+ className,
56
+ )}
57
+ {...props}
58
+ />
59
+ );
60
+ }
61
+
62
+ function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
63
+ return (
64
+ <div data-slot="alert-action" className={cn("absolute top-2 right-2", className)} {...props} />
65
+ );
66
+ }
67
+
68
+ export { Alert, AlertAction, AlertDescription, AlertTitle };