@arronqzy/ui 0.0.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 (64) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/.turbo/turbo-lint.log +4 -0
  3. package/components.json +20 -0
  4. package/dist/styles.css +3105 -0
  5. package/index.ts +7 -0
  6. package/package.json +76 -0
  7. package/postcss.config.cjs +7 -0
  8. package/scripts/sync-shadcn.mjs +144 -0
  9. package/src/hooks/use-toast.ts +194 -0
  10. package/src/lib/overlay-z-index.ts +8 -0
  11. package/src/lib/utils.ts +7 -0
  12. package/src/styles.css +89 -0
  13. package/src/theme-provider.tsx +36 -0
  14. package/src/ui/accordion.tsx +58 -0
  15. package/src/ui/alert-dialog.tsx +149 -0
  16. package/src/ui/alert.tsx +59 -0
  17. package/src/ui/aspect-ratio.tsx +7 -0
  18. package/src/ui/avatar.tsx +50 -0
  19. package/src/ui/badge.tsx +36 -0
  20. package/src/ui/breadcrumb.tsx +115 -0
  21. package/src/ui/button.tsx +56 -0
  22. package/src/ui/calendar.tsx +213 -0
  23. package/src/ui/card.tsx +79 -0
  24. package/src/ui/carousel.tsx +262 -0
  25. package/src/ui/chart.tsx +403 -0
  26. package/src/ui/checkbox.tsx +30 -0
  27. package/src/ui/collapsible.tsx +11 -0
  28. package/src/ui/command.tsx +153 -0
  29. package/src/ui/context-menu.tsx +200 -0
  30. package/src/ui/dialog.tsx +130 -0
  31. package/src/ui/drawer.tsx +124 -0
  32. package/src/ui/dropdown-menu.tsx +200 -0
  33. package/src/ui/empty.tsx +50 -0
  34. package/src/ui/form.tsx +178 -0
  35. package/src/ui/hover-card.tsx +29 -0
  36. package/src/ui/index.ts +50 -0
  37. package/src/ui/input-otp.tsx +71 -0
  38. package/src/ui/input.tsx +22 -0
  39. package/src/ui/label.tsx +26 -0
  40. package/src/ui/menubar.tsx +256 -0
  41. package/src/ui/navigation-menu.tsx +128 -0
  42. package/src/ui/pagination.tsx +117 -0
  43. package/src/ui/popover.tsx +31 -0
  44. package/src/ui/progress.tsx +28 -0
  45. package/src/ui/radio-group.tsx +44 -0
  46. package/src/ui/resizable.tsx +46 -0
  47. package/src/ui/scroll-area.tsx +48 -0
  48. package/src/ui/select.tsx +160 -0
  49. package/src/ui/separator.tsx +31 -0
  50. package/src/ui/sheet.tsx +146 -0
  51. package/src/ui/skeleton.tsx +15 -0
  52. package/src/ui/slider.tsx +28 -0
  53. package/src/ui/sonner.tsx +45 -0
  54. package/src/ui/switch.tsx +29 -0
  55. package/src/ui/table.tsx +117 -0
  56. package/src/ui/tabs.tsx +55 -0
  57. package/src/ui/textarea.tsx +22 -0
  58. package/src/ui/toast.tsx +129 -0
  59. package/src/ui/toaster.tsx +35 -0
  60. package/src/ui/toggle-group.tsx +61 -0
  61. package/src/ui/toggle.tsx +45 -0
  62. package/src/ui/tooltip.tsx +32 -0
  63. package/tailwind.config.ts +8 -0
  64. package/tsconfig.json +5 -0
package/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // components
2
+
3
+ export * from "./src/ui/index";
4
+ export * from "./src/lib/utils";
5
+ export { MODAL_Z_INDEX } from "./src/lib/overlay-z-index";
6
+ export * from "./src/hooks/use-toast";
7
+ export * from "./src/theme-provider";
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@arronqzy/ui",
3
+ "version": "0.0.0",
4
+ "exports": {
5
+ ".": "./index.ts",
6
+ "./counter": "./components/counter.ts",
7
+ "./header": "./components/header.ts",
8
+ "./setup-counter": "./utils/counter.ts",
9
+ "./styles.css": "./dist/styles.css"
10
+ },
11
+ "license": "MIT",
12
+ "dependencies": {
13
+ "@hookform/resolvers": "^5.2.1",
14
+ "@radix-ui/react-accordion": "^1.2.12",
15
+ "@radix-ui/react-alert-dialog": "^1.1.15",
16
+ "@radix-ui/react-aspect-ratio": "^1.1.7",
17
+ "@radix-ui/react-avatar": "^1.1.10",
18
+ "@radix-ui/react-checkbox": "^1.3.2",
19
+ "@radix-ui/react-collapsible": "^1.1.12",
20
+ "@radix-ui/react-context-menu": "^2.2.15",
21
+ "@radix-ui/react-dialog": "^1.1.15",
22
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
23
+ "@radix-ui/react-hover-card": "^1.1.15",
24
+ "@radix-ui/react-label": "^2.1.7",
25
+ "@radix-ui/react-menubar": "^1.1.15",
26
+ "@radix-ui/react-navigation-menu": "^1.2.14",
27
+ "@radix-ui/react-popover": "^1.1.15",
28
+ "@radix-ui/react-progress": "^1.1.7",
29
+ "@radix-ui/react-radio-group": "^1.3.7",
30
+ "@radix-ui/react-scroll-area": "^1.2.10",
31
+ "@radix-ui/react-select": "^2.2.5",
32
+ "@radix-ui/react-separator": "^1.1.7",
33
+ "@radix-ui/react-slider": "^1.3.5",
34
+ "@radix-ui/react-slot": "^1.2.3",
35
+ "@radix-ui/react-switch": "^1.2.5",
36
+ "@radix-ui/react-tabs": "^1.1.12",
37
+ "@radix-ui/react-toast": "^1.2.15",
38
+ "@radix-ui/react-toggle": "^1.1.9",
39
+ "@radix-ui/react-toggle-group": "^1.1.10",
40
+ "@radix-ui/react-tooltip": "^1.2.8",
41
+ "class-variance-authority": "^0.7.1",
42
+ "clsx": "^2.1.1",
43
+ "cmdk": "^1.1.1",
44
+ "date-fns": "^4.1.0",
45
+ "embla-carousel-react": "^8.6.0",
46
+ "input-otp": "^1.4.2",
47
+ "lucide-react": "^0.545.0",
48
+ "next-themes": "^0.4.6",
49
+ "react-day-picker": "^9.9.0",
50
+ "react": "^19",
51
+ "react-dom": "^19",
52
+ "react-hook-form": "^7.63.0",
53
+ "react-resizable-panels": "^3.0.5",
54
+ "recharts": "^3.2.1",
55
+ "sonner": "^2.0.7",
56
+ "tailwind-merge": "^2.5.5",
57
+ "vaul": "^1.1.2",
58
+ "zod": "^4.1.9"
59
+ },
60
+ "devDependencies": {
61
+ "@types/react": "^19.0.1",
62
+ "@types/react-dom": "^19.0.2",
63
+ "eslint": "^8.57.0",
64
+ "typescript": "^5.5.4",
65
+ "@arronqzy/eslint-config": "0.0.0",
66
+ "@arronqzy/typescript-config": "0.0.0",
67
+ "@arronqzy/tailwind": "0.0.1"
68
+ },
69
+ "scripts": {
70
+ "lint": "eslint \"**/*.{ts,tsx}\"",
71
+ "typecheck": "tsc -p tsconfig.json --noEmit",
72
+ "build:css": "arronqzy-tailwind -c tailwind.config.ts -i ./src/styles.css -o ./dist/styles.css",
73
+ "dev:css": "arronqzy-tailwind -c tailwind.config.ts -i ./src/styles.css -o ./dist/styles.css --watch",
74
+ "build": "pnpm run lint && pnpm run typecheck && pnpm run build:css"
75
+ }
76
+ }
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
7
+
@@ -0,0 +1,144 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const REGISTRY_BASE = "https://ui.shadcn.com/r/styles/default";
5
+
6
+ /**
7
+ * 说明:
8
+ * - 不使用 `shadcn add`,避免触发 pnpm 安装依赖(你当前 node_modules 权限有残留会失败)
9
+ * - 直接从 shadcn registry 拉取组件源码并写入 packages/ui/src/components/ui
10
+ */
11
+
12
+ const componentNames = [
13
+ "accordion",
14
+ "alert",
15
+ "alert-dialog",
16
+ "aspect-ratio",
17
+ "avatar",
18
+ "badge",
19
+ "breadcrumb",
20
+ "button",
21
+ "calendar",
22
+ "card",
23
+ "carousel",
24
+ "chart",
25
+ "checkbox",
26
+ "collapsible",
27
+ "command",
28
+ "context-menu",
29
+ "dialog",
30
+ "drawer",
31
+ "dropdown-menu",
32
+ "form",
33
+ "hover-card",
34
+ "input",
35
+ "input-otp",
36
+ "label",
37
+ "menubar",
38
+ "navigation-menu",
39
+ "pagination",
40
+ "popover",
41
+ "progress",
42
+ "radio-group",
43
+ "resizable",
44
+ "scroll-area",
45
+ "select",
46
+ "separator",
47
+ "sheet",
48
+ "skeleton",
49
+ "slider",
50
+ "sonner",
51
+ "switch",
52
+ "table",
53
+ "tabs",
54
+ "textarea",
55
+ "toast",
56
+ "toggle",
57
+ "toggle-group",
58
+ "tooltip",
59
+ // 注意:registry 里 `combobox.json` 缺失,会导致 add --all 失败;这里故意不包含 combobox
60
+ ];
61
+
62
+ function isObject(v) {
63
+ return typeof v === "object" && v !== null && !Array.isArray(v);
64
+ }
65
+
66
+ function normalizeEol(s) {
67
+ return s.replace(/\r\n/g, "\n");
68
+ }
69
+
70
+ async function writeFileEnsured(filePath, content) {
71
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
72
+ await fs.writeFile(filePath, content, "utf8");
73
+ }
74
+
75
+ async function fetchJson(url) {
76
+ const res = await fetch(url);
77
+ if (!res.ok) {
78
+ const msg = await res.text().catch(() => "");
79
+ const err = new Error(`Fetch failed ${res.status} ${url}\n${msg}`);
80
+ err.status = res.status;
81
+ throw err;
82
+ }
83
+ return await res.json();
84
+ }
85
+
86
+ function mapRegistryPathToLocal(filePath) {
87
+ // registry 通常是 `components/ui/*.tsx`、`lib/utils.ts` 等
88
+ // 我们把它统一落到 `packages/ui/src/` 下面,避免污染根目录结构
89
+ if (filePath.startsWith("components/")) return path.join("src", filePath);
90
+ if (filePath.startsWith("lib/")) return path.join("src", filePath);
91
+ return path.join("src", filePath);
92
+ }
93
+
94
+ async function main() {
95
+ const root = path.resolve(process.cwd(), "packages/ui");
96
+ const written = new Set();
97
+ const exportedUiModules = new Set();
98
+
99
+ for (const name of componentNames) {
100
+ const url = `${REGISTRY_BASE}/${name}.json`;
101
+ try {
102
+ const item = await fetchJson(url);
103
+ if (!isObject(item) || !Array.isArray(item.files)) {
104
+ throw new Error(`Invalid registry item shape for ${name}`);
105
+ }
106
+
107
+ for (const f of item.files) {
108
+ if (!f?.path || typeof f.path !== "string" || typeof f.content !== "string") continue;
109
+ const rel = mapRegistryPathToLocal(f.path);
110
+ const abs = path.join(root, rel);
111
+ await writeFileEnsured(abs, normalizeEol(f.content));
112
+ written.add(rel);
113
+
114
+ if (rel.startsWith("src/components/ui/") && rel.endsWith(".tsx")) {
115
+ const mod = "./" + path.basename(rel, ".tsx");
116
+ exportedUiModules.add(mod);
117
+ }
118
+ }
119
+ // eslint-disable-next-line no-console
120
+ console.log(`synced: ${name}`);
121
+ } catch (e) {
122
+ // eslint-disable-next-line no-console
123
+ console.warn(`skip: ${name} (${e?.status ?? "err"})`);
124
+ }
125
+ }
126
+
127
+ // 生成 ui barrel:src/components/ui/index.ts
128
+ const uiIndexPath = path.join(root, "src/components/ui/index.ts");
129
+ const exports = Array.from(exportedUiModules)
130
+ .sort()
131
+ .map((m) => `export * from "${m}";`)
132
+ .join("\n");
133
+ await writeFileEnsured(uiIndexPath, exports + "\n");
134
+
135
+ // eslint-disable-next-line no-console
136
+ console.log(`done. files: ${written.size}`);
137
+ }
138
+
139
+ main().catch((e) => {
140
+ // eslint-disable-next-line no-console
141
+ console.error(e);
142
+ process.exit(1);
143
+ });
144
+
@@ -0,0 +1,194 @@
1
+ "use client"
2
+
3
+ // Inspired by react-hot-toast library
4
+ import * as React from "react"
5
+
6
+ import type {
7
+ ToastActionElement,
8
+ ToastProps,
9
+ } from "../ui/toast.tsx"
10
+
11
+ const TOAST_LIMIT = 1
12
+ const TOAST_REMOVE_DELAY = 1000000
13
+
14
+ type ToasterToast = ToastProps & {
15
+ id: string
16
+ title?: React.ReactNode
17
+ description?: React.ReactNode
18
+ action?: ToastActionElement
19
+ }
20
+
21
+ const actionTypes = {
22
+ ADD_TOAST: "ADD_TOAST",
23
+ UPDATE_TOAST: "UPDATE_TOAST",
24
+ DISMISS_TOAST: "DISMISS_TOAST",
25
+ REMOVE_TOAST: "REMOVE_TOAST",
26
+ } as const
27
+
28
+ let count = 0
29
+
30
+ function genId() {
31
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
32
+ return count.toString()
33
+ }
34
+
35
+ type ActionType = typeof actionTypes
36
+
37
+ type Action =
38
+ | {
39
+ type: ActionType["ADD_TOAST"]
40
+ toast: ToasterToast
41
+ }
42
+ | {
43
+ type: ActionType["UPDATE_TOAST"]
44
+ toast: Partial<ToasterToast>
45
+ }
46
+ | {
47
+ type: ActionType["DISMISS_TOAST"]
48
+ toastId?: ToasterToast["id"]
49
+ }
50
+ | {
51
+ type: ActionType["REMOVE_TOAST"]
52
+ toastId?: ToasterToast["id"]
53
+ }
54
+
55
+ interface State {
56
+ toasts: ToasterToast[]
57
+ }
58
+
59
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
60
+
61
+ const addToRemoveQueue = (toastId: string) => {
62
+ if (toastTimeouts.has(toastId)) {
63
+ return
64
+ }
65
+
66
+ const timeout = setTimeout(() => {
67
+ toastTimeouts.delete(toastId)
68
+ dispatch({
69
+ type: "REMOVE_TOAST",
70
+ toastId: toastId,
71
+ })
72
+ }, TOAST_REMOVE_DELAY)
73
+
74
+ toastTimeouts.set(toastId, timeout)
75
+ }
76
+
77
+ export const reducer = (state: State, action: Action): State => {
78
+ switch (action.type) {
79
+ case "ADD_TOAST":
80
+ return {
81
+ ...state,
82
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83
+ }
84
+
85
+ case "UPDATE_TOAST":
86
+ return {
87
+ ...state,
88
+ toasts: state.toasts.map((t) =>
89
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
90
+ ),
91
+ }
92
+
93
+ case "DISMISS_TOAST": {
94
+ const { toastId } = action
95
+
96
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
97
+ // but I'll keep it here for simplicity
98
+ if (toastId) {
99
+ addToRemoveQueue(toastId)
100
+ } else {
101
+ state.toasts.forEach((toast) => {
102
+ addToRemoveQueue(toast.id)
103
+ })
104
+ }
105
+
106
+ return {
107
+ ...state,
108
+ toasts: state.toasts.map((t) =>
109
+ t.id === toastId || toastId === undefined
110
+ ? {
111
+ ...t,
112
+ open: false,
113
+ }
114
+ : t
115
+ ),
116
+ }
117
+ }
118
+ case "REMOVE_TOAST":
119
+ if (action.toastId === undefined) {
120
+ return {
121
+ ...state,
122
+ toasts: [],
123
+ }
124
+ }
125
+ return {
126
+ ...state,
127
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
128
+ }
129
+ }
130
+ }
131
+
132
+ const listeners: Array<(state: State) => void> = []
133
+
134
+ let memoryState: State = { toasts: [] }
135
+
136
+ function dispatch(action: Action) {
137
+ memoryState = reducer(memoryState, action)
138
+ listeners.forEach((listener) => {
139
+ listener(memoryState)
140
+ })
141
+ }
142
+
143
+ type Toast = Omit<ToasterToast, "id">
144
+
145
+ function toast({ ...props }: Toast) {
146
+ const id = genId()
147
+
148
+ const update = (props: ToasterToast) =>
149
+ dispatch({
150
+ type: "UPDATE_TOAST",
151
+ toast: { ...props, id },
152
+ })
153
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154
+
155
+ dispatch({
156
+ type: "ADD_TOAST",
157
+ toast: {
158
+ ...props,
159
+ id,
160
+ open: true,
161
+ onOpenChange: (open) => {
162
+ if (!open) dismiss()
163
+ },
164
+ },
165
+ })
166
+
167
+ return {
168
+ id: id,
169
+ dismiss,
170
+ update,
171
+ }
172
+ }
173
+
174
+ function useToast() {
175
+ const [state, setState] = React.useState<State>(memoryState)
176
+
177
+ React.useEffect(() => {
178
+ listeners.push(setState)
179
+ return () => {
180
+ const index = listeners.indexOf(setState)
181
+ if (index > -1) {
182
+ listeners.splice(index, 1)
183
+ }
184
+ }
185
+ }, [state])
186
+
187
+ return {
188
+ ...state,
189
+ toast,
190
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191
+ }
192
+ }
193
+
194
+ export { useToast, toast }
@@ -0,0 +1,8 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ /** 弹窗层级:高于画布 Ruler(999)、工具栏(1200)、Menubar(10100) 等 */
4
+ export const MODAL_Z_INDEX = 20000;
5
+ export const MODAL_CONTENT_Z_INDEX = 20001;
6
+
7
+ export const modalOverlayStyle: CSSProperties = { zIndex: MODAL_Z_INDEX };
8
+ export const modalContentStyle: CSSProperties = { zIndex: MODAL_CONTENT_Z_INDEX };
@@ -0,0 +1,7 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
package/src/styles.css ADDED
@@ -0,0 +1,89 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+
10
+ --card: 0 0% 100%;
11
+ --card-foreground: 222.2 84% 4.9%;
12
+
13
+ --popover: 0 0% 100%;
14
+ --popover-foreground: 222.2 84% 4.9%;
15
+
16
+ --primary: 222.2 47.4% 11.2%;
17
+ --primary-foreground: 210 40% 98%;
18
+
19
+ --secondary: 210 40% 96.1%;
20
+ --secondary-foreground: 222.2 47.4% 11.2%;
21
+
22
+ --muted: 210 40% 96.1%;
23
+ --muted-foreground: 215.4 16.3% 46.9%;
24
+
25
+ --accent: 210 40% 96.1%;
26
+ --accent-foreground: 222.2 47.4% 11.2%;
27
+
28
+ --destructive: 0 84.2% 60.2%;
29
+ --destructive-foreground: 210 40% 98%;
30
+
31
+ --border: 214.3 31.8% 91.4%;
32
+ --input: 214.3 31.8% 91.4%;
33
+ --ring: 222.2 84% 4.9%;
34
+
35
+ --radius: 0.5rem;
36
+ }
37
+
38
+ .dark {
39
+ --background: 222.2 84% 4.9%;
40
+ --foreground: 210 40% 98%;
41
+
42
+ --card: 222.2 84% 4.9%;
43
+ --card-foreground: 210 40% 98%;
44
+
45
+ --popover: 222.2 84% 4.9%;
46
+ --popover-foreground: 210 40% 98%;
47
+
48
+ --primary: 210 40% 98%;
49
+ --primary-foreground: 222.2 47.4% 11.2%;
50
+
51
+ --secondary: 217.2 32.6% 17.5%;
52
+ --secondary-foreground: 210 40% 98%;
53
+
54
+ --muted: 217.2 32.6% 17.5%;
55
+ --muted-foreground: 215 20.2% 65.1%;
56
+
57
+ --accent: 217.2 32.6% 17.5%;
58
+ --accent-foreground: 210 40% 98%;
59
+
60
+ --destructive: 0 62.8% 30.6%;
61
+ --destructive-foreground: 210 40% 98%;
62
+
63
+ --border: 217.2 32.6% 17.5%;
64
+ --input: 217.2 32.6% 17.5%;
65
+ --ring: 212.7 26.8% 83.9%;
66
+ }
67
+
68
+ * {
69
+ @apply border-border;
70
+ }
71
+
72
+ body {
73
+ @apply bg-background text-foreground;
74
+ }
75
+ }
76
+
77
+ /* 弹窗蒙层全屏覆盖,层级高于画布 Ruler / 工具栏 */
78
+ [data-radix-dialog-overlay],
79
+ [data-radix-alert-dialog-overlay],
80
+ [data-vaul-overlay] {
81
+ z-index: 20000 !important;
82
+ }
83
+
84
+ [data-radix-dialog-content],
85
+ [data-radix-alert-dialog-content],
86
+ [data-vaul-drawer-wrapper] {
87
+ z-index: 20001 !important;
88
+ }
89
+
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ThemeProvider as NextThemesProvider } from "next-themes";
5
+ import type { ThemeProviderProps } from "next-themes";
6
+
7
+ export type AppTheme = "light" | "dark";
8
+
9
+ export type ArronqzyThemeProviderProps = Omit<
10
+ ThemeProviderProps,
11
+ "attribute" | "defaultTheme" | "enableSystem"
12
+ > & {
13
+ defaultTheme?: AppTheme;
14
+ enableSystem?: boolean;
15
+ };
16
+
17
+ export function ThemeProvider({
18
+ children,
19
+ defaultTheme = "dark",
20
+ enableSystem = true,
21
+ ...props
22
+ }: ArronqzyThemeProviderProps) {
23
+ return (
24
+ <NextThemesProvider
25
+ attribute="class"
26
+ defaultTheme={defaultTheme}
27
+ enableSystem={enableSystem}
28
+ {...props}
29
+ >
30
+ {children}
31
+ </NextThemesProvider>
32
+ );
33
+ }
34
+
35
+ export { useTheme } from "next-themes";
36
+
@@ -0,0 +1,58 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+ import { ChevronDown } from "lucide-react"
6
+
7
+ import { cn } from "../lib/utils"
8
+
9
+ const Accordion = AccordionPrimitive.Root
10
+
11
+ const AccordionItem = React.forwardRef<
12
+ React.ElementRef<typeof AccordionPrimitive.Item>,
13
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14
+ >(({ className, ...props }, ref) => (
15
+ <AccordionPrimitive.Item
16
+ ref={ref}
17
+ className={cn("border-b", className)}
18
+ {...props}
19
+ />
20
+ ))
21
+ AccordionItem.displayName = "AccordionItem"
22
+
23
+ const AccordionTrigger = React.forwardRef<
24
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
26
+ >(({ className, children, ...props }, ref) => (
27
+ <AccordionPrimitive.Header className="flex">
28
+ <AccordionPrimitive.Trigger
29
+ ref={ref}
30
+ className={cn(
31
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
32
+ className
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
38
+ </AccordionPrimitive.Trigger>
39
+ </AccordionPrimitive.Header>
40
+ ))
41
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42
+
43
+ const AccordionContent = React.forwardRef<
44
+ React.ElementRef<typeof AccordionPrimitive.Content>,
45
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
46
+ >(({ className, children, ...props }, ref) => (
47
+ <AccordionPrimitive.Content
48
+ ref={ref}
49
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
50
+ {...props}
51
+ >
52
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
53
+ </AccordionPrimitive.Content>
54
+ ))
55
+
56
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
57
+
58
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }